mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 03:23:29 +01:00
feat(builders): modal select menus in builders v1 (#11138)
* feat(builders): modal select menus * chore: fix test * fix: move setRequired up * fix: pack * chore: forwardport https://github.com/discordjs/discord.js/pull/11139 * types: expect errors * fix: id validator being required * fix: validators 2 * Apply suggestion from @Jiralite * Apply suggestion from @Jiralite * Apply suggestion from @Jiralite * fix: replace tests * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -100,7 +100,7 @@ describe('Text Input Components', () => {
|
||||
.setPlaceholder('hello')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).toThrowError();
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type APIContainerComponent, ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { ButtonBuilder } from '../../../dist/index.mjs';
|
||||
import { ActionRowBuilder } from '../../../src/components/ActionRow.js';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { ButtonBuilder } from '../../../src/components/button/Button.js';
|
||||
import { ContainerBuilder } from '../../../src/components/v2/Container.js';
|
||||
import { FileBuilder } from '../../../src/components/v2/File.js';
|
||||
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js';
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"@discordjs/formatters": "workspace:^",
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"discord-api-types": "^0.38.26",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
APIMessageComponent,
|
||||
APIModalComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { idValidator } from './Assertions';
|
||||
|
||||
@@ -14,7 +15,8 @@ import { idValidator } from './Assertions';
|
||||
export type AnyAPIActionRowComponent =
|
||||
| APIActionRowComponent<APIComponentInActionRow>
|
||||
| APIComponentInActionRow
|
||||
| APIMessageComponent;
|
||||
| APIMessageComponent
|
||||
| APIModalComponent;
|
||||
|
||||
/**
|
||||
* The base component builder that contains common symbols for all sorts of components.
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './ActionRow.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
import { ButtonBuilder } from './button/Button.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';
|
||||
@@ -100,6 +101,10 @@ export interface MappedComponentTypes {
|
||||
* The media gallery component type is associated with a {@link MediaGalleryBuilder}.
|
||||
*/
|
||||
[ComponentType.MediaGallery]: MediaGalleryBuilder;
|
||||
/**
|
||||
* The label component type is associated with a {@link LabelBuilder}.
|
||||
*/
|
||||
[ComponentType.Label]: LabelBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +166,8 @@ export function createComponentBuilder(
|
||||
return new ThumbnailBuilder(data);
|
||||
case ComponentType.MediaGallery:
|
||||
return new MediaGalleryBuilder(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}`);
|
||||
|
||||
29
packages/builders/src/components/label/Assertions.ts
Normal file
29
packages/builders/src/components/label/Assertions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { idValidator } from '../Assertions.js';
|
||||
import {
|
||||
selectMenuChannelPredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuStringPredicate,
|
||||
selectMenuUserPredicate,
|
||||
} from '../selectMenu/Assertions.js';
|
||||
import { textInputPredicate } from '../textInput/Assertions.js';
|
||||
|
||||
export const labelPredicate = s
|
||||
.object({
|
||||
id: idValidator.optional(),
|
||||
type: s.literal(ComponentType.Label),
|
||||
label: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45),
|
||||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(),
|
||||
component: s.union([
|
||||
textInputPredicate,
|
||||
selectMenuUserPredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuChannelPredicate,
|
||||
selectMenuStringPredicate,
|
||||
]),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
198
packages/builders/src/components/label/Label.ts
Normal file
198
packages/builders/src/components/label/Label.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type {
|
||||
APIChannelSelectComponent,
|
||||
APILabelComponent,
|
||||
APIMentionableSelectComponent,
|
||||
APIRoleSelectComponent,
|
||||
APIStringSelectComponent,
|
||||
APITextInputComponent,
|
||||
APIUserSelectComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from '../selectMenu/RoleSelectMenu.js';
|
||||
import { StringSelectMenuBuilder } from '../selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from '../selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from '../textInput/TextInput.js';
|
||||
import { labelPredicate } from './Assertions.js';
|
||||
|
||||
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> {
|
||||
component?:
|
||||
| ChannelSelectMenuBuilder
|
||||
| MentionableSelectMenuBuilder
|
||||
| RoleSelectMenuBuilder
|
||||
| StringSelectMenuBuilder
|
||||
| TextInputBuilder
|
||||
| UserSelectMenuBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for labels.
|
||||
*/
|
||||
export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public override 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,
|
||||
* }).setLabel('new text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APILabelComponent> = {}) {
|
||||
super({ type: ComponentType.Label });
|
||||
|
||||
const { component, ...rest } = data;
|
||||
|
||||
this.data = {
|
||||
...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 user select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setUserSelectMenuComponent(
|
||||
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, UserSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a role select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setRoleSelectMenuComponent(
|
||||
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, RoleSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a mentionable select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setMentionableSelectMenuComponent(
|
||||
input:
|
||||
| APIMentionableSelectComponent
|
||||
| MentionableSelectMenuBuilder
|
||||
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, MentionableSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a channel select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setChannelSelectMenuComponent(
|
||||
input:
|
||||
| APIChannelSelectComponent
|
||||
| ChannelSelectMenuBuilder
|
||||
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, ChannelSelectMenuBuilder);
|
||||
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(): APILabelComponent {
|
||||
const { component, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...rest,
|
||||
// The label predicate validates the component.
|
||||
component: component?.toJSON(),
|
||||
};
|
||||
|
||||
labelPredicate.parse(data);
|
||||
|
||||
return data as APILabelComponent;
|
||||
}
|
||||
}
|
||||
92
packages/builders/src/components/selectMenu/Assertions.ts
Normal file
92
packages/builders/src/components/selectMenu/Assertions.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Result, s } from '@sapphire/shapeshift';
|
||||
import { ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { customIdValidator, emojiValidator, idValidator } from '../Assertions.js';
|
||||
import { labelValidator } from '../textInput/Assertions.js';
|
||||
|
||||
const selectMenuBasePredicate = s.object({
|
||||
id: idValidator.optional(),
|
||||
placeholder: s.string().lengthLessThanOrEqual(150).optional(),
|
||||
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(),
|
||||
max_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(),
|
||||
custom_id: customIdValidator,
|
||||
disabled: s.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuChannelPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.ChannelSelect),
|
||||
channel_types: s.nativeEnum(ChannelType).array().optional(),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Channel) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuMentionablePredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.MentionableSelect),
|
||||
default_values: s
|
||||
.object({
|
||||
id: s.string(),
|
||||
type: s.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]),
|
||||
})
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuRolePredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.RoleSelect),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Role) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuUserPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.UserSelect),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.User) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuStringOptionPredicate = s
|
||||
.object({
|
||||
label: labelValidator,
|
||||
value: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100),
|
||||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(),
|
||||
emoji: emojiValidator.optional(),
|
||||
default: s.boolean().optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.StringSelect),
|
||||
options: selectMenuStringOptionPredicate.array().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(25),
|
||||
})
|
||||
.reshape((value) => {
|
||||
if (value.min_values !== undefined && value.options.length < value.min_values) {
|
||||
return Result.err(new RangeError(`The number of options must be greater than or equal to min_values`));
|
||||
}
|
||||
|
||||
if (value.min_values !== undefined && value.max_values !== undefined && value.min_values > value.max_values) {
|
||||
return Result.err(
|
||||
new RangeError(`The maximum amount of options must be greater than or equal to the minimum amount of options`),
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { APISelectMenuComponent } from 'discord-api-types/v10';
|
||||
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { requiredValidator } from '../textInput/Assertions.js';
|
||||
|
||||
/**
|
||||
* The base select menu builder that contains common symbols for select menu builders.
|
||||
@@ -60,6 +61,17 @@ export abstract class BaseSelectMenuBuilder<
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this select menu is required.
|
||||
*
|
||||
* @remarks Only for use in modals.
|
||||
* @param required - Whether this select menu is required
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = requiredValidator.parse(required);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { TextInputStyle } from 'discord-api-types/v10';
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { customIdValidator } from '../Assertions.js';
|
||||
import { customIdValidator, idValidator } from '../Assertions.js';
|
||||
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle).setValidationEnabled(isValidationEnabled);
|
||||
export const minLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
@@ -16,7 +16,7 @@ export const maxLengthValidator = s
|
||||
.greaterThanOrEqual(1)
|
||||
.lessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const requiredValidator = s.boolean();
|
||||
export const requiredValidator = s.boolean().setValidationEnabled(isValidationEnabled);
|
||||
export const valueValidator = s.string().lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled);
|
||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled);
|
||||
export const labelValidator = s
|
||||
@@ -25,8 +25,21 @@ export const labelValidator = s
|
||||
.lengthLessThanOrEqual(45)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
||||
export const textInputPredicate = s
|
||||
.object({
|
||||
type: s.literal(ComponentType.TextInput),
|
||||
custom_id: customIdValidator,
|
||||
style: textInputStyleValidator,
|
||||
id: idValidator.optional(),
|
||||
min_length: minLengthValidator.optional(),
|
||||
max_length: maxLengthValidator.optional(),
|
||||
placeholder: placeholderValidator.optional(),
|
||||
value: valueValidator.optional(),
|
||||
required: requiredValidator.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle) {
|
||||
customIdValidator.parse(customId);
|
||||
textInputStyleValidator.parse(style);
|
||||
labelValidator.parse(label);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TextInputBuilder
|
||||
* ```ts
|
||||
* const textInput = new TextInputBuilder({
|
||||
* custom_id: 'a cool text input',
|
||||
* label: 'Type something',
|
||||
* placeholder: 'Type something',
|
||||
* style: TextInputStyle.Short,
|
||||
* });
|
||||
* ```
|
||||
@@ -38,7 +38,7 @@ export class TextInputBuilder
|
||||
* 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);
|
||||
@@ -62,6 +62,7 @@ export class TextInputBuilder
|
||||
* Sets the label for this text input.
|
||||
*
|
||||
* @param label - The label to use
|
||||
* @deprecated Use a label builder to create a label (and optionally a description) instead.
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = labelValidator.parse(label);
|
||||
@@ -132,7 +133,7 @@ export class TextInputBuilder
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APITextInputComponent {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style);
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
|
||||
@@ -34,6 +34,9 @@ export {
|
||||
export * from './components/selectMenu/StringSelectMenuOption.js';
|
||||
export * from './components/selectMenu/UserSelectMenu.js';
|
||||
|
||||
export * from './components/label/Label.js';
|
||||
export * as LabelAssertions from './components/label/Assertions.js';
|
||||
|
||||
export * as ComponentsV2Assertions from './components/v2/Assertions.js';
|
||||
export * from './components/v2/Container.js';
|
||||
export * from './components/v2/File.js';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
||||
import { customIdValidator } from '../../components/Assertions.js';
|
||||
import { LabelBuilder } from '../../components/label/Label.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
|
||||
export const titleValidator = s
|
||||
@@ -9,7 +10,7 @@ export const titleValidator = s
|
||||
.lengthLessThanOrEqual(45)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const componentsValidator = s
|
||||
.instance(ActionRowBuilder)
|
||||
.union([s.instance(ActionRowBuilder), s.instance(LabelBuilder)])
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
@@ -17,7 +18,7 @@ export const componentsValidator = s
|
||||
export function validateRequiredParameters(
|
||||
customId?: string,
|
||||
title?: string,
|
||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
||||
components?: (ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder)[],
|
||||
) {
|
||||
customIdValidator.parse(customId);
|
||||
titleValidator.parse(title);
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APITextInputComponent,
|
||||
APIActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
APILabelComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
||||
import { customIdValidator } from '../../components/Assertions.js';
|
||||
import { createComponentBuilder } from '../../components/Components.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../../components/Components.js';
|
||||
import { LabelBuilder } from '../../components/label/Label.js';
|
||||
import { TextInputBuilder } from '../../components/textInput/TextInput.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { titleValidator, validateRequiredParameters } from './Assertions.js';
|
||||
|
||||
@@ -24,7 +29,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
/**
|
||||
* The components within this modal.
|
||||
*/
|
||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
||||
public readonly components: (ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder)[] = [];
|
||||
|
||||
/**
|
||||
* Creates a new modal from API data.
|
||||
@@ -33,8 +38,10 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||
this.data = { ...data };
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ??
|
||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as (
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| LabelBuilder
|
||||
)[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,28 +68,150 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
* Adds components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
* @deprecated Use {@link ModalBuilder.addLabelComponents} or {@link ModalBuilder.addActionRowComponents} instead
|
||||
*/
|
||||
public addComponents(
|
||||
...components: RestOrArray<
|
||||
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| APILabelComponent
|
||||
| APITextInputComponent
|
||||
| LabelBuilder
|
||||
| TextInputBuilder
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) =>
|
||||
component instanceof ActionRowBuilder
|
||||
? component
|
||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
||||
),
|
||||
...normalizeArray(components).map((component, idx) => {
|
||||
if (component instanceof ActionRowBuilder || component instanceof LabelBuilder) {
|
||||
return component;
|
||||
}
|
||||
|
||||
if (component instanceof TextInputBuilder) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(component);
|
||||
}
|
||||
|
||||
if ('type' in component) {
|
||||
if (component.type === ComponentType.ActionRow) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>(component);
|
||||
}
|
||||
|
||||
if (component.type === ComponentType.Label) {
|
||||
return new LabelBuilder(component);
|
||||
}
|
||||
|
||||
if (component.type === ComponentType.TextInput) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
||||
new TextInputBuilder(component),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid component passed in ModalBuilder.addComponents at index ${idx}!`);
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public addLabelComponents(
|
||||
...components: RestOrArray<APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder)>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((label) => resolveBuilder(label, LabelBuilder));
|
||||
|
||||
this.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds action rows to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
* @deprecated Use {@link ModalBuilder.addLabelComponents} instead
|
||||
*/
|
||||
public addActionRowComponents(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ((
|
||||
builder: ActionRowBuilder<ModalActionRowComponentBuilder>,
|
||||
) => ActionRowBuilder<ModalActionRowComponentBuilder>)
|
||||
>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder<ModalActionRowComponentBuilder>));
|
||||
|
||||
this.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the labels for this modal.
|
||||
*
|
||||
* @param components - The components to set
|
||||
*/
|
||||
public setLabelComponents(
|
||||
...components: RestOrArray<APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder)>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
this.spliceLabelComponents(0, this.components.length, ...normalized);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 labels that can be added is 5.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing labels of a modal.
|
||||
* @example
|
||||
* Remove the first label:
|
||||
* ```ts
|
||||
* modal.spliceLabelComponents(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n labels:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* modal.spliceLabelComponents(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last label:
|
||||
* ```ts
|
||||
* modal.spliceLabelComponents(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of labels to remove
|
||||
* @param labels - The replacing label objects
|
||||
*/
|
||||
public spliceLabelComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...labels: (APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder))[]
|
||||
): this {
|
||||
const resolved = labels.map((label) => resolveBuilder(label, LabelBuilder));
|
||||
this.components.splice(index, deleteCount, ...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets components for this modal.
|
||||
*
|
||||
* @param components - The components to set
|
||||
* @deprecated Use {@link ModalBuilder.setLabelComponents} instead
|
||||
*/
|
||||
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder>>) {
|
||||
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder>) {
|
||||
this.components.splice(0, this.components.length, ...normalizeArray(components));
|
||||
return this;
|
||||
}
|
||||
|
||||
6
packages/discord.js/typings/index.d.ts
vendored
6
packages/discord.js/typings/index.d.ts
vendored
@@ -314,8 +314,9 @@ export class ActionRowBuilder<
|
||||
>,
|
||||
);
|
||||
public static from<ComponentType extends AnyComponentBuilder = AnyComponentBuilder>(
|
||||
other:
|
||||
| JSONEncodable<APIActionRowComponent<ReturnType<ComponentType['toJSON']>>>
|
||||
other: // @ts-expect-error builders/1.x.
|
||||
| JSONEncodable<APIActionRowComponent<ReturnType<ComponentType['toJSON']>>>
|
||||
// @ts-expect-error builders/1.x.
|
||||
| APIActionRowComponent<ReturnType<ComponentType['toJSON']>>,
|
||||
): ActionRowBuilder<ComponentType>;
|
||||
}
|
||||
@@ -6461,6 +6462,7 @@ export interface BaseMessageOptions {
|
||||
)[];
|
||||
components?: readonly (
|
||||
| JSONEncodable<APIActionRowComponent<APIComponentInMessageActionRow>>
|
||||
// @ts-expect-error builders/1.x.
|
||||
| ActionRowData<MessageActionRowComponentData | MessageActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
||||
)[];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user