feat(components): Add radio group, checkbox group & checkbox components (#4763)

* feat(components): implementation of the brand new modal component types Radio Group, Checkbox Group, Checkbox

* refactor(components): streamline checkbox component transformation and enhance type definitions

* Update packages/bot/src/transformers/reverse/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/reverse/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/types.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/reverse/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/reverse/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/bot/src/transformers/component.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* refractor(component): transformation logic and update type definitions for options handling

* Update packages/bot/src/transformers/component.ts

Co-authored-by: Awesome Stickz <awesome@stickz.dev>

* refractor(components): move types of new components below file upload to match discord docs

* Update packages/types/src/discord/components.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

* Update packages/types/src/discordeno/components.ts

Co-authored-by: Fleny <Fleny113@outlook.com>

---------

Co-authored-by: Fleny <Fleny113@outlook.com>
Co-authored-by: Awesome Stickz <awesome@stickz.dev>
This commit is contained in:
adam
2026-02-16 23:57:39 +02:00
committed by GitHub
parent 1c68720b68
commit 70da0ac9ff
6 changed files with 301 additions and 9 deletions

View File

@@ -327,6 +327,7 @@ export function createDesiredPropertiesObject<T extends RecursivePartial<Transfo
style: defaultValue,
label: defaultValue,
value: defaultValue,
default: defaultValue,
emoji: defaultValue,
url: defaultValue,
channelTypes: defaultValue,

View File

@@ -1,6 +1,10 @@
import {
type DiscordActionRow,
type DiscordButtonComponent,
type DiscordCheckboxComponent,
type DiscordCheckboxGroupComponent,
type DiscordCheckboxGroupInteractionResponse,
type DiscordCheckboxInteractionResponse,
type DiscordChannelSelectComponent,
type DiscordChannelSelectInteractionResponseFromModal,
type DiscordContainerComponent,
@@ -15,6 +19,8 @@ import {
type DiscordMentionableSelectInteractionResponseFromModal,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordRadioGroupComponent,
type DiscordRadioGroupInteractionResponse,
type DiscordRoleSelectComponent,
type DiscordRoleSelectInteractionResponseFromModal,
type DiscordSectionComponent,
@@ -91,6 +97,15 @@ export function transformComponent(bot: Bot, payload: DiscordMessageComponent |
case MessageComponentTypes.FileUpload:
component = transformFileUploadComponent(bot, payload);
break;
case MessageComponentTypes.RadioGroup:
component = transformRadioGroupComponent(bot, payload);
break;
case MessageComponentTypes.CheckboxGroup:
component = transformCheckboxGroupComponent(bot, payload);
break;
case MessageComponentTypes.Checkbox:
component = transformCheckboxComponent(bot, payload);
break;
}
return bot.transformers.customizers.component(bot, payload, component);
@@ -444,3 +459,57 @@ function transformFileUploadComponent(bot: Bot, payload: DiscordFileUploadCompon
return fileUpload;
}
function transformRadioGroupComponent(bot: Bot, payload: DiscordRadioGroupComponent | DiscordRadioGroupInteractionResponse) {
const props = bot.transformers.desiredProperties.component;
const radioGroup = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) radioGroup.type = payload.type;
if (props.id && payload.id) radioGroup.id = payload.id;
if (props.customId && payload.custom_id) radioGroup.customId = payload.custom_id;
// Check if this is the component (has options) or the interaction response (modal submit, has value)
if ('options' in payload) {
if (props.options && payload.options) radioGroup.options = payload.options;
if (props.required && payload.required !== undefined) radioGroup.required = payload.required;
} else {
if (props.value) radioGroup.value = payload.value ?? undefined;
}
return radioGroup;
}
function transformCheckboxGroupComponent(bot: Bot, payload: DiscordCheckboxGroupComponent | DiscordCheckboxGroupInteractionResponse) {
const props = bot.transformers.desiredProperties.component;
const checkboxGroup = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) checkboxGroup.type = payload.type;
if (props.id && payload.id) checkboxGroup.id = payload.id;
if (props.customId && payload.custom_id) checkboxGroup.customId = payload.custom_id;
// Check if this is the component (has options) or the interaction response (modal submit, has values)
if ('options' in payload) {
if (props.options && payload.options) checkboxGroup.options = payload.options;
if (props.minValues && payload.min_values !== undefined) checkboxGroup.minValues = payload.min_values;
if (props.maxValues && payload.max_values !== undefined) checkboxGroup.maxValues = payload.max_values;
if (props.required && payload.required !== undefined) checkboxGroup.required = payload.required;
} else {
if (props.values && payload.values) checkboxGroup.values = payload.values;
}
return checkboxGroup;
}
function transformCheckboxComponent(bot: Bot, payload: DiscordCheckboxComponent | DiscordCheckboxInteractionResponse) {
const props = bot.transformers.desiredProperties.component;
const checkbox = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) checkbox.type = payload.type;
if (props.id && payload.id) checkbox.id = payload.id;
if (props.customId && payload.custom_id) checkbox.customId = payload.custom_id;
if (props.value && 'value' in payload) checkbox.value = payload.value;
if (props.default && 'default' in payload) checkbox.default = payload.default;
return checkbox;
}

