feat: v1 builders file uploads support (#11196)

* feat: support file uploads

* test: add tests

* style: sort imports

* feat(Label): add method
This commit is contained in:
Jiralite
2025-10-24 16:10:47 +01:00
committed by GitHub
parent 4288afbc35
commit 1417c498a4
9 changed files with 190 additions and 6 deletions

View File

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

View File

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

View File

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

View File

@@ -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(),
});

View File

@@ -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<APIFileUploadComponent> {
/**
* 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<APIFileUploadComponent> = {}) {
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;
}
}

View File

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

View File

@@ -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<Omit<APILabelComponent, 'component'>> {
component?:
| ChannelSelectMenuBuilder
| FileUploadBuilder
| MentionableSelectMenuBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
@@ -179,6 +182,18 @@ export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
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}
*/

View File

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

10
pnpm-lock.yaml generated
View File

@@ -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: {}