Skip to content

Commit 1417c49

Browse files
authored
feat: v1 builders file uploads support (#11196)
* feat: support file uploads * test: add tests * style: sort imports * feat(Label): add method
1 parent 4288afb commit 1417c49

File tree

9 files changed

+190
-6
lines changed

9 files changed

+190
-6
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { APIFileUploadComponent } from 'discord-api-types/v10';
2+
import { ComponentType } from 'discord-api-types/v10';
3+
import { describe, test, expect } from 'vitest';
4+
import { FileUploadBuilder } from '../../src/components/fileUpload/FileUpload.js';
5+
6+
describe('File Upload Components', () => {
7+
test('Valid builder does not throw.', () => {
8+
expect(() => new FileUploadBuilder().setCustomId('file_upload').toJSON()).not.toThrowError();
9+
expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(5).toJSON()).not.toThrowError();
10+
11+
expect(() =>
12+
new FileUploadBuilder().setCustomId('file_upload').setMaxValues(5).setMinValues(2).toJSON(),
13+
).not.toThrowError();
14+
15+
expect(() => new FileUploadBuilder().setCustomId('file_upload').setRequired(false).toJSON()).not.toThrowError();
16+
});
17+
18+
test('Invalid builder does throw.', () => {
19+
expect(() => new FileUploadBuilder().toJSON()).toThrowError();
20+
expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(-3).toJSON()).toThrowError();
21+
expect(() => new FileUploadBuilder().setMaxValues(5).setMinValues(2).setId(10).toJSON()).toThrowError();
22+
expect(() => new FileUploadBuilder().setCustomId('file_upload').setMaxValues(500).toJSON()).toThrowError();
23+
24+
expect(() =>
25+
new FileUploadBuilder().setCustomId('file_upload').setMinValues(500).setMaxValues(501).toJSON(),
26+
).toThrowError();
27+
28+
expect(() => new FileUploadBuilder().setRequired(false).toJSON()).toThrowError();
29+
});
30+
31+
test('API data equals toJSON().', () => {
32+
const fileUploadData = {
33+
type: ComponentType.FileUpload,
34+
custom_id: 'file_upload',
35+
min_values: 4,
36+
max_values: 9,
37+
required: false,
38+
} satisfies APIFileUploadComponent;
39+
40+
expect(new FileUploadBuilder(fileUploadData).toJSON()).toEqual(fileUploadData);
41+
42+
expect(
43+
new FileUploadBuilder().setCustomId('file_upload').setMinValues(4).setMaxValues(9).setRequired(false).toJSON(),
44+
).toEqual(fileUploadData);
45+
});
46+
});

packages/builders/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"@discordjs/formatters": "workspace:^",
6969
"@discordjs/util": "workspace:^",
7070
"@sapphire/shapeshift": "^4.0.0",
71-
"discord-api-types": "^0.38.26",
71+
"discord-api-types": "^0.38.31",
7272
"fast-deep-equal": "^3.1.3",
7373
"ts-mixer": "^6.0.4",
7474
"tslib": "^2.6.3"

packages/builders/src/components/Components.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from './ActionRow.js';
99
import { ComponentBuilder } from './Component.js';
1010
import { ButtonBuilder } from './button/Button.js';
11+
import { FileUploadBuilder } from './fileUpload/FileUpload.js';
1112
import { LabelBuilder } from './label/Label.js';
1213
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
1314
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
@@ -105,6 +106,10 @@ export interface MappedComponentTypes {
105106
* The label component type is associated with a {@link LabelBuilder}.
106107
*/
107108
[ComponentType.Label]: LabelBuilder;
109+
/**
110+
* The file upload component type is associated with a {@link FileUploadBuilder}.
111+
*/
112+
[ComponentType.FileUpload]: FileUploadBuilder;
108113
}
109114

