Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new select menus #8793

Merged
merged 24 commits into from Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ed5a261
feat(builders): new select menus
didinele Oct 28, 2022
71e2871
chore: better re-exporting of deprecated classes
didinele Oct 28, 2022
1d4055a
feat: new select menus
didinele Oct 28, 2022
aecbc03
chore: typings
didinele Oct 28, 2022
85a6cc1
chore: add missing todo comment
didinele Oct 28, 2022
e6eeb88
chore: finish updating tests
didinele Oct 28, 2022
05226bd
chore: add runtime deprecation warnings
didinele Oct 28, 2022
40703b4
chore: format deprecation warning
didinele Oct 28, 2022
03be08f
feat(BaseInteraction): isAnySelectMenu
didinele Oct 28, 2022
4ee4aa0
chore: requested changes
didinele Oct 28, 2022
20580d1
fix: deprecation comments
didinele Oct 28, 2022
8becd97
chore: update @deprecated comments in typings
didinele Oct 29, 2022
41ad75e
chore: add tests for select menu type narrowing
didinele Oct 29, 2022
e559511
fix: bad auto imports
didinele Oct 31, 2022
f7c9df8
fix: properly handle resolved members
didinele Oct 31, 2022
ad908c2
fix: collectors
didinele Oct 31, 2022
74a0ce6
chore: suggested changes
didinele Oct 31, 2022
9149eac
fix(typings): bad class extends
didinele Nov 1, 2022
a316475
feat(ChannelSelectMenuBuilder): validation
didinele Nov 1, 2022
92ec4ad
chore: update todo comment
didinele Nov 1, 2022
62f5240
refactor(ChannelSelectMenu): better handling of channel_types state
didinele Nov 1, 2022
f5d654c
chore: style nit
didinele Nov 1, 2022
08b4bf3
chore: suggested nits
didinele Nov 1, 2022
5bdf316
Merge branch 'main' into feat/new-select-menus
kodiakhq[bot] Nov 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 10 additions & 10 deletions packages/builders/__tests__/components/actionRow.test.ts
Expand Up @@ -9,8 +9,8 @@ import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
SelectMenuBuilder,
SelectMenuOptionBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from '../../src/index.js';

const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
Expand All @@ -29,7 +29,7 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.SelectMenu,
type: ComponentType.StringSelect,
custom_id: '1234',
options: [
{
Expand Down Expand Up @@ -73,7 +73,7 @@ describe('Action Row Components', () => {
url: 'https://google.com',
},
{
type: ComponentType.SelectMenu,
type: ComponentType.StringSelect,
placeholder: 'test',
custom_id: 'test',
options: [
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('Action Row Components', () => {
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.SelectMenu,
type: ComponentType.StringSelect,
custom_id: '1234',
options: [
{
Expand All @@ -134,17 +134,17 @@ describe('Action Row Components', () => {

test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuBuilder()
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234')
.setMaxValues(10)
.setMinValues(12)
.setOptions(
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
)
.setOptions([
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
]);

expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
Expand Down
10 changes: 5 additions & 5 deletions packages/builders/__tests__/components/components.test.ts
Expand Up @@ -13,12 +13,12 @@ import {
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
SelectMenuBuilder,
StringSelectMenuBuilder,
TextInputBuilder,
} from '../../src/index.js';

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

test('GIVEN a select menu component THEN returns a SelectMenuBuilder', () => {
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {
const selectMenu: APISelectMenuComponent = {
custom_id: 'abc',
options: [],
type: ComponentType.SelectMenu,
type: ComponentType.StringSelect,
};

expect(createComponentBuilder(selectMenu)).toBeInstanceOf(SelectMenuBuilder);
expect(createComponentBuilder(selectMenu)).toBeInstanceOf(StringSelectMenuBuilder);
});

test('GIVEN a text input component THEN returns a TextInputBuilder', () => {
Expand Down
16 changes: 8 additions & 8 deletions packages/builders/__tests__/components/selectMenu.test.ts
@@ -1,9 +1,9 @@
import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index.js';
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';

const selectMenu = () => new SelectMenuBuilder();
const selectMenuOption = () => new SelectMenuOptionBuilder();
const selectMenu = () => new StringSelectMenuBuilder();
const selectMenuOption = () => new StringSelectMenuOptionBuilder();

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

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

test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new StringSelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(),
).toEqual(selectMenuData);
expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)])
new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions([new StringSelectMenuOptionBuilder(selectMenuOptionData)])
.toJSON(),
).toEqual(selectMenuData);
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
expect(new StringSelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});
});
});
2 changes: 1 addition & 1 deletion packages/builders/package.json
Expand Up @@ -56,7 +56,7 @@
"dependencies": {
"@discordjs/util": "workspace:^",
"@sapphire/shapeshift": "^3.7.0",
"discord-api-types": "^0.37.14",
"discord-api-types": "^0.37.15",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.1",
"tslib": "^2.4.0"
Expand Down
14 changes: 12 additions & 2 deletions packages/builders/src/components/ActionRow.ts
Expand Up @@ -11,14 +11,24 @@ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
import { ComponentBuilder } from './Component.js';
import { createComponentBuilder } from './Components.js';
import type { ButtonBuilder } from './button/Button.js';
import type { SelectMenuBuilder } from './selectMenu/SelectMenu.js';
import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import type { TextInputBuilder } from './textInput/TextInput.js';

export type MessageComponentBuilder =
| ActionRowBuilder<MessageActionRowComponentBuilder>
| MessageActionRowComponentBuilder;
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
export type MessageActionRowComponentBuilder =
| ButtonBuilder
| ChannelSelectMenuBuilder
| MentionableSelectMenuBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder;
export type ModalActionRowComponentBuilder = TextInputBuilder;
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;

Expand Down
10 changes: 6 additions & 4 deletions packages/builders/src/components/Assertions.ts
@@ -1,7 +1,7 @@
import { s } from '@sapphire/shapeshift';
import { ButtonStyle, type APIMessageComponentEmoji } from 'discord-api-types/v10';
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
import { isValidationEnabled } from '../util/validation.js';
import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption.js';
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';

export const customIdValidator = s.string
.lengthGreaterThanOrEqual(1)
Expand Down Expand Up @@ -46,7 +46,7 @@ export const jsonOptionValidator = s
})
.setValidationEnabled(isValidationEnabled);

export const optionValidator = s.instance(SelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);

export const optionsValidator = optionValidator.array
.lengthGreaterThanOrEqual(0)
Expand All @@ -56,7 +56,7 @@ export const optionsLengthValidator = s.number.int
.lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled);

export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
}
Expand All @@ -68,6 +68,8 @@ export function validateRequiredSelectMenuOptionParameters(label?: string, value
labelValueDescriptionValidator.parse(value);
}

export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled);

export const urlValidator = s.string
.url({
allowedProtocols: ['http:', 'https:', 'discord:'],
Expand Down
24 changes: 20 additions & 4 deletions packages/builders/src/components/Components.ts
Expand Up @@ -7,14 +7,22 @@ import {
} from './ActionRow.js';
import { ComponentBuilder } from './Component.js';
import { ButtonBuilder } from './button/Button.js';
import { SelectMenuBuilder } from './selectMenu/SelectMenu.js';
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import { TextInputBuilder } from './textInput/TextInput.js';

export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
[ComponentType.Button]: ButtonBuilder;
[ComponentType.SelectMenu]: SelectMenuBuilder;
[ComponentType.StringSelect]: StringSelectMenuBuilder;
[ComponentType.TextInput]: TextInputBuilder;
[ComponentType.UserSelect]: UserSelectMenuBuilder;
[ComponentType.RoleSelect]: RoleSelectMenuBuilder;
[ComponentType.MentionableSelect]: MentionableSelectMenuBuilder;
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder;
}

/**
Expand All @@ -39,10 +47,18 @@ export function createComponentBuilder(
return new ActionRowBuilder(data);
case ComponentType.Button:
return new ButtonBuilder(data);
case ComponentType.SelectMenu:
return new SelectMenuBuilder(data);
case ComponentType.StringSelect:
return new StringSelectMenuBuilder(data);
case ComponentType.TextInput:
return new TextInputBuilder(data);
case ComponentType.UserSelect:
return new UserSelectMenuBuilder(data);
case ComponentType.RoleSelect:
return new RoleSelectMenuBuilder(data);
case ComponentType.MentionableSelect:
return new MentionableSelectMenuBuilder(data);
case ComponentType.ChannelSelect:
return new ChannelSelectMenuBuilder(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}`);
Expand Down
64 changes: 64 additions & 0 deletions packages/builders/src/components/selectMenu/BaseSelectMenu.ts
@@ -0,0 +1,64 @@
import type { APISelectMenuComponent } from 'discord-api-types/v10';
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
import { ComponentBuilder } from '../Component.js';

export class BaseSelectMenuBuilder<
SelectMenuType extends APISelectMenuComponent,
> extends ComponentBuilder<SelectMenuType> {
/**
* Sets the placeholder for this select menu
*
* @param placeholder - The placeholder to use for this select menu
*/
public setPlaceholder(placeholder: string) {
this.data.placeholder = placeholderValidator.parse(placeholder);
return this;
}

/**
* Sets the minimum values that must be selected in the select menu
*
* @param minValues - The minimum values that must be selected
*/
public setMinValues(minValues: number) {
this.data.min_values = minMaxValidator.parse(minValues);
return this;
}

/**
* Sets the maximum values that must be selected in the select menu
*
* @param maxValues - The maximum values that must be selected
*/
public setMaxValues(maxValues: number) {
this.data.max_values = minMaxValidator.parse(maxValues);
return this;
}

/**
* Sets the custom id for this select menu
*
* @param customId - The custom id to use for this select menu
*/
public setCustomId(customId: string) {
this.data.custom_id = customIdValidator.parse(customId);
return this;
}

/**
* Sets whether this select menu is disabled
*
* @param disabled - Whether this select menu is disabled
*/
public setDisabled(disabled = true) {
this.data.disabled = disabledValidator.parse(disabled);
return this;
}

public toJSON(): SelectMenuType {
customIdValidator.parse(this.data.custom_id);
return {
...this.data,
} as SelectMenuType;
}
}
@@ -0,0 +1,63 @@
import type { APIChannelSelectComponent, ChannelType } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { channelTypesValidator, customIdValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';

export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
/**
* Creates a new select menu from API data
*
* @param data - The API data to create this select menu with
* @example
* Creating a select menu from an API data object
* ```ts
* const selectMenu = new ChannelSelectMenuBuilder({
* custom_id: 'a cool select menu',
* placeholder: 'select an option',
* max_values: 2,
* });
* ```
* @example
* Creating a select menu using setters and API data
* ```ts
* const selectMenu = new ChannelSelectMenuBuilder({
* custom_id: 'a cool select menu',
* })
* .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
* .setMinValues(2)
* ```
*/
public constructor(data?: Partial<APIChannelSelectComponent>) {
super({ ...data, type: ComponentType.ChannelSelect });
}

public addChannelTypes(...types: RestOrArray<ChannelType>) {
didinele marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-param-reassign
types = normalizeArray(types);

this.data.channel_types ??= [];
this.data.channel_types.push(...channelTypesValidator.parse(types));
return this;
}

public setChannelTypes(...types: RestOrArray<ChannelType>) {
// eslint-disable-next-line no-param-reassign
types = normalizeArray(types);

this.data.channel_types ??= [];
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(types));
return this;
}

/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(): APIChannelSelectComponent {
customIdValidator.parse(this.data.custom_id);

return {
...this.data,
} as APIChannelSelectComponent;
}
}