feat(builders): add checkbox, checkboxgroup, and radiogroup builders (#11410)

* feat(builders): add checkbox, checkboxgroup, and radiogroup builders

* Update packages/builders/src/components/checkbox/Assertions.ts

fix incorrect wording about default option count in radio groups

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* fix(builders): remove length validators from add/splice options

* chore: remove directives

* fix(builders): documentation fixes

* fix(builders): return Result.err instead of throw in validators

---------

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
faceboy
2026-03-11 08:39:16 -04:00
committed by GitHub
parent ec5d921b75
commit ca7719e822
11 changed files with 1142 additions and 2 deletions

View File

@@ -0,0 +1,415 @@
import { ComponentType, type APICheckboxComponent } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { LabelBuilder } from '../../src';
import { CheckboxBuilder } from '../../src/components/checkbox/Checkbox';
import { CheckboxGroupBuilder } from '../../src/components/checkbox/CheckboxGroup';
import { CheckboxGroupOptionBuilder } from '../../src/components/checkbox/CheckboxGroupOption';
import { RadioGroupBuilder } from '../../src/components/checkbox/RadioGroup';
import { RadioGroupOptionBuilder } from '../../src/components/checkbox/RadioGroupOption';
const longStr = ':3'.repeat(5_000);
const fiveCheckboxOptions = [
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new CheckboxGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
new CheckboxGroupOptionBuilder().setLabel('Option 3').setValue('option_3'),
new CheckboxGroupOptionBuilder().setLabel('Option 4').setValue('option_4'),
new CheckboxGroupOptionBuilder().setLabel('Option 5').setValue('option_5'),
];
const elevenCheckboxOptions = [
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new CheckboxGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
new CheckboxGroupOptionBuilder().setLabel('Option 3').setValue('option_3'),
new CheckboxGroupOptionBuilder().setLabel('Option 4').setValue('option_4'),
new CheckboxGroupOptionBuilder().setLabel('Option 5').setValue('option_5'),
new CheckboxGroupOptionBuilder().setLabel('Option 6').setValue('option_6'),
new CheckboxGroupOptionBuilder().setLabel('Option 7').setValue('option_7'),
new CheckboxGroupOptionBuilder().setLabel('Option 8').setValue('option_8'),
new CheckboxGroupOptionBuilder().setLabel('Option 9').setValue('option_9'),
new CheckboxGroupOptionBuilder().setLabel('Option 10').setValue('option_10'),
new CheckboxGroupOptionBuilder().setLabel('Option 11').setValue('option_11'),
];
const fiveRadioOptions = [
new RadioGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new RadioGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
new RadioGroupOptionBuilder().setLabel('Option 3').setValue('option_3'),
new RadioGroupOptionBuilder().setLabel('Option 4').setValue('option_4'),
new RadioGroupOptionBuilder().setLabel('Option 5').setValue('option_5'),
];
const elevenRadioOptions = [
new RadioGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new RadioGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
new RadioGroupOptionBuilder().setLabel('Option 3').setValue('option_3'),
new RadioGroupOptionBuilder().setLabel('Option 4').setValue('option_4'),
new RadioGroupOptionBuilder().setLabel('Option 5').setValue('option_5'),
new RadioGroupOptionBuilder().setLabel('Option 6').setValue('option_6'),
new RadioGroupOptionBuilder().setLabel('Option 7').setValue('option_7'),
new RadioGroupOptionBuilder().setLabel('Option 8').setValue('option_8'),
new RadioGroupOptionBuilder().setLabel('Option 9').setValue('option_9'),
new RadioGroupOptionBuilder().setLabel('Option 10').setValue('option_10'),
new RadioGroupOptionBuilder().setLabel('Option 11').setValue('option_11'),
];
describe('Checkbox Components', () => {
describe('CheckboxBuilder', () => {
test('Valid builder does not throw.', () => {
expect(() => new CheckboxBuilder().setCustomId('checkbox').toJSON()).not.toThrowError();
expect(() => new CheckboxBuilder().setCustomId('checkbox').setDefault(true).toJSON()).not.toThrowError();
});
test('Invalid builder does throw.', () => {
expect(() => new CheckboxBuilder().toJSON()).toThrowError();
expect(() => new CheckboxBuilder().setDefault(true).toJSON()).toThrowError();
expect(() => new CheckboxBuilder().setCustomId(longStr).toJSON()).toThrowError();
});
test('API data equals toJSON().', () => {
const checkboxData = {
type: ComponentType.Checkbox,
custom_id: 'checkbox',
default: true,
} satisfies APICheckboxComponent;
expect(new CheckboxBuilder(checkboxData).toJSON()).toEqual(checkboxData);
expect(new CheckboxBuilder().setCustomId('checkbox').setDefault(true).toJSON()).toEqual(checkboxData);
});
});
describe('CheckboxGroupBuilder', () => {
test('Valid builder does not throw.', () => {
expect(() =>
new CheckboxGroupBuilder({
custom_id: 'checkbox_group',
options: fiveCheckboxOptions.map((option) => option.toJSON()),
}).toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId('checkbox_group').setOptions(fiveCheckboxOptions).toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions([
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new CheckboxGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
])
.toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId('checkbox_group').addOptions(fiveCheckboxOptions).toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setMinValues(1)
.setMaxValues(2)
.setOptions([
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new CheckboxGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
])
.toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.spliceOptions(2, 1, ...elevenCheckboxOptions.slice(7, 9))
.spliceOptions(0, 1, { label: 'New Option', value: 'new_option' }),
).not.toThrowError();
});
test('Invalid builder does throw.', () => {
expect(() => new CheckboxGroupBuilder().toJSON()).toThrowError();
expect(() => new CheckboxGroupBuilder().addOptions([]).toJSON()).toThrowError();
expect(() => new CheckboxGroupBuilder().setMinValues(2).setMaxValues(1).toJSON()).toThrowError();
expect(() =>
new CheckboxGroupBuilder().setMinValues(2).setMaxValues(1).addOptions(fiveCheckboxOptions).toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setMinValues(2)
.setMaxValues(1)
.addOptions(fiveCheckboxOptions)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId('checkbox_group').setMinValues(5).setMaxValues(11).toJSON(),
).toThrowError();
expect(() => new CheckboxGroupBuilder().setCustomId('checkbox_group').setOptions([]).toJSON()).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.setMinValues(6)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.setMaxValues(6)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId('checkbox_group').setOptions(elevenCheckboxOptions).toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(elevenCheckboxOptions)
.setMaxValues(12)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId(longStr).setOptions(fiveCheckboxOptions).toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.setMinValues(0)
.setRequired(true)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setMaxValues(4)
.setOptions([
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1').setDefault(true),
new CheckboxGroupOptionBuilder().setLabel('Option 2').setValue('option_2').setDefault(true),
new CheckboxGroupOptionBuilder().setLabel('Option 3').setValue('option_3').setDefault(true),
new CheckboxGroupOptionBuilder().setLabel('Option 4').setValue('option_4').setDefault(true),
new CheckboxGroupOptionBuilder().setLabel('Option 5').setValue('option_5').setDefault(true),
])
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.addOptions(fiveCheckboxOptions)
.toJSON(),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(elevenCheckboxOptions.slice(0, 5))
.addOptions(elevenCheckboxOptions.slice(5, 11))
.toJSON(),
).toThrowError();
expect(
() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.spliceOptions(2, 1, new CheckboxGroupOptionBuilder().setLabel('Option 6')), // no value
).toThrowError();
expect(() =>
new CheckboxGroupBuilder()
.setCustomId('checkbox_group')
.setOptions(fiveCheckboxOptions)
.spliceOptions(2, 1, { value: 'hi', label: longStr }),
).toThrowError();
expect(() =>
new CheckboxGroupBuilder().setCustomId('checkbox_group').addOptions({ value: 'hi', label: longStr }),
).toThrowError();
});
});
describe('CheckboxGroupOptionBuilder', () => {
test('Valid builder does not throw.', () => {
expect(() =>
new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue('option_1').toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupOptionBuilder()
.setLabel('Option 2')
.setValue('option_2')
.setDescription('This is option 2')
.toJSON(),
).not.toThrowError();
expect(() =>
new CheckboxGroupOptionBuilder().setLabel('Option 3').setValue('option_3').setDefault(true).toJSON(),
).not.toThrowError();
});
test('Invalid builder does throw.', () => {
expect(() => new CheckboxGroupOptionBuilder().toJSON()).toThrowError();
expect(() => new CheckboxGroupOptionBuilder().setValue('option_1').toJSON()).toThrowError();
expect(() => new CheckboxGroupOptionBuilder().setLabel('Option 1').toJSON()).toThrowError();
expect(() => new CheckboxGroupOptionBuilder().setLabel(longStr).setValue('option_1').toJSON()).toThrowError();
expect(() => new CheckboxGroupOptionBuilder().setLabel('Option 1').setValue(longStr).toJSON()).toThrowError();
});
test('toJSON returns correct data.', () => {
const option = new CheckboxGroupOptionBuilder()
.setLabel('Option 1')
.setValue('option_1')
.setDescription('This is option 1')
.setDefault(true);
expect(option.toJSON()).toEqual({
label: 'Option 1',
value: 'option_1',
description: 'This is option 1',
default: true,
});
});
});
describe('RadioGroupBuilder', () => {
test('Valid builder does not throw.', () => {
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').addOptions(fiveRadioOptions).toJSON(),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.setOptions([
new RadioGroupOptionBuilder().setLabel('Option 1').setValue('option_1'),
new RadioGroupOptionBuilder().setLabel('Option 2').setValue('option_2'),
])
.toJSON(),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').addOptions(fiveRadioOptions).setRequired(false),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').addOptions(fiveRadioOptions).setRequired(true),
).not.toThrowError();
expect(() => new RadioGroupBuilder().setCustomId('radio_group').setOptions(fiveRadioOptions)).not.toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.setOptions(fiveRadioOptions)
.spliceOptions(2, 1, elevenRadioOptions.slice(7, 9)),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.addOptions(elevenRadioOptions.slice(0, 5))
.spliceOptions(0, 1, { label: 'New Option', value: 'new_option' }),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder({
custom_id: 'radio_group',
options: fiveRadioOptions.map((option) => option.toJSON()),
}).toJSON(),
).not.toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.addOptions(fiveRadioOptions.map((option) => option.toJSON()))
.toJSON(),
).not.toThrowError();
});
test('Invalid builder does throw.', () => {
expect(() => new RadioGroupBuilder().toJSON()).toThrowError();
expect(() => new RadioGroupBuilder().addOptions([]).toJSON()).toThrowError();
// needs at least 2 options
expect(() => new RadioGroupBuilder().addOptions([fiveRadioOptions[0]]).toJSON()).toThrowError();
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').setOptions([fiveRadioOptions[0]]).toJSON(),
).toThrowError();
expect(() => new RadioGroupBuilder().setCustomId('radio_group').setOptions([]).toJSON()).toThrowError();
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').setOptions(elevenRadioOptions).toJSON(),
).toThrowError();
expect(() => new RadioGroupBuilder().setCustomId(longStr).setOptions(fiveRadioOptions).toJSON()).toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.setOptions([
new RadioGroupOptionBuilder().setLabel('Option 1').setValue('option_1').setDefault(true),
new RadioGroupOptionBuilder().setLabel('Option 2').setValue('option_2').setDefault(true),
])
.toJSON(),
).toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.addOptions(fiveRadioOptions)
.addOptions(fiveRadioOptions)
.toJSON(),
).toThrowError();
expect(() =>
new RadioGroupBuilder()
.setCustomId('radio_group')
.setOptions(fiveRadioOptions)
.spliceOptions(2, 1, { value: 'hi', label: longStr }),
).toThrowError();
expect(() =>
new RadioGroupBuilder().setCustomId('radio_group').addOptions({ value: 'hi', label: longStr }),
).toThrowError();
});
});
describe('RadioGroupOptionBuilder', () => {
test('Valid builder does not throw.', () => {
expect(() => new RadioGroupOptionBuilder().setLabel('Option 1').setValue('option_1').toJSON()).not.toThrowError();
expect(() =>
new RadioGroupOptionBuilder()
.setLabel('Option 2')
.setValue('option_2')
.setDescription('This is option 2')
.toJSON(),
).not.toThrowError();
expect(() =>
new RadioGroupOptionBuilder().setLabel('Option 3').setValue('option_3').setDefault(true).toJSON(),
).not.toThrowError();
});
test('Invalid builder does throw.', () => {
expect(() => new RadioGroupOptionBuilder().toJSON()).toThrowError();
expect(() => new RadioGroupOptionBuilder().setValue('option_1').toJSON()).toThrowError();
expect(() => new RadioGroupOptionBuilder().setLabel('Option 1').toJSON()).toThrowError();
expect(() => new RadioGroupOptionBuilder().setLabel(longStr).setValue('option_1').toJSON()).toThrowError();
expect(() => new RadioGroupOptionBuilder().setLabel('Option 1').setValue(longStr).toJSON()).toThrowError();
});
test('toJSON returns correct data.', () => {
const option = new RadioGroupOptionBuilder()
.setLabel('Option 1')
.setValue('option_1')
.setDescription('This is option 1')
.setDefault(true);
expect(option.toJSON()).toEqual({
label: 'Option 1',
value: 'option_1',
description: 'This is option 1',
default: true,
});
});
});
});
describe('LabelBuilder with Checkbox Components', () => {
test('LabelBuilder can set Checkbox component.', () => {
const checkbox = new CheckboxBuilder().setCustomId('checkbox').setDefault(true);
const label = new LabelBuilder().setLabel('Checkbox Label').setCheckboxComponent(checkbox);
expect(() => label.toJSON()).not.toThrowError();
expect(label.toJSON().component).toEqual(checkbox.toJSON());
});
test('LabelBuilder can set CheckboxGroup component.', () => {
const checkboxGroup = new CheckboxGroupBuilder().setCustomId('checkbox_group').setOptions(fiveCheckboxOptions);
const label = new LabelBuilder().setLabel('Checkbox Group Label').setCheckboxGroupComponent(checkboxGroup);
expect(() => label.toJSON()).not.toThrowError();
expect(label.toJSON().component).toEqual(checkboxGroup.toJSON());
});
test('LabelBuilder can set RadioGroup component.', () => {
const radioGroup = new RadioGroupBuilder().setCustomId('radio_group').setOptions(fiveRadioOptions);
const label = new LabelBuilder().setLabel('Radio Group Label').setRadioGroupComponent(radioGroup);
expect(() => label.toJSON()).not.toThrowError();
expect(label.toJSON().component).toEqual(radioGroup.toJSON());
});
});