View File

@@ -2,6 +2,8 @@ import {
type ButtonStyles,
type DiscordActionRow,
type DiscordButtonComponent,
type DiscordCheckboxComponent,
type DiscordCheckboxGroupComponent,
type DiscordChannelSelectComponent,
type DiscordChannelSelectInteractionResponseFromModal,
type DiscordContainerComponent,
@@ -14,6 +16,7 @@ import {
type DiscordMentionableSelectInteractionResponseFromModal,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordRadioGroupComponent,
type DiscordRoleSelectComponent,
type DiscordRoleSelectInteractionResponseFromModal,
type DiscordSectionComponent,
@@ -27,6 +30,7 @@ import {
type DiscordUserSelectComponent,
type DiscordUserSelectInteractionResponseFromModal,
MessageComponentTypes,
type SelectOption,
type TextStyles,
} from '@discordeno/types';
import type { Bot } from '../../bot.js';
@@ -68,6 +72,12 @@ export function transformComponentToDiscordComponent(
return transformLabelComponent(bot, payload);
case MessageComponentTypes.FileUpload:
return transformFileUploadComponent(bot, payload);
case MessageComponentTypes.RadioGroup:
return transformRadioGroupComponent(bot, payload);
case MessageComponentTypes.CheckboxGroup:
return transformCheckboxGroupComponent(bot, payload);
case MessageComponentTypes.Checkbox:
return transformCheckboxComponent(bot, payload);
case MessageComponentTypes.Separator:
case MessageComponentTypes.TextDisplay:
// As of now they are compatible
@@ -143,7 +153,7 @@ function transformInputTextComponent(_bot: Bot, payload: Component): DiscordText
style: payload.style as TextStyles,
custom_id: payload.customId!,
label: payload.label!,
value: payload.value,
value: payload.value as string | undefined,
max_length: payload.maxLength,
min_length: payload.minLength,
placeholder: payload.placeholder,
@@ -171,7 +181,7 @@ function transformStringSelectMenuComponent(
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
options: payload.options?.map((option) => ({
options: (payload.options as SelectOption[] | undefined)?.map((option) => ({
label: option.label,
value: option.value,
description: option.description,
@@ -354,3 +364,34 @@ function transformFileUploadComponent(bot: Bot, payload: Component): DiscordFile
required: payload.required,
};
}
function transformRadioGroupComponent(bot: Bot, payload: Component): DiscordRadioGroupComponent {
return {
type: MessageComponentTypes.RadioGroup,
id: payload.id,
custom_id: payload.customId!,
options: payload.options ?? [],
required: payload.required,
};
}
function transformCheckboxGroupComponent(bot: Bot, payload: Component): DiscordCheckboxGroupComponent {
return {
type: MessageComponentTypes.CheckboxGroup,
id: payload.id,
custom_id: payload.customId!,
options: payload.options ?? [],
min_values: payload.minValues,
max_values: payload.maxValues,
required: payload.required,
};
}
function transformCheckboxComponent(bot: Bot, payload: Component): DiscordCheckboxComponent {
return {
type: MessageComponentTypes.Checkbox,
id: payload.id,
custom_id: payload.customId!,
default: payload.default,
};
}

View File

@@ -13,6 +13,7 @@ import type {
ButtonStyles,
Camelize,
ChannelTypes,
CheckboxGroupOption,
DefaultMessageNotificationLevels,
DiscordActivityInstanceResource,
DiscordActivityLocationKind,
@@ -54,6 +55,7 @@ import type {
PremiumTiers,
PremiumTypes,
PresenceStatus,
RadioGroupOption,
RoleFlags,
ScheduledEventEntityType,
ScheduledEventPrivacyLevel,
@@ -576,8 +578,8 @@ export interface Component {
style?: ButtonStyles | TextStyles;
/** text that appears on the button (max 80 characters) */
label?: string;
/** the dev-define value of the option, max 100 characters for select or 4000 for input. */
value?: string;
/** the dev-define value of the option, max 100 characters for select or 4000 for input; or boolean for checkbox response. */
value?: string | boolean;
/** Emoji object that includes fields of name, id, and animated supporting unicode and custom emojis. */
emoji?: Pick<Partial<Emoji>, 'id' | 'name' | 'animated'>;
/** optional url for link-style buttons that can navigate a user to the web. Only type 5 Link buttons can have a url */
@@ -585,7 +587,8 @@ export interface Component {
/** List of channel types to include in a channel select menu options list */
channelTypes?: ChannelTypes[];
/** The choices! Maximum of 25 items. */
options?: SelectOption[];
/** The string select options or the radio or checkbox group options */
options?: SelectOption[] | RadioGroupOption[] | CheckboxGroupOption[];
/** A custom placeholder text if nothing is selected. Maximum 150 characters. */
placeholder?: string;
/** The minimum number of items that must be selected. Default 1. Between 1-25. */
@@ -632,6 +635,8 @@ export interface Component {
component?: Component;
/** The text of the selected options */
values?: string[];
/** Whether the checkbox is selected by default (Checkbox component). */
default?: boolean;
/** Resolved entities from selected options */
resolved?: InteractionDataResolved;
}

View File

@@ -41,6 +41,12 @@ export enum MessageComponentTypes {
Label,
/** Component for uploading files */
FileUpload,
/** An interactive component for selecting exactly one option from a defined list */
RadioGroup = 21,
/** An interactive component for selecting one or many options via checkboxes */
CheckboxGroup = 22,
/** A single interactive component for yes/no style questions */
Checkbox = 23,
}
export type DiscordMessageComponents = DiscordMessageComponent[];
@@ -61,12 +67,18 @@ export type DiscordMessageComponent =
| DiscordContainerComponent
| DiscordFileComponent
| DiscordLabelComponent
| DiscordFileUploadComponent;
| DiscordFileUploadComponent
| DiscordRadioGroupComponent
| DiscordCheckboxGroupComponent
| DiscordCheckboxComponent;
export type DiscordMessageComponentFromModalInteractionResponse =
| DiscordTextInputInteractionResponse
| DiscordTextDisplayInteractionResponse
| DiscordLabelInteractionResponse
| DiscordRadioGroupInteractionResponse
| DiscordCheckboxGroupInteractionResponse
| DiscordCheckboxInteractionResponse
| DiscordRoleSelectInteractionResponseFromModal
| DiscordUserSelectInteractionResponseFromModal
| DiscordStringSelectInteractionResponseFromModal
@@ -699,6 +711,10 @@ export interface DiscordLabelComponent extends DiscordBaseComponent {
/** The component within the label */
component:
| DiscordTextInputComponent
| DiscordFileUploadComponent
| DiscordRadioGroupComponent
| DiscordCheckboxGroupComponent
| DiscordCheckboxComponent
| DiscordStringSelectComponent
| DiscordUserSelectComponent
| DiscordRoleSelectComponent
@@ -720,7 +736,10 @@ export interface DiscordLabelInteractionResponse {
| DiscordRoleSelectInteractionResponseFromModal
| DiscordMentionableSelectInteractionResponseFromModal
| DiscordChannelSelectInteractionResponseFromModal
| DiscordFileUploadInteractionResponse;
| DiscordFileUploadInteractionResponse
| DiscordRadioGroupInteractionResponse
| DiscordCheckboxGroupInteractionResponse
| DiscordCheckboxInteractionResponse;
}
/** https://docs.discord.com/developers/components/reference#file-upload-file-upload-structure */
@@ -766,6 +785,98 @@ export interface DiscordFileUploadInteractionResponse {
values: string[];
}
/** https://docs.discord.com/developers/components/reference#radio-group-structure */
export interface DiscordRadioGroupComponent extends DiscordBaseComponent {
type: MessageComponentTypes.RadioGroup;
/** A dev-defined unique string for the component; 1-100 characters. */
custom_id: string;
/** List of options to show; min 2, max 10. */
options: DiscordRadioGroupOption[];
/** Whether a selection is required to submit the modal. Defaults to `true`. */
required?: boolean;
}
/** https://docs.discord.com/developers/components/reference#radio-group-option-structure */
export interface DiscordRadioGroupOption {
/** The dev-defined value of the option. Maximum 100 characters. */
value: string;
/** The user-facing label of the option. Maximum 100 characters. */
label: string;
/** An optional description for the option. Maximum 100 characters. */
description?: string;
/** Will render this option as already-selected by default. */
default?: boolean;
}
/** https://docs.discord.com/developers/components/reference#radio-group-interaction-response-structure */
export interface DiscordRadioGroupInteractionResponse {
type: MessageComponentTypes.RadioGroup;
/** 32 bit integer used as an optional identifier for component */
id: number;
/** The custom id for the radio group */
custom_id: string;
/** The value of the selected option, or null if no option is selected */
value?: string | null;
}
/** https://docs.discord.com/developers/components/reference#checkbox-group-structure */
export interface DiscordCheckboxGroupComponent extends DiscordBaseComponent {
type: MessageComponentTypes.CheckboxGroup;
/** A dev-defined unique string for the component; 1-100 characters. */
custom_id: string;
/** List of options to show; min 1, max 10. */
options: DiscordCheckboxGroupOption[];
/** Minimum number of items that must be chosen; min 0, max 10. Defaults to 1; if set to 0 required must be false. */
min_values?: number;
/** Maximum number of items that can be chosen; min 1, max 10. Defaults to the number of options. */
max_values?: number;
/** Whether selecting within the group is required. Defaults to `true`. */
required?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-group-option-structure */
export interface DiscordCheckboxGroupOption {
/** The dev-defined value of the option. Maximum 100 characters. */
value: string;
/** The user-facing label of the option. Maximum 100 characters. */
label: string;
/** An optional description for the option. Maximum 100 characters. */
description?: string;
/** Will render this option as already-selected by default. */
default?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-group-interaction-response-structure */
export interface DiscordCheckboxGroupInteractionResponse {
type: MessageComponentTypes.CheckboxGroup;
/** 32 bit integer used as an optional identifier for component */
id: number;
/** The custom id for the checkbox group */
custom_id: string;
/** The values of the selected options, or an empty array if no options are selected */
values: string[];
}
/** https://docs.discord.com/developers/components/reference#checkbox-structure */
export interface DiscordCheckboxComponent extends DiscordBaseComponent {
type: MessageComponentTypes.Checkbox;
/** A dev-defined unique string for the component; 1-100 characters. */
custom_id: string;
/** Whether the checkbox is selected by default. */
default?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-interaction-response-structure */
export interface DiscordCheckboxInteractionResponse {
type: MessageComponentTypes.Checkbox;
/** 32 bit integer used as an optional identifier for component */
id: number;
/** The custom id for the checkbox */
custom_id: string;
/** The state of the checkbox (true if checked, false if unchecked) */
value: boolean;
}
/** https://docs.discord.com/developers/components/reference#unfurled-media-item-structure */
export interface DiscordUnfurledMediaItem {
/** Supports arbitrary urls and attachment://<filename> references */

View File

@@ -29,7 +29,10 @@ export type MessageComponent =
| ContainerComponent
| FileComponent
| LabelComponent
| FileUploadComponent;
| FileUploadComponent
| RadioGroupComponent
| CheckboxGroupComponent
| CheckboxComponent;
/** https://docs.discord.com/developers/components/reference#anatomy-of-a-component */
export interface BaseComponent {
@@ -440,7 +443,10 @@ export interface LabelComponent extends BaseComponent {
| RoleSelectComponent
| MentionableSelectComponent
| ChannelSelectComponent
| FileUploadComponent;
| FileUploadComponent
| RadioGroupComponent
| CheckboxGroupComponent
| CheckboxComponent;
}
/** https://docs.discord.com/developers/components/reference#file-upload-file-upload-structure */
@@ -469,3 +475,62 @@ export interface FileUploadComponent extends BaseComponent {
*/
required?: boolean;
}
/** https://docs.discord.com/developers/components/reference#radio-group-structure */
export interface RadioGroupComponent extends BaseComponent {
type: MessageComponentTypes.RadioGroup;
/** A custom identifier for this component. Maximum 100 characters. */
customId: string;
/** List of options to show; min 2, max 10. */
options: RadioGroupOption[];
/** Whether a selection is required to submit the modal. Defaults to `true`. */
required?: boolean;
}
/** https://docs.discord.com/developers/components/reference#radio-group-option-structure */
export interface RadioGroupOption {
/** The dev-defined value of the option. Maximum 100 characters. */
value: string;
/** The user-facing label of the option. Maximum 100 characters. */
label: string;
/** An optional description for the option. Maximum 100 characters. */
description?: string;
/** Will render this option as already-selected by default. */
default?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-group-structure */
export interface CheckboxGroupComponent extends BaseComponent {
type: MessageComponentTypes.CheckboxGroup;
/** A custom identifier for this component. Maximum 100 characters. */
customId: string;
/** List of options to show; min 1, max 10. */
options: CheckboxGroupOption[];
/** Minimum number of items that must be chosen; min 0, max 10. Defaults to 1; if set to 0 required must be false. */
minValues?: number;
/** Maximum number of items that can be chosen; min 1, max 10. Defaults to the number of options. */
maxValues?: number;
/** Whether selecting within the group is required. Defaults to `true`. */
required?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-group-option-structure */
export interface CheckboxGroupOption {
/** The dev-defined value of the option. Maximum 100 characters. */
value: string;
/** The user-facing label of the option. Maximum 100 characters. */
label: string;
/** An optional description for the option. Maximum 100 characters. */
description?: string;
/** Will render this option as already-selected by default. */
default?: boolean;
}
/** https://docs.discord.com/developers/components/reference#checkbox-structure */
export interface CheckboxComponent extends BaseComponent {
type: MessageComponentTypes.Checkbox;
/** A custom identifier for this component. Maximum 100 characters. */
customId: string;
/** Whether the checkbox is selected by default. */
default?: boolean;
}