From f7c77a73ded8baeec2e76d292759918c1b631272 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:56:14 +0400 Subject: [PATCH] feat(builders)!: Support select in modals (#11034) BREAKING CHANGE: Text inputs no longer accept a label. BREAKING CHANGE: Modals now only set labels instead of action rows. --- .../__tests__/components/label.test.ts | 109 +++++++++++++++ .../__tests__/components/textInput.test.ts | 21 ++- .../__tests__/interactions/modal.test.ts | 79 ++++++----- .../__tests__/messages/message.test.ts | 2 +- .../builders/src/components/Components.ts | 12 +- .../src/components/label/Assertions.ts | 11 ++ .../builders/src/components/label/Label.ts | 127 ++++++++++++++++++ .../components/selectMenu/StringSelectMenu.ts | 12 ++ .../src/components/textInput/Assertions.ts | 1 - .../src/components/textInput/TextInput.ts | 14 +- packages/builders/src/index.ts | 3 + .../src/interactions/modals/Assertions.ts | 18 ++- .../builders/src/interactions/modals/Modal.ts | 70 ++++------ 13 files changed, 364 insertions(+), 115 deletions(-) create mode 100644 packages/builders/__tests__/components/label.test.ts create mode 100644 packages/builders/src/components/label/Assertions.ts create mode 100644 packages/builders/src/components/label/Label.ts diff --git a/packages/builders/__tests__/components/label.test.ts b/packages/builders/__tests__/components/label.test.ts new file mode 100644 index 000000000..fbe120936 --- /dev/null +++ b/packages/builders/__tests__/components/label.test.ts @@ -0,0 +1,109 @@ +import type { APILabelComponent, APIStringSelectComponent, APITextInputComponent } from 'discord-api-types/v10'; +import { ComponentType, TextInputStyle } from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { LabelBuilder } from '../../src/index.js'; + +describe('Label components', () => { + describe('Assertion Tests', () => { + test('GIVEN valid fields THEN builder does not throw', () => { + expect(() => + new LabelBuilder() + .setLabel('label') + .setStringSelectMenuComponent((stringSelectMenu) => + stringSelectMenu + .setCustomId('test') + .setOptions((stringSelectMenuOption) => stringSelectMenuOption.setLabel('label').setValue('value')) + .setRequired(), + ) + .toJSON(), + ).not.toThrow(); + + expect(() => + new LabelBuilder() + .setLabel('label') + .setId(5) + .setTextInputComponent((textInput) => + textInput.setCustomId('test').setStyle(TextInputStyle.Paragraph).setRequired(), + ) + .toJSON(), + ).not.toThrow(); + }); + + test('GIVEN invalid fields THEN build does throw', () => { + expect(() => new LabelBuilder().toJSON()).toThrow(); + expect(() => new LabelBuilder().setId(5).toJSON()).toThrow(); + expect(() => new LabelBuilder().setLabel('label').toJSON()).toThrow(); + + expect(() => + new LabelBuilder() + .setLabel('l'.repeat(1_000)) + .setStringSelectMenuComponent((stringSelectMenu) => stringSelectMenu) + .toJSON(), + ).toThrow(); + }); + + test('GIVEN valid input THEN valid JSON outputs are given', () => { + const labelWithTextInputData = { + type: ComponentType.Label, + component: { + type: ComponentType.TextInput, + custom_id: 'custom_id', + placeholder: 'placeholder', + style: TextInputStyle.Paragraph, + } satisfies APITextInputComponent, + label: 'label', + description: 'description', + id: 5, + } satisfies APILabelComponent; + + const labelWithStringSelectData = { + type: ComponentType.Label, + component: { + type: ComponentType.StringSelect, + custom_id: 'custom_id', + placeholder: 'placeholder', + options: [ + { label: 'first', value: 'first' }, + { label: 'second', value: 'second' }, + ], + required: true, + } satisfies APIStringSelectComponent, + label: 'label', + description: 'description', + id: 5, + } satisfies APILabelComponent; + + expect(new LabelBuilder(labelWithTextInputData).toJSON()).toEqual(labelWithTextInputData); + expect(new LabelBuilder(labelWithStringSelectData).toJSON()).toEqual(labelWithStringSelectData); + + expect( + new LabelBuilder() + .setTextInputComponent((textInput) => + textInput.setCustomId('custom_id').setPlaceholder('placeholder').setStyle(TextInputStyle.Paragraph), + ) + .setLabel('label') + .setDescription('description') + .setId(5) + .toJSON(), + ).toEqual(labelWithTextInputData); + + expect( + new LabelBuilder() + .setStringSelectMenuComponent((stringSelectMenu) => + stringSelectMenu + .setCustomId('custom_id') + .setPlaceholder('placeholder') + .setOptions( + (stringSelectMenuOption) => stringSelectMenuOption.setLabel('first').setValue('first'), + (stringSelectMenuOption) => stringSelectMenuOption.setLabel('second').setValue('second'), + ) + .setRequired(), + ) + .setLabel('label') + .setDescription('description') + .setId(5) + .toJSON(), + ).toEqual(labelWithStringSelectData); + }); + }); +}); diff --git a/packages/builders/__tests__/components/textInput.test.ts b/packages/builders/__tests__/components/textInput.test.ts index 1fc8b0f2a..2b758a6da 100644 --- a/packages/builders/__tests__/components/textInput.test.ts +++ b/packages/builders/__tests__/components/textInput.test.ts @@ -8,13 +8,12 @@ describe('Text Input Components', () => { describe('Assertion Tests', () => { test('GIVEN valid fields THEN builder does not throw', () => { expect(() => { - textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON(); + textInputComponent().setCustomId('foobar').setStyle(TextInputStyle.Paragraph).toJSON(); }).not.toThrowError(); expect(() => { textInputComponent() .setCustomId('foobar') - .setLabel('test') .setMaxLength(100) .setMinLength(1) .setPlaceholder('bar') @@ -24,7 +23,7 @@ describe('Text Input Components', () => { }).not.toThrowError(); expect(() => { - textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON(); + textInputComponent().setCustomId('Custom').setStyle(TextInputStyle.Short).toJSON(); }).not.toThrowError(); }); }); @@ -33,10 +32,10 @@ describe('Text Input Components', () => { expect(() => textInputComponent().toJSON()).toThrowError(); expect(() => { textInputComponent() - .setCustomId('test') + .setCustomId('a'.repeat(500)) .setMaxLength(100) - .setPlaceholder('hello') - .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('a'.repeat(500)) + .setStyle(3 as TextInputStyle) .toJSON(); }).toThrowError(); }); @@ -44,7 +43,6 @@ describe('Text Input Components', () => { test('GIVEN valid input THEN valid JSON outputs are given', () => { const textInputData = { type: ComponentType.TextInput, - label: 'label', custom_id: 'custom id', placeholder: 'placeholder', max_length: 100, @@ -58,11 +56,10 @@ describe('Text Input Components', () => { expect( textInputComponent() .setCustomId(textInputData.custom_id) - .setLabel(textInputData.label) - .setPlaceholder(textInputData.placeholder!) - .setMaxLength(textInputData.max_length!) - .setMinLength(textInputData.min_length!) - .setValue(textInputData.value!) + .setPlaceholder(textInputData.placeholder) + .setMaxLength(textInputData.max_length) + .setMinLength(textInputData.min_length) + .setValue(textInputData.value) .setRequired(textInputData.required) .setStyle(textInputData.style) .toJSON(), diff --git a/packages/builders/__tests__/interactions/modal.test.ts b/packages/builders/__tests__/interactions/modal.test.ts index 7adfcf844..bc416de5e 100644 --- a/packages/builders/__tests__/interactions/modal.test.ts +++ b/packages/builders/__tests__/interactions/modal.test.ts @@ -1,17 +1,22 @@ import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js'; +import { ModalBuilder, TextInputBuilder, LabelBuilder } from '../../src/index.js'; const modal = () => new ModalBuilder(); -const textInput = () => - new ActionRowBuilder().addTextInputComponent( - new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short), - ); + +const label = () => + new LabelBuilder() + .setLabel('label') + .setTextInputComponent(new TextInputBuilder().setCustomId('text').setStyle(TextInputStyle.Short)); describe('Modals', () => { test('GIVEN valid fields THEN builder does not throw', () => { - expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError(); - expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError(); + expect(() => + modal().setTitle('test').setCustomId('foobar').setLabelComponents(label()).toJSON(), + ).not.toThrowError(); + expect(() => + modal().setTitle('test').setCustomId('foobar').setLabelComponents(label()).toJSON(), + ).not.toThrowError(); }); test('GIVEN invalid fields THEN builder does throw', () => { @@ -21,34 +26,33 @@ describe('Modals', () => { }); test('GIVEN valid input THEN valid JSON outputs are given', () => { - const modalData: APIModalInteractionResponseCallbackData = { + const modalData = { title: 'title', custom_id: 'custom id', components: [ { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.TextInput, - label: 'label', - style: TextInputStyle.Paragraph, - custom_id: 'custom id', - }, - ], + type: ComponentType.Label, + id: 33, + label: 'label', + description: 'description', + component: { + type: ComponentType.TextInput, + style: TextInputStyle.Paragraph, + custom_id: 'custom id', + }, }, { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.TextInput, - label: 'label', - style: TextInputStyle.Paragraph, - custom_id: 'custom id', - }, - ], + type: ComponentType.Label, + label: 'label', + description: 'description', + component: { + type: ComponentType.TextInput, + style: TextInputStyle.Paragraph, + custom_id: 'custom id', + }, }, ], - }; + } satisfies APIModalInteractionResponseCallbackData; expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData); @@ -56,16 +60,19 @@ describe('Modals', () => { modal() .setTitle(modalData.title) .setCustomId('custom id') - .setActionRows( - new ActionRowBuilder().addTextInputComponent( - new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), - ), + .setLabelComponents( + new LabelBuilder() + .setId(33) + .setLabel('label') + .setDescription('description') + .setTextInputComponent(new TextInputBuilder().setCustomId('custom id').setStyle(TextInputStyle.Paragraph)), + ) + .addLabelComponents( + new LabelBuilder() + .setLabel('label') + .setDescription('description') + .setTextInputComponent(new TextInputBuilder().setCustomId('custom id').setStyle(TextInputStyle.Paragraph)), ) - .addActionRows([ - new ActionRowBuilder().addTextInputComponent( - new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), - ), - ]) .toJSON(), ).toEqual(modalData); }); diff --git a/packages/builders/__tests__/messages/message.test.ts b/packages/builders/__tests__/messages/message.test.ts index c527b134c..3e05eecd0 100644 --- a/packages/builders/__tests__/messages/message.test.ts +++ b/packages/builders/__tests__/messages/message.test.ts @@ -19,7 +19,7 @@ describe('Message', () => { test('GIVEN bad action row THEN it throws', () => { const message = new MessageBuilder().addActionRowComponents((row) => - row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')), + row.addTextInputComponent((input) => input.setCustomId('abc')), ); expect(() => message.toJSON()).toThrow(); }); diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 207f759d6..bf2c655fc 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -17,6 +17,7 @@ import { } from './button/CustomIdButton.js'; import { LinkButtonBuilder } from './button/LinkButton.js'; import { PremiumButtonBuilder } from './button/PremiumButton.js'; +import { LabelBuilder } from './label/Label.js'; import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js'; import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js'; import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; @@ -54,7 +55,7 @@ export type MessageComponentBuilder = /** * The builders that may be used for modals. */ -export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder; +export type ModalComponentBuilder = ActionRowBuilder | LabelBuilder | ModalActionRowComponentBuilder; /** * Any button builder @@ -152,6 +153,10 @@ export interface MappedComponentTypes { * The container component type is associated with a {@link ContainerBuilder}. */ [ComponentType.Container]: ContainerBuilder; + /** + * The label component type is associated with a {@link LabelBuilder}. + */ + [ComponentType.Label]: LabelBuilder; } /** @@ -182,8 +187,6 @@ export function createComponentBuilder( return data; } - // https://github.com/discordjs/discord.js/pull/11034 - // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (data.type) { case ComponentType.ActionRow: return new ActionRowBuilder(data); @@ -215,7 +218,10 @@ export function createComponentBuilder( return new SectionBuilder(data); case ComponentType.Container: return new ContainerBuilder(data); + case ComponentType.Label: + return new LabelBuilder(data); default: + // @ts-expect-error This case can still occur if we get a newer unsupported component type throw new Error(`Cannot properly serialize component type: ${data.type}`); } } diff --git a/packages/builders/src/components/label/Assertions.ts b/packages/builders/src/components/label/Assertions.ts new file mode 100644 index 000000000..07118c3a6 --- /dev/null +++ b/packages/builders/src/components/label/Assertions.ts @@ -0,0 +1,11 @@ +import { ComponentType } from 'discord-api-types/v10'; +import { z } from 'zod'; +import { selectMenuStringPredicate } from '../Assertions'; +import { textInputPredicate } from '../textInput/Assertions'; + +export const labelPredicate = z.object({ + type: z.literal(ComponentType.Label), + label: z.string().min(1).max(45), + description: z.string().min(1).max(100).optional(), + component: z.union([selectMenuStringPredicate, textInputPredicate]), +}); diff --git a/packages/builders/src/components/label/Label.ts b/packages/builders/src/components/label/Label.ts new file mode 100644 index 000000000..c48cc3804 --- /dev/null +++ b/packages/builders/src/components/label/Label.ts @@ -0,0 +1,127 @@ +import type { APILabelComponent, APIStringSelectComponent, APITextInputComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { resolveBuilder } from '../../util/resolveBuilder.js'; +import { validate } from '../../util/validation.js'; +import { ComponentBuilder } from '../Component.js'; +import { createComponentBuilder } from '../Components.js'; +import { StringSelectMenuBuilder } from '../selectMenu/StringSelectMenu.js'; +import { TextInputBuilder } from '../textInput/TextInput.js'; +import { labelPredicate } from './Assertions.js'; + +export interface LabelBuilderData extends Partial> { + component?: StringSelectMenuBuilder | TextInputBuilder; +} + +/** + * A builder that creates API-compatible JSON data for labels. + */ +export class LabelBuilder extends ComponentBuilder { + /** + * @internal + */ + protected readonly data: LabelBuilderData; + + /** + * Creates a new label. + * + * @param data - The API data to create this label with + * @example + * Creating a label from an API data object: + * ```ts + * const label = new LabelBuilder({ + * label: "label", + * component, + * }); + * ``` + * @example + * Creating a label using setters and API data: + * ```ts + * const label = new LabelBuilder({ + * label: 'label', + * component, + * }).setContent('new text'); + * ``` + */ + public constructor(data: Partial = {}) { + super(); + + const { component, ...rest } = data; + + this.data = { + ...structuredClone(rest), + component: component ? createComponentBuilder(component) : undefined, + type: ComponentType.Label, + }; + } + + /** + * Sets the label for this label. + * + * @param label - The label to use + */ + public setLabel(label: string) { + this.data.label = label; + return this; + } + + /** + * Sets the description for this label. + * + * @param description - The description to use + */ + public setDescription(description: string) { + this.data.description = description; + return this; + } + + /** + * Clears the description for this label. + */ + public clearDescription() { + this.data.description = undefined; + return this; + } + + /** + * Sets a string select menu component to this label. + * + * @param input - A function that returns a component builder or an already built builder + */ + public setStringSelectMenuComponent( + input: + | APIStringSelectComponent + | StringSelectMenuBuilder + | ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder), + ): this { + this.data.component = resolveBuilder(input, StringSelectMenuBuilder); + return this; + } + + /** + * Sets a text input component to this label. + * + * @param input - A function that returns a component builder or an already built builder + */ + public setTextInputComponent( + input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder), + ): this { + this.data.component = resolveBuilder(input, TextInputBuilder); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(validationOverride?: boolean): APILabelComponent { + const { component, ...rest } = this.data; + + const data = { + ...structuredClone(rest), + component: component?.toJSON(false), + }; + + validate(labelPredicate, data, validationOverride); + + return data as APILabelComponent; + } +} diff --git a/packages/builders/src/components/selectMenu/StringSelectMenu.ts b/packages/builders/src/components/selectMenu/StringSelectMenu.ts index eca016aa2..14d3c126c 100644 --- a/packages/builders/src/components/selectMenu/StringSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/StringSelectMenu.ts @@ -9,6 +9,7 @@ import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js'; export interface StringSelectMenuData extends Partial> { options: StringSelectMenuOptionBuilder[]; + required?: boolean; } /** @@ -146,6 +147,17 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder { * ```ts * const textInput = new TextInputBuilder({ * custom_id: 'a cool text input', - * label: 'Type something', + * placeholder: 'Type something', * style: TextInputStyle.Short, * }); * ``` @@ -29,7 +29,7 @@ export class TextInputBuilder extends ComponentBuilder { * Creating a text input using setters and API data: * ```ts * const textInput = new TextInputBuilder({ - * label: 'Type something else', + * placeholder: 'Type something else', * }) * .setCustomId('woah') * .setStyle(TextInputStyle.Paragraph); @@ -50,16 +50,6 @@ export class TextInputBuilder extends ComponentBuilder { return this; } - /** - * Sets the label for this text input. - * - * @param label - The label to use - */ - public setLabel(label: string) { - this.data.label = label; - return this; - } - /** * Sets the style for this text input. * diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 8ad98d448..7e3f83f67 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -4,6 +4,9 @@ export * from './components/button/CustomIdButton.js'; export * from './components/button/LinkButton.js'; export * from './components/button/PremiumButton.js'; +export * from './components/label/Label.js'; +export * from './components/label/Assertions.js'; + export * from './components/selectMenu/BaseSelectMenu.js'; export * from './components/selectMenu/ChannelSelectMenu.js'; export * from './components/selectMenu/MentionableSelectMenu.js'; diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index 84bf36861..8a31f1f7b 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/packages/builders/src/interactions/modals/Assertions.ts @@ -1,6 +1,7 @@ import { ComponentType } from 'discord-api-types/v10'; import { z } from 'zod'; import { customIdPredicate } from '../../Assertions.js'; +import { labelPredicate } from '../../components/label/Assertions.js'; const titlePredicate = z.string().min(1).max(45); @@ -8,13 +9,16 @@ export const modalPredicate = z.object({ title: titlePredicate, custom_id: customIdPredicate, components: z - .object({ - type: z.literal(ComponentType.ActionRow), - components: z - .object({ type: z.literal(ComponentType.TextInput) }) - .array() - .length(1), - }) + .union([ + z.object({ + type: z.literal(ComponentType.ActionRow), + components: z + .object({ type: z.literal(ComponentType.TextInput) }) + .array() + .length(1), + }), + labelPredicate, + ]) .array() .min(1) .max(5), diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index 43399b7b8..8b08efd2f 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -1,18 +1,15 @@ import type { JSONEncodable } from '@discordjs/util'; -import type { - APIActionRowComponent, - APIComponentInModalActionRow, - APIModalInteractionResponseCallbackData, -} from 'discord-api-types/v10'; -import { ActionRowBuilder } from '../../components/ActionRow.js'; +import type { APILabelComponent, APIModalInteractionResponseCallbackData } from 'discord-api-types/v10'; +import type { ActionRowBuilder } from '../../components/ActionRow.js'; import { createComponentBuilder } from '../../components/Components.js'; +import { LabelBuilder } from '../../components/label/Label.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { resolveBuilder } from '../../util/resolveBuilder.js'; import { validate } from '../../util/validation.js'; import { modalPredicate } from './Assertions.js'; export interface ModalBuilderData extends Partial> { - components: ActionRowBuilder[]; + components: (ActionRowBuilder | LabelBuilder)[]; } /** @@ -27,7 +24,7 @@ export class ModalBuilder implements JSONEncodable createComponentBuilder(component)), }; } @@ -67,19 +63,15 @@ export class ModalBuilder implements JSONEncodable - | ((builder: ActionRowBuilder) => ActionRowBuilder) - > + public addLabelComponents( + ...components: RestOrArray LabelBuilder)> ) { const normalized = normalizeArray(components); - const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder)); + const resolved = normalized.map((label) => resolveBuilder(label, LabelBuilder)); this.data.components.push(...resolved); @@ -87,62 +79,54 @@ export class ModalBuilder implements JSONEncodable - | ((builder: ActionRowBuilder) => ActionRowBuilder) - > + public setLabelComponents( + ...components: RestOrArray LabelBuilder)> ) { const normalized = normalizeArray(components); - this.spliceActionRows(0, this.data.components.length, ...normalized); + this.spliceLabelComponents(0, this.data.components.length, ...normalized); return this; } /** - * Removes, replaces, or inserts action rows for this modal. + * Removes, replaces, or inserts labels for this modal. * * @remarks * This method behaves similarly * to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}. - * The maximum amount of action rows that can be added is 5. + * The maximum amount of labels that can be added is 5. * - * It's useful for modifying and adjusting order of the already-existing action rows of a modal. + * It's useful for modifying and adjusting order of the already-existing labels of a modal. * @example - * Remove the first action row: + * Remove the first label: * ```ts - * embed.spliceActionRows(0, 1); + * modal.spliceLabelComponents(0, 1); * ``` * @example - * Remove the first n action rows: + * Remove the first n labels: * ```ts * const n = 4; - * embed.spliceActionRows(0, n); + * modal.spliceLabelComponents(0, n); * ``` * @example - * Remove the last action row: + * Remove the last label: * ```ts - * embed.spliceActionRows(-1, 1); + * modal.spliceLabelComponents(-1, 1); * ``` * @param index - The index to start at - * @param deleteCount - The number of action rows to remove - * @param rows - The replacing action row objects + * @param deleteCount - The number of labels to remove + * @param labels - The replacing label objects */ - public spliceActionRows( + public spliceLabelComponents( index: number, deleteCount: number, - ...rows: ( - | ActionRowBuilder - | APIActionRowComponent - | ((builder: ActionRowBuilder) => ActionRowBuilder) - )[] + ...labels: (APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder))[] ): this { - const resolved = rows.map((row) => resolveBuilder(row, ActionRowBuilder)); + const resolved = labels.map((label) => resolveBuilder(label, LabelBuilder)); this.data.components.splice(index, deleteCount, ...resolved); return this;