View File

@@ -8,6 +8,9 @@ import {
} from './ActionRow.js';
import { ComponentBuilder } from './Component.js';
import { ButtonBuilder } from './button/Button.js';
import { CheckboxBuilder } from './checkbox/Checkbox.js';
import { CheckboxGroupBuilder } from './checkbox/CheckboxGroup.js';
import { RadioGroupBuilder } from './checkbox/RadioGroup.js';
import { FileUploadBuilder } from './fileUpload/FileUpload.js';
import { LabelBuilder } from './label/Label.js';
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
@@ -110,6 +113,18 @@ export interface MappedComponentTypes {
* The file upload component type is associated with a {@link FileUploadBuilder}.
*/
[ComponentType.FileUpload]: FileUploadBuilder;
/**
* The checkbox component type is associated with a {@link CheckboxBuilder}.
*/
[ComponentType.Checkbox]: CheckboxBuilder;
/**
* The checkbox group component type is associated with a {@link CheckboxGroupBuilder}.
*/
[ComponentType.CheckboxGroup]: CheckboxGroupBuilder;
/**
* The radio group component type is associated with a {@link RadioGroupBuilder}.
*/
[ComponentType.RadioGroup]: RadioGroupBuilder;
}
/**
@@ -175,8 +190,14 @@ export function createComponentBuilder(
return new LabelBuilder(data);
case ComponentType.FileUpload:
return new FileUploadBuilder(data);
case ComponentType.Checkbox:
return new CheckboxBuilder(data);
case ComponentType.CheckboxGroup:
return new CheckboxGroupBuilder(data);
case ComponentType.RadioGroup:
return new RadioGroupBuilder(data);
default:
// https://github.com/discordjs/discord.js/pull/11410
// @ts-expect-error This case can still occur if we get a newer unsupported component type
throw new Error(`Cannot properly serialize component type: ${data.type}`);
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,81 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APICheckboxGroupOption } from 'discord-api-types/v10';
import { checkboxGroupOptionPredicate } from './Assertions';
/**
* A builder that creates API-compatible JSON data for checkbox group options.
*/
export class CheckboxGroupOptionBuilder implements JSONEncodable<APICheckboxGroupOption> {
/**
* Creates a new checkbox group option from API data.
*
* @param data - The API data to create this checkbox group option with
* @example
* Creating a checkbox group option from an API data object:
* ```ts
* const option = new CheckboxGroupOptionBuilder({
* label: 'Option 1',
* value: 'option_1',
* });
* ```
* @example
* Creating a checkbox group option using setters and API data:
* ```ts
* const option = new CheckboxGroupOptionBuilder()
* .setLabel('Option 2')
* .setValue('option_2');
* ```
*/
public constructor(public data: Partial<APICheckboxGroupOption> = {}) {}
/**
* Sets the label for this option.
*
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = label;
return this;
}
/**
* Sets the value for this option.
*
* @param value - The value to use
*/
public setValue(value: string) {
this.data.value = value;
return this;
}
/**
* Sets the description for this option.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Sets whether this option is selected by default.
*
* @param isDefault - Whether the option should be selected by default
*/
public setDefault(isDefault: boolean) {
this.data.default = isDefault;
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(): APICheckboxGroupOption {
checkboxGroupOptionPredicate.parse(this.data);
return {
...this.data,
} as APICheckboxGroupOption;
}
}

View File

@@ -0,0 +1,152 @@
import type { APIRadioGroupComponent, APIRadioGroupOption } 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 { radioGroupOptionPredicate, radioGroupPredicate } from './Assertions';
import { RadioGroupOptionBuilder } from './RadioGroupOption';
/**
* A builder that creates API-compatible JSON data for radio groups.
*/
export class RadioGroupBuilder extends ComponentBuilder<APIRadioGroupComponent> {
/**
* The options within this radio group.
*/
public readonly options: RadioGroupOptionBuilder[];
/**
* Creates a new radio group from API data.
*
* @param data - The API data to create this radio group with
* @example
* Creating a radio group from an API data object:
* ```ts
* const radioGroup = new RadioGroupBuilder({
* custom_id: 'select_options',
* options: [
* { label: 'Option 1', value: 'option_1' },
* { label: 'Option 2', value: 'option_2' },
* ],
* });
* ```
* @example
* Creating a radio group using setters and API data:
* ```ts
* const radioGroup = new RadioGroupBuilder()
* .setCustomId('choose_items')
* .setOptions([
* { label: 'Item A', value: 'item_a' },
* { label: 'Item B', value: 'item_b' },
* ])
* ```
*/
public constructor(data?: Partial<APIRadioGroupComponent>) {
const { options, ...initData } = data ?? {};
super({ ...initData, type: ComponentType.RadioGroup });
this.options = options?.map((option: APIRadioGroupOption) => new RadioGroupOptionBuilder(option)) ?? [];
}
/**
* Sets the custom id of this radio group.
*
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
return this;
}
/**
* Adds options to this radio group.
*
* @param options - The options to add
*/
public addOptions(...options: RestOrArray<APIRadioGroupOption | RadioGroupOptionBuilder>) {
const normalizedOptions = normalizeArray(options);
this.options.push(
...normalizedOptions.map((normalizedOption) => {
// I do this because TS' duck typing causes issues,
// if I put in a CheckboxGroupOption, 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 RadioGroupOptionBuilder(json);
radioGroupOptionPredicate.parse(option.toJSON());
return option;
}),
);
return this;
}
/**
* Sets the options for this radio group.
*
* @param options - The options to use
*/
public setOptions(options: RestOrArray<APIRadioGroupOption | RadioGroupOptionBuilder>) {
return this.spliceOptions(0, this.options.length, ...options);
}
/**
* Removes, replaces, or inserts options for this radio 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<APIRadioGroupOption | RadioGroupOptionBuilder>
) {
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 CheckboxGroupOption, TS lets it pass but
// it fails to convert to a radio group option at runtime
const json = 'toJSON' in normalizedOption ? normalizedOption.toJSON() : normalizedOption;
const option = new RadioGroupOptionBuilder(json);
radioGroupOptionPredicate.parse(option.toJSON());
return option;
}),
);
this.options.splice(0, this.options.length, ...clone);
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(): APIRadioGroupComponent {
const data = {
...this.data,
options: this.options.map((option) => option.toJSON()),
};
radioGroupPredicate.parse(data);
return data as APIRadioGroupComponent;
}
}

View File

@@ -0,0 +1,81 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIRadioGroupOption } from 'discord-api-types/v10';
import { radioGroupOptionPredicate } from './Assertions';
/**
* A builder that creates API-compatible JSON data for radio group options.
*/
export class RadioGroupOptionBuilder implements JSONEncodable<APIRadioGroupOption> {
/**
* Creates a new radio group option from API data.
*
* @param data - The API data to create this radio group option with
* @example
* Creating a radio group option from an API data object:
* ```ts
* const option = new RadioGroupOptionBuilder({
* label: 'Option 1',
* value: 'option_1',
* });
* ```
* @example
* Creating a radio group option using setters and API data:
* ```ts
* const option = new RadioGroupOptionBuilder()
* .setLabel('Option 2')
* .setValue('option_2');
* ```
*/
public constructor(public data: Partial<APIRadioGroupOption> = {}) {}
/**
* Sets the label for this option.
*
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = label;
return this;
}
/**
* Sets the value for this option.
*
* @param value - The value to use
*/
public setValue(value: string) {
this.data.value = value;
return this;
}
/**
* Sets the description for this option.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Sets whether this option is selected by default.
*
* @param isDefault - Whether the option should be selected by default
*/
public setDefault(isDefault: boolean) {
this.data.default = isDefault;
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(): APIRadioGroupOption {
radioGroupOptionPredicate.parse(this.data);
return {
...this.data,
} as APIRadioGroupOption;
}
}

View File

@@ -2,6 +2,7 @@ import { s } from '@sapphire/shapeshift';
import { ComponentType } from 'discord-api-types/v10';
import { isValidationEnabled } from '../../util/validation.js';
import { idValidator } from '../Assertions.js';
import { checkboxGroupPredicate, checkboxPredicate, radioGroupPredicate } from '../checkbox/Assertions.js';
import { fileUploadPredicate } from '../fileUpload/Assertions.js';
import {
selectMenuChannelPredicate,
@@ -26,6 +27,9 @@ export const labelPredicate = s
selectMenuChannelPredicate,
selectMenuStringPredicate,
fileUploadPredicate,
checkboxPredicate,
checkboxGroupPredicate,
radioGroupPredicate,
]),
})
.setValidationEnabled(isValidationEnabled);

