feat: label component and select in modal for v14 (#11090)

* 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

* chore: missing required in typings
This commit is contained in:
Naiyar
2025-09-16 01:58:07 +05:30
committed by GitHub
parent 270d9f1047
commit abaae4ff16
8 changed files with 230 additions and 34 deletions

View File

@@ -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');

View File

@@ -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;

View File

@@ -11,7 +11,8 @@ class ModalSubmitFields {
constructor(components) {
/**
* The components within the modal
* @type {ActionRowModalData[]}
*
* @type {Array<ActionRowModalData | LabelModalData>}
*/
this.components = components;
@@ -20,7 +21,16 @@ class ModalSubmitFields {
* @type {Collection<string, ModalData>}
*/
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;

View File

@@ -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<ActionRowModalData | LabelModalData>}
*/
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;
}
/**

View File

@@ -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

View File

@@ -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<ActionRow | LabelData>} 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 = {

View File

@@ -55,6 +55,7 @@ import {
APIInteractionDataResolvedChannel,
APIInteractionDataResolvedGuildMember,
APIInteractionGuildMember,
APILabelComponent,
APIMessage,
APIMessageComponent,
APIOverwrite,
@@ -352,6 +353,14 @@ export class ActionRowBuilder<
): ActionRowBuilder<ComponentType>;
}
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<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;
@@ -2757,33 +2772,49 @@ export interface ModalComponentData {
customId: string;
title: string;
components: readonly (
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow> | APILabelComponent>
| ActionRowData<ModalActionRowComponentData>
| LabelData
)[];
}
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 {
type: ComponentType.ActionRow;
components: readonly TextInputModalData[];
}
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): { type: Type } & TextInputModalData;
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>
@@ -2807,7 +2838,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;
@@ -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 {

View File

@@ -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);