fix(bot)!: Split DiscordSelectComponent into specific select components (#4690)

* refactor(bot)!: setup desired properties for all transformers

SetupDesiredProps when is given an object that does not corrispond to a transformer object that supports desired properties will behave like TransformProperty on the entire object as when it tries to get the properties for said object it will find `never` as the props and for `IsKeyDesired` a props of `never` means that all props are desired.

* Use Equals helper, clean up a bit the code

* Explicit the IsKeyDesired TProps never behavior

* fix(bot): Bug with return type in functions

* fix(bot)!: Split DiscordSelectComponent into specific select components

And add missing resolved property from the responses, since this now correctly transformers the responses, resolved has to be added.
The reverse transformer does not transform resolved as it requires a bunch of others, such as a user, role, message reverse transformers etc.

* Apply suggestions from code review

Co-authored-by: Awesome Stickz <awesome@stickz.dev>
This commit is contained in:
Fleny
2026-02-16 23:37:57 +05:30
committed by GitHub
co-authored by Awesome Stickz
parent 8939cc8492
commit 3e35160504
6 changed files with 455 additions and 59 deletions
+1
View File
@@ -354,6 +354,7 @@ export function createDesiredPropertiesObject<T extends RecursivePartial<Transfo
size: defaultValue,
component: defaultValue,
values: defaultValue,
resolved: defaultValue,
...desiredProperties.component,
},
forumTag: {
+134 -12
View File
@@ -1,6 +1,8 @@
import {
type DiscordActionRow,
type DiscordButtonComponent,
type DiscordChannelSelectComponent,
type DiscordChannelSelectInteractionResponseFromModal,
type DiscordContainerComponent,
type DiscordFileComponent,
type DiscordFileUploadComponent,
@@ -9,18 +11,24 @@ import {
type DiscordLabelInteractionResponse,
type DiscordMediaGalleryComponent,
type DiscordMediaGalleryItem,
type DiscordMentionableSelectComponent,
type DiscordMentionableSelectInteractionResponseFromModal,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordRoleSelectComponent,
type DiscordRoleSelectInteractionResponseFromModal,
type DiscordSectionComponent,
type DiscordSelectMenuComponent,
type DiscordSeparatorComponent,
type DiscordStringSelectInteractionResponse,
type DiscordStringSelectComponent,
type DiscordStringSelectInteractionResponseFromModal,
type DiscordTextDisplayComponent,
type DiscordTextDisplayInteractionResponse,
type DiscordTextInputComponent,
type DiscordTextInputInteractionResponse,
type DiscordThumbnailComponent,
type DiscordUnfurledMediaItem,
type DiscordUserSelectComponent,
type DiscordUserSelectInteractionResponseFromModal,
MessageComponentTypes,
} from '@discordeno/types';
import type { Bot } from '../bot.js';
@@ -45,11 +53,19 @@ export function transformComponent(bot: Bot, payload: DiscordMessageComponent |
component = transformInputTextComponent(bot, payload);
break;
case MessageComponentTypes.StringSelect:
component = transformStringSelectMenuComponent(bot, payload);
break;
case MessageComponentTypes.UserSelect:
component = transformUserSelectMenuComponent(bot, payload);
break;
case MessageComponentTypes.RoleSelect:
component = transformRoleSelectMenuComponent(bot, payload);
break;
case MessageComponentTypes.MentionableSelect:
component = transformMentionableSelectMenuComponent(bot, payload);
break;
case MessageComponentTypes.ChannelSelect:
component = transformSelectMenuComponent(bot, payload);
component = transformChannelSelectMenuComponent(bot, payload);
break;
case MessageComponentTypes.Section:
component = transformSectionComponent(bot, payload);
@@ -170,7 +186,7 @@ function transformInputTextComponent(bot: Bot, payload: DiscordTextInputComponen
return input;
}
function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuComponent | DiscordStringSelectInteractionResponse) {
function transformStringSelectMenuComponent(bot: Bot, payload: DiscordStringSelectComponent | DiscordStringSelectInteractionResponseFromModal) {
const props = bot.transformers.desiredProperties.component;
const select = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
@@ -185,12 +201,6 @@ function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuCompon
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder;
if (props.minValues && payload.min_values) select.minValues = payload.min_values;
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values;
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}));
if (props.channelTypes && payload.channel_types) select.channelTypes = payload.channel_types;
if (props.options && payload.options)
select.options = payload.options.map((option) => ({
label: option.label,
@@ -211,6 +221,118 @@ function transformSelectMenuComponent(bot: Bot, payload: DiscordSelectMenuCompon
return select;
}
function transformUserSelectMenuComponent(bot: Bot, payload: DiscordUserSelectComponent | DiscordUserSelectInteractionResponseFromModal) {
const props = bot.transformers.desiredProperties.component;
const select = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) select.type = payload.type;
if (props.id && payload.id) select.id = payload.id;
if (props.customId && payload.custom_id) select.customId = payload.custom_id;
// Check if this is the user select response
if ('values' in payload) {
if (props.values && payload.values) select.values = payload.values;
if (props.resolved && payload.resolved) select.resolved = bot.transformers.interactionDataResolved(bot, payload.resolved);
} else {
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder;
if (props.minValues && payload.min_values) select.minValues = payload.min_values;
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values;
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}));
if (props.disabled && payload.disabled) select.disabled = payload.disabled;
}
return select;
}
function transformRoleSelectMenuComponent(bot: Bot, payload: DiscordRoleSelectComponent | DiscordRoleSelectInteractionResponseFromModal) {
const props = bot.transformers.desiredProperties.component;
const select = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) select.type = payload.type;
if (props.id && payload.id) select.id = payload.id;
if (props.customId && payload.custom_id) select.customId = payload.custom_id;
// Check if this is the role select response
if ('values' in payload) {
if (props.values && payload.values) select.values = payload.values;
if (props.resolved && payload.resolved) select.resolved = bot.transformers.interactionDataResolved(bot, payload.resolved);
} else {
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder;
if (props.minValues && payload.min_values) select.minValues = payload.min_values;
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values;
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}));
if (props.disabled && payload.disabled) select.disabled = payload.disabled;
}
return select;
}
function transformMentionableSelectMenuComponent(
bot: Bot,
payload: DiscordMentionableSelectComponent | DiscordMentionableSelectInteractionResponseFromModal,
) {
const props = bot.transformers.desiredProperties.component;
const select = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) select.type = payload.type;
if (props.id && payload.id) select.id = payload.id;
if (props.customId && payload.custom_id) select.customId = payload.custom_id;
// Check if this is the mentionable select response
if ('values' in payload) {
if (props.values && payload.values) select.values = payload.values;
if (props.resolved && payload.resolved) select.resolved = bot.transformers.interactionDataResolved(bot, payload.resolved);
} else {
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder;
if (props.minValues && payload.min_values) select.minValues = payload.min_values;
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values;
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}));
if (props.disabled && payload.disabled) select.disabled = payload.disabled;
}
return select;
}
function transformChannelSelectMenuComponent(bot: Bot, payload: DiscordChannelSelectComponent | DiscordChannelSelectInteractionResponseFromModal) {
const props = bot.transformers.desiredProperties.component;
const select = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) select.type = payload.type;
if (props.id && payload.id) select.id = payload.id;
if (props.customId && payload.custom_id) select.customId = payload.custom_id;
// Check if this is the channel select response
if ('values' in payload) {
if (props.values && payload.values) select.values = payload.values;
if (props.resolved && payload.resolved) select.resolved = bot.transformers.interactionDataResolved(bot, payload.resolved);
} else {
if (props.placeholder && payload.placeholder) select.placeholder = payload.placeholder;
if (props.minValues && payload.min_values) select.minValues = payload.min_values;
if (props.maxValues && payload.max_values) select.maxValues = payload.max_values;
if (props.defaultValues && payload.default_values)
select.defaultValues = payload.default_values.map((defaultValue) => ({
id: bot.transformers.snowflake(defaultValue.id),
type: defaultValue.type,
}));
if (props.channelTypes && payload.channel_types) select.channelTypes = payload.channel_types;
if (props.disabled && payload.disabled) select.disabled = payload.disabled;
}
return select;
}
function transformSectionComponent(bot: Bot, payload: DiscordSectionComponent) {
const props = bot.transformers.desiredProperties.component;
const section = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
@@ -303,9 +425,9 @@ function transformLabelComponent(bot: Bot, payload: DiscordLabelComponent | Disc
return label;
}
function transformFileUploadComponent(bot: Bot, payload: DiscordFileUploadComponent | DiscordFileUploadInteractionResponse): Component {
function transformFileUploadComponent(bot: Bot, payload: DiscordFileUploadComponent | DiscordFileUploadInteractionResponse) {
const props = bot.transformers.desiredProperties.component;
const fileUpload = {} as Component;
const fileUpload = {} as SetupDesiredProps<Component, TransformersDesiredProperties, DesiredPropertiesBehavior>;
if (props.type && payload.type) fileUpload.type = payload.type;
if (props.id && payload.id) fileUpload.id = payload.id;
@@ -2,22 +2,30 @@ import {
type ButtonStyles,
type DiscordActionRow,
type DiscordButtonComponent,
type DiscordChannelSelectComponent,
type DiscordChannelSelectInteractionResponseFromModal,
type DiscordContainerComponent,
type DiscordFileComponent,
type DiscordFileUploadComponent,
type DiscordLabelComponent,
type DiscordMediaGalleryComponent,
type DiscordMediaGalleryItem,
type DiscordMentionableSelectComponent,
type DiscordMentionableSelectInteractionResponseFromModal,
type DiscordMessageComponent,
type DiscordMessageComponentFromModalInteractionResponse,
type DiscordRoleSelectComponent,
type DiscordRoleSelectInteractionResponseFromModal,
type DiscordSectionComponent,
type DiscordSelectMenuComponent,
type DiscordStringSelectComponent,
type DiscordStringSelectInteractionResponseFromModal,
type DiscordTextDisplayComponent,
type DiscordTextInputComponent,
type DiscordTextInputInteractionResponse,
type DiscordThumbnailComponent,
type DiscordUnfurledMediaItem,
type DiscordUserSelectComponent,
type DiscordUserSelectInteractionResponseFromModal,
MessageComponentTypes,
type TextStyles,
} from '@discordeno/types';
@@ -39,11 +47,15 @@ export function transformComponentToDiscordComponent(
case MessageComponentTypes.TextInput:
return transformInputTextComponent(bot, payload);
case MessageComponentTypes.StringSelect:
case MessageComponentTypes.ChannelSelect:
case MessageComponentTypes.RoleSelect:
return transformStringSelectMenuComponent(bot, payload);
case MessageComponentTypes.UserSelect:
return transformUserSelectMenuComponent(bot, payload);
case MessageComponentTypes.RoleSelect:
return transformRoleSelectMenuComponent(bot, payload);
case MessageComponentTypes.MentionableSelect:
return transformSelectMenuComponent(bot, payload);
return transformMentionableSelectMenuComponent(bot, payload);
case MessageComponentTypes.ChannelSelect:
return transformChannelSelectMenuComponent(bot, payload);
case MessageComponentTypes.Section:
return transformSectionComponent(bot, payload);
case MessageComponentTypes.File:
@@ -139,25 +151,23 @@ function transformInputTextComponent(_bot: Bot, payload: Component): DiscordText
};
}
function transformSelectMenuComponent(bot: Bot, payload: Component): DiscordSelectMenuComponent | DiscordStringSelectInteractionResponseFromModal {
function transformStringSelectMenuComponent(
bot: Bot,
payload: Component,
): DiscordStringSelectComponent | DiscordStringSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.StringSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
};
} satisfies DiscordStringSelectInteractionResponseFromModal;
}
return {
type: payload.type as DiscordSelectMenuComponent['type'],
type: MessageComponentTypes.StringSelect,
id: payload.id,
custom_id: payload.customId!,
channel_types: payload.channelTypes,
default_values: payload.defaultValues?.map((defaultValue) => ({
id: bot.transformers.reverse.snowflake(defaultValue.id),
type: defaultValue.type,
})),
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
@@ -173,10 +183,117 @@ function transformSelectMenuComponent(bot: Bot, payload: Component): DiscordSele
}
: undefined,
default: option.default,
})),
}))!,
placeholder: payload.placeholder,
required: payload.required,
};
} satisfies DiscordStringSelectComponent;
}
function transformUserSelectMenuComponent(bot: Bot, payload: Component): DiscordUserSelectComponent | DiscordUserSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.UserSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
// TODO: Add resolved reverse transformer
resolved: {},
} satisfies DiscordUserSelectInteractionResponseFromModal;
}
return {
type: MessageComponentTypes.UserSelect,
id: payload.id,
custom_id: payload.customId!,
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
placeholder: payload.placeholder,
default_values: payload.defaultValues?.map((value) => ({ id: bot.transformers.reverse.snowflake(value.id), type: value.type })),
required: payload.required,
} satisfies DiscordUserSelectComponent;
}
function transformRoleSelectMenuComponent(bot: Bot, payload: Component): DiscordRoleSelectComponent | DiscordRoleSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.RoleSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
// TODO: Add resolved reverse transformer
resolved: {},
} satisfies DiscordRoleSelectInteractionResponseFromModal;
}
return {
type: MessageComponentTypes.RoleSelect,
id: payload.id,
custom_id: payload.customId!,
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
placeholder: payload.placeholder,
default_values: payload.defaultValues?.map((value) => ({ id: bot.transformers.reverse.snowflake(value.id), type: value.type })),
required: payload.required,
} satisfies DiscordRoleSelectComponent;
}
function transformMentionableSelectMenuComponent(
bot: Bot,
payload: Component,
): DiscordMentionableSelectComponent | DiscordMentionableSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.MentionableSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
// TODO: Add resolved reverse transformer
resolved: {},
} satisfies DiscordMentionableSelectInteractionResponseFromModal;
}
return {
type: MessageComponentTypes.MentionableSelect,
id: payload.id,
custom_id: payload.customId!,
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
placeholder: payload.placeholder,
default_values: payload.defaultValues?.map((value) => ({ id: bot.transformers.reverse.snowflake(value.id), type: value.type })),
required: payload.required,
} satisfies DiscordMentionableSelectComponent;
}
function transformChannelSelectMenuComponent(
bot: Bot,
payload: Component,
): DiscordChannelSelectComponent | DiscordChannelSelectInteractionResponseFromModal {
if (payload.values) {
return {
type: MessageComponentTypes.ChannelSelect,
values: payload.values,
custom_id: payload.customId!,
id: payload.id!,
// TODO: Add resolved reverse transformer
resolved: {},
} satisfies DiscordChannelSelectInteractionResponseFromModal;
}
return {
type: MessageComponentTypes.ChannelSelect,
id: payload.id,
custom_id: payload.customId!,
disabled: payload.disabled,
max_values: payload.maxValues,
min_values: payload.minValues,
default_values: payload.defaultValues?.map((value) => ({ id: bot.transformers.reverse.snowflake(value.id), type: value.type })),
placeholder: payload.placeholder,
required: payload.required,
channel_types: payload.channelTypes,
} satisfies DiscordChannelSelectComponent;
}
function transformSectionComponent(bot: Bot, payload: Component): DiscordSectionComponent {
+4 -2
View File
@@ -599,7 +599,7 @@ export interface Component {
/** a list of child components */
components?: Component[];
/** List of default values for auto-populated select menu components; number of default values must be in the range defined by min_values and max_values */
defaultValues?: DiscordComponentDefaultValue[];
defaultValues?: ComponentDefaultValue[];
/** Identifier for a purchasable SKU, only available when using premium-style buttons */
skuId?: bigint;
/** Optional identifier for component */
@@ -632,6 +632,8 @@ export interface Component {
component?: Component;
/** The text of the selected options */
values?: string[];
/** Resolved entities from selected options */
resolved?: InteractionDataResolved;
}
export interface UnfurledMediaItem {
@@ -658,7 +660,7 @@ export interface MediaGalleryItem {
spoiler?: boolean;
}
export interface DiscordComponentDefaultValue {
export interface ComponentDefaultValue {
/** ID of a user, role, or channel */
id: bigint;
/** Type of value that id represents. */