110115
/**
@@ -168,6 +173,8 @@ export function createComponentBuilder(
168173
return new MediaGalleryBuilder(data);
169174
case ComponentType.Label:
170175
return new LabelBuilder(data);
176+
case ComponentType.FileUpload:
177+
return new FileUploadBuilder(data);
171178
default:
172179
// @ts-expect-error This case can still occur if we get a newer unsupported component type
173180
throw new Error(`Cannot properly serialize component type: ${data.type}`);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { s } from '@sapphire/shapeshift';
2+
import { ComponentType } from 'discord-api-types/v10';
3+
import { customIdValidator, idValidator } from '../Assertions.js';
4+
5+
export const fileUploadPredicate = s.object({
6+
type: s.literal(ComponentType.FileUpload),
7+
id: idValidator.optional(),
8+
custom_id: customIdValidator,
9+
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(10).optional(),
10+
max_values: s.number().greaterThanOrEqual(1).lessThanOrEqual(10).optional(),
11+
required: s.boolean().optional(),
12+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { type APIFileUploadComponent, ComponentType } from 'discord-api-types/v10';
2+
import { ComponentBuilder } from '../Component.js';
3+
import { fileUploadPredicate } from './Assertions.js';
4+
5+
/**
6+
* A builder that creates API-compatible JSON data for file uploads.
7+
*/
8+
export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent> {
9+
/**
10+
* Creates a new file upload.
11+
*
12+
* @param data - The API data to create this file upload with
13+
* @example
14+
* Creating a file upload from an API data object:
15+
* ```ts
16+
* const fileUpload = new FileUploadBuilder({
17+
* custom_id: "file_upload",
18+
* min_values: 2,
19+
* max_values: 5,
20+
* });
21+
* ```
22+
* @example
23+
* Creating a file upload using setters and API data:
24+
* ```ts
25+
* const fileUpload = new FileUploadBuilder({
26+
* custom_id: "file_upload",
27+
* min_values: 2,
28+
* max_values: 5,
29+
* }).setRequired();
30+
* ```
31+
*/
32+
public constructor(data: Partial<APIFileUploadComponent> = {}) {
33+
super({ type: ComponentType.FileUpload, ...data });
34+
}
35+
36+
/**
37+
* Sets the custom id for this file upload.
38+
*
39+
* @param customId - The custom id to use
40+
*/
41+
public setCustomId(customId: string) {
42+
this.data.custom_id = customId;
43+
return this;
44+
}
45+
46+
/**
47+
* Sets the minimum number of file uploads required.
48+
*
49+
* @param minValues - The minimum values that must be uploaded
50+
*/
51+
public setMinValues(minValues: number) {
52+
this.data.min_values = minValues;
53+
return this;
54+
}
55+
56+
/**
57+
* Clears the minimum values.
58+
*/
59+
public clearMinValues() {
60+
this.data.min_values = undefined;
61+
return this;
62+
}
63+
64+
/**
65+
* Sets the maximum number of file uploads required.
66+
*
67+
* @param maxValues - The maximum values that must be uploaded
68+
*/
69+
public setMaxValues(maxValues: number) {
70+
this.data.max_values = maxValues;
71+
return this;
72+
}
73+
74+
/**
75+
* Clears the maximum values.
76+
*/
77+
public clearMaxValues() {
78+
this.data.max_values = undefined;
79+
return this;
80+
}
81+
82+
/**
83+
* Sets whether this file upload is required.
84+
*
85+
* @param required - Whether this file upload is required
86+
*/
87+
public setRequired(required = true) {
88+
this.data.required = required;
89+
return this;
90+
}
91+
92+
/**
93+
* {@inheritDoc ComponentBuilder.toJSON}
94+
*/
95+
public toJSON(): APIFileUploadComponent {
96+
fileUploadPredicate.parse(this.data);
97+
return this.data as APIFileUploadComponent;
98+
}
99+
}

packages/builders/src/components/label/Assertions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { s } from '@sapphire/shapeshift';
22
import { ComponentType } from 'discord-api-types/v10';
33
import { isValidationEnabled } from '../../util/validation.js';
44
import { idValidator } from '../Assertions.js';
5+
import { fileUploadPredicate } from '../fileUpload/Assertions.js';
56
import {
67
selectMenuChannelPredicate,
78
selectMenuMentionablePredicate,
@@ -24,6 +25,7 @@ export const labelPredicate = s
2425
selectMenuMentionablePredicate,
2526
selectMenuChannelPredicate,
2627
selectMenuStringPredicate,
28+
fileUploadPredicate,
2729
]),
2830
})
2931
.setValidationEnabled(isValidationEnabled);

packages/builders/src/components/label/Label.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
APIChannelSelectComponent,
3+
APIFileUploadComponent,
34
APILabelComponent,
45
APIMentionableSelectComponent,
56
APIRoleSelectComponent,
@@ -10,6 +11,7 @@ import type {
1011
import { ComponentType } from 'discord-api-types/v10';
1112
import { ComponentBuilder } from '../Component.js';
1213
import { createComponentBuilder, resolveBuilder } from '../Components.js';
14+
import { FileUploadBuilder } from '../fileUpload/FileUpload.js';
1315
import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js';
1416
import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js';
1517
import { RoleSelectMenuBuilder } from '../selectMenu/RoleSelectMenu.js';
@@ -21,6 +23,7 @@ import { labelPredicate } from './Assertions.js';
2123
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> {
2224
component?:
2325
| ChannelSelectMenuBuilder
26+
| FileUploadBuilder
2427
| MentionableSelectMenuBuilder
2528
| RoleSelectMenuBuilder
2629
| StringSelectMenuBuilder
@@ -179,6 +182,18 @@ export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
179182
return this;
180183
}
181184

185+
/**
186+
* Sets a file upload component to this label.
187+
*
188+
* @param input - A function that returns a component builder or an already built builder
189+
*/
190+
public setFileUploadComponent(
191+
input: APIFileUploadComponent | FileUploadBuilder | ((builder: FileUploadBuilder) => FileUploadBuilder),
192+
): this {
193+
this.data.component = resolveBuilder(input, FileUploadBuilder);
194+
return this;
195+
}
196+
182197
/**
183198
* {@inheritDoc ComponentBuilder.toJSON}
184199
*/

packages/builders/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export {
3434
export * from './components/selectMenu/StringSelectMenuOption.js';
3535
export * from './components/selectMenu/UserSelectMenu.js';
3636

37+
export * from './components/fileUpload/FileUpload.js';
38+
export * as FileUploadAssertions from './components/fileUpload/Assertions.js';
39+
3740
export * from './components/label/Label.js';
3841
export * as LabelAssertions from './components/label/Assertions.js';
3942

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)