Skip to content

Commit 8589471

Browse files
committed
feat: add select menu handling
1 parent 31b46e1 commit 8589471

File tree

5 files changed

+216
-5
lines changed

5 files changed

+216
-5
lines changed

src/define.ts

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import {
88
ButtonBuilder,
99
ButtonStyle,
1010
type AnyComponentBuilder,
11-
type RestOrArray
11+
type RestOrArray,
12+
StringSelectMenuBuilder,
13+
UserSelectMenuBuilder,
14+
ChannelSelectMenuBuilder,
15+
RoleSelectMenuBuilder,
16+
StringSelectMenuOptionBuilder,
17+
MentionableSelectMenuBuilder
1218
} from 'discord.js'
1319
import type { Stream } from 'node:stream'
1420
import type {
1521
ButtonOptions,
22+
ChannelSelectMenuOptions,
1623
CommandConfig,
1724
CommandExecute,
1825
ContextMenuCallback,
@@ -32,10 +39,16 @@ import type {
3239
HarmonixContextMenu,
3340
HarmonixEvent,
3441
HarmonixEvents,
42+
MentionableSelectMenuOptions,
3543
ModalOptions,
3644
OptionsDef,
37-
PreconditionCallback
45+
PreconditionCallback,
46+
RoleSelectMenuOptions,
47+
SelectMenuOptions,
48+
StringSelectMenuOptions,
49+
UserSelectMenuOptions
3850
} from './types'
51+
import { createError } from './harmonix'
3952

4053
export const defineHarmonixConfig = (config: HarmonixConfig) => {
4154
return config
@@ -221,3 +234,134 @@ export const defineButton = (options: ButtonOptions) => {
221234

222235
return builder
223236
}
237+
238+
export const defineSelectMenu = (options: SelectMenuOptions) => {
239+
const { id, placeholder, type, disabled, minValues, maxValues } = options
240+
241+
switch (type) {
242+
case 'String': {
243+
const stringOptions = options as StringSelectMenuOptions
244+
const selectMenu = new StringSelectMenuBuilder()
245+
.setCustomId(id)
246+
.setPlaceholder(placeholder)
247+
248+
if (disabled) {
249+
selectMenu.setDisabled(disabled)
250+
}
251+
if (minValues) {
252+
selectMenu.setMinValues(minValues)
253+
}
254+
if (maxValues) {
255+
selectMenu.setMaxValues(maxValues)
256+
}
257+
stringOptions.options.forEach((option) => {
258+
const optionBuilder = new StringSelectMenuOptionBuilder()
259+
.setLabel(option.label)
260+
.setValue(option.value)
261+
262+
if (option.description) {
263+
optionBuilder.setDescription(option.description)
264+
}
265+
if (option.emoji) {
266+
optionBuilder.setEmoji(option.emoji)
267+
}
268+
if (option.default) {
269+
optionBuilder.setDefault(true)
270+
}
271+
272+
selectMenu.addOptions(optionBuilder)
273+
})
274+
275+
return selectMenu
276+
}
277+
case 'User': {
278+
const userOptions = options as UserSelectMenuOptions
279+
const selectMenu = new UserSelectMenuBuilder()
280+
.setCustomId(id)
281+
.setPlaceholder(placeholder)
282+
283+
if (disabled) {
284+
selectMenu.setDisabled(disabled)
285+
}
286+
if (minValues) {
287+
selectMenu.setMinValues(minValues)
288+
}
289+
if (maxValues) {
290+
selectMenu.setMaxValues(maxValues)
291+
}
292+
if (userOptions.defaultUsers) {
293+
selectMenu.setDefaultUsers(userOptions.defaultUsers)
294+
}
295+
296+
return selectMenu
297+
}
298+
case 'Channel': {
299+
const channelOptions = options as ChannelSelectMenuOptions
300+
const selectMenu = new ChannelSelectMenuBuilder()
301+
.setCustomId(id)
302+
.setPlaceholder(placeholder)
303+
304+
if (disabled) {
305+
selectMenu.setDisabled(disabled)
306+
}
307+
if (minValues) {
308+
selectMenu.setMinValues(minValues)
309+
}
310+
if (maxValues) {
311+
selectMenu.setMaxValues(maxValues)
312+
}
313+
if (channelOptions.channelTypes) {
314+
selectMenu.addChannelTypes(...channelOptions.channelTypes)
315+
}
316+
if (channelOptions.defaultChannels) {
317+
selectMenu.setDefaultChannels(channelOptions.defaultChannels)
318+
}
319+
320+
return selectMenu
321+
}
322+
case 'Role': {
323+
const roleOptions = options as RoleSelectMenuOptions
324+
const selectMenu = new RoleSelectMenuBuilder()
325+
.setCustomId(id)
326+
.setPlaceholder(placeholder)
327+
328+
if (disabled) {
329+
selectMenu.setDisabled(disabled)
330+
}
331+
if (minValues) {
332+
selectMenu.setMinValues(minValues)
333+
}
334+
if (maxValues) {
335+
selectMenu.setMaxValues(maxValues)
336+
}
337+
if (roleOptions.defaultRoles) {
338+
selectMenu.setDefaultRoles(roleOptions.defaultRoles)
339+
}
340+
341+
return selectMenu
342+
}
343+
case 'Mentionable': {
344+
const mentionableOptions = options as MentionableSelectMenuOptions
345+
const selectMenu = new MentionableSelectMenuBuilder()
346+
.setCustomId(id)
347+
.setPlaceholder(placeholder)
348+
349+
if (disabled) {
350+
selectMenu.setDisabled(disabled)
351+
}
352+
if (minValues) {
353+
selectMenu.setMinValues(minValues)
354+
}
355+
if (maxValues) {
356+
selectMenu.setMaxValues(maxValues)
357+
}
358+
if (mentionableOptions.defaultValues) {
359+
selectMenu.setDefaultValues(mentionableOptions.defaultValues)
360+
}
361+
362+
return selectMenu
363+
}
364+
default:
365+
throw createError('Invalid select menu type')
366+
}
367+
}

src/register.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ export const registerEvents = (harmonix: Harmonix) => {
4545
event.callback(interaction)
4646
})
4747
})
48+
harmonix.client?.on(Events.InteractionCreate, (interaction) => {
49+
if (!interaction.isAnySelectMenu()) return
50+
const event = harmonix.events
51+
.filter((evt) => evt.options.type === 'select')
52+
.find((evt) => evt.options.name === interaction.customId)
53+
54+
if (!event) return
55+
ctx.call(harmonix, () => {
56+
event.callback(interaction)
57+
})
58+
})
4859
}
4960

5061
export const registerCommands = (harmonix: Harmonix) => {

src/resolve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const resolveEvent = (
3131
once: event.options.once || filename(_evtPath).endsWith('.once'),
3232
type:
3333
event.options.type ||
34-
((['modals', 'buttons'].includes(filename(dirname(_evtPath)))
34+
((['modals', 'buttons', 'selects'].includes(filename(dirname(_evtPath)))
3535
? filename(dirname(_evtPath)).slice(0, -1)
3636
: undefined) as EventOptions['type'])
3737
}

src/types/events.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
AnySelectMenuInteraction,
23
ButtonInteraction,
34
ClientEvents,
45
ModalSubmitInteraction
@@ -7,6 +8,7 @@ import type {
78
export interface HarmonixEvents extends ClientEvents {
89
modal: [interaction: ModalSubmitInteraction]
910
button: [interaction: ButtonInteraction]
11+
select: [interaction: AnySelectMenuInteraction]
1012
}
1113

1214
export type EventCallback<Event extends keyof HarmonixEvents | any = any> = (
@@ -16,7 +18,7 @@ export type EventCallback<Event extends keyof HarmonixEvents | any = any> = (
1618
export interface EventOptions {
1719
name?: string
1820
once?: boolean
19-
type?: 'modal' | 'button'
21+
type?: 'modal' | 'button' | 'select'
2022
}
2123

2224
export type DefineEvent = <Event extends keyof HarmonixEvents = any>(

src/types/rich.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import type {
55
APIEmbedField,
66
TextInputStyle,
77
ButtonStyle,
8-
ComponentEmojiResolvable
8+
ComponentEmojiResolvable,
9+
ChannelType,
10+
APISelectMenuDefaultValue,
11+
SelectMenuDefaultValueType
912
} from 'discord.js'
1013

1114
interface TextInput {
@@ -50,3 +53,54 @@ export interface ButtonOptions {
5053
url?: string
5154
disabled?: boolean
5255
}
56+
57+
export interface StringSelectMenuOptions {
58+
type: 'String'
59+
options: {
60+
label: string
61+
value: string
62+
description?: string
63+
emoji?: ComponentEmojiResolvable
64+
default?: boolean
65+
}[]
66+
}
67+
68+
export interface UserSelectMenuOptions {
69+
type: 'User'
70+
defaultUsers?: string[]
71+
}
72+
73+
export interface ChannelSelectMenuOptions {
74+
type: 'Channel'
75+
channelTypes?: ChannelType[]
76+
defaultChannels?: string[]
77+
}
78+
79+
export interface RoleSelectMenuOptions {
80+
type: 'Role'
81+
defaultRoles?: string[]
82+
}
83+
84+
export interface MentionableSelectMenuOptions {
85+
type: 'Mentionable'
86+
defaultValues?:
87+
| APISelectMenuDefaultValue<SelectMenuDefaultValueType.Role>[]
88+
| APISelectMenuDefaultValue<SelectMenuDefaultValueType.User>[]
89+
}
90+
91+
export interface BaseSelectMenuOptions {
92+
id: string
93+
placeholder: string
94+
disabled?: boolean
95+
minValues?: number
96+
maxValues?: number
97+
}
98+
99+
export type SelectMenuOptions = BaseSelectMenuOptions &
100+
(
101+
| StringSelectMenuOptions
102+
| UserSelectMenuOptions
103+
| ChannelSelectMenuOptions
104+
| RoleSelectMenuOptions
105+
| MentionableSelectMenuOptions
106+
)

0 commit comments

Comments
 (0)