feat: Add Modals and Text Inputs (#7023)

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Ryan Munro <monbrey@gmail.com>
Co-authored-by: Vitor <milagre.vitor@gmail.com>
This commit is contained in:
Suneet Tipirneni
2022-03-04 02:53:41 -05:00
committed by GitHub
parent 53defb82e3
commit ed92015634
31 changed files with 1075 additions and 115 deletions

View File

@@ -1,26 +1,42 @@
import { type APIActionRowComponent, ComponentType, APIMessageComponent } from 'discord-api-types/v9';
import type { ButtonComponent, SelectMenuComponent } from '..';
import {
APIActionRowComponent,
APIMessageActionRowComponent,
APIModalActionRowComponent,
ComponentType,
} from 'discord-api-types/v9';
import type { ButtonComponent, SelectMenuComponent, TextInputComponent } from '../index';
import { Component } from './Component';
import { createComponent } from './Components';
import isEqual from 'fast-deep-equal';
export type MessageComponent = ActionRowComponent | ActionRow;
export type MessageComponent = MessageActionRowComponent | ActionRow<MessageActionRowComponent>;
export type ModalComponent = ModalActionRowComponent | ActionRow<ModalActionRowComponent>;
export type ActionRowComponent = ButtonComponent | SelectMenuComponent;
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
// TODO: Add valid form component types
/**
* Represents an action row component
*/
export class ActionRow<T extends ActionRowComponent = ActionRowComponent> extends Component<
Omit<Partial<APIActionRowComponent<APIMessageComponent>> & { type: ComponentType.ActionRow }, 'components'>
export class ActionRow<
T extends ModalActionRowComponent | MessageActionRowComponent = ModalActionRowComponent | MessageActionRowComponent,
> extends Component<
Omit<
Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> & {
type: ComponentType.ActionRow;
},
'components'
>
> {
/**
* The components within this action row
*/
public readonly components: T[];
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIMessageComponent>> = {}) {
public constructor({
components,
...data
}: Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> = {}) {
super({ type: ComponentType.ActionRow, ...data });
this.components = (components?.map((c) => createComponent(c)) ?? []) as T[];
}
@@ -44,14 +60,14 @@ export class ActionRow<T extends ActionRowComponent = ActionRowComponent> extend
return this;
}
public toJSON(): APIActionRowComponent<APIMessageComponent> {
public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> {
return {
...this.data,
components: this.components.map((component) => component.toJSON()),
components: this.components.map((component) => component.toJSON()) as ReturnType<T['toJSON']>[],
};
}
public equals(other: APIActionRowComponent<APIMessageComponent> | ActionRow) {
public equals(other: APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent> | ActionRow) {
if (other instanceof ActionRow) {
return isEqual(other.data, this.data) && isEqual(other.components, this.components);
}

View File

@@ -1,9 +1,13 @@
import type { JSONEncodable } from '../util/jsonEncodable';
import type {
APIActionRowComponent,
APIActionRowComponentTypes,
APIBaseComponent,
APIMessageActionRowComponent,
APIModalActionRowComponent,
APIMessageComponent,
ComponentType,
APIModalComponent,
} from 'discord-api-types/v9';
import type { Equatable } from '../util/equatable';
@@ -14,16 +18,33 @@ export abstract class Component<
DataType extends Partial<APIBaseComponent<ComponentType>> & {
type: ComponentType;
} = APIBaseComponent<ComponentType>,
> implements JSONEncodable<APIMessageComponent>, Equatable<Component | APIActionRowComponentTypes>
> implements
JSONEncodable<
| APIModalComponent
| APIMessageComponent
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
>,
Equatable<
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
>
{
/**
* The API data associated with this component
*/
public readonly data: DataType;
public abstract toJSON(): APIMessageComponent;
public abstract toJSON():
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>;
public abstract equals(other: Component | APIActionRowComponentTypes): boolean;
public abstract equals(
other:
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>,
): boolean;
public constructor(data: DataType) {
this.data = data;

View File

@@ -1,11 +1,12 @@
import { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Component, SelectMenuComponent } from '../index';
import type { MessageComponent } from './ActionRow';
import { APIBaseComponent, APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Component, SelectMenuComponent, TextInputComponent } from '../index';
import type { MessageComponent, ModalActionRowComponent } from './ActionRow';
export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRow;
[ComponentType.Button]: ButtonComponent;
[ComponentType.SelectMenu]: SelectMenuComponent;
[ComponentType.TextInput]: TextInputComponent;
}
/**
@@ -13,10 +14,10 @@ export interface MappedComponentTypes {
* @param data The api data to transform to a component class
*/
export function createComponent<T extends keyof MappedComponentTypes>(
data: APIMessageComponent & { type: T },
data: (APIMessageComponent | APIModalComponent) & { type: T },
): MappedComponentTypes[T];
export function createComponent<C extends MessageComponent>(data: C): C;
export function createComponent(data: APIMessageComponent | MessageComponent): Component {
export function createComponent<C extends MessageComponent | ModalActionRowComponent>(data: C): C;
export function createComponent(data: APIModalComponent | APIMessageComponent | Component): Component {
if (data instanceof Component) {
return data;
}
@@ -28,8 +29,9 @@ export function createComponent(data: APIMessageComponent | MessageComponent): C
return new ButtonComponent(data);
case ComponentType.SelectMenu:
return new SelectMenuComponent(data);
case ComponentType.TextInput:
return new TextInputComponent(data);
default:
// @ts-expect-error
throw new Error(`Cannot serialize component type: ${data.type as number}`);
throw new Error(`Cannot serialize component type: ${(data as APIBaseComponent<ComponentType>).type}`);
}
}

View File

@@ -0,0 +1,17 @@
import { TextInputStyle } from 'discord-api-types/v9';
import { z } from 'zod';
import { customIdValidator } from '../Assertions';
export const textInputStyleValidator = z.nativeEnum(TextInputStyle);
export const minLengthValidator = z.number().int().min(0).max(4000);
export const maxLengthValidator = z.number().int().min(1).max(4000);
export const requiredValidator = z.boolean();
export const valueValidator = z.string().max(4000);
export const placeholderValidator = z.string().max(100);
export const labelValidator = z.string().min(1).max(45);
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
customIdValidator.parse(customId);
textInputStyleValidator.parse(style);
labelValidator.parse(label);
}

View File

@@ -0,0 +1,37 @@
import type { APITextInputComponent } from 'discord-api-types/v9';
import {
maxLengthValidator,
minLengthValidator,
placeholderValidator,
requiredValidator,
valueValidator,
validateRequiredParameters,
} from './Assertions';
import { UnsafeTextInputComponent } from './UnsafeTextInput';
export class TextInputComponent extends UnsafeTextInputComponent {
public override setMinLength(minLength: number) {
return super.setMinLength(minLengthValidator.parse(minLength));
}
public override setMaxLength(maxLength: number) {
return super.setMaxLength(maxLengthValidator.parse(maxLength));
}
public override setRequired(required = true) {
return super.setRequired(requiredValidator.parse(required));
}
public override setValue(value: string) {
return super.setValue(valueValidator.parse(value));
}
public override setPlaceholder(placeholder: string) {
return super.setPlaceholder(placeholderValidator.parse(placeholder));
}
public override toJSON(): APITextInputComponent {
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
return super.toJSON();
}
}

View File

@@ -0,0 +1,154 @@
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v9';
import { Component } from '../../index';
import isEqual from 'fast-deep-equal';
export class UnsafeTextInputComponent extends Component<
Partial<APITextInputComponent> & { type: ComponentType.TextInput }
> {
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
super({ type: ComponentType.TextInput, ...data });
}
/**
* The style of this text input
*/
public get style() {
return this.data.style;
}
/**
* The custom id of this text input
*/
public get customId() {
return this.data.custom_id;
}
/**
* The label for this text input
*/
public get label() {
return this.data.label;
}
/**
* The placeholder text for this text input
*/
public get placeholder() {
return this.data.placeholder;
}
/**
* The default value for this text input
*/
public get value() {
return this.data.value;
}
/**
* The minimum length of this text input
*/
public get minLength() {
return this.data.min_length;
}
/**
* The maximum length of this text input
*/
public get maxLength() {
return this.data.max_length;
}
/**
* Whether this text input is required
*/
public get required() {
return this.data.required;
}
/**
* Sets the custom id for this text input
* @param customId The custom id of this text input
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
return this;
}
/**
* Sets the label for this text input
* @param label The label for this text input
*/
public setLabel(label: string) {
this.data.label = label;
return this;
}
/**
* Sets the style for this text input
* @param style The style for this text input
*/
public setStyle(style: TextInputStyle) {
this.data.style = style;
return this;
}
/**
* Sets the minimum length of text for this text input
* @param minLength The minimum length of text for this text input
*/
public setMinLength(minLength: number) {
this.data.min_length = minLength;
return this;
}
/**
* Sets the maximum length of text for this text input
* @param maxLength The maximum length of text for this text input
*/
public setMaxLength(maxLength: number) {
this.data.max_length = maxLength;
return this;
}
/**
* Sets the placeholder of this text input
* @param placeholder The placeholder of this text input
*/
public setPlaceholder(placeholder: string) {
this.data.placeholder = placeholder;
return this;
}
/**
* Sets the value of this text input
* @param value The value for this text input
*/
public setValue(value: string) {
this.data.value = value;
return this;
}
/**
* Sets whether this text input is required or not
* @param required Whether this text input is required or not
*/
public setRequired(required = true) {
this.data.required = required;
return this;
}
public toJSON(): APITextInputComponent {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return {
...this.data,
} as APITextInputComponent;
}
public equals(other: UnsafeTextInputComponent | APITextInputComponent): boolean {
if (other instanceof UnsafeTextInputComponent) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
}