diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index cb2f7d722..15ed02045 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -162,6 +162,7 @@ exports.InteractionWebhook = require('./structures/InteractionWebhook'); exports.Invite = require('./structures/Invite'); exports.InviteStageInstance = require('./structures/InviteStageInstance'); exports.InviteGuild = require('./structures/InviteGuild'); +exports.LabelComponent = require('./structures/LabelComponent'); exports.Message = require('./structures/Message').Message; exports.Attachment = require('./structures/Attachment'); exports.AttachmentBuilder = require('./structures/AttachmentBuilder'); diff --git a/packages/discord.js/src/structures/LabelComponent.js b/packages/discord.js/src/structures/LabelComponent.js new file mode 100644 index 000000000..f191e72e8 --- /dev/null +++ b/packages/discord.js/src/structures/LabelComponent.js @@ -0,0 +1,54 @@ +'use strict'; + +const { Component } = require('./Component.js'); +const { createComponent } = require('../util/Components.js'); + +/** + * Represents a label component + * + * @extends {Component} + */ +class LabelComponent extends Component { + constructor({ component, ...data }) { + super(data); + + /** + * The component in this label + * + * @type {Component} + * @readonly + */ + this.component = createComponent(component); + } + + /** + * The label of the component + * + * @type {string} + * @readonly + */ + get label() { + return this.data.label; + } + + /** + * The description of this component + * + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } + + /** + * Returns the API-compatible JSON for this component + * + * @returns {APILabelComponent} + */ + toJSON() { + return { ...this.data, component: this.component.toJSON() }; + } +} + +module.exports = LabelComponent; diff --git a/packages/discord.js/src/structures/ModalSubmitFields.js b/packages/discord.js/src/structures/ModalSubmitFields.js index a2f691f3a..af44499fd 100644 --- a/packages/discord.js/src/structures/ModalSubmitFields.js +++ b/packages/discord.js/src/structures/ModalSubmitFields.js @@ -11,7 +11,8 @@ class ModalSubmitFields { constructor(components) { /** * The components within the modal - * @type {ActionRowModalData[]} + * + * @type {Array} */ this.components = components; @@ -20,7 +21,16 @@ class ModalSubmitFields { * @type {Collection} */ this.fields = components.reduce((accumulator, next) => { - next.components.forEach(component => accumulator.set(component.customId, component)); + // NOTE: for legacy support of action rows in modals, which has `components` + if ('components' in next) { + for (const component of next.components) accumulator.set(component.customId, component); + } + + // For label component + if ('component' in next) { + accumulator.set(next.component.customId, next.component); + } + return accumulator; }, new Collection()); } @@ -50,6 +60,16 @@ class ModalSubmitFields { getTextInputValue(customId) { return this.getField(customId, ComponentType.TextInput).value; } + + /** + * Gets the values of a string select component given a custom id + * + * @param {string} customId The custom id of the string select component + * @returns {string[]} + */ + getStringSelectValues(customId) { + return this.getField(customId, ComponentType.StringSelect).values; + } } module.exports = ModalSubmitFields; diff --git a/packages/discord.js/src/structures/ModalSubmitInteraction.js b/packages/discord.js/src/structures/ModalSubmitInteraction.js index 0b23b9ab8..79a7a6987 100644 --- a/packages/discord.js/src/structures/ModalSubmitInteraction.js +++ b/packages/discord.js/src/structures/ModalSubmitInteraction.js @@ -9,16 +9,38 @@ const InteractionResponses = require('./interfaces/InteractionResponses'); const getMessage = lazy(() => require('./Message').Message); /** - * @typedef {Object} ModalData - * @property {string} value The value of the field + * @typedef {Object} BaseModalData * @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 + */ + +/** + * @typedef {BaseModalData} TextInputModalData + * @property {string} value The value of the field + */ + +/** + * @typedef {BaseModalData} StringSelectModalData + * @property {string[]} values The values of the field + */ + +/** + * @typedef {TextInputModalData | StringSelectModalData} ModalData + */ + +/** + * @typedef {Object} LabelModalData + * @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 - * @property {ModalData[]} 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 */ /** @@ -47,7 +69,8 @@ class ModalSubmitInteraction extends BaseInteraction { /** * The components within the modal - * @type {ActionRowModalData[]} + * + * @type {Array} */ this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component)); @@ -88,16 +111,32 @@ class ModalSubmitInteraction extends BaseInteraction { * @returns {ModalData[]} */ static transformComponent(rawComponent) { - return rawComponent.components - ? { - type: rawComponent.type, - components: rawComponent.components.map(component => this.transformComponent(component)), - } - : { - value: rawComponent.value, - type: rawComponent.type, - customId: rawComponent.custom_id, - }; + if ('components' in rawComponent) { + return { + type: rawComponent.type, + id: rawComponent.id, + components: rawComponent.components.map(component => this.transformComponent(component)), + }; + } + + if ('component' in rawComponent) { + return { + type: rawComponent.type, + id: rawComponent.id, + component: this.transformComponent(rawComponent.component), + }; + } + + const data = { + type: rawComponent.type, + customId: rawComponent.custom_id, + id: rawComponent.id, + }; + + if (rawComponent.value) data.value = rawComponent.value; + if (rawComponent.values) data.values = rawComponent.values; + + return data; } /** diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index 30c410738..9de8f9379 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -15,13 +15,6 @@ const MessagePayload = require('../MessagePayload'); let deprecationEmittedForEphemeralOption = false; let deprecationEmittedForFetchReplyOption = false; -/** - * @typedef {Object} ModalComponentData - * @property {string} title The title of the modal - * @property {string} customId The custom id of the modal - * @property {ActionRow[]} components The components within this modal - */ - /** * Interface for classes that support shared interaction response types. * @interface diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 4c8369d8c..f8a40da08 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -14,6 +14,20 @@ const { ComponentType } = require('discord-api-types/v10'); * @property {ComponentData[]} components The components in this action row */ +/** + * @typedef {Object} ModalComponentData + * @property {string} title The title of the modal + * @property {string} customId The custom id of the modal + * @property {Array} components The components within this modal + */ + +/** + * @typedef {BaseComponentData} LabelData + * @property {string} label The label to use + * @property {string} [description] The optional description for the label + * @property {StringSelectMenuComponentData|TextInputComponentData} component The component within the label + */ + /** * @typedef {BaseComponentData} ButtonComponentData * @property {ButtonStyle} style The style of the button @@ -24,6 +38,17 @@ const { ComponentType } = require('discord-api-types/v10'); * @property {string} [url] The URL of the button */ +/** + * @typedef {BaseComponentData} StringSelectMenuComponentData + * @property {string} customId The custom id of the select menu + * @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} [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 {boolean} [required] Whether this component is required in modals + */ + /** * @typedef {object} SelectMenuComponentOptionData * @property {string} label The label of the option @@ -199,6 +224,7 @@ const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuCompo const Component = require('../structures/Component'); const ContainerComponent = require('../structures/ContainerComponent'); const FileComponent = require('../structures/FileComponent'); +const LabelComponent = require('../structures/LabelComponent'); const MediaGalleryComponent = require('../structures/MediaGalleryComponent'); const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder'); const MentionableSelectMenuComponent = require('../structures/MentionableSelectMenuComponent'); @@ -231,6 +257,7 @@ const ComponentTypeToComponent = { [ComponentType.Section]: SectionComponent, [ComponentType.Separator]: SeparatorComponent, [ComponentType.Thumbnail]: ThumbnailComponent, + [ComponentType.Label]: LabelComponent, }; const ComponentTypeToBuilder = { diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 5f933203a..b256e3aac 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -55,6 +55,7 @@ import { APIInteractionDataResolvedChannel, APIInteractionDataResolvedGuildMember, APIInteractionGuildMember, + APILabelComponent, APIMessage, APIMessageComponent, APIOverwrite, @@ -352,6 +353,14 @@ export class ActionRowBuilder< ): ActionRowBuilder; } +export type ComponentInLabelData = StringSelectMenuComponentData | TextInputComponentData; + +export interface LabelData extends BaseComponentData { + component: ComponentInLabelData; + description?: string; + label: string; +} + export type MessageActionRowComponent = | ButtonComponent | StringSelectMenuComponent @@ -912,6 +921,12 @@ export class TextInputComponent extends Component { public get value(): string; } +export class LabelComponent extends Component { + public component: StringSelectMenuComponent | TextInputComponent; + public get label(): string; + public get description(): string | null; +} + export class BaseSelectMenuComponent extends Component { protected constructor(data: Data); public get placeholder(): string | null; @@ -2757,33 +2772,49 @@ export interface ModalComponentData { customId: string; title: string; components: readonly ( - | JSONEncodable> + | JSONEncodable | APILabelComponent> | ActionRowData + | LabelData )[]; } -export interface BaseModalData { +export interface BaseModalData { customId: string; - type: ComponentType; + id: number; + type: Type; } -export interface TextInputModalData extends BaseModalData { - type: ComponentType.TextInput; +export interface TextInputModalData extends BaseModalData { value: string; } +export interface StringSelectModalData extends BaseModalData { + values: readonly string[]; +} + +export type ModalData = StringSelectModalData | TextInputModalData; + +export interface LabelModalData { + component: readonly ModalData[]; + id: number; + type: ComponentType.Label; +} export interface ActionRowModalData { type: ComponentType.ActionRow; components: readonly TextInputModalData[]; } export class ModalSubmitFields { - private constructor(components: readonly (readonly ModalActionRowComponent[])[]); - public components: ActionRowModalData[]; - public fields: Collection; - public getField(customId: string, type: Type): { type: Type } & TextInputModalData; - public getField(customId: string, type?: ComponentType): TextInputModalData; + private constructor(components: readonly (ActionRowModalData | LabelModalData)[]); + public components: (ActionRowModalData | LabelModalData)[]; + public fields: Collection; + public getField( + customId: string, + type: Type, + ): { type: Type } & (StringSelectModalData | TextInputModalData); + public getField(customId: string, type?: ComponentType): StringSelectModalData | TextInputModalData; public getTextInputValue(customId: string): string; + public getStringSelectValues(customId: string): readonly string[]; } export interface ModalMessageModalSubmitInteraction @@ -2807,7 +2838,7 @@ export class ModalSubmitInteraction extend private constructor(client: Client, data: APIModalSubmitInteraction); public type: InteractionType.ModalSubmit; public readonly customId: string; - public readonly components: ActionRowModalData[]; + public readonly components: (ActionRowModalData | LabelModalData)[]; public readonly fields: ModalSubmitFields; public deferred: boolean; public ephemeral: boolean | null; @@ -4036,6 +4067,8 @@ export class Formatters extends null { export type ComponentData = | MessageActionRowComponentData | ModalActionRowComponentData + | LabelData + | ComponentInLabelData | ComponentInContainerData | ContainerComponentData | ThumbnailComponentData; @@ -7238,6 +7271,7 @@ export interface BaseSelectMenuComponentData extends BaseComponentData { export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData { type: ComponentType.StringSelect; options: readonly SelectMenuComponentOptionData[]; + required?: boolean; } export interface UserSelectMenuComponentData extends BaseSelectMenuComponentData { diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 6be38e4bc..f64e54c9c 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -2566,6 +2566,34 @@ chatInputInteraction.showModal({ ], }); +chatInputInteraction.showModal({ + title: 'abc', + custom_id: 'abc', + components: [ + { + type: ComponentType.Label, + label: 'label', + component: { + custom_id: 'aa', + type: ComponentType.TextInput, + style: TextInputStyle.Short, + label: 'label', + }, + }, + { + components: [ + { + custom_id: 'aa', + label: 'label', + style: TextInputStyle.Short, + type: ComponentType.TextInput, + }, + ], + type: ComponentType.ActionRow, + }, + ], +}); + declare const stringSelectMenuData: APIStringSelectComponent; StringSelectMenuBuilder.from(stringSelectMenuData);