Skip to content

Commit 5152abf

Browse files
didineleJulianVennenalmeidxkyranetkodiakhq[bot]
authored
feat: new select menus (#8793)
* feat(builders): new select menus * chore: better re-exporting of deprecated classes * feat: new select menus * chore: typings * chore: add missing todo comment * chore: finish updating tests * chore: add runtime deprecation warnings * chore: format deprecation warning * feat(BaseInteraction): isAnySelectMenu * chore: requested changes * fix: deprecation comments * chore: update @deprecated comments in typings * chore: add tests for select menu type narrowing * fix: bad auto imports Co-authored-by: Julian Vennen <[email protected]> * fix: properly handle resolved members * fix: collectors * chore: suggested changes Co-authored-by: Almeida <[email protected]> * fix(typings): bad class extends * feat(ChannelSelectMenuBuilder): validation * chore: update todo comment * refactor(ChannelSelectMenu): better handling of channel_types state * chore: style nit * chore: suggested nits Co-authored-by: Aura Román <[email protected]> Co-authored-by: Julian Vennen <[email protected]> Co-authored-by: Almeida <[email protected]> Co-authored-by: Aura Román <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 8b400ca commit 5152abf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1530
-471
lines changed

packages/builders/__tests__/components/actionRow.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
ActionRowBuilder,
1010
ButtonBuilder,
1111
createComponentBuilder,
12-
SelectMenuBuilder,
13-
SelectMenuOptionBuilder,
12+
StringSelectMenuBuilder,
13+
StringSelectMenuOptionBuilder,
1414
} from '../../src/index.js';
1515

