-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
feat(builders): add checkbox, checkboxgroup, and radiogroup builders #11410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4d0e88d
feat(builders): add checkbox, checkboxgroup, and radiogroup builders
faceboy1392 c9f77cb
Update packages/builders/src/components/checkbox/Assertions.ts
faceboy1392 db61fb6
fix(builders): remove length validators from add/splice options
faceboy1392 2ffee5c
Merge branch 'v14' into v14
Jiralite 212e6fd
chore: remove directives
Jiralite 0644fe7
fix(builders): documentation fixes
faceboy1392 6e8c8df
fix(builders): return Result.err instead of throw in validators
faceboy1392 cf38149
Merge branch 'v14' into v14
Qjuh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
415 changes: 415 additions & 0 deletions
415
packages/builders/__tests__/components/checkbox.test.ts
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { Result, s } from '@sapphire/shapeshift'; | ||
| import { ComponentType } from 'discord-api-types/v10'; | ||
| import { isValidationEnabled } from '../../util/validation'; | ||
| import { customIdValidator, idValidator } from '../Assertions'; | ||
|
|
||
| export const checkboxPredicate = s | ||
| .object({ | ||
| type: s.literal(ComponentType.Checkbox), | ||
| custom_id: customIdValidator, | ||
| id: idValidator.optional(), | ||
| default: s.boolean().optional(), | ||
| }) | ||
| .setValidationEnabled(isValidationEnabled); | ||
|
|
||
| export const checkboxGroupOptionPredicate = s | ||
| .object({ | ||
| label: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100), | ||
| value: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100), | ||
| description: s.string().lengthLessThanOrEqual(100).optional(), | ||
| default: s.boolean().optional(), | ||
| }) | ||
| .setValidationEnabled(isValidationEnabled); | ||
|
|
||
| export const checkboxGroupPredicate = s | ||
| .object({ | ||
| type: s.literal(ComponentType.CheckboxGroup), | ||
| custom_id: customIdValidator, | ||
| id: idValidator.optional(), | ||
| options: s.array(checkboxGroupOptionPredicate).lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(10), | ||
| min_values: s.number().int().greaterThanOrEqual(0).lessThanOrEqual(10).optional(), | ||
| max_values: s.number().int().greaterThanOrEqual(1).lessThanOrEqual(10).optional(), | ||
| required: s.boolean().optional(), | ||
| }) | ||
| .reshape((data) => { | ||
| // Ensure min_values is not greater than max_values | ||
| if (data.min_values !== undefined && data.max_values !== undefined && data.min_values > data.max_values) { | ||
| return Result.err(new RangeError('min_values cannot be greater than max_values')); | ||
| } | ||
|
|
||
| // Ensure max_values is not greater than the number of options | ||
| if (data.max_values !== undefined && data.max_values > data.options.length) { | ||
| return Result.err(new RangeError('max_values cannot be greater than the number of options')); | ||
| } | ||
|
|
||
| // Ensure min_values is not greater than the number of options | ||
| if (data.min_values !== undefined && data.min_values > data.options.length) { | ||
| return Result.err(new RangeError('min_values cannot be greater than the number of options')); | ||
| } | ||
|
|
||
| // Ensure required is consistent with min_values | ||
| if (data.required === true && data.min_values === 0) { | ||
| return Result.err(new RangeError('If required is true, min_values must be at least 1')); | ||
| } | ||
|
|
||
| // Ensure there are not more default values than max_values | ||
| const defaultCount = data.options.filter((option) => option.default === true).length; | ||
| if (data.max_values !== undefined && defaultCount > data.max_values) { | ||
| return Result.err(new RangeError('The number of default options cannot be greater than max_values')); | ||
| } | ||
|
|
||
| // Ensure each option's value is unique | ||
| const values = data.options.map((option) => option.value); | ||
| const uniqueValues = new Set(values); | ||
| if (uniqueValues.size !== values.length) { | ||
| return Result.err(new RangeError('Each option in a checkbox group must have a unique value')); | ||
| } | ||
|
|
||
| return Result.ok(data); | ||
| }) | ||
| .setValidationEnabled(isValidationEnabled); | ||
|
|
||
| export const radioGroupOptionPredicate = checkboxGroupOptionPredicate; | ||
|
|
||
| export const radioGroupPredicate = s | ||
| .object({ | ||
| type: s.literal(ComponentType.RadioGroup), | ||
| custom_id: customIdValidator, | ||
| id: idValidator.optional(), | ||
| options: s.array(radioGroupOptionPredicate).lengthGreaterThanOrEqual(2).lengthLessThanOrEqual(10), | ||
| required: s.boolean().optional(), | ||
| }) | ||
| .reshape((data) => { | ||
| // Ensure there is exactly one default option | ||
| const defaultCount = data.options.filter((option) => option.default === true).length; | ||
| if (defaultCount > 1) { | ||
| return Result.err(new RangeError('There can be at most one default option in a radio group')); | ||
| } | ||
|
|
||
| // Ensure each option's value is unique | ||
| const values = data.options.map((option) => option.value); | ||
| const uniqueValues = new Set(values); | ||
| if (uniqueValues.size !== values.length) { | ||
| return Result.err(new RangeError('Each option in a radio group must have a unique value')); | ||
| } | ||
|
|
||
| return Result.ok(data); | ||
| }) | ||
| .setValidationEnabled(isValidationEnabled); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type { APICheckboxComponent } from 'discord-api-types/v10'; | ||
| import { ComponentType } from 'discord-api-types/v10'; | ||
| import { ComponentBuilder } from '../Component'; | ||
| import { checkboxPredicate } from './Assertions'; | ||
|
|
||
| /** | ||
| * A builder that creates API-compatible JSON data for checkboxes. | ||
| */ | ||
| export class CheckboxBuilder extends ComponentBuilder<APICheckboxComponent> { | ||
| /** | ||
| * Creates a new checkbox from API data. | ||
| * | ||
| * @param data - The API data to create this checkbox with | ||
| * @example | ||
| * Creating a checkbox from an API data object: | ||
| * ```ts | ||
| * const checkbox = new CheckboxBuilder({ | ||
| * custom_id: 'accept_terms', | ||
| * default: false, | ||
| * }); | ||
| * ``` | ||
| * @example | ||
| * Creating a checkbox using setters and API data: | ||
| * ```ts | ||
| * const checkbox = new CheckboxBuilder() | ||
| * .setCustomId('subscribe_newsletter') | ||
| * .setDefault(true); | ||
| * ``` | ||
| */ | ||
| public constructor(data?: Partial<APICheckboxComponent>) { | ||
| super({ type: ComponentType.Checkbox, ...data }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets the custom id of this checkbox. | ||
| * | ||
| * @param customId - The custom id to use | ||
| */ | ||
| public setCustomId(customId: string) { | ||
| this.data.custom_id = customId; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets whether this checkbox is checked by default. | ||
| * | ||
| * @param isDefault - Whether the checkbox should be checked by default | ||
| */ | ||
| public setDefault(isDefault: boolean) { | ||
| this.data.default = isDefault; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritDoc ComponentBuilder.toJSON} | ||
| */ | ||
| public override toJSON(): APICheckboxComponent { | ||
| checkboxPredicate.parse(this.data); | ||
| return { | ||
| ...this.data, | ||
| } as APICheckboxComponent; | ||
| } | ||
| } |
174 changes: 174 additions & 0 deletions
174
packages/builders/src/components/checkbox/CheckboxGroup.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import type { APICheckboxGroupComponent, APICheckboxGroupOption } from 'discord-api-types/v10'; | ||
| import { ComponentType } from 'discord-api-types/v10'; | ||
| import type { RestOrArray } from '../../util/normalizeArray'; | ||
| import { normalizeArray } from '../../util/normalizeArray'; | ||
| import { ComponentBuilder } from '../Component'; | ||
| import { checkboxGroupOptionPredicate, checkboxGroupPredicate } from './Assertions'; | ||
| import { CheckboxGroupOptionBuilder } from './CheckboxGroupOption'; | ||
|
|
||
| /** | ||
| * A builder that creates API-compatible JSON data for checkbox groups. | ||
| */ | ||
| export class CheckboxGroupBuilder extends ComponentBuilder<APICheckboxGroupComponent> { | ||
| /** | ||
| * The options within this checkbox group. | ||
| */ | ||
| public readonly options: CheckboxGroupOptionBuilder[]; | ||
|
|
||
| /** | ||
| * Creates a new checkbox group from API data. | ||
| * | ||
| * @param data - The API data to create this checkbox group with | ||
| * @example | ||
| * Creating a checkbox group from an API data object: | ||
| * ```ts | ||
| * const checkboxGroup = new CheckboxGroupBuilder({ | ||
| * custom_id: 'select_options', | ||
| * options: [ | ||
| * { label: 'Option 1', value: 'option_1' }, | ||
| * { label: 'Option 2', value: 'option_2' }, | ||
| * ], | ||
| * }); | ||
| * ``` | ||
| * @example | ||
| * Creating a checkbox group using setters and API data: | ||
| * ```ts | ||
| * const checkboxGroup = new CheckboxGroupBuilder() | ||
| * .setCustomId('choose_items') | ||
| * .setOptions([ | ||
| * { label: 'Item A', value: 'item_a' }, | ||
| * { label: 'Item B', value: 'item_b' }, | ||
| * ]) | ||
| * .setMinValues(1) | ||
| * .setMaxValues(2); | ||
| * ``` | ||
| */ | ||
| public constructor(data?: Partial<APICheckboxGroupComponent>) { | ||
| const { options, ...initData } = data ?? {}; | ||
| super({ ...initData, type: ComponentType.CheckboxGroup }); | ||
| this.options = options?.map((option: APICheckboxGroupOption) => new CheckboxGroupOptionBuilder(option)) ?? []; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the custom id of this checkbox group. | ||
| * | ||
| * @param customId - The custom id to use | ||
| */ | ||
| public setCustomId(customId: string) { | ||
| this.data.custom_id = customId; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Adds options to this checkbox group. | ||
| * | ||
| * @param options - The options to add | ||
| */ | ||
| public addOptions(...options: RestOrArray<APICheckboxGroupOption | CheckboxGroupOptionBuilder>) { | ||
| const normalizedOptions = normalizeArray(options); | ||
|
|
||
| this.options.push( | ||
| ...normalizedOptions.map((normalizedOption) => { | ||
| // I do this because TS' duck typing causes issues, | ||
| // if I put in a RadioGroupOption, TS lets it pass but | ||
| // it fails to convert to a checkbox group option at runtime | ||
| const json = 'toJSON' in normalizedOption ? normalizedOption.toJSON() : normalizedOption; | ||
| const option = new CheckboxGroupOptionBuilder(json); | ||
| checkboxGroupOptionPredicate.parse(option.toJSON()); | ||
| return option; | ||
| }), | ||
| ); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the options for this checkbox group. | ||
| * | ||
| * @param options - The options to use | ||
| */ | ||
| public setOptions(options: RestOrArray<APICheckboxGroupOption | CheckboxGroupOptionBuilder>) { | ||
| return this.spliceOptions(0, this.options.length, ...options); | ||
| } | ||
|
|
||
| /** | ||
| * Removes, replaces, or inserts options for this checkbox group. | ||
| * | ||
| * @remarks | ||
| * This method behaves similarly | ||
| * to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}. | ||
| * It's useful for modifying and adjusting the order of existing options. | ||
| * @param index - The index to start at | ||
| * @param deleteCount - The number of options to remove | ||
| * @param options - The replacing option objects or builders | ||
| */ | ||
| public spliceOptions( | ||
| index: number, | ||
| deleteCount: number, | ||
| ...options: RestOrArray<APICheckboxGroupOption | CheckboxGroupOptionBuilder> | ||
| ) { | ||
| const normalizedOptions = normalizeArray(options); | ||
|
|
||
| const clone = [...this.options]; | ||
|
|
||
| clone.splice( | ||
| index, | ||
| deleteCount, | ||
| ...normalizedOptions.map((normalizedOption) => { | ||
| // I do this because TS' duck typing causes issues, | ||
| // if I put in a RadioGroupOption, TS lets it pass but | ||
| // it fails to convert to a checkbox group option at runtime | ||
| const json = 'toJSON' in normalizedOption ? normalizedOption.toJSON() : normalizedOption; | ||
| const option = new CheckboxGroupOptionBuilder(json); | ||
| checkboxGroupOptionPredicate.parse(option.toJSON()); | ||
| return option; | ||
| }), | ||
| ); | ||
|
|
||
| this.options.splice(0, this.options.length, ...clone); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the minimum number of options that must be selected. | ||
| * | ||
| * @param minValues - The minimum number of options that must be selected | ||
| */ | ||
| public setMinValues(minValues: number) { | ||
| this.data.min_values = minValues; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the maximum number of options that can be selected. | ||
| * | ||
| * @param maxValues - The maximum number of options that can be selected | ||
| */ | ||
| public setMaxValues(maxValues: number) { | ||
| this.data.max_values = maxValues; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets whether selecting options is required. | ||
| * | ||
| * @param required - Whether selecting options is required | ||
| */ | ||
| public setRequired(required: boolean) { | ||
| this.data.required = required; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritDoc ComponentBuilder.toJSON} | ||
| */ | ||
| public override toJSON(): APICheckboxGroupComponent { | ||
| const data = { | ||
| ...this.data, | ||
| options: this.options.map((option) => option.toJSON()), | ||
| }; | ||
|
|
||
| checkboxGroupPredicate.parse(data); | ||
|
|
||
| return data as APICheckboxGroupComponent; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.