feat: text display and more selects in modal for v14 (#11096)

* feat: handle recieve label components

* chore: missed fixes

* fix: missing id when transforming

* chore: add missing things

* fix: test

* feat: send label

* fix: un-break it

* chore: test

* feat: more selects in modals

* chore: make resolved read-only

* chore: import order

* chore: add missing cached generic

* style: spacing

* docs: consistency

* docs: make it a type

* docs: Add `APISelectMenuDefaultValue`

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
Naiyar
2025-09-22 02:58:33 +05:30
committed by GitHub
parent abaae4ff16
commit 93e0f4cd10
10 changed files with 402 additions and 56 deletions

View File

@@ -168,6 +168,8 @@
* @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound * @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound
* @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType * @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType
* @property {'ModalSubmitInteractionFieldEmpty'} ModalSubmitInteractionFieldEmpty
* @property {'ModalSubmitInteractionFieldInvalidChannelType'} ModalSubmitInteractionFieldInvalidChannelType
* @property {'InvalidMissingScopes'} InvalidMissingScopes * @property {'InvalidMissingScopes'} InvalidMissingScopes
* @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions * @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions
@@ -327,6 +329,8 @@ const keys = [
'ModalSubmitInteractionFieldNotFound', 'ModalSubmitInteractionFieldNotFound',
'ModalSubmitInteractionFieldType', 'ModalSubmitInteractionFieldType',
'ModalSubmitInteractionFieldEmpty',
'ModalSubmitInteractionFieldInvalidChannelType',
'InvalidMissingScopes', 'InvalidMissingScopes',
'InvalidScopesWithPermissions', 'InvalidScopesWithPermissions',

View File

@@ -161,6 +161,10 @@ const Messages = {
`Required field with custom id "${customId}" not found.`, `Required field with custom id "${customId}" not found.`,
[DjsErrorCodes.ModalSubmitInteractionFieldType]: (customId, type, expected) => [DjsErrorCodes.ModalSubmitInteractionFieldType]: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`, `Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
[DjsErrorCodes.ModalSubmitInteractionFieldEmpty]: (customId, type) =>
`Required field with custom id "${customId}" is of type: ${type}; expected a non-empty value.`,
[DjsErrorCodes.ModalSubmitInteractionFieldInvalidChannelType]: (customId, type, expected) =>
`The type of channel of the field with custom id "${customId}" is: ${type}; expected ${expected}.`,
[DjsErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite', [DjsErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite',
[DjsErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.', [DjsErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.',

View File

@@ -80,12 +80,17 @@ class CommandInteraction extends BaseInteraction {
} }
/** /**
* Represents the resolved data of a received command interaction. * @typedef {Object} BaseInteractionResolvedData
* @typedef {Object} CommandInteractionResolvedData
* @property {Collection<Snowflake, User>} [users] The resolved users * @property {Collection<Snowflake, User>} [users] The resolved users
* @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved guild members * @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved guild members
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles * @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
* @property {Collection<Snowflake, BaseChannel|APIChannel>} [channels] The resolved channels * @property {Collection<Snowflake, BaseChannel|APIChannel>} [channels] The resolved channels
*/
/**
* Represents the resolved data of a received command interaction.
*
* @typedef {BaseInteractionResolvedData} CommandInteractionResolvedData
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages * @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
* @property {Collection<Snowflake, Attachment>} [attachments] The resolved attachments * @property {Collection<Snowflake, Attachment>} [attachments] The resolved attachments
*/ */

View File

@@ -623,7 +623,7 @@ class Message extends Base {
* Similar to createReactionCollector but in promise form. * Similar to createReactionCollector but in promise form.
* Resolves with a collection of reactions that pass the specified filter. * Resolves with a collection of reactions that pass the specified filter.
* @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
* @returns {Promise<Collection<string | Snowflake, MessageReaction>>} * @returns {Promise<Collection<string|Snowflake, MessageReaction>>}
* @example * @example
* // Create a reaction collector * // Create a reaction collector
* const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId' * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'

View File

@@ -4,29 +4,44 @@ const { Collection } = require('@discordjs/collection');
const { ComponentType } = require('discord-api-types/v10'); const { ComponentType } = require('discord-api-types/v10');
const { DiscordjsTypeError, ErrorCodes } = require('../errors'); const { DiscordjsTypeError, ErrorCodes } = require('../errors');
/**
* @typedef {Object} ModalSelectedMentionables
* @property {Collection<Snowflake, User>} users The selected users
* @property {Collection<Snowflake, GuildMember | APIGuildMember>} members The selected members
* @property {Collection<Snowflake, Role | APIRole>} roles The selected roles
*/
/** /**
* Represents the serialized fields from a modal submit interaction * Represents the serialized fields from a modal submit interaction
*/ */
class ModalSubmitFields { class ModalSubmitFields {
constructor(components) { constructor(components, resolved) {
/** /**
* The components within the modal * The components within the modal
* *
* @type {Array<ActionRowModalData | LabelModalData>} * @type {Array<ActionRowModalData|LabelModalData|TextDisplayModalData>}
*/ */
this.components = components; this.components = components;
/**
* The interaction resolved data
*
* @name ModalSubmitFields#resolved
* @type {?Readonly<BaseInteractionResolvedData>}
*/
Object.defineProperty(this, 'resolved', { value: resolved ? Object.freeze(resolved) : null });
/** /**
* The extracted fields from the modal * The extracted fields from the modal
* @type {Collection<string, ModalData>} * @type {Collection<string, ModalData>}
*/ */
this.fields = components.reduce((accumulator, next) => { this.fields = components.reduce((accumulator, next) => {
// NOTE: for legacy support of action rows in modals, which has `components` // For legacy support of action rows
if ('components' in next) { if ('components' in next) {
for (const component of next.components) accumulator.set(component.customId, component); for (const component of next.components) accumulator.set(component.customId, component);
} }
// For label component // For label components
if ('component' in next) { if ('component' in next) {
accumulator.set(next.component.customId, next.component); accumulator.set(next.component.customId, next.component);
} }
@@ -52,13 +67,39 @@ class ModalSubmitFields {
return field; return field;
} }
/**
* Gets a component by custom id and property and checks its type.
*
* @param {string} customId The custom id of the component.
* @param {ComponentType[]} allowedTypes The allowed types of the component.
* @param {string[]} properties The properties to check for for `required`.
* @param {boolean} required Whether to throw an error if the component value(s) are not found.
* @returns {ModalData} The option, if found.
* @private
*/
_getTypedComponent(customId, allowedTypes, properties, required) {
const component = this.getField(customId);
if (!allowedTypes.includes(component.type)) {
throw new DiscordjsTypeError(
ErrorCodes.ModalSubmitInteractionFieldNotFound,
customId,
component.type,
allowedTypes.join(', '),
);
} else if (required && properties.every(prop => component[prop] === null || component[prop] === undefined)) {
throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldEmpty, customId, component.type);
}
return component;
}
/** /**
* Gets the value of a text input component given a custom id * Gets the value of a text input component given a custom id
* @param {string} customId The custom id of the text input component * @param {string} customId The custom id of the text input component
* @returns {string} * @returns {string}
*/ */
getTextInputValue(customId) { getTextInputValue(customId) {
return this.getField(customId, ComponentType.TextInput).value; return this._getTypedComponent(customId, [ComponentType.TextInput]).value;
} }
/** /**
@@ -68,7 +109,112 @@ class ModalSubmitFields {
* @returns {string[]} * @returns {string[]}
*/ */
getStringSelectValues(customId) { getStringSelectValues(customId) {
return this.getField(customId, ComponentType.StringSelect).values; return this._getTypedComponent(customId, [ComponentType.StringSelect]).values;
}
/**
* Gets users component
*
* @param {string} customId The custom id of the component
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
* @returns {?Collection<Snowflake, User>} The selected users, or null if none were selected and not required
*/
getSelectedUsers(customId, required = false) {
const component = this._getTypedComponent(
customId,
[ComponentType.UserSelect, ComponentType.MentionableSelect],
['users'],
required,
);
return component.users ?? null;
}
/**
* Gets roles component
*
* @param {string} customId The custom id of the component
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
* @returns {?Collection<Snowflake, Role|APIRole>} The selected roles, or null if none were selected and not required
*/
getSelectedRoles(customId, required = false) {
const component = this._getTypedComponent(
customId,
[ComponentType.RoleSelect, ComponentType.MentionableSelect],
['roles'],
required,
);
return component.roles ?? null;
}
/**
* Gets channels component
*
* @param {string} customId The custom id of the component
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
* @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed.
* @returns {?Collection<Snowflake, GuildChannel|ThreadChannel|APIChannel>} The selected channels,
* or null if none were selected and not required
*/
getSelectedChannels(customId, required = false, channelTypes = []) {
const component = this._getTypedComponent(customId, [ComponentType.ChannelSelect], ['channels'], required);
const channels = component.channels;
if (channels && channelTypes.length > 0) {
for (const channel of channels.values()) {
if (!channelTypes.includes(channel.type)) {
throw new DiscordjsTypeError(
ErrorCodes.ModalSubmitInteractionComponentInvalidChannelType,
customId,
channel.type,
channelTypes.join(', '),
);
}
}
}
return channels ?? null;
}
/**
* Gets members component
*
* @param {string} customId The custom id of the component
* @returns {?Collection<Snowflake, GuildMember|APIGuildMember>} The selected members,
* or null if none were selected or the users were not present in the guild
*/
getSelectedMembers(customId) {
const component = this._getTypedComponent(
customId,
[ComponentType.UserSelect, ComponentType.MentionableSelect],
['members'],
false,
);
return component.members ?? null;
}
/**
* Gets mentionables component
*
* @param {string} customId The custom id of the component
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
* @returns {?ModalSelectedMentionables} The selected mentionables, or null if none were selected and not required
*/
getSelectedMentionables(customId, required = false) {
const component = this._getTypedComponent(
customId,
[ComponentType.MentionableSelect],
['users', 'members', 'roles'],
required,
);
if (component.users || component.members || component.roles) {
return {
users: component.users ?? new Collection(),
members: component.members ?? new Collection(),
roles: component.roles ?? new Collection(),
};
}
return null;
} }
} }

View File

@@ -1,46 +1,53 @@
'use strict'; 'use strict';
const { Collection } = require('@discordjs/collection');
const { lazy } = require('@discordjs/util'); const { lazy } = require('@discordjs/util');
const BaseInteraction = require('./BaseInteraction'); const BaseInteraction = require('./BaseInteraction');
const InteractionWebhook = require('./InteractionWebhook'); const InteractionWebhook = require('./InteractionWebhook');
const ModalSubmitFields = require('./ModalSubmitFields'); const ModalSubmitFields = require('./ModalSubmitFields');
const InteractionResponses = require('./interfaces/InteractionResponses'); const InteractionResponses = require('./interfaces/InteractionResponses');
const { transformResolved } = require('../util/Util');
const getMessage = lazy(() => require('./Message').Message); const getMessage = lazy(() => require('./Message').Message);
/** /**
* @typedef {Object} BaseModalData * @typedef {Object} BaseModalData
* @property {ComponentType} type The component type of the field * @property {ComponentType} type The component type of the field
* @property {string} customId The custom id of the field
* @property {number} id The id of the field * @property {number} id The id of the field
*/ */
/** /**
* @typedef {BaseModalData} TextInputModalData * @typedef {BaseModalData} TextInputModalData
* @property {string} customId The custom id of the field
* @property {string} value The value of the field * @property {string} value The value of the field
*/ */
/** /**
* @typedef {BaseModalData} StringSelectModalData * @typedef {BaseModalData} SelectMenuModalData
* @property {string} customId The custom id of the field
* @property {string[]} values The values of the field * @property {string[]} values The values of the field
* @property {Collection<string, GuildMember|APIGuildMember>} [members] The resolved members
* @property {Collection<string, User|APIUser>} [users] The resolved users
* @property {Collection<string, Role|APIRole>} [roles] The resolved roles
* @property {Collection<string, BaseChannel|APIChannel>} [channels] The resolved channels
*/ */
/** /**
* @typedef {TextInputModalData | StringSelectModalData} ModalData * @typedef {BaseModalData} TextDisplayModalData
*/ */
/** /**
* @typedef {Object} LabelModalData * @typedef {SelectMenuModalData|TextInputModalData} ModalData
*/
/**
* @typedef {BaseModalData} LabelModalData
* @property {ModalData} component The component within the label * @property {ModalData} component The component within the label
* @property {ComponentType} type The component type of the label
* @property {number} id The id of the label
*/ */
/** /**
* @typedef {Object} ActionRowModalData * @typedef {BaseModalData} ActionRowModalData
* @property {TextInputModalData[]} components The components of this action row * @property {TextInputModalData[]} components The components of this action row
* @property {ComponentType} type The component type of the action row
* @property {number} id The id of the action row
*/ */
/** /**
@@ -70,15 +77,20 @@ class ModalSubmitInteraction extends BaseInteraction {
/** /**
* The components within the modal * The components within the modal
* *
* @type {Array<ActionRowModalData | LabelModalData>} * @type {Array<ActionRowModalData | LabelModalData | TextDisplayModalData>}
*/ */
this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component)); this.components = data.data.components?.map(component =>
ModalSubmitInteraction.transformComponent(component, data.data.resolved),
);
/** /**
* The fields within the modal * The fields within the modal
* @type {ModalSubmitFields} * @type {ModalSubmitFields}
*/ */
this.fields = new ModalSubmitFields(this.components); this.fields = new ModalSubmitFields(
this.components,
transformResolved({ client: this.client, guild: this.guild, channel: this.channel }, data.data.resolved),
);
/** /**
* Whether the reply to this interaction has been deferred * Whether the reply to this interaction has been deferred
@@ -108,14 +120,15 @@ class ModalSubmitInteraction extends BaseInteraction {
/** /**
* Transforms component data to discord.js-compatible data * Transforms component data to discord.js-compatible data
* @param {*} rawComponent The data to transform * @param {*} rawComponent The data to transform
* @param {APIInteractionDataResolved} [resolved] The resolved data for the interaction
* @returns {ModalData[]} * @returns {ModalData[]}
*/ */
static transformComponent(rawComponent) { static transformComponent(rawComponent, resolved) {
if ('components' in rawComponent) { if ('components' in rawComponent) {
return { return {
type: rawComponent.type, type: rawComponent.type,
id: rawComponent.id, id: rawComponent.id,
components: rawComponent.components.map(component => this.transformComponent(component)), components: rawComponent.components.map(component => this.transformComponent(component, resolved)),
}; };
} }
@@ -123,18 +136,50 @@ class ModalSubmitInteraction extends BaseInteraction {
return { return {
type: rawComponent.type, type: rawComponent.type,
id: rawComponent.id, id: rawComponent.id,
component: this.transformComponent(rawComponent.component), component: this.transformComponent(rawComponent.component, resolved),
}; };
} }
const data = { const data = {
type: rawComponent.type, type: rawComponent.type,
customId: rawComponent.custom_id,
id: rawComponent.id, id: rawComponent.id,
}; };
// Text display components do not have custom ids.
if (rawComponent.custom_id) data.customId = rawComponent.custom_id;
if (rawComponent.value) data.value = rawComponent.value; if (rawComponent.value) data.value = rawComponent.value;
if (rawComponent.values) data.values = rawComponent.values;
if (rawComponent.values) {
data.values = rawComponent.values;
if (resolved) {
const resolveCollection = (resolvedData, resolver) => {
const collection = new Collection();
for (const value of data.values) {
if (resolvedData?.[value]) {
collection.set(value, resolver(resolvedData[value]));
}
}
return collection.size ? collection : null;
};
const users = resolveCollection(resolved.users, user => this.client.users._add(user));
if (users) data.users = users;
const channels = resolveCollection(
resolved.channels,
channel => this.client.channels._add(channel, this.guild) ?? channel,
);
if (channels) data.channels = channels;
const members = resolveCollection(resolved.members, member => this.guild?.members._add(member) ?? member);
if (members) data.members = members;
const roles = resolveCollection(resolved.roles, role => this.guild?.roles._add(role) ?? role);
if (roles) data.roles = roles;
}
}
return data; return data;
} }

View File

@@ -245,6 +245,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption} * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption}
*/ */
/**
* @external APISelectMenuDefaultValue
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuDefaultValue}
*/
/** /**
* @external APISticker * @external APISticker
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISticker} * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISticker}

View File

@@ -18,14 +18,19 @@ const { ComponentType } = require('discord-api-types/v10');
* @typedef {Object} ModalComponentData * @typedef {Object} ModalComponentData
* @property {string} title The title of the modal * @property {string} title The title of the modal
* @property {string} customId The custom id of the modal * @property {string} customId The custom id of the modal
* @property {Array<ActionRow | LabelData>} components The components within this modal * @property {Array<ActionRow|TextDisplayComponentData|LabelData>} components The components within this modal
*/
/**
* @typedef {StringSelectMenuComponentData|TextInputComponentData|UserSelectMenuComponentData|
* RoleSelectMenuComponentData|MentionableSelectMenuComponentData|ChannelSelectMenuComponentData} ComponentInLabelData
*/ */
/** /**
* @typedef {BaseComponentData} LabelData * @typedef {BaseComponentData} LabelData
* @property {string} label The label to use * @property {string} label The label to use
* @property {string} [description] The optional description for the label * @property {string} [description] The optional description for the label
* @property {StringSelectMenuComponentData|TextInputComponentData} component The component within the label * @property {ComponentInLabelData} component The component within the label
*/ */
/** /**
@@ -39,16 +44,41 @@ const { ComponentType } = require('discord-api-types/v10');
*/ */
/** /**
* @typedef {BaseComponentData} StringSelectMenuComponentData * @typedef {BaseComponentData} BaseSelectMenuComponentData
* @property {string} customId The custom id of the select menu * @property {string} customId The custom id of the select menu
* @property {boolean} [disabled] Whether the select menu is disabled or not * @property {boolean} [disabled] Whether the select menu is disabled or not
* @property {number} [maxValues] The maximum amount of options that can be selected * @property {number} [maxValues] The maximum amount of options that can be selected
* @property {number} [minValues] The minimum amount of options that can be selected * @property {number} [minValues] The minimum amount of options that can be selected
* @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
* @property {string} [placeholder] The placeholder of the select menu * @property {string} [placeholder] The placeholder of the select menu
* @property {boolean} [required] Whether this component is required in modals * @property {boolean} [required] Whether this component is required in modals
*/ */
/**
* @typedef {BaseSelectMenuComponentData} StringSelectMenuComponentData
* @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
*/
/**
* @typedef {BaseSelectMenuComponentData} UserSelectMenuComponentData
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
*/
/**
* @typedef {BaseSelectMenuComponentData} RoleSelectMenuComponentData
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
*/
/**
* @typedef {BaseSelectMenuComponentData} MentionableSelectMenuComponentData
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
*/
/**
* @typedef {BaseSelectMenuComponentData} ChannelSelectMenuComponentData
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
* @property {ChannelType[]} [channelTypes] The types of channels that can be selected
*/
/** /**
* @typedef {object} SelectMenuComponentOptionData * @typedef {object} SelectMenuComponentOptionData
* @property {string} label The label of the option * @property {string} label The label of the option

View File

@@ -353,7 +353,13 @@ export class ActionRowBuilder<
): ActionRowBuilder<ComponentType>; ): ActionRowBuilder<ComponentType>;
} }
export type ComponentInLabelData = StringSelectMenuComponentData | TextInputComponentData; export type ComponentInLabelData =
| StringSelectMenuComponentData
| TextInputComponentData
| UserSelectMenuComponentData
| ChannelSelectMenuComponentData
| RoleSelectMenuComponentData
| MentionableSelectMenuComponentData;
export interface LabelData extends BaseComponentData { export interface LabelData extends BaseComponentData {
component: ComponentInLabelData; component: ComponentInLabelData;
@@ -2775,46 +2781,116 @@ export interface ModalComponentData {
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow> | APILabelComponent> | JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow> | APILabelComponent>
| ActionRowData<ModalActionRowComponentData> | ActionRowData<ModalActionRowComponentData>
| LabelData | LabelData
| TextDisplayComponentData
)[]; )[];
} }
export interface BaseModalData<Type extends ComponentType> { export interface BaseModalData<Type extends ComponentType> {
customId: string;
id: number; id: number;
type: Type; type: Type;
} }
export interface TextInputModalData extends BaseModalData<ComponentType.TextInput> { export interface TextInputModalData extends BaseModalData<ComponentType.TextInput> {
customId: string;
value: string; value: string;
} }
export interface StringSelectModalData extends BaseModalData<ComponentType.StringSelect> { export interface SelectMenuModalData<Cached extends CacheType = CacheType>
extends BaseModalData<
| ComponentType.ChannelSelect
| ComponentType.MentionableSelect
| ComponentType.RoleSelect
| ComponentType.StringSelect
| ComponentType.UserSelect
> {
channels?: ReadonlyCollection<
Snowflake,
CacheTypeReducer<Cached, GuildBasedChannel, APIInteractionDataResolvedChannel>
>;
customId: string;
members?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIInteractionDataResolvedGuildMember>>;
roles?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
users?: ReadonlyCollection<Snowflake, User>;
values: readonly string[]; values: readonly string[];
} }
export type ModalData = StringSelectModalData | TextInputModalData; export type ModalData = SelectMenuModalData | TextInputModalData;
export interface LabelModalData { export interface LabelModalData extends BaseModalData<ComponentType.Label> {
component: readonly ModalData[]; component: readonly ModalData[];
id: number;
type: ComponentType.Label;
} }
export interface ActionRowModalData { export interface ActionRowModalData extends BaseModalData<ComponentType.ActionRow> {
type: ComponentType.ActionRow;
components: readonly TextInputModalData[]; components: readonly TextInputModalData[];
} }
export class ModalSubmitFields { export interface TextDisplayModalData extends BaseModalData<ComponentType.TextDisplay> {}
private constructor(components: readonly (ActionRowModalData | LabelModalData)[]);
public components: (ActionRowModalData | LabelModalData)[]; export interface ModalSelectedMentionables<Cached extends CacheType = CacheType> {
public fields: Collection<string, StringSelectModalData | TextInputModalData>; members: NonNullable<SelectMenuModalData<Cached>['members']>;
public getField<Type extends ComponentType>( roles: NonNullable<SelectMenuModalData<Cached>['roles']>;
users: NonNullable<SelectMenuModalData<Cached>['users']>;
}
export class ModalSubmitFields<Cached extends CacheType = CacheType> {
private constructor(
components: readonly (ActionRowModalData | LabelModalData | TextDisplayModalData)[],
resolved?: BaseInteractionResolvedData,
);
public components: (ActionRowModalData | LabelModalData | TextDisplayModalData)[];
public resolved: Readonly<BaseInteractionResolvedData<Cached>> | null;
public fields: Collection<string, ModalData>;
public getField<Type extends ComponentType>(customId: string, type: Type): Extract<ModalData, { type: Type }>;
public getField(customId: string, type?: ComponentType): ModalData;
private _getTypedComponent(
customId: string, customId: string,
type: Type, allowedTypes: readonly ComponentType[],
): { type: Type } & (StringSelectModalData | TextInputModalData); properties: string,
public getField(customId: string, type?: ComponentType): StringSelectModalData | TextInputModalData; required: boolean,
): ModalData;
public getTextInputValue(customId: string): string; public getTextInputValue(customId: string): string;
public getStringSelectValues(customId: string): readonly string[]; public getStringSelectValues(customId: string): readonly string[];
public getSelectedUsers(customId: string, required: true): ReadonlyCollection<Snowflake, User>;
public getSelectedUsers(customId: string, required?: boolean): ReadonlyCollection<Snowflake, User> | null;
public getSelectedMembers(customId: string): NonNullable<SelectMenuModalData<Cached>['members']> | null;
public getSelectedChannels<const Type extends ChannelType = ChannelType>(
customId: string,
required: true,
channelTypes?: readonly Type[],
): ReadonlyCollection<
Snowflake,
Extract<
NonNullable<CommandInteractionOption<Cached>['channel']>,
{
type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread
? ChannelType.AnnouncementThread | ChannelType.PublicThread
: Type;
}
>
>;
public getSelectedChannels<const Type extends ChannelType = ChannelType>(
customId: string,
required?: boolean,
channelTypes?: readonly Type[],
): ReadonlyCollection<
Snowflake,
Extract<
NonNullable<CommandInteractionOption<Cached>['channel']>,
{
type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread
? ChannelType.AnnouncementThread | ChannelType.PublicThread
: Type;
}
>
> | null;
public getSelectedRoles(customId: string, required: true): NonNullable<SelectMenuModalData<Cached>['roles']>;
public getSelectedRoles(
customId: string,
required?: boolean,
): NonNullable<SelectMenuModalData<Cached>['roles']> | null;
public getSelectedMentionables(customId: string, required: true): ModalSelectedMentionables<Cached>;
public getSelectedMentionables(customId: string, required?: boolean): ModalSelectedMentionables<Cached> | null;
} }
export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType> export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType>
@@ -2839,7 +2915,7 @@ export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extend
public type: InteractionType.ModalSubmit; public type: InteractionType.ModalSubmit;
public readonly customId: string; public readonly customId: string;
public readonly components: (ActionRowModalData | LabelModalData)[]; public readonly components: (ActionRowModalData | LabelModalData)[];
public readonly fields: ModalSubmitFields; public readonly fields: ModalSubmitFields<Cached>;
public deferred: boolean; public deferred: boolean;
public ephemeral: boolean | null; public ephemeral: boolean | null;
public message: Message<BooleanCache<Cached>> | null; public message: Message<BooleanCache<Cached>> | null;
@@ -4556,6 +4632,8 @@ export enum DiscordjsErrorCodes {
ModalSubmitInteractionFieldNotFound = 'ModalSubmitInteractionFieldNotFound', ModalSubmitInteractionFieldNotFound = 'ModalSubmitInteractionFieldNotFound',
ModalSubmitInteractionFieldType = 'ModalSubmitInteractionFieldType', ModalSubmitInteractionFieldType = 'ModalSubmitInteractionFieldType',
ModalSubmitInteractionFieldEmpty = 'ModalSubmitInteractionComponentEmpty',
ModalSubmitInteractionFieldInvalidChannelType = 'ModalSubmitInteractionFieldInvalidChannelType',
InvalidMissingScopes = 'InvalidMissingScopes', InvalidMissingScopes = 'InvalidMissingScopes',
InvalidScopesWithPermissions = 'InvalidScopesWithPermissions', InvalidScopesWithPermissions = 'InvalidScopesWithPermissions',
@@ -6095,13 +6173,17 @@ export interface CommandInteractionOption<Cached extends CacheType = CacheType>
message?: Message<BooleanCache<Cached>>; message?: Message<BooleanCache<Cached>>;
} }
export interface CommandInteractionResolvedData<Cached extends CacheType = CacheType> { export interface BaseInteractionResolvedData<Cached extends CacheType = CacheType> {
users?: ReadonlyCollection<Snowflake, User>; channels?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Channel, APIInteractionDataResolvedChannel>>;
members?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIInteractionDataResolvedGuildMember>>; members?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIInteractionDataResolvedGuildMember>>;
roles?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>; roles?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
channels?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Channel, APIInteractionDataResolvedChannel>>; users?: ReadonlyCollection<Snowflake, User>;
messages?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Message, APIMessage>>; }
export interface CommandInteractionResolvedData<Cached extends CacheType = CacheType>
extends BaseInteractionResolvedData<Cached> {
attachments?: ReadonlyCollection<Snowflake, Attachment>; attachments?: ReadonlyCollection<Snowflake, Attachment>;
messages?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Message, APIMessage>>;
} }
export interface AutocompleteFocusedOption extends Pick<CommandInteractionOption, 'name'> { export interface AutocompleteFocusedOption extends Pick<CommandInteractionOption, 'name'> {
@@ -7266,12 +7348,12 @@ export interface BaseSelectMenuComponentData extends BaseComponentData {
maxValues?: number; maxValues?: number;
minValues?: number; minValues?: number;
placeholder?: string; placeholder?: string;
required?: true;
} }
export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData { export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData {
type: ComponentType.StringSelect; type: ComponentType.StringSelect;
options: readonly SelectMenuComponentOptionData[]; options: readonly SelectMenuComponentOptionData[];
required?: boolean;
} }
export interface UserSelectMenuComponentData extends BaseSelectMenuComponentData { export interface UserSelectMenuComponentData extends BaseSelectMenuComponentData {

View File

@@ -2568,13 +2568,13 @@ chatInputInteraction.showModal({
chatInputInteraction.showModal({ chatInputInteraction.showModal({
title: 'abc', title: 'abc',
custom_id: 'abc', customId: 'abc',
components: [ components: [
{ {
type: ComponentType.Label, type: ComponentType.Label,
label: 'label', label: 'label',
component: { component: {
custom_id: 'aa', customId: 'aa',
type: ComponentType.TextInput, type: ComponentType.TextInput,
style: TextInputStyle.Short, style: TextInputStyle.Short,
label: 'label', label: 'label',
@@ -2583,7 +2583,7 @@ chatInputInteraction.showModal({
{ {
components: [ components: [
{ {
custom_id: 'aa', customId: 'aa',
label: 'label', label: 'label',
style: TextInputStyle.Short, style: TextInputStyle.Short,
type: ComponentType.TextInput, type: ComponentType.TextInput,
@@ -2591,6 +2591,31 @@ chatInputInteraction.showModal({
], ],
type: ComponentType.ActionRow, type: ComponentType.ActionRow,
}, },
{
type: ComponentType.Label,
label: 'Lll',
component: {
customId: 'aa',
type: ComponentType.UserSelect,
},
},
{
type: ComponentType.Label,
label: 'Lll',
component: {
customId: 'aa',
type: ComponentType.ChannelSelect,
channelTypes: [ChannelType.GuildText, ChannelType.GuildVoice],
},
},
{
type: ComponentType.Label,
label: 'Lll',
component: {
customId: 'aa',
type: ComponentType.RoleSelect,
},
},
], ],
}); });