feat!: label component and selects in modal (#11081)

BREAKING CHANGE: TextInputComponentData no longer accepts label
BREAKING CHANGE: ActionRow and ActionRowData no longer accept TextInput
BREAKING CHANGE: `ModalSubmitInteraction#transformComponent` is now private and no longer exposed publicly

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
Naiyar
2025-09-11 02:04:57 +05:30
committed by GitHub
parent f1bcff46b6
commit 4ec03ae517
8 changed files with 234 additions and 57 deletions

View File

@@ -180,6 +180,7 @@ exports.InteractionCallbackResponse =
exports.InteractionCollector = require('./structures/InteractionCollector.js').InteractionCollector;
exports.InteractionWebhook = require('./structures/InteractionWebhook.js').InteractionWebhook;
exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild;
exports.LabelComponent = require('./structures/LabelComponent.js').LabelComponent;
exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel;
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent;
exports.MediaGalleryItem = require('./structures/MediaGalleryItem.js').MediaGalleryItem;

View File

@@ -0,0 +1,54 @@
'use strict';
const { createComponent } = require('../util/Components.js');
const { Component } = require('./Component.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() };
}
}
exports.LabelComponent = LabelComponent;

View File

@@ -12,7 +12,7 @@ class ModalSubmitFields {
/**
* The components within the modal
*
* @type {ActionRowModalData[]}
* @type {Array<ActionRowModalData | LabelModalData>}
*/
this.components = components;
@@ -22,7 +22,16 @@ class ModalSubmitFields {
* @type {Collection<string, ModalData>}
*/
this.fields = components.reduce((accumulator, next) => {
// 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());
}
@@ -54,6 +63,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;
}
}
exports.ModalSubmitFields = ModalSubmitFields;

View File

@@ -9,16 +9,38 @@ const { InteractionResponses } = require('./interfaces/InteractionResponses.js')
const getMessage = lazy(() => require('./Message.js').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
*/
/**
@@ -51,7 +73,7 @@ class ModalSubmitInteraction extends BaseInteraction {
/**
* The components within the modal
*
* @type {ActionRowModalData[]}
* @type {Array<ActionRowModalData | LabelModalData>}
*/
this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component));
@@ -96,18 +118,35 @@ class ModalSubmitInteraction extends BaseInteraction {
*
* @param {*} rawComponent The data to transform
* @returns {ModalData[]}
* @private
*/
static transformComponent(rawComponent) {
return rawComponent.components
? {
if ('components' in rawComponent) {
return {
type: rawComponent.type,
id: rawComponent.id,
components: rawComponent.components.map(component => this.transformComponent(component)),
};
}
: {
value: rawComponent.value,
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;
}
/**

View File

@@ -9,13 +9,6 @@ const { InteractionCallbackResponse } = require('../InteractionCallbackResponse.
const { InteractionCollector } = require('../InteractionCollector.js');
const { MessagePayload } = require('../MessagePayload.js');
/**
* @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.
*

View File

@@ -15,6 +15,24 @@ 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 {LabelData[]} components The components within this modal
*/
/**
* @typedef {StringSelectMenuComponentData|TextInputComponentData} ComponentInLabelData
*/
/**
* @typedef {BaseComponentData} LabelData
* @property {string} label The label to use
* @property {string} [description] The optional description for the label
* @property {ComponentInLabelData} component The component within the label
*/
/**
* @typedef {BaseComponentData} ButtonComponentData
* @property {ButtonStyle} style The style of the button
@@ -25,6 +43,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
@@ -52,7 +81,6 @@ const { ComponentType } = require('discord-api-types/v10');
* @typedef {BaseComponentData} TextInputComponentData
* @property {string} customId The custom id of the text input
* @property {TextInputStyle} style The style of the text input
* @property {string} label The text that appears on top of the text input field
* @property {number} [minLength] The minimum number of characters that can be entered in the text input
* @property {number} [maxLength] The maximum number of characters that can be entered in the text input
* @property {boolean} [required] Whether or not the text input is required or not
@@ -186,6 +214,7 @@ const { ChannelSelectMenuComponent } = require('../structures/ChannelSelectMenuC
const { Component } = require('../structures/Component.js');
const { ContainerComponent } = require('../structures/ContainerComponent.js');
const { FileComponent } = require('../structures/FileComponent.js');
const { LabelComponent } = require('../structures/LabelComponent.js');
const { MediaGalleryComponent } = require('../structures/MediaGalleryComponent.js');
const { MentionableSelectMenuComponent } = require('../structures/MentionableSelectMenuComponent.js');
const { RoleSelectMenuComponent } = require('../structures/RoleSelectMenuComponent.js');
@@ -213,4 +242,5 @@ const ComponentTypeToClass = {
[ComponentType.Section]: SectionComponent,
[ComponentType.Separator]: SeparatorComponent,
[ComponentType.Thumbnail]: ThumbnailComponent,
[ComponentType.Label]: LabelComponent,
};

View File

@@ -50,6 +50,7 @@ import {
APIInteractionDataResolvedChannel,
APIInteractionDataResolvedGuildMember,
APIInteractionGuildMember,
APILabelComponent,
APIMediaGalleryComponent,
APIMediaGalleryItem,
APIMentionableSelectComponent,
@@ -65,6 +66,7 @@ import {
APIMessageTopLevelComponent,
APIMessageUserSelectInteractionData,
APIModalComponent,
APIModalInteractionResponseCallbackComponent,
APIModalInteractionResponseCallbackData,
APIModalSubmitInteraction,
APIOverwrite,
@@ -248,6 +250,7 @@ export class Activity {
export type ActivityFlagsString = keyof typeof ActivityFlags;
export interface BaseComponentData {
id?: number;
type: ComponentType;
}
@@ -260,17 +263,22 @@ export type MessageActionRowComponentData =
| StringSelectMenuComponentData
| UserSelectMenuComponentData;
export type ModalActionRowComponentData = JSONEncodable<APIComponentInModalActionRow> | TextInputComponentData;
export type ActionRowComponentData = MessageActionRowComponentData;
export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData;
export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent;
export type ActionRowComponent = MessageActionRowComponent;
export interface ActionRowData<ComponentType extends ActionRowComponentData | JSONEncodable<APIComponentInActionRow>>
extends BaseComponentData {
components: readonly ComponentType[];
}
export type ComponentInLabelData = StringSelectMenuComponentData | TextInputComponentData;
export interface LabelData extends BaseComponentData {
component: ComponentInLabelData;
description?: string;
label: string;
}
export type MessageActionRowComponent =
| ButtonComponent
| ChannelSelectMenuComponent
@@ -278,12 +286,11 @@ export type MessageActionRowComponent =
| RoleSelectMenuComponent
| StringSelectMenuComponent
| UserSelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<ComponentType extends MessageActionRowComponent | ModalActionRowComponent> extends Component<
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
export class ActionRow<ComponentType extends MessageActionRowComponent> extends Component<
APIActionRowComponent<APIComponentInMessageActionRow>
> {
private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>);
private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow>);
public readonly components: ComponentType[];
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
}
@@ -740,6 +747,12 @@ export class TextInputComponent extends Component<APITextInputComponent> {
public get value(): string;
}
export class LabelComponent extends Component<APILabelComponent> {
public component: StringSelectMenuComponent | TextInputComponent;
public get label(): string;
public get description(): string | null;
}
export class BaseSelectMenuComponent<Data extends APISelectMenuComponent> extends Component<Data> {
protected constructor(data: Data);
public get placeholder(): string | null;
@@ -2527,36 +2540,48 @@ export interface MessageReactionEventDetails {
}
export interface ModalComponentData {
components: readonly (
| ActionRowData<ModalActionRowComponentData>
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
)[];
components: readonly LabelData[];
customId: string;
title: string;
}
export interface BaseModalData {
export interface BaseModalData<Type extends ComponentType> {
customId: string;
type: ComponentType;
id: number;
type: Type;
}
export interface TextInputModalData extends BaseModalData {
type: ComponentType.TextInput;
export interface TextInputModalData extends BaseModalData<ComponentType.TextInput> {
value: string;
}
export interface StringSelectModalData extends BaseModalData<ComponentType.StringSelect> {
values: readonly string[];
}
export type ModalData = StringSelectModalData | TextInputModalData;
export interface LabelModalData {
component: readonly ModalData[];
id: number;
type: ComponentType.Label;
}
export interface ActionRowModalData {
components: readonly TextInputModalData[];
type: ComponentType.ActionRow;
}
export class ModalSubmitFields {
private constructor(components: readonly (readonly ModalActionRowComponent[])[]);
public components: ActionRowModalData[];
public fields: Collection<string, TextInputModalData>;
public getField<Type extends ComponentType>(customId: string, type: Type): TextInputModalData & { type: Type };
public getField(customId: string, type?: ComponentType): TextInputModalData;
private constructor(components: readonly (ActionRowModalData | LabelModalData)[]);
public components: (ActionRowModalData | LabelModalData)[];
public fields: Collection<string, StringSelectModalData | TextInputModalData>;
public getField<Type extends ComponentType>(
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<Cached extends CacheType = CacheType>
@@ -2579,7 +2604,7 @@ export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extend
private constructor(client: Client<true>, 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;
@@ -3645,9 +3670,10 @@ export function verifyString(data: string, error?: typeof Error, errorMessage?:
export type ComponentData =
| ComponentInContainerData
| ComponentInLabelData
| ContainerComponentData
| LabelData
| MessageActionRowComponentData
| ModalActionRowComponentData
| ThumbnailComponentData;
export interface SendSoundboardSoundOptions {
@@ -6669,6 +6695,7 @@ export interface BaseSelectMenuComponentData extends BaseComponentData {
export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData {
options: readonly SelectMenuComponentOptionData[];
required?: boolean;
type: ComponentType.StringSelect;
}
@@ -6718,7 +6745,6 @@ export interface SelectMenuComponentOptionData {
export interface TextInputComponentData extends BaseComponentData {
customId: string;
label: string;
maxLength?: number;
minLength?: number;
placeholder?: string;

View File

@@ -2584,15 +2584,30 @@ await chatInputInteraction.showModal({
custom_id: 'abc',
components: [
{
components: [
{
component: {
type: ComponentType.StringSelect,
id: 2,
custom_id: 'aa',
label: 'label',
style: TextInputStyle.Short,
type: ComponentType.TextInput,
options: [{ label: 'a', value: 'b' }],
},
type: ComponentType.Label,
label: 'yo',
},
],
type: ComponentType.ActionRow,
});
await chatInputInteraction.showModal({
title: 'abc',
customId: 'abc',
components: [
{
type: ComponentType.Label,
component: {
type: ComponentType.TextInput,
style: TextInputStyle.Short,
customId: 'aa',
},
label: 'yo',
},
],
});