View File

@@ -1,8 +1,11 @@
import type {
APIChannelSelectComponent,
APICheckboxComponent,
APICheckboxGroupComponent,
APIFileUploadComponent,
APILabelComponent,
APIMentionableSelectComponent,
APIRadioGroupComponent,
APIRoleSelectComponent,
APIStringSelectComponent,
APITextInputComponent,
@@ -11,6 +14,9 @@ import type {
import { ComponentType } from 'discord-api-types/v10';
import { ComponentBuilder } from '../Component.js';
import { createComponentBuilder, resolveBuilder } from '../Components.js';
import { CheckboxBuilder } from '../checkbox/Checkbox.js';
import { CheckboxGroupBuilder } from '../checkbox/CheckboxGroup.js';
import { RadioGroupBuilder } from '../checkbox/RadioGroup.js';
import { FileUploadBuilder } from '../fileUpload/FileUpload.js';
import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js';
import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js';
@@ -23,8 +29,11 @@ import { labelPredicate } from './Assertions.js';
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> {
component?:
| ChannelSelectMenuBuilder
| CheckboxBuilder
| CheckboxGroupBuilder
| FileUploadBuilder
| MentionableSelectMenuBuilder
| RadioGroupBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
| TextInputBuilder
@@ -68,7 +77,6 @@ export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
this.data = {
...rest,
// @ts-expect-error https://github.com/discordjs/discord.js/pull/11410
component: component ? createComponentBuilder(component) : undefined,
type: ComponentType.Label,
};
@@ -195,6 +203,42 @@ export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
return this;
}
/**
* Sets a checkbox component to this label.
*
* @param input - A function that returns a component builder or an already built builder
*/
public setCheckboxComponent(
input: APICheckboxComponent | CheckboxBuilder | ((builder: CheckboxBuilder) => CheckboxBuilder),
): this {
this.data.component = resolveBuilder(input, CheckboxBuilder);
return this;
}
/**
* Sets a checkbox group component to this label.
*
* @param input - A function that returns a component builder or an already built builder
*/
public setCheckboxGroupComponent(
input: APICheckboxGroupComponent | CheckboxGroupBuilder | ((builder: CheckboxGroupBuilder) => CheckboxGroupBuilder),
): this {
this.data.component = resolveBuilder(input, CheckboxGroupBuilder);
return this;
}
/**
* Sets a radio group component to this label.
*
* @param input - A function that returns a component builder or an already built builder
*/
public setRadioGroupComponent(
input: APIRadioGroupComponent | RadioGroupBuilder | ((builder: RadioGroupBuilder) => RadioGroupBuilder),
): this {
this.data.component = resolveBuilder(input, RadioGroupBuilder);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/

View File

@@ -51,6 +51,13 @@ export * from './components/v2/Separator.js';
export * from './components/v2/TextDisplay.js';
export * from './components/v2/Thumbnail.js';
export * from './components/checkbox/Checkbox.js';
export * from './components/checkbox/CheckboxGroup.js';
export * from './components/checkbox/CheckboxGroupOption.js';
export * from './components/checkbox/RadioGroup.js';
export * from './components/checkbox/RadioGroupOption.js';
export * as CheckboxAssertions from './components/checkbox/Assertions.js';
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
export * from './interactions/slashCommands/SlashCommandBuilder.js';
export * from './interactions/slashCommands/SlashCommandSubcommands.js';