diff --git a/packages/builders/__tests__/components/fileUpload.test.ts b/packages/builders/__tests__/components/fileUpload.test.ts new file mode 100644 index 000000000..deaee5cc6 --- /dev/null +++ b/packages/builders/__tests__/components/fileUpload.test.ts @@ -0,0 +1,46 @@ +import type { APIFileUploadComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { FileUploadBuilder } from '../../src/components/fileUpload/FileUpload.js'; + +describe('File Upload Components', () => { + test('Valid builder does not throw.', () => { + expect(() => new FileUploadBuilder().setCustomId('file_upload').toJSON()).not.toThrowError(); + expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(5).toJSON()).not.toThrowError(); + + expect(() => + new FileUploadBuilder().setCustomId('file_upload').setMaxValues(5).setMinValues(2).toJSON(), + ).not.toThrowError(); + + expect(() => new FileUploadBuilder().setCustomId('file_upload').setRequired(false).toJSON()).not.toThrowError(); + }); + + test('Invalid builder does throw.', () => { + expect(() => new FileUploadBuilder().toJSON()).toThrowError(); + expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(-3).toJSON()).toThrowError(); + expect(() => new FileUploadBuilder().setMaxValues(5).setMinValues(2).setId(10).toJSON()).toThrowError(); + expect(() => new FileUploadBuilder().setCustomId('file_upload').setMaxValues(500).toJSON()).toThrowError(); + + expect(() => + new FileUploadBuilder().setCustomId('file_upload').setMinValues(500).setMaxValues(501).toJSON(), + ).toThrowError(); + + expect(() => new FileUploadBuilder().setRequired(false).toJSON()).toThrowError(); + }); + + test('API data equals toJSON().', () => { + const fileUploadData = { + type: ComponentType.FileUpload, + custom_id: 'file_upload', + min_values: 4, + max_values: 9, + required: false, + } satisfies APIFileUploadComponent; + + expect(new FileUploadBuilder(fileUploadData).toJSON()).toEqual(fileUploadData); + + expect( + new FileUploadBuilder().setCustomId('file_upload').setMinValues(4).setMaxValues(9).setRequired(false).toJSON(), + ).toEqual(fileUploadData); + }); +}); diff --git a/packages/builders/package.json b/packages/builders/package.json index e62dd5227..16a52875f 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -68,7 +68,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.26", + "discord-api-types": "^0.38.31", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 4f9e174ca..243694f06 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -8,6 +8,7 @@ import { } from './ActionRow.js'; import { ComponentBuilder } from './Component.js'; import { ButtonBuilder } from './button/Button.js'; +import { FileUploadBuilder } from './fileUpload/FileUpload.js'; import { LabelBuilder } from './label/Label.js'; import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js'; import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js'; @@ -105,6 +106,10 @@ export interface MappedComponentTypes { * The label component type is associated with a {@link LabelBuilder}. */ [ComponentType.Label]: LabelBuilder; + /** + * The file upload component type is associated with a {@link FileUploadBuilder}. + */ + [ComponentType.FileUpload]: FileUploadBuilder; } /** @@ -168,6 +173,8 @@ export function createComponentBuilder( return new MediaGalleryBuilder(data); case ComponentType.Label: return new LabelBuilder(data); + case ComponentType.FileUpload: + return new FileUploadBuilder(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/fileUpload/Assertions.ts b/packages/builders/src/components/fileUpload/Assertions.ts new file mode 100644 index 000000000..8f3683d16 --- /dev/null +++ b/packages/builders/src/components/fileUpload/Assertions.ts @@ -0,0 +1,12 @@ +import { s } from '@sapphire/shapeshift'; +import { ComponentType } from 'discord-api-types/v10'; +import { customIdValidator, idValidator } from '../Assertions.js'; + +export const fileUploadPredicate = s.object({ + type: s.literal(ComponentType.FileUpload), + id: idValidator.optional(), + custom_id: customIdValidator, + min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(10).optional(), + max_values: s.number().greaterThanOrEqual(1).lessThanOrEqual(10).optional(), + required: s.boolean().optional(), +}); diff --git a/packages/builders/src/components/fileUpload/FileUpload.ts b/packages/builders/src/components/fileUpload/FileUpload.ts new file mode 100644 index 000000000..6bbb147af --- /dev/null +++ b/packages/builders/src/components/fileUpload/FileUpload.ts @@ -0,0 +1,99 @@ +import { type APIFileUploadComponent, ComponentType } from 'discord-api-types/v10'; +import { ComponentBuilder } from '../Component.js'; +import { fileUploadPredicate } from './Assertions.js'; + +/** + * A builder that creates API-compatible JSON data for file uploads. + */ +export class FileUploadBuilder extends ComponentBuilder { + /** + * Creates a new file upload. + * + * @param data - The API data to create this file upload with + * @example + * Creating a file upload from an API data object: + * ```ts + * const fileUpload = new FileUploadBuilder({ + * custom_id: "file_upload", + * min_values: 2, + * max_values: 5, + * }); + * ``` + * @example + * Creating a file upload using setters and API data: + * ```ts + * const fileUpload = new FileUploadBuilder({ + * custom_id: "file_upload", + * min_values: 2, + * max_values: 5, + * }).setRequired(); + * ``` + */ + public constructor(data: Partial = {}) { + super({ type: ComponentType.FileUpload, ...data }); + } + + /** + * Sets the custom id for this file upload. + * + * @param customId - The custom id to use + */ + public setCustomId(customId: string) { + this.data.custom_id = customId; + return this; + } + + /** + * Sets the minimum number of file uploads required. + * + * @param minValues - The minimum values that must be uploaded + */ + public setMinValues(minValues: number) { + this.data.min_values = minValues; + return this; + } + + /** + * Clears the minimum values. + */ + public clearMinValues() { + this.data.min_values = undefined; + return this; + } + + /** + * Sets the maximum number of file uploads required. + * + * @param maxValues - The maximum values that must be uploaded + */ + public setMaxValues(maxValues: number) { + this.data.max_values = maxValues; + return this; + } + + /** + * Clears the maximum values. + */ + public clearMaxValues() { + this.data.max_values = undefined; + return this; + } + + /** + * Sets whether this file upload is required. + * + * @param required - Whether this file upload is required + */ + public setRequired(required = true) { + this.data.required = required; + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public toJSON(): APIFileUploadComponent { + fileUploadPredicate.parse(this.data); + return this.data as APIFileUploadComponent; + } +} diff --git a/packages/builders/src/components/label/Assertions.ts b/packages/builders/src/components/label/Assertions.ts index 6da7a68fb..8e17ca4c3 100644 --- a/packages/builders/src/components/label/Assertions.ts +++ b/packages/builders/src/components/label/Assertions.ts @@ -2,6 +2,7 @@ import { s } from '@sapphire/shapeshift'; import { ComponentType } from 'discord-api-types/v10'; import { isValidationEnabled } from '../../util/validation.js'; import { idValidator } from '../Assertions.js'; +import { fileUploadPredicate } from '../fileUpload/Assertions.js'; import { selectMenuChannelPredicate, selectMenuMentionablePredicate, @@ -24,6 +25,7 @@ export const labelPredicate = s selectMenuMentionablePredicate, selectMenuChannelPredicate, selectMenuStringPredicate, + fileUploadPredicate, ]), }) .setValidationEnabled(isValidationEnabled); diff --git a/packages/builders/src/components/label/Label.ts b/packages/builders/src/components/label/Label.ts index 98e656d1f..a68589d24 100644 --- a/packages/builders/src/components/label/Label.ts +++ b/packages/builders/src/components/label/Label.ts @@ -1,5 +1,6 @@ import type { APIChannelSelectComponent, + APIFileUploadComponent, APILabelComponent, APIMentionableSelectComponent, APIRoleSelectComponent, @@ -10,6 +11,7 @@ import type { import { ComponentType } from 'discord-api-types/v10'; import { ComponentBuilder } from '../Component.js'; import { createComponentBuilder, resolveBuilder } from '../Components.js'; +import { FileUploadBuilder } from '../fileUpload/FileUpload.js'; import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js'; import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js'; import { RoleSelectMenuBuilder } from '../selectMenu/RoleSelectMenu.js'; @@ -21,6 +23,7 @@ import { labelPredicate } from './Assertions.js'; export interface LabelBuilderData extends Partial> { component?: | ChannelSelectMenuBuilder + | FileUploadBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder @@ -179,6 +182,18 @@ export class LabelBuilder extends ComponentBuilder { return this; } + /** + * Sets a file upload component to this label. + * + * @param input - A function that returns a component builder or an already built builder + */ + public setFileUploadComponent( + input: APIFileUploadComponent | FileUploadBuilder | ((builder: FileUploadBuilder) => FileUploadBuilder), + ): this { + this.data.component = resolveBuilder(input, FileUploadBuilder); + return this; + } + /** * {@inheritDoc ComponentBuilder.toJSON} */ diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 84ceb8aeb..3fe81285e 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -34,6 +34,9 @@ export { export * from './components/selectMenu/StringSelectMenuOption.js'; export * from './components/selectMenu/UserSelectMenu.js'; +export * from './components/fileUpload/FileUpload.js'; +export * as FileUploadAssertions from './components/fileUpload/Assertions.js'; + export * from './components/label/Label.js'; export * as LabelAssertions from './components/label/Assertions.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b1865817..d512c179d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,8 +680,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 discord-api-types: - specifier: ^0.38.26 - version: 0.38.26 + specifier: ^0.38.31 + version: 0.38.31 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -7654,8 +7654,8 @@ packages: discord-api-types@0.38.16: resolution: {integrity: sha512-Cz42dC5WqJD17Yk0bRy7YLTJmh3NKo4FGpxZuA8MHqT0RPxKSrll5YhlODZ2z5DiEV/gpHMeTSrTFTWpSXjT1Q==} - discord-api-types@0.38.26: - resolution: {integrity: sha512-xpmPviHjIJ6dFu1eNwNDIGQ3N6qmPUUYFVAx/YZ64h7ZgPkTcKjnciD8bZe8Vbeji7yS5uYljyciunpq0J5NSw==} + discord-api-types@0.38.31: + resolution: {integrity: sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ==} dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -21553,7 +21553,7 @@ snapshots: discord-api-types@0.38.16: {} - discord-api-types@0.38.26: {} + discord-api-types@0.38.31: {} dlv@1.1.3: {}