1616
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
@@ -29,7 +29,7 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
2929
type: ComponentType.ActionRow,
3030
components: [
3131
{
32-
type: ComponentType.SelectMenu,
32+
type: ComponentType.StringSelect,
3333
custom_id: '1234',
3434
options: [
3535
{
@@ -73,7 +73,7 @@ describe('Action Row Components', () => {
7373
url: 'https://google.com',
7474
},
7575
{
76-
type: ComponentType.SelectMenu,
76+
type: ComponentType.StringSelect,
7777
placeholder: 'test',
7878
custom_id: 'test',
7979
options: [
@@ -108,7 +108,7 @@ describe('Action Row Components', () => {
108108
type: ComponentType.ActionRow,
109109
components: [
110110
{
111-
type: ComponentType.SelectMenu,
111+
type: ComponentType.StringSelect,
112112
custom_id: '1234',
113113
options: [
114114
{
@@ -134,17 +134,17 @@ describe('Action Row Components', () => {
134134

135135
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
136136
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
137-
const selectMenu = new SelectMenuBuilder()
137+
const selectMenu = new StringSelectMenuBuilder()
138138
.setCustomId('1234')
139139
.setMaxValues(10)
140140
.setMinValues(12)
141141
.setOptions(
142-
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
143-
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
142+
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
143+
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
144144
)
145145
.setOptions([
146-
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
147-
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
146+
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
147+
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
148148
]);
149149

150150
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);

packages/builders/__tests__/components/components.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import {
1313
ActionRowBuilder,
1414
ButtonBuilder,
1515
createComponentBuilder,
16-
SelectMenuBuilder,
16+
StringSelectMenuBuilder,
1717
TextInputBuilder,
1818
} from '../../src/index.js';
1919

2020
describe('createComponentBuilder', () => {
21-
test.each([ButtonBuilder, SelectMenuBuilder, TextInputBuilder])(
21+
test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])(
2222
'passing an instance of %j should return itself',
2323
(Builder) => {
2424
const builder = new Builder();
@@ -45,14 +45,14 @@ describe('createComponentBuilder', () => {
4545
expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder);
4646
});
4747

48-
test('GIVEN a select menu component THEN returns a SelectMenuBuilder', () => {
48+
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {
4949
const selectMenu: APISelectMenuComponent = {
5050
custom_id: 'abc',
5151
options: [],
52-
type: ComponentType.SelectMenu,
52+
type: ComponentType.StringSelect,
5353
};
5454

55-
expect(createComponentBuilder(selectMenu)).toBeInstanceOf(SelectMenuBuilder);
55+
expect(createComponentBuilder(selectMenu)).toBeInstanceOf(StringSelectMenuBuilder);
5656
});
5757

5858
test('GIVEN a text input component THEN returns a TextInputBuilder', () => {

packages/builders/__tests__/components/selectMenu.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10';
22
import { describe, test, expect } from 'vitest';
3-
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index.js';
3+
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';
44

5-
const selectMenu = () => new SelectMenuBuilder();
6-
const selectMenuOption = () => new SelectMenuOptionBuilder();
5+
const selectMenu = () => new StringSelectMenuBuilder();
6+
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
77

88
const longStr = 'a'.repeat(256);
99

@@ -165,16 +165,16 @@ describe('Select Menu Components', () => {
165165

166166
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
167167
expect(
168-
new SelectMenuBuilder(selectMenuDataWithoutOptions)
169-
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
168+
new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
169+
.addOptions(new StringSelectMenuOptionBuilder(selectMenuOptionData))
170170
.toJSON(),
171171
).toEqual(selectMenuData);
172172
expect(
173-
new SelectMenuBuilder(selectMenuDataWithoutOptions)
174-
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)])
173+
new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
174+
.addOptions([new StringSelectMenuOptionBuilder(selectMenuOptionData)])
175175
.toJSON(),
176176
).toEqual(selectMenuData);
177-
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
177+
expect(new StringSelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
178178
});
179179
});
180180
});

packages/builders/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"dependencies": {
5757
"@discordjs/util": "workspace:^",
5858
"@sapphire/shapeshift": "^3.7.0",
59-
"discord-api-types": "^0.37.14",
59+
"discord-api-types": "^0.37.15",
6060
"fast-deep-equal": "^3.1.3",
6161
"ts-mixer": "^6.0.1",
6262
"tslib": "^2.4.0"

packages/builders/src/components/ActionRow.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,24 @@ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
1111
import { ComponentBuilder } from './Component.js';
1212
import { createComponentBuilder } from './Components.js';
1313
import type { ButtonBuilder } from './button/Button.js';
14-
import type { SelectMenuBuilder } from './selectMenu/SelectMenu.js';
14+
import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
15+
import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
16+
import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
17+
import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
18+
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
1519
import type { TextInputBuilder } from './textInput/TextInput.js';
1620

1721
export type MessageComponentBuilder =
1822
| ActionRowBuilder<MessageActionRowComponentBuilder>
1923
| MessageActionRowComponentBuilder;
2024
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
21-
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
25+
export type MessageActionRowComponentBuilder =
26+
| ButtonBuilder
27+
| ChannelSelectMenuBuilder
28+
| MentionableSelectMenuBuilder
29+
| RoleSelectMenuBuilder
30+
| StringSelectMenuBuilder
31+
| UserSelectMenuBuilder;
2232
export type ModalActionRowComponentBuilder = TextInputBuilder;
2333
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
2434

packages/builders/src/components/Assertions.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { s } from '@sapphire/shapeshift';
2-
import { ButtonStyle, type APIMessageComponentEmoji } from 'discord-api-types/v10';
2+
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
33
import { isValidationEnabled } from '../util/validation.js';
4-
import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption.js';
4+
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
55

66
export const customIdValidator = s.string
77
.lengthGreaterThanOrEqual(1)
@@ -46,7 +46,7 @@ export const jsonOptionValidator = s
4646
})
4747
.setValidationEnabled(isValidationEnabled);
4848

49-
export const optionValidator = s.instance(SelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
49+
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
5050

5151
export const optionsValidator = optionValidator.array
5252
.lengthGreaterThanOrEqual(0)
@@ -56,7 +56,7 @@ export const optionsLengthValidator = s.number.int
5656
.lessThanOrEqual(25)
5757
.setValidationEnabled(isValidationEnabled);
5858

59-
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
59+
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
6060
customIdValidator.parse(customId);
6161
optionsValidator.parse(options);
6262
}
@@ -68,6 +68,8 @@ export function validateRequiredSelectMenuOptionParameters(label?: string, value
6868
labelValueDescriptionValidator.parse(value);
6969
}
7070

71+
export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled);
72+
7173
export const urlValidator = s.string
7274
.url({
7375
allowedProtocols: ['http:', 'https:', 'discord:'],

packages/builders/src/components/Components.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ import {
77
} from './ActionRow.js';
88
import { ComponentBuilder } from './Component.js';
99
import { ButtonBuilder } from './button/Button.js';
10-
import { SelectMenuBuilder } from './selectMenu/SelectMenu.js';
10+
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
11+
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
12+
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
13+
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
14+
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
1115
import { TextInputBuilder } from './textInput/TextInput.js';
1216

1317
export interface MappedComponentTypes {
1418
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
1519
[ComponentType.Button]: ButtonBuilder;
16-
[ComponentType.SelectMenu]: SelectMenuBuilder;
20+
[ComponentType.StringSelect]: StringSelectMenuBuilder;
1721
[ComponentType.TextInput]: TextInputBuilder;
22+
[ComponentType.UserSelect]: UserSelectMenuBuilder;
23+
[ComponentType.RoleSelect]: RoleSelectMenuBuilder;
24+
[ComponentType.MentionableSelect]: MentionableSelectMenuBuilder;
25+
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder;
1826
}
1927

2028
/**
@@ -39,10 +47,18 @@ export function createComponentBuilder(
3947
return new ActionRowBuilder(data);
4048
case ComponentType.Button:
4149
return new ButtonBuilder(data);
42-
case ComponentType.SelectMenu:
43-
return new SelectMenuBuilder(data);
50+
case ComponentType.StringSelect:
51+
return new StringSelectMenuBuilder(data);
4452
case ComponentType.TextInput:
4553
return new TextInputBuilder(data);
54+
case ComponentType.UserSelect:
55+
return new UserSelectMenuBuilder(data);
56+
case ComponentType.RoleSelect:
57+
return new RoleSelectMenuBuilder(data);
58+
case ComponentType.MentionableSelect:
59+
return new MentionableSelectMenuBuilder(data);
60+
case ComponentType.ChannelSelect:
61+
return new ChannelSelectMenuBuilder(data);
4662
default:
4763
// @ts-expect-error: This case can still occur if we get a newer unsupported component type
4864
throw new Error(`Cannot properly serialize component type: ${data.type}`);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { APISelectMenuComponent } from 'discord-api-types/v10';
2+
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
3+
import { ComponentBuilder } from '../Component.js';
4+
5+
export class BaseSelectMenuBuilder<
6+
SelectMenuType extends APISelectMenuComponent,
7+
> extends ComponentBuilder<SelectMenuType> {
8+
/**
9+
* Sets the placeholder for this select menu
10+
*
11+
* @param placeholder - The placeholder to use for this select menu
12+
*/
13+
public setPlaceholder(placeholder: string) {
14+
this.data.placeholder = placeholderValidator.parse(placeholder);
15+
return this;
16+
}
17+
18+
/**
19+
* Sets the minimum values that must be selected in the select menu
20+
*
21+
* @param minValues - The minimum values that must be selected
22+
*/
23+
public setMinValues(minValues: number) {
24+
this.data.min_values = minMaxValidator.parse(minValues);
25+
return this;
26+
}
27+
28+
/**
29+
* Sets the maximum values that must be selected in the select menu
30+
*
31+
* @param maxValues - The maximum values that must be selected
32+
*/
33+
public setMaxValues(maxValues: number) {
34+
this.data.max_values = minMaxValidator.parse(maxValues);
35+
return this;
36+
}
37+
38+
/**
39+
* Sets the custom id for this select menu
40+
*
41+
* @param customId - The custom id to use for this select menu
42+
*/
43+
public setCustomId(customId: string) {
44+
this.data.custom_id = customIdValidator.parse(customId);
45+
return this;
46+
}
47+
48+
/**
49+
* Sets whether this select menu is disabled
50+
*
51+
* @param disabled - Whether this select menu is disabled
52+
*/
53+
public setDisabled(disabled = true) {
54+
this.data.disabled = disabledValidator.parse(disabled);
55+
return this;
56+
}
57+
58+
public toJSON(): SelectMenuType {
59+
customIdValidator.parse(this.data.custom_id);
60+
return {
61+
...this.data,
62+
} as SelectMenuType;
63+
}
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { APIChannelSelectComponent, ChannelType } from 'discord-api-types/v10';
2+
import { ComponentType } from 'discord-api-types/v10';
3+
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
4+
import { channelTypesValidator, customIdValidator } from '../Assertions.js';
5+
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
6+
7+
export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
8+
/**
9+
* Creates a new select menu from API data
10+
*
11+
* @param data - The API data to create this select menu with
12+
* @example
13+
* Creating a select menu from an API data object
14+
* ```ts
15+
* const selectMenu = new ChannelSelectMenuBuilder({
16+
* custom_id: 'a cool select menu',
17+
* placeholder: 'select an option',
18+
* max_values: 2,
19+
* });
20+
* ```
21+
* @example
22+
* Creating a select menu using setters and API data
23+
* ```ts
24+
* const selectMenu = new ChannelSelectMenuBuilder({
25+
* custom_id: 'a cool select menu',
26+
* })
27+
* .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
28+
* .setMinValues(2)
29+
* ```
30+
*/
31+
public constructor(data?: Partial<APIChannelSelectComponent>) {
32+
super({ ...data, type: ComponentType.ChannelSelect });
33+
}
34+
35+
public addChannelTypes(...types: RestOrArray<ChannelType>) {
36+
// eslint-disable-next-line no-param-reassign
37+
types = normalizeArray(types);
38+
39+
this.data.channel_types ??= [];
40+
this.data.channel_types.push(...channelTypesValidator.parse(types));
41+
return this;
42+
}
43+
44+
public setChannelTypes(...types: RestOrArray<ChannelType>) {
45+
// eslint-disable-next-line no-param-reassign
46+
types = normalizeArray(types);
47+
48+
this.data.channel_types ??= [];
49+
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(types));
50+
return this;
51+
}
52+
53+
/**
54+
* {@inheritDoc ComponentBuilder.toJSON}
55+
*/
56+
public override toJSON(): APIChannelSelectComponent {
57+
customIdValidator.parse(this.data.custom_id);
58+
59+
return {
60+
...this.data,
61+
} as APIChannelSelectComponent;
62+
}
63+
}

0 commit comments

Comments
 (0)