mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 20:13:30 +01:00
feat: components v2 in builders v1 (#10787)
* feat(builders): components v2 in builders v1 * feat: implemented the first components * fix: tests * fix: tests * fix: export the new stuff * feat: add rest of components * feat: add callback syntax * feat: callback syntax for section * fix: missing implements * fix: accessory property * fix: apply suggestions from v2 PR * chore: bring in line with builders v2 * fix: add missing type * chore: split accessory methods * chore: backport changes from v2 PR * fix: accent_color is nullish * fix: allow passing raw json to MediaGallery methods * fix: add test * chore: add Container#addXComponents * fix: docs * chore: bump discord-api-types * Update packages/builders/src/components/Assertions.ts Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com> --------- Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import {
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
type APIActionRowComponent,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIComponentInMessageActionRow,
|
||||
} from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
StringSelectMenuOptionBuilder,
|
||||
} from '../../src/index.js';
|
||||
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -57,7 +57,7 @@ describe('Action Row Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const actionRowData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -92,7 +92,7 @@ describe('Action Row Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -104,7 +104,7 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ComponentType,
|
||||
TextInputStyle,
|
||||
type APIButtonComponent,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIComponentInMessageActionRow,
|
||||
type APISelectMenuComponent,
|
||||
type APITextInputComponent,
|
||||
type APIActionRowComponent,
|
||||
@@ -27,7 +27,7 @@ describe('createComponentBuilder', () => {
|
||||
);
|
||||
|
||||
test('GIVEN an action row component THEN returns a ActionRowBuilder', () => {
|
||||
const actionRow: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const actionRow: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
components: [],
|
||||
type: ComponentType.ActionRow,
|
||||
};
|
||||
|
||||
248
packages/builders/__tests__/components/v2/container.test.ts
Normal file
248
packages/builders/__tests__/components/v2/container.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
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 { ContainerBuilder } from '../../../src/components/v2/Container.js';
|
||||
import { FileBuilder } from '../../../src/components/v2/File.js';
|
||||
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js';
|
||||
import { SectionBuilder } from '../../../src/components/v2/Section.js';
|
||||
import { SeparatorBuilder } from '../../../src/components/v2/Separator.js';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js';
|
||||
|
||||
const containerWithTextDisplay: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const containerWithSeparatorData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0x00ff00,
|
||||
};
|
||||
|
||||
const containerWithSeparatorDataNoColor: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Container Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() =>
|
||||
new ContainerBuilder().addActionRowComponents(
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()),
|
||||
),
|
||||
).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addFileComponents(new FileBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addMediaGalleryComponents(new MediaGalleryBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSectionComponents(new SectionBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()])).not.toThrowError();
|
||||
expect(() =>
|
||||
new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const containerData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 3,
|
||||
},
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
spacing: SeparatorSpacingSize.Large,
|
||||
divider: true,
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
type: ComponentType.File,
|
||||
file: {
|
||||
url: 'attachment://file.png',
|
||||
},
|
||||
spoiler: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
spoiler: true,
|
||||
};
|
||||
|
||||
expect(new ContainerBuilder(containerData).toJSON()).toEqual(containerData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const containerWithTextDisplay: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const containerWithSeparatorData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0x00ff00,
|
||||
};
|
||||
|
||||
expect(new ContainerBuilder(containerWithTextDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder(containerWithSeparatorData).toJSON()).toEqual(containerWithSeparatorData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const textDisplay = new TextDisplayBuilder().setContent('test').setId(123);
|
||||
const separator = new SeparatorBuilder().setId(1_234).setSpacing(SeparatorSpacingSize.Small).setDivider(false);
|
||||
|
||||
expect(new ContainerBuilder().addTextDisplayComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder().addSeparatorComponents(separator).toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
expect(new ContainerBuilder().addTextDisplayComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder().addSeparatorComponents([separator]).toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN valid accent color THEN valid JSON output is given', () => {
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor([255, 0, 255])
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor(0xff00ff)
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor([255, 0, 255])
|
||||
.clearAccentColor()
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(new ContainerBuilder(containerWithSeparatorData).clearAccentColor().toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN valid method parameters THEN valid JSON is given', () => {
|
||||
expect(
|
||||
new ContainerBuilder()
|
||||
.addTextDisplayComponents(new TextDisplayBuilder().setId(3).clearId().setContent('test'))
|
||||
.setSpoiler()
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
spoiler: true,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder()
|
||||
.addTextDisplayComponents({ type: ComponentType.TextDisplay, content: 'test' })
|
||||
.setSpoiler(false)
|
||||
.setId(5)
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
spoiler: false,
|
||||
id: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
44
packages/builders/__tests__/components/v2/file.test.ts
Normal file
44
packages/builders/__tests__/components/v2/file.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { FileBuilder } from '../../../src/components/v2/File';
|
||||
|
||||
const dummy = {
|
||||
type: ComponentType.File as const,
|
||||
file: { url: 'attachment://owo.png' },
|
||||
};
|
||||
|
||||
describe('File', () => {
|
||||
describe('File url', () => {
|
||||
test('GIVEN a file with a pre-defined url THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ file: { url: 'attachment://owo.png' } });
|
||||
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://owo.png' } });
|
||||
});
|
||||
|
||||
test('GIVEN a file using File#setURL THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder();
|
||||
file.setURL('attachment://uwu.png');
|
||||
|
||||
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://uwu.png' } });
|
||||
});
|
||||
|
||||
test('GIVEN a file with an invalid url THEN throws error', () => {
|
||||
const file = new FileBuilder();
|
||||
|
||||
expect(() => file.setURL('https://google.com')).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('File spoiler', () => {
|
||||
test('GIVEN a file with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ ...dummy, spoiler: true });
|
||||
expect(file.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||
});
|
||||
|
||||
test('GIVEN a file using File#setSpoiler THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ ...dummy });
|
||||
file.setSpoiler(false);
|
||||
|
||||
expect(file.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
150
packages/builders/__tests__/components/v2/mediagallery.test.ts
Normal file
150
packages/builders/__tests__/components/v2/mediagallery.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { type APIMediaGalleryItem, type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js';
|
||||
import { MediaGalleryItemBuilder } from '../../../src/components/v2/MediaGalleryItem.js';
|
||||
|
||||
const galleryHttpsDisplay: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const galleryAttachmentData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
],
|
||||
id: 123,
|
||||
};
|
||||
|
||||
describe('Media Gallery Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN an empty media gallery THEN throws error', () => {
|
||||
const gallery = new MediaGalleryBuilder();
|
||||
expect(() => gallery.toJSON()).toThrow();
|
||||
});
|
||||
|
||||
test('GIVEN valid items THEN do not throw', () => {
|
||||
expect(() => new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder())).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().spliceItems(0, 0, new MediaGalleryItemBuilder())).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder()])).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().spliceItems(0, 0, [new MediaGalleryItemBuilder()])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const mediaGalleryData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
},
|
||||
{
|
||||
media: { url: 'https://discord.js.org/logo.jpg' },
|
||||
spoiler: true,
|
||||
},
|
||||
],
|
||||
id: 1_234,
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder(mediaGalleryData).toJSON()).toEqual(mediaGalleryData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const galleryHttpsDisplay: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const galleryAttachmentData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
],
|
||||
id: 123,
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder(galleryHttpsDisplay).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder(galleryAttachmentData).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const item1 = new MediaGalleryItemBuilder()
|
||||
.setDescription('test')
|
||||
.setSpoiler(false)
|
||||
.setURL('https://discord.com/logo.png');
|
||||
const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png');
|
||||
|
||||
expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON options THEN valid JSON output is given 2', () => {
|
||||
const item1: APIMediaGalleryItem = {
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
};
|
||||
const item2 = {
|
||||
media: { url: 'attachment://file.png' },
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
});
|
||||
|
||||
test('GIVEN valid builder callback THEN valid JSON output is given', () => {
|
||||
const item1 = new MediaGalleryItemBuilder()
|
||||
.setDescription('test')
|
||||
.setSpoiler(false)
|
||||
.setURL('https://discord.com/logo.png');
|
||||
const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png');
|
||||
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.addItems((item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png'))
|
||||
.toJSON(),
|
||||
).toEqual(galleryHttpsDisplay);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.spliceItems(0, 0, (item) => item.setURL('attachment://file.png'))
|
||||
.setId(123)
|
||||
.toJSON(),
|
||||
).toEqual(galleryAttachmentData);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.addItems([(item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png')])
|
||||
.toJSON(),
|
||||
).toEqual(galleryHttpsDisplay);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.spliceItems(0, 0, [(item) => item.setDescription('test').clearDescription().setURL('attachment://file.png')])
|
||||
.setId(123)
|
||||
.toJSON(),
|
||||
).toEqual(galleryAttachmentData);
|
||||
});
|
||||
});
|
||||
});
|
||||
191
packages/builders/__tests__/components/v2/section.test.ts
Normal file
191
packages/builders/__tests__/components/v2/section.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { type APISectionComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { ButtonBuilder } from '../../../src/components/button/Button.js';
|
||||
import { SectionBuilder } from '../../../src/components/v2/Section.js';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js';
|
||||
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail.js';
|
||||
|
||||
const sectionWithButtonData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
};
|
||||
|
||||
const sectionWithThumbnailData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
spoiler: true,
|
||||
description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Section Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() => new SectionBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new SectionBuilder().spliceTextDisplayComponents(0, 0, new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new SectionBuilder().addTextDisplayComponents([new TextDisplayBuilder()])).not.toThrowError();
|
||||
expect(() =>
|
||||
new SectionBuilder().spliceTextDisplayComponents(0, 0, [new TextDisplayBuilder()]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const sectionData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(new SectionBuilder(sectionData).toJSON()).toEqual(sectionData);
|
||||
expect(() =>
|
||||
createComponentBuilder({
|
||||
type: ComponentType.Section,
|
||||
components: [],
|
||||
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://discord.com/logo.png' } },
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const sectionWithButtonData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
};
|
||||
|
||||
const sectionWithThumbnailData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
spoiler: true,
|
||||
description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
expect(new SectionBuilder(sectionWithButtonData).toJSON()).toEqual(sectionWithButtonData);
|
||||
expect(new SectionBuilder(sectionWithThumbnailData).toJSON()).toEqual(sectionWithThumbnailData);
|
||||
expect(() =>
|
||||
createComponentBuilder({
|
||||
type: ComponentType.Section,
|
||||
components: [],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler().setURL('attachment://file.png');
|
||||
const textDisplay = new TextDisplayBuilder().setContent('test');
|
||||
|
||||
expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setButtonAccessory(button).toJSON()).toEqual(
|
||||
sectionWithButtonData,
|
||||
);
|
||||
expect(
|
||||
new SectionBuilder().addTextDisplayComponents(textDisplay).setThumbnailAccessory(thumbnail).toJSON(),
|
||||
).toEqual(sectionWithThumbnailData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([textDisplay])
|
||||
.setButtonAccessory((button) => button.setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'))
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([textDisplay])
|
||||
.setThumbnailAccessory((thumbnail) =>
|
||||
thumbnail.setDescription('test').setSpoiler().setURL('attachment://file.png'),
|
||||
)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithThumbnailData);
|
||||
});
|
||||
|
||||
test('GIVEN valid builder callback THEN valid JSON output is given', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents((textDisplay) => textDisplay.setContent('test'))
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.spliceTextDisplayComponents(0, 0, (textDisplay) => textDisplay.setContent('test'))
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([(textDisplay) => textDisplay.setContent('test')])
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.spliceTextDisplayComponents(0, 0, [(textDisplay) => textDisplay.setContent('test')])
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { SeparatorBuilder } from '../../../src/components/v2/Separator';
|
||||
|
||||
describe('Separator', () => {
|
||||
describe('Divider', () => {
|
||||
test('GIVEN a separator with a pre-defined divider THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ divider: true });
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: true });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set divider THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder().setDivider(false);
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Spacing', () => {
|
||||
test('GIVEN a separator with a pre-defined spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Small });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large);
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Large });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set spacing THEN clear spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||
separator.clearSpacing();
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay';
|
||||
|
||||
describe('TextDisplay', () => {
|
||||
describe('TextDisplay content', () => {
|
||||
test('GIVEN a text display with a pre-defined content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a set content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder().setContent('foo');
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a pre-defined content THEN overwritten content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||
textDisplay.setContent('bar');
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'bar' });
|
||||
});
|
||||
});
|
||||
});
|
||||
69
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
69
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail';
|
||||
|
||||
const dummy = {
|
||||
type: ComponentType.Thumbnail as const,
|
||||
media: { url: 'https://google.com' },
|
||||
};
|
||||
|
||||
describe('Thumbnail', () => {
|
||||
describe('Thumbnail url', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined url THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ media: { url: 'https://google.com' } });
|
||||
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set url THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder().setURL('https://google.com');
|
||||
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||
});
|
||||
|
||||
test.each(['owo', 'discord://user'])('GIVEN a thumbnail with an invalid URL (%s) THEN throws error', (input) => {
|
||||
const thumbnail = new ThumbnailBuilder();
|
||||
|
||||
expect(() => thumbnail.setURL(input)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Thumbnail description', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy, description: 'foo' });
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||
thumbnail.setDescription('foo');
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ description: 'foo', ...dummy });
|
||||
thumbnail.clearDescription();
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with an invalid description THEN throws error', () => {
|
||||
const thumbnail = new ThumbnailBuilder();
|
||||
|
||||
expect(() => thumbnail.setDescription('a'.repeat(1_025))).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Thumbnail spoiler', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy, spoiler: true });
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set spoiler status THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||
thumbnail.setSpoiler(false);
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,7 +68,7 @@
|
||||
"@discordjs/formatters": "workspace:^",
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import {
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIModalActionRowComponent,
|
||||
type APIActionRowComponentTypes,
|
||||
type APIComponentInMessageActionRow,
|
||||
type APIComponentInModalActionRow,
|
||||
type APIComponentInActionRow,
|
||||
} from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
@@ -18,13 +18,6 @@ import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import type { TextInputBuilder } from './textInput/TextInput.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder =
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
||||
| MessageActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used for modals.
|
||||
*/
|
||||
@@ -57,7 +50,7 @@ export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalAction
|
||||
* @typeParam ComponentType - The types of components this action row holds
|
||||
*/
|
||||
export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
|
||||
> {
|
||||
/**
|
||||
* The components within this action row.
|
||||
@@ -98,7 +91,7 @@ export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends
|
||||
* .addComponents(button2, button3);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIComponentInActionRow>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord
|
||||
import { isValidationEnabled } from '../util/validation.js';
|
||||
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
|
||||
|
||||
export const idValidator = s
|
||||
.number()
|
||||
.safeInt()
|
||||
.greaterThanOrEqual(1)
|
||||
.lessThan(4_294_967_296) // 2^32 - 1
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const customIdValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIComponentInActionRow,
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
APIMessageComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { idValidator } from './Assertions';
|
||||
|
||||
/**
|
||||
* Any action row component data represented as an object.
|
||||
*/
|
||||
export type AnyAPIActionRowComponent = APIActionRowComponent<APIActionRowComponentTypes> | APIActionRowComponentTypes;
|
||||
export type AnyAPIActionRowComponent =
|
||||
| APIActionRowComponent<APIComponentInActionRow>
|
||||
| APIComponentInActionRow
|
||||
| APIMessageComponent;
|
||||
|
||||
/**
|
||||
* The base component builder that contains common symbols for all sorts of components.
|
||||
@@ -42,4 +47,22 @@ export abstract class ComponentBuilder<
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id (not the custom id) for this component.
|
||||
*
|
||||
* @param id - The id for this component
|
||||
*/
|
||||
public setId(id: number) {
|
||||
this.data.id = idValidator.parse(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the id of this component, defaulting to a default incremented id.
|
||||
*/
|
||||
public clearId() {
|
||||
this.data.id = undefined;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
type MessageActionRowComponentBuilder,
|
||||
type AnyComponentBuilder,
|
||||
type MessageComponentBuilder,
|
||||
type ModalComponentBuilder,
|
||||
} from './ActionRow.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
@@ -13,6 +14,27 @@ 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 { ContainerBuilder } from './v2/Container.js';
|
||||
import { FileBuilder } from './v2/File.js';
|
||||
import { MediaGalleryBuilder } from './v2/MediaGallery.js';
|
||||
import { SectionBuilder } from './v2/Section.js';
|
||||
import { SeparatorBuilder } from './v2/Separator.js';
|
||||
import { TextDisplayBuilder } from './v2/TextDisplay.js';
|
||||
import { ThumbnailBuilder } from './v2/Thumbnail.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder =
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
||||
| ContainerBuilder
|
||||
| FileBuilder
|
||||
| MediaGalleryBuilder
|
||||
| MessageActionRowComponentBuilder
|
||||
| SectionBuilder
|
||||
| SeparatorBuilder
|
||||
| TextDisplayBuilder
|
||||
| ThumbnailBuilder;
|
||||
|
||||
/**
|
||||
* Components here are mapped to their respective builder.
|
||||
@@ -50,6 +72,34 @@ export interface MappedComponentTypes {
|
||||
* The channel select component type is associated with a {@link ChannelSelectMenuBuilder}.
|
||||
*/
|
||||
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder;
|
||||
/**
|
||||
* The file component type is associated with a {@link FileBuilder}.
|
||||
*/
|
||||
[ComponentType.File]: FileBuilder;
|
||||
/**
|
||||
* The separator component type is associated with a {@link SeparatorBuilder}.
|
||||
*/
|
||||
[ComponentType.Separator]: SeparatorBuilder;
|
||||
/**
|
||||
* The container component type is associated with a {@link ContainerBuilder}.
|
||||
*/
|
||||
[ComponentType.Container]: ContainerBuilder;
|
||||
/**
|
||||
* The text display component type is associated with a {@link TextDisplayBuilder}.
|
||||
*/
|
||||
[ComponentType.TextDisplay]: TextDisplayBuilder;
|
||||
/**
|
||||
* The thumbnail component type is associated with a {@link ThumbnailBuilder}.
|
||||
*/
|
||||
[ComponentType.Thumbnail]: ThumbnailBuilder;
|
||||
/**
|
||||
* The section component type is associated with a {@link SectionBuilder}.
|
||||
*/
|
||||
[ComponentType.Section]: SectionBuilder;
|
||||
/**
|
||||
* The media gallery component type is associated with a {@link MediaGalleryBuilder}.
|
||||
*/
|
||||
[ComponentType.MediaGallery]: MediaGalleryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +147,44 @@ export function createComponentBuilder(
|
||||
return new MentionableSelectMenuBuilder(data);
|
||||
case ComponentType.ChannelSelect:
|
||||
return new ChannelSelectMenuBuilder(data);
|
||||
case ComponentType.File:
|
||||
return new FileBuilder(data);
|
||||
case ComponentType.Container:
|
||||
return new ContainerBuilder(data);
|
||||
case ComponentType.Section:
|
||||
return new SectionBuilder(data);
|
||||
case ComponentType.Separator:
|
||||
return new SeparatorBuilder(data);
|
||||
case ComponentType.TextDisplay:
|
||||
return new TextDisplayBuilder(data);
|
||||
case ComponentType.Thumbnail:
|
||||
return new ThumbnailBuilder(data);
|
||||
case ComponentType.MediaGallery:
|
||||
return new MediaGalleryBuilder(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}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isBuilder<Builder extends JSONEncodable<any>>(
|
||||
builder: unknown,
|
||||
Constructor: new () => Builder,
|
||||
): builder is Builder {
|
||||
return builder instanceof Constructor;
|
||||
}
|
||||
|
||||
export function resolveBuilder<ComponentType extends Record<PropertyKey, any>, Builder extends JSONEncodable<any>>(
|
||||
builder: Builder | ComponentType | ((builder: Builder) => Builder),
|
||||
Constructor: new (data?: ComponentType) => Builder,
|
||||
) {
|
||||
if (isBuilder(builder, Constructor)) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
if (typeof builder === 'function') {
|
||||
return builder(new Constructor());
|
||||
}
|
||||
|
||||
return new Constructor(builder);
|
||||
}
|
||||
|
||||
71
packages/builders/src/components/v2/Assertions.ts
Normal file
71
packages/builders/src/components/v2/Assertions.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { colorPredicate } from '../../messages/embed/Assertions';
|
||||
import { isValidationEnabled } from '../../util/validation';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { ButtonBuilder } from '../button/Button';
|
||||
import type { ContainerComponentBuilder } from './Container';
|
||||
import type { MediaGalleryItemBuilder } from './MediaGalleryItem';
|
||||
import type { TextDisplayBuilder } from './TextDisplay';
|
||||
import { ThumbnailBuilder } from './Thumbnail';
|
||||
|
||||
export const unfurledMediaItemPredicate = s
|
||||
.object({
|
||||
url: s
|
||||
.string()
|
||||
.url(
|
||||
{ allowedProtocols: ['http:', 'https:', 'attachment:'] },
|
||||
{ message: 'Invalid protocol for media URL. Must be http:, https:, or attachment:' },
|
||||
),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const descriptionPredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(1_024)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const filePredicate = s
|
||||
.object({
|
||||
url: s
|
||||
.string()
|
||||
.url({ allowedProtocols: ['attachment:'] }, { message: 'Invalid protocol for file URL. Must be attachment:' }),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const spoilerPredicate = s.boolean();
|
||||
|
||||
export const dividerPredicate = s.boolean();
|
||||
|
||||
export const spacingPredicate = s.nativeEnum(SeparatorSpacingSize);
|
||||
|
||||
export const textDisplayContentPredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const accessoryPredicate = s
|
||||
.instance(ButtonBuilder)
|
||||
.or(s.instance(ThumbnailBuilder))
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const containerColorPredicate = colorPredicate.nullish();
|
||||
|
||||
export function assertReturnOfBuilder<ReturnType extends MediaGalleryItemBuilder | TextDisplayBuilder>(
|
||||
input: unknown,
|
||||
ExpectedInstanceOf: new () => ReturnType,
|
||||
): asserts input is ReturnType {
|
||||
s.instance(ExpectedInstanceOf).parse(input);
|
||||
}
|
||||
|
||||
export function validateComponentArray<
|
||||
ReturnType extends ContainerComponentBuilder | MediaGalleryItemBuilder = ContainerComponentBuilder,
|
||||
>(input: unknown, min: number, max: number, ExpectedInstanceOf?: new () => ReturnType): asserts input is ReturnType[] {
|
||||
(ExpectedInstanceOf ? s.instance(ExpectedInstanceOf) : s.instance(ComponentBuilder))
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(min)
|
||||
.lengthLessThanOrEqual(max)
|
||||
.parse(input);
|
||||
}
|
||||
240
packages/builders/src/components/v2/Container.ts
Normal file
240
packages/builders/src/components/v2/Container.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIComponentInContainer,
|
||||
APIComponentInMessageActionRow,
|
||||
APIContainerComponent,
|
||||
APIFileComponent,
|
||||
APIMediaGalleryComponent,
|
||||
APISectionComponent,
|
||||
APISeparatorComponent,
|
||||
APITextDisplayComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import type { RGBTuple } from '../../index.js';
|
||||
import { MediaGalleryBuilder, SectionBuilder } from '../../index.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import type { AnyComponentBuilder, MessageActionRowComponentBuilder } from '../ActionRow.js';
|
||||
import { ActionRowBuilder } from '../ActionRow.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { containerColorPredicate, spoilerPredicate, validateComponentArray } from './Assertions.js';
|
||||
import { FileBuilder } from './File.js';
|
||||
import { SeparatorBuilder } from './Separator.js';
|
||||
import { TextDisplayBuilder } from './TextDisplay.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used within a container.
|
||||
*/
|
||||
export type ContainerComponentBuilder =
|
||||
| ActionRowBuilder<AnyComponentBuilder>
|
||||
| FileBuilder
|
||||
| MediaGalleryBuilder
|
||||
| SectionBuilder
|
||||
| SeparatorBuilder
|
||||
| TextDisplayBuilder;
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a container.
|
||||
*/
|
||||
export class ContainerBuilder extends ComponentBuilder<APIContainerComponent> {
|
||||
/**
|
||||
* The components within this container.
|
||||
*/
|
||||
public readonly components: ContainerComponentBuilder[];
|
||||
|
||||
/**
|
||||
* Creates a new container from API data.
|
||||
*
|
||||
* @param data - The API data to create this container with
|
||||
* @example
|
||||
* Creating a container from an API data object:
|
||||
* ```ts
|
||||
* const container = new ContainerBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "Some text here",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a container using setters and API data:
|
||||
* ```ts
|
||||
* const container = new ContainerBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "# Heading",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .addComponents(separator, section);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIContainerComponent> = {}) {
|
||||
super({ type: ComponentType.Container, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ??
|
||||
[]) as ContainerComponentBuilder[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accent color of this container.
|
||||
*
|
||||
* @param color - The color to use
|
||||
*/
|
||||
public setAccentColor(color?: RGBTuple | number): this {
|
||||
// Data assertions
|
||||
containerColorPredicate.parse(color);
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
const [red, green, blue] = color;
|
||||
this.data.accent_color = (red << 16) + (green << 8) + blue;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.accent_color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the accent color of this container.
|
||||
*/
|
||||
public clearAccentColor() {
|
||||
this.data.accent_color = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds action row components to this container.
|
||||
*
|
||||
* @param components - The action row components to add
|
||||
*/
|
||||
public addActionRowComponents<ComponentType extends MessageActionRowComponentBuilder>(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder<ComponentType>
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
||||
| ((builder: ActionRowBuilder<ComponentType>) => ActionRowBuilder<ComponentType>)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder<ComponentType>)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds file components to this container.
|
||||
*
|
||||
* @param components - The file components to add
|
||||
*/
|
||||
public addFileComponents(
|
||||
...components: RestOrArray<APIFileComponent | FileBuilder | ((builder: FileBuilder) => FileBuilder)>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, FileBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds media gallery components to this container.
|
||||
*
|
||||
* @param components - The media gallery components to add
|
||||
*/
|
||||
public addMediaGalleryComponents(
|
||||
...components: RestOrArray<
|
||||
APIMediaGalleryComponent | MediaGalleryBuilder | ((builder: MediaGalleryBuilder) => MediaGalleryBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, MediaGalleryBuilder)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds section components to this container.
|
||||
*
|
||||
* @param components - The section components to add
|
||||
*/
|
||||
public addSectionComponents(
|
||||
...components: RestOrArray<APISectionComponent | SectionBuilder | ((builder: SectionBuilder) => SectionBuilder)>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SectionBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds separator components to this container.
|
||||
*
|
||||
* @param components - The separator components to add
|
||||
*/
|
||||
public addSeparatorComponents(
|
||||
...components: RestOrArray<
|
||||
APISeparatorComponent | SeparatorBuilder | ((builder: SeparatorBuilder) => SeparatorBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SeparatorBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text display components to this container.
|
||||
*
|
||||
* @param components - The text display components to add
|
||||
*/
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, TextDisplayBuilder)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts components for this container.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting components
|
||||
* @param deleteCount - The amount of components to remove
|
||||
* @param components - The components to set
|
||||
*/
|
||||
public spliceComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...components: RestOrArray<APIComponentInContainer | ContainerComponentBuilder>
|
||||
) {
|
||||
this.components.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(components).map((component) =>
|
||||
component instanceof ComponentBuilder ? component : createComponentBuilder(component),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this container.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIContainerComponent {
|
||||
validateComponentArray(this.components, 1, 10);
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIContainerComponent;
|
||||
}
|
||||
}
|
||||
63
packages/builders/src/components/v2/File.ts
Normal file
63
packages/builders/src/components/v2/File.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ComponentType, type APIFileComponent } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { filePredicate, spoilerPredicate } from './Assertions';
|
||||
|
||||
export class FileBuilder extends ComponentBuilder<APIFileComponent> {
|
||||
/**
|
||||
* Creates a new file from API data.
|
||||
*
|
||||
* @param data - The API data to create this file with
|
||||
* @example
|
||||
* Creating a file from an API data object:
|
||||
* ```ts
|
||||
* const file = new FileBuilder({
|
||||
* spoiler: true,
|
||||
* file: {
|
||||
* url: 'attachment://file.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a file using setters and API data:
|
||||
* ```ts
|
||||
* const file = new FileBuilder({
|
||||
* file: {
|
||||
* url: 'attachment://image.jpg',
|
||||
* },
|
||||
* })
|
||||
* .setSpoiler(false);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIFileComponent> = {}) {
|
||||
super({ type: ComponentType.File, ...data, file: data.file ? { url: data.file.url } : undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this file.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this file.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.file = filePredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIFileComponent {
|
||||
filePredicate.parse(this.data.file);
|
||||
|
||||
return { ...this.data, file: { ...this.data.file } } as APIFileComponent;
|
||||
}
|
||||
}
|
||||
117
packages/builders/src/components/v2/MediaGallery.ts
Normal file
117
packages/builders/src/components/v2/MediaGallery.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { APIMediaGalleryComponent, APIMediaGalleryItem } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { resolveBuilder } from '../Components.js';
|
||||
import { assertReturnOfBuilder, validateComponentArray } from './Assertions.js';
|
||||
import { MediaGalleryItemBuilder } from './MediaGalleryItem.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a container.
|
||||
*/
|
||||
export class MediaGalleryBuilder extends ComponentBuilder<APIMediaGalleryComponent> {
|
||||
/**
|
||||
* The components within this container.
|
||||
*/
|
||||
public readonly items: MediaGalleryItemBuilder[];
|
||||
|
||||
/**
|
||||
* Creates a new media gallery from API data.
|
||||
*
|
||||
* @param data - The API data to create this media gallery with
|
||||
* @example
|
||||
* Creating a media gallery from an API data object:
|
||||
* ```ts
|
||||
* const mediaGallery = new MediaGalleryBuilder({
|
||||
* items: [
|
||||
* {
|
||||
* description: "Some text here",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a media gallery using setters and API data:
|
||||
* ```ts
|
||||
* const mediaGallery = new MediaGalleryBuilder({
|
||||
* items: [
|
||||
* {
|
||||
* description: "alt text",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .addItems(item2, item3);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ items, ...data }: Partial<APIMediaGalleryComponent> = {}) {
|
||||
super({ type: ComponentType.MediaGallery, ...data });
|
||||
this.items = items?.map((item) => new MediaGalleryItemBuilder(item)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to this media gallery.
|
||||
*
|
||||
* @param items - The items to add
|
||||
*/
|
||||
public addItems(
|
||||
...items: RestOrArray<
|
||||
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
|
||||
>
|
||||
) {
|
||||
this.items.push(
|
||||
...normalizeArray(items).map((input) => {
|
||||
const result = resolveBuilder(input, MediaGalleryItemBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, MediaGalleryItemBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts media gallery items for this media gallery.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting items
|
||||
* @param deleteCount - The amount of items to remove
|
||||
* @param items - The items to insert
|
||||
*/
|
||||
public spliceItems(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...items: RestOrArray<
|
||||
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
|
||||
>
|
||||
) {
|
||||
this.items.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(items).map((input) => {
|
||||
const result = resolveBuilder(input, MediaGalleryItemBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, MediaGalleryItemBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIMediaGalleryComponent {
|
||||
validateComponentArray(this.items, 1, 10, MediaGalleryItemBuilder);
|
||||
return {
|
||||
...this.data,
|
||||
items: this.items.map((item) => item.toJSON()),
|
||||
} as APIMediaGalleryComponent;
|
||||
}
|
||||
}
|
||||
90
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal file
90
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APIMediaGalleryItem } from 'discord-api-types/v10';
|
||||
import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions';
|
||||
|
||||
export class MediaGalleryItemBuilder implements JSONEncodable<APIMediaGalleryItem> {
|
||||
/**
|
||||
* The API data associated with this media gallery item.
|
||||
*/
|
||||
public readonly data: Partial<APIMediaGalleryItem>;
|
||||
|
||||
/**
|
||||
* Creates a new media gallery item from API data.
|
||||
*
|
||||
* @param data - The API data to create this media gallery item with
|
||||
* @example
|
||||
* Creating a media gallery item from an API data object:
|
||||
* ```ts
|
||||
* const item = new MediaGalleryItemBuilder({
|
||||
* description: "Some text here",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a media gallery item using setters and API data:
|
||||
* ```ts
|
||||
* const item = new MediaGalleryItemBuilder({
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
|
||||
* },
|
||||
* })
|
||||
* .setDescription("alt text");
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIMediaGalleryItem> = {}) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this media gallery item.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = descriptionPredicate.parse(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description of this media gallery item.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this media gallery item.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this media gallery item.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.media = unfurledMediaItemPredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* @remarks
|
||||
* This method runs validations on the data before serializing it.
|
||||
* As such, it may throw an error if the data is invalid.
|
||||
*/
|
||||
public toJSON(): APIMediaGalleryItem {
|
||||
unfurledMediaItemPredicate.parse(this.data.media);
|
||||
|
||||
return { ...this.data } as APIMediaGalleryItem;
|
||||
}
|
||||
}
|
||||
153
packages/builders/src/components/v2/Section.ts
Normal file
153
packages/builders/src/components/v2/Section.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type {
|
||||
APIButtonComponent,
|
||||
APISectionComponent,
|
||||
APITextDisplayComponent,
|
||||
APIThumbnailComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ButtonBuilder, ThumbnailBuilder } from '../../index.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { accessoryPredicate, assertReturnOfBuilder, validateComponentArray } from './Assertions.js';
|
||||
import { TextDisplayBuilder } from './TextDisplay.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a section.
|
||||
*/
|
||||
export class SectionBuilder extends ComponentBuilder<APISectionComponent> {
|
||||
/**
|
||||
* The components within this section.
|
||||
*/
|
||||
public readonly components: ComponentBuilder[];
|
||||
|
||||
/**
|
||||
* The accessory of this section.
|
||||
*/
|
||||
public readonly accessory?: ButtonBuilder | ThumbnailBuilder;
|
||||
|
||||
/**
|
||||
* Creates a new section from API data.
|
||||
*
|
||||
* @param data - The API data to create this section with
|
||||
* @example
|
||||
* Creating a section from an API data object:
|
||||
* ```ts
|
||||
* const section = new SectionBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "Some text here",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* accessory: {
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/3.png',
|
||||
* },
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a section using setters and API data:
|
||||
* ```ts
|
||||
* const section = new SectionBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "# Heading",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .setPrimaryButtonAccessory(button);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, accessory, ...data }: Partial<APISectionComponent> = {}) {
|
||||
super({ type: ComponentType.Section, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[];
|
||||
this.accessory = accessory ? createComponentBuilder(accessory) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accessory of this section to a button.
|
||||
*
|
||||
* @param accessory - The accessory to use
|
||||
*/
|
||||
public setButtonAccessory(
|
||||
accessory: APIButtonComponent | ButtonBuilder | ((builder: ButtonBuilder) => ButtonBuilder),
|
||||
): this {
|
||||
Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ButtonBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accessory of this section to a thumbnail.
|
||||
*
|
||||
* @param accessory - The accessory to use
|
||||
*/
|
||||
public setThumbnailAccessory(
|
||||
accessory: APIThumbnailComponent | ThumbnailBuilder | ((builder: ThumbnailBuilder) => ThumbnailBuilder),
|
||||
): this {
|
||||
Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ThumbnailBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text display components to this section.
|
||||
*
|
||||
* @param components - The text display components to add
|
||||
*/
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((input) => {
|
||||
const result = resolveBuilder(input, TextDisplayBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, TextDisplayBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts text display components for this section.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting text display components
|
||||
* @param deleteCount - The amount of text display components to remove
|
||||
* @param components - The text display components to insert
|
||||
*/
|
||||
public spliceTextDisplayComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...components: RestOrArray<
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(components).map((input) => {
|
||||
const result = resolveBuilder(input, TextDisplayBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, TextDisplayBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APISectionComponent {
|
||||
validateComponentArray(this.components, 1, 3, TextDisplayBuilder);
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
accessory: accessoryPredicate.parse(this.accessory).toJSON(),
|
||||
} as APISectionComponent;
|
||||
}
|
||||
}
|
||||
69
packages/builders/src/components/v2/Separator.ts
Normal file
69
packages/builders/src/components/v2/Separator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { SeparatorSpacingSize, APISeparatorComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { dividerPredicate, spacingPredicate } from './Assertions';
|
||||
|
||||
export class SeparatorBuilder extends ComponentBuilder<APISeparatorComponent> {
|
||||
/**
|
||||
* Creates a new separator from API data.
|
||||
*
|
||||
* @param data - The API data to create this separator with
|
||||
* @example
|
||||
* Creating a separator from an API data object:
|
||||
* ```ts
|
||||
* const separator = new SeparatorBuilder({
|
||||
* spacing: SeparatorSpacingSize.Small,
|
||||
* divider: true,
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a separator using setters and API data:
|
||||
* ```ts
|
||||
* const separator = new SeparatorBuilder({
|
||||
* spacing: SeparatorSpacingSize.Large,
|
||||
* })
|
||||
* .setDivider(false);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APISeparatorComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.Separator,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this separator should show a divider line.
|
||||
*
|
||||
* @param divider - Whether to show a divider line
|
||||
*/
|
||||
public setDivider(divider = true) {
|
||||
this.data.divider = dividerPredicate.parse(divider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spacing of this separator.
|
||||
*
|
||||
* @param spacing - The spacing to use
|
||||
*/
|
||||
public setSpacing(spacing: SeparatorSpacingSize) {
|
||||
this.data.spacing = spacingPredicate.parse(spacing);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the spacing of this separator.
|
||||
*/
|
||||
public clearSpacing() {
|
||||
this.data.spacing = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APISeparatorComponent {
|
||||
return { ...this.data } as APISeparatorComponent;
|
||||
}
|
||||
}
|
||||
52
packages/builders/src/components/v2/TextDisplay.ts
Normal file
52
packages/builders/src/components/v2/TextDisplay.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { APITextDisplayComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { textDisplayContentPredicate } from './Assertions';
|
||||
|
||||
export class TextDisplayBuilder extends ComponentBuilder<APITextDisplayComponent> {
|
||||
/**
|
||||
* Creates a new text display from API data.
|
||||
*
|
||||
* @param data - The API data to create this text display with
|
||||
* @example
|
||||
* Creating a text display from an API data object:
|
||||
* ```ts
|
||||
* const textDisplay = new TextDisplayBuilder({
|
||||
* content: 'some text',
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a text display using setters and API data:
|
||||
* ```ts
|
||||
* const textDisplay = new TextDisplayBuilder({
|
||||
* content: 'old text',
|
||||
* })
|
||||
* .setContent('new text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APITextDisplayComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.TextDisplay,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text of this text display.
|
||||
*
|
||||
* @param content - The text to use
|
||||
*/
|
||||
public setContent(content: string) {
|
||||
this.data.content = textDisplayContentPredicate.parse(content);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APITextDisplayComponent {
|
||||
textDisplayContentPredicate.parse(this.data.content);
|
||||
|
||||
return { ...this.data } as APITextDisplayComponent;
|
||||
}
|
||||
}
|
||||
86
packages/builders/src/components/v2/Thumbnail.ts
Normal file
86
packages/builders/src/components/v2/Thumbnail.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { APIThumbnailComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions';
|
||||
|
||||
export class ThumbnailBuilder extends ComponentBuilder<APIThumbnailComponent> {
|
||||
/**
|
||||
* Creates a new thumbnail from API data.
|
||||
*
|
||||
* @param data - The API data to create this thumbnail with
|
||||
* @example
|
||||
* Creating a thumbnail from an API data object:
|
||||
* ```ts
|
||||
* const thumbnaik = new ThumbnailBuilder({
|
||||
* description: 'some text',
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/4.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a thumbnail using setters and API data:
|
||||
* ```ts
|
||||
* const thumbnail = new ThumbnailBuilder({
|
||||
* media: {
|
||||
* url: 'attachment://image.png',
|
||||
* },
|
||||
* })
|
||||
* .setDescription('alt text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIThumbnailComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.Thumbnail,
|
||||
...data,
|
||||
media: data.media ? { url: data.media.url } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this thumbnail.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = descriptionPredicate.parse(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description of this thumbnail.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this thumbnail.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this thumbnail.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.media = unfurledMediaItemPredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIThumbnailComponent {
|
||||
unfurledMediaItemPredicate.parse(this.data.media);
|
||||
|
||||
return { ...this.data } as APIThumbnailComponent;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,16 @@ export {
|
||||
export * from './components/selectMenu/StringSelectMenuOption.js';
|
||||
export * from './components/selectMenu/UserSelectMenu.js';
|
||||
|
||||
export * as ComponentsV2Assertions from './components/v2/Assertions.js';
|
||||
export * from './components/v2/Container.js';
|
||||
export * from './components/v2/File.js';
|
||||
export * from './components/v2/MediaGallery.js';
|
||||
export * from './components/v2/MediaGalleryItem.js';
|
||||
export * from './components/v2/Section.js';
|
||||
export * from './components/v2/Separator.js';
|
||||
export * from './components/v2/TextDisplay.js';
|
||||
export * from './components/v2/Thumbnail.js';
|
||||
|
||||
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
|
||||
export * from './interactions/slashCommands/SlashCommandBuilder.js';
|
||||
export * from './interactions/slashCommands/SlashCommandSubcommands.js';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
||||
@@ -64,7 +64,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
*/
|
||||
public addComponents(
|
||||
...components: RestOrArray<
|
||||
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
|
||||
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIComponentInModalActionRow>
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@discordjs/ws": "1.1.1",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"tslib": "^2.6.3",
|
||||
|
||||
42
packages/discord.js/typings/index.d.ts
vendored
42
packages/discord.js/typings/index.d.ts
vendored
@@ -111,13 +111,13 @@ import {
|
||||
AuditLogEvent,
|
||||
APIMessageComponentEmoji,
|
||||
EmbedType,
|
||||
APIActionRowComponentTypes,
|
||||
APIComponentInActionRow,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
APIModalSubmitInteraction,
|
||||
APIMessageActionRowComponent,
|
||||
APIComponentInMessageActionRow,
|
||||
TextInputStyle,
|
||||
APITextInputComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
APIModalComponent,
|
||||
APISelectMenuOption,
|
||||
APIEmbedField,
|
||||
@@ -285,7 +285,7 @@ export interface BaseComponentData {
|
||||
}
|
||||
|
||||
export type MessageActionRowComponentData =
|
||||
| JSONEncodable<APIMessageActionRowComponent>
|
||||
| JSONEncodable<APIComponentInMessageActionRow>
|
||||
| ButtonComponentData
|
||||
| StringSelectMenuComponentData
|
||||
| UserSelectMenuComponentData
|
||||
@@ -293,13 +293,13 @@ export type MessageActionRowComponentData =
|
||||
| MentionableSelectMenuComponentData
|
||||
| ChannelSelectMenuComponentData;
|
||||
|
||||
export type ModalActionRowComponentData = JSONEncodable<APIModalActionRowComponent> | TextInputComponentData;
|
||||
export type ModalActionRowComponentData = JSONEncodable<APIComponentInModalActionRow> | TextInputComponentData;
|
||||
|
||||
export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData;
|
||||
|
||||
export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent;
|
||||
|
||||
export interface ActionRowData<ComponentType extends JSONEncodable<APIActionRowComponentTypes> | ActionRowComponentData>
|
||||
export interface ActionRowData<ComponentType extends JSONEncodable<APIComponentInActionRow> | ActionRowComponentData>
|
||||
extends BaseComponentData {
|
||||
components: readonly ComponentType[];
|
||||
}
|
||||
@@ -309,8 +309,8 @@ export class ActionRowBuilder<
|
||||
> extends BuilderActionRow<ComponentType> {
|
||||
public constructor(
|
||||
data?: Partial<
|
||||
| ActionRowData<ActionRowComponentData | JSONEncodable<APIActionRowComponentTypes>>
|
||||
| APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
| ActionRowData<ActionRowComponentData | JSONEncodable<APIComponentInActionRow>>
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
|
||||
>,
|
||||
);
|
||||
public static from<ComponentType extends AnyComponentBuilder = AnyComponentBuilder>(
|
||||
@@ -330,9 +330,9 @@ export type MessageActionRowComponent =
|
||||
export type ModalActionRowComponent = TextInputComponent;
|
||||
|
||||
export class ActionRow<ComponentType extends MessageActionRowComponent | ModalActionRowComponent> extends Component<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
|
||||
> {
|
||||
private constructor(data: APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>);
|
||||
private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>);
|
||||
public readonly components: ComponentType[];
|
||||
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
|
||||
}
|
||||
@@ -740,7 +740,7 @@ export class ButtonInteraction<Cached extends CacheType = CacheType> extends Mes
|
||||
export type AnyComponent =
|
||||
| APIMessageComponent
|
||||
| APIModalComponent
|
||||
| APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>;
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>;
|
||||
|
||||
export class Component<RawComponentData extends AnyComponent = AnyComponent> {
|
||||
public readonly data: Readonly<RawComponentData>;
|
||||
@@ -2086,7 +2086,13 @@ export interface MessageCall {
|
||||
participants: readonly Snowflake[];
|
||||
}
|
||||
|
||||
export type MessageComponentType = Exclude<ComponentType, ComponentType.TextInput | ComponentType.ActionRow>;
|
||||
export type MessageComponentType =
|
||||
| ComponentType.Button
|
||||
| ComponentType.ChannelSelect
|
||||
| ComponentType.MentionableSelect
|
||||
| ComponentType.RoleSelect
|
||||
| ComponentType.StringSelect
|
||||
| ComponentType.UserSelect;
|
||||
|
||||
export interface MessageCollectorOptionsParams<
|
||||
ComponentType extends MessageComponentType,
|
||||
@@ -2278,9 +2284,9 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
|
||||
public get component(): CacheTypeReducer<
|
||||
Cached,
|
||||
MessageActionRowComponent,
|
||||
Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>,
|
||||
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>,
|
||||
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>
|
||||
Exclude<APIMessageComponent, APIActionRowComponent<APIComponentInMessageActionRow>>,
|
||||
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIComponentInMessageActionRow>>,
|
||||
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIComponentInMessageActionRow>>
|
||||
>;
|
||||
public componentType: Exclude<ComponentType, ComponentType.ActionRow | ComponentType.TextInput>;
|
||||
public customId: string;
|
||||
@@ -2457,7 +2463,7 @@ export interface ModalComponentData {
|
||||
customId: string;
|
||||
title: string;
|
||||
components: readonly (
|
||||
| JSONEncodable<APIActionRowComponent<APIModalActionRowComponent>>
|
||||
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
|
||||
| ActionRowData<ModalActionRowComponentData>
|
||||
)[];
|
||||
}
|
||||
@@ -6454,9 +6460,9 @@ export interface BaseMessageOptions {
|
||||
| AttachmentPayload
|
||||
)[];
|
||||
components?: readonly (
|
||||
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
|
||||
| JSONEncodable<APIActionRowComponent<APIComponentInMessageActionRow>>
|
||||
| ActionRowData<MessageActionRowComponentData | MessageActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIMessageActionRowComponent>
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
||||
)[];
|
||||
poll?: PollData;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
ApplicationCommandType,
|
||||
APIMessage,
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIComponentInActionRow,
|
||||
APIStringSelectComponent,
|
||||
APIUserSelectComponent,
|
||||
APIRoleSelectComponent,
|
||||
@@ -2347,7 +2347,7 @@ EmbedBuilder.from(embedData);
|
||||
declare const embedComp: Embed;
|
||||
EmbedBuilder.from(embedComp);
|
||||
|
||||
declare const actionRowData: APIActionRowComponent<APIActionRowComponentTypes>;
|
||||
declare const actionRowData: APIActionRowComponent<APIComponentInActionRow>;
|
||||
ActionRowBuilder.from(actionRowData);
|
||||
|
||||
declare const actionRowComp: ActionRow<ActionRowComponent>;
|
||||
@@ -2359,7 +2359,7 @@ declare const buttonsActionRowComp: ActionRow<ButtonComponent>;
|
||||
expectType<ActionRowBuilder<ButtonBuilder>>(ActionRowBuilder.from<ButtonBuilder>(buttonsActionRowData));
|
||||
expectType<ActionRowBuilder<ButtonBuilder>>(ActionRowBuilder.from<ButtonBuilder>(buttonsActionRowComp));
|
||||
|
||||
declare const anyComponentsActionRowData: APIActionRowComponent<APIActionRowComponentTypes>;
|
||||
declare const anyComponentsActionRowData: APIActionRowComponent<APIComponentInActionRow>;
|
||||
declare const anyComponentsActionRowComp: ActionRow<ActionRowComponent>;
|
||||
|
||||
expectType<ActionRowBuilder>(ActionRowBuilder.from(anyComponentsActionRowData));
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getUserAgentAppendix } from '@discordjs/util';
|
||||
import { APIVersion } from 'discord-api-types/v10';
|
||||
import { APIVersion, type ImageSize } from 'discord-api-types/v10';
|
||||
import { getDefaultStrategy } from '../../environment.js';
|
||||
import type { RESTOptions, ResponseLike } from './types.js';
|
||||
|
||||
@@ -48,11 +48,10 @@ export enum RESTEvents {
|
||||
|
||||
export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const satisfies readonly string[];
|
||||
export const ALLOWED_STICKER_EXTENSIONS = ['png', 'json', 'gif'] as const satisfies readonly string[];
|
||||
export const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] as const satisfies readonly number[];
|
||||
export const ALLOWED_SIZES: readonly number[] = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] satisfies ImageSize[];
|
||||
|
||||
export type ImageExtension = (typeof ALLOWED_EXTENSIONS)[number];
|
||||
export type StickerExtension = (typeof ALLOWED_STICKER_EXTENSIONS)[number];
|
||||
export type ImageSize = (typeof ALLOWED_SIZES)[number];
|
||||
|
||||
export const OverwrittenMimeTypes = {
|
||||
// https://github.com/discordjs/discord.js/issues/8557
|
||||
@@ -67,3 +66,5 @@ export const BurstHandlerMajorIdKey = 'burst';
|
||||
* @internal
|
||||
*/
|
||||
export const DEPRECATION_WARNING_PREFIX = 'DeprecationWarning' as const;
|
||||
|
||||
export { type ImageSize } from 'discord-api-types/v10';
|
||||
|
||||
Reference in New Issue
Block a user