-
-
Notifications
You must be signed in to change notification settings - Fork 4k
feat(builders): modal select menus in builders v1 #11138
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
Merged
+672
−2,709
Merged
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
adebc70
feat(builders): modal select menus
vladfrangu b314218
chore: fix test
vladfrangu 517f58e
fix: move setRequired up
vladfrangu 0f9b0ab
fix: pack
vladfrangu e99f57b
chore: forwardport https://github.com/discordjs/discord.js/pull/11139
vladfrangu e89fe43
types: expect errors
Jiralite 7375637
fix: id validator being required
vladfrangu cb96cef
fix: validators 2
vladfrangu 94877bf
Apply suggestion from @Jiralite
Jiralite 0b179c3
Apply suggestion from @Jiralite
Jiralite 966a14e
Apply suggestion from @Jiralite
Jiralite 9c4ca31
fix: replace tests
Jiralite 11199cc
Apply suggestion from @Copilot
Jiralite 6906fec
Apply suggestion from @Copilot
Jiralite File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { s } from '@sapphire/shapeshift'; | ||
import { ComponentType } from 'discord-api-types/v10'; | ||
import { isValidationEnabled } from '../../util/validation.js'; | ||
import { idValidator } from '../Assertions.js'; | ||
import { | ||
selectMenuChannelPredicate, | ||
selectMenuMentionablePredicate, | ||
selectMenuRolePredicate, | ||
selectMenuStringPredicate, | ||
selectMenuUserPredicate, | ||
} from '../selectMenu/Assertions.js'; | ||
import { textInputPredicate } from '../textInput/Assertions.js'; | ||
|
||
export const labelPredicate = s | ||
.object({ | ||
id: idValidator.optional(), | ||
type: s.literal(ComponentType.Label), | ||
label: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45), | ||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(), | ||
component: s.union([ | ||
textInputPredicate, | ||
selectMenuUserPredicate, | ||
selectMenuRolePredicate, | ||
selectMenuMentionablePredicate, | ||
selectMenuChannelPredicate, | ||
selectMenuStringPredicate, | ||
]), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import type { | ||
APIChannelSelectComponent, | ||
APILabelComponent, | ||
APIMentionableSelectComponent, | ||
APIRoleSelectComponent, | ||
APIStringSelectComponent, | ||
APITextInputComponent, | ||
APIUserSelectComponent, | ||
} from 'discord-api-types/v10'; | ||
import { ComponentType } from 'discord-api-types/v10'; | ||
import { ComponentBuilder } from '../Component.js'; | ||
import { createComponentBuilder, resolveBuilder } from '../Components.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'; | ||
import { labelPredicate } from './Assertions.js'; | ||
|
||
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> { | ||
component?: | ||
| ChannelSelectMenuBuilder | ||
| MentionableSelectMenuBuilder | ||
| RoleSelectMenuBuilder | ||
| StringSelectMenuBuilder | ||
| TextInputBuilder | ||
| UserSelectMenuBuilder; | ||
} | ||
|
||
/** | ||
* A builder that creates API-compatible JSON data for labels. | ||
*/ | ||
export class LabelBuilder extends ComponentBuilder<LabelBuilderData> { | ||
/** | ||
* @internal | ||
*/ | ||
public override readonly data: LabelBuilderData; | ||
|
||
/** | ||
* Creates a new label. | ||
* | ||
* @param data - The API data to create this label with | ||
* @example | ||
* Creating a label from an API data object: | ||
* ```ts | ||
* const label = new LabelBuilder({ | ||
* label: "label", | ||
* component, | ||
* }); | ||
* ``` | ||
* @example | ||
* Creating a label using setters and API data: | ||
* ```ts | ||
* const label = new LabelBuilder({ | ||
* label: 'label', | ||
* component, | ||
* }).setContent('new text'); | ||
* ``` | ||
*/ | ||
public constructor(data: Partial<APILabelComponent> = {}) { | ||
super({ type: ComponentType.Label }); | ||
|
||
const { component, ...rest } = data; | ||
|
||
this.data = { | ||
...rest, | ||
component: component ? createComponentBuilder(component) : undefined, | ||
type: ComponentType.Label, | ||
}; | ||
} | ||
|
||
/** | ||
* Sets the label for this label. | ||
* | ||
* @param label - The label to use | ||
*/ | ||
public setLabel(label: string) { | ||
this.data.label = label; | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets the description for this label. | ||
* | ||
* @param description - The description to use | ||
*/ | ||
public setDescription(description: string) { | ||
this.data.description = description; | ||
return this; | ||
} | ||
|
||
/** | ||
* Clears the description for this label. | ||
*/ | ||
public clearDescription() { | ||
this.data.description = undefined; | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a string select menu component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setStringSelectMenuComponent( | ||
input: | ||
| APIStringSelectComponent | ||
| StringSelectMenuBuilder | ||
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, StringSelectMenuBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a user select menu component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setUserSelectMenuComponent( | ||
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, UserSelectMenuBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a role select menu component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setRoleSelectMenuComponent( | ||
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, RoleSelectMenuBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a mentionable select menu component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setMentionableSelectMenuComponent( | ||
input: | ||
| APIMentionableSelectComponent | ||
| MentionableSelectMenuBuilder | ||
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, MentionableSelectMenuBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a channel select menu component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setChannelSelectMenuComponent( | ||
input: | ||
| APIChannelSelectComponent | ||
| ChannelSelectMenuBuilder | ||
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, ChannelSelectMenuBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets a text input component to this label. | ||
* | ||
* @param input - A function that returns a component builder or an already built builder | ||
*/ | ||
public setTextInputComponent( | ||
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder), | ||
): this { | ||
this.data.component = resolveBuilder(input, TextInputBuilder); | ||
return this; | ||
} | ||
|
||
/** | ||
* {@inheritDoc ComponentBuilder.toJSON} | ||
*/ | ||
public override toJSON(): APILabelComponent { | ||
const { component, ...rest } = this.data; | ||
|
||
const data = { | ||
...rest, | ||
// The label predicate validates the component. | ||
component: component?.toJSON(), | ||
}; | ||
|
||
labelPredicate.parse(data); | ||
|
||
return data as APILabelComponent; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { Result, s } from '@sapphire/shapeshift'; | ||
import { ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10'; | ||
import { isValidationEnabled } from '../../util/validation.js'; | ||
import { customIdValidator, emojiValidator, idValidator } from '../Assertions'; | ||
Jiralite marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
import { labelValidator } from '../textInput/Assertions.js'; | ||
|
||
const selectMenuBasePredicate = s.object({ | ||
id: idValidator.optional(), | ||
placeholder: s.string().lengthLessThanOrEqual(150).optional(), | ||
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(), | ||
max_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(), | ||
custom_id: customIdValidator, | ||
disabled: s.boolean().optional(), | ||
}); | ||
|
||
export const selectMenuChannelPredicate = selectMenuBasePredicate | ||
.extend({ | ||
type: s.literal(ComponentType.ChannelSelect), | ||
channel_types: s.nativeEnum(ChannelType).array().optional(), | ||
default_values: s | ||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Channel) }) | ||
.array() | ||
.lengthLessThanOrEqual(25) | ||
.optional(), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); | ||
|
||
export const selectMenuMentionablePredicate = selectMenuBasePredicate | ||
.extend({ | ||
type: s.literal(ComponentType.MentionableSelect), | ||
default_values: s | ||
.object({ | ||
id: s.string(), | ||
type: s.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]), | ||
}) | ||
.array() | ||
.lengthLessThanOrEqual(25) | ||
.optional(), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); | ||
|
||
export const selectMenuRolePredicate = selectMenuBasePredicate | ||
.extend({ | ||
type: s.literal(ComponentType.RoleSelect), | ||
default_values: s | ||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Role) }) | ||
.array() | ||
.lengthLessThanOrEqual(25) | ||
.optional(), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); | ||
|
||
export const selectMenuUserPredicate = selectMenuBasePredicate | ||
.extend({ | ||
type: s.literal(ComponentType.UserSelect), | ||
default_values: s | ||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.User) }) | ||
.array() | ||
.lengthLessThanOrEqual(25) | ||
.optional(), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); | ||
|
||
export const selectMenuStringOptionPredicate = s | ||
.object({ | ||
label: labelValidator, | ||
value: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100), | ||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(), | ||
emoji: emojiValidator.optional(), | ||
default: s.boolean().optional(), | ||
}) | ||
.setValidationEnabled(isValidationEnabled); | ||
|
||
export const selectMenuStringPredicate = selectMenuBasePredicate | ||
.extend({ | ||
type: s.literal(ComponentType.StringSelect), | ||
options: selectMenuStringOptionPredicate.array().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(25), | ||
}) | ||
.reshape((value) => { | ||
if (value.min_values !== undefined && value.options.length < value.min_values) { | ||
return Result.err(new RangeError(`The number of options must be greater than or equal to min_values`)); | ||
} | ||
|
||
if (value.min_values !== undefined && value.max_values !== undefined && value.min_values > value.max_values) { | ||
return Result.err( | ||
new RangeError(`The maximum amount of options must be greater than or equal to the minimum amount of options`), | ||
); | ||
} | ||
|
||
return Result.ok(value); | ||
}) | ||
.setValidationEnabled(isValidationEnabled); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.