implement select
This commit is contained in:
@@ -18,7 +18,7 @@ export class DiscordJsAdapter implements Adapter<Discord.CommandInteraction> {
|
||||
listener: (interaction: ComponentInteraction) => void,
|
||||
) {
|
||||
this.client.on("interactionCreate", (interaction) => {
|
||||
if (interaction.isButton()) {
|
||||
if (interaction.isMessageComponent()) {
|
||||
listener(createReacordComponentInteraction(interaction))
|
||||
}
|
||||
})
|
||||
@@ -56,15 +56,32 @@ export class DiscordJsAdapter implements Adapter<Discord.CommandInteraction> {
|
||||
function createReacordComponentInteraction(
|
||||
interaction: Discord.MessageComponentInteraction,
|
||||
): ComponentInteraction {
|
||||
return {
|
||||
type: "button",
|
||||
id: interaction.id,
|
||||
channelId: interaction.channelId,
|
||||
customId: interaction.customId,
|
||||
update: async (options) => {
|
||||
await interaction.update(getDiscordMessageOptions(options))
|
||||
},
|
||||
if (interaction.isButton()) {
|
||||
return {
|
||||
type: "button",
|
||||
id: interaction.id,
|
||||
channelId: interaction.channelId,
|
||||
customId: interaction.customId,
|
||||
update: async (options) => {
|
||||
await interaction.update(getDiscordMessageOptions(options))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isSelectMenu()) {
|
||||
return {
|
||||
type: "select",
|
||||
id: interaction.id,
|
||||
channelId: interaction.channelId,
|
||||
customId: interaction.customId,
|
||||
values: interaction.values,
|
||||
update: async (options) => {
|
||||
await interaction.update(getDiscordMessageOptions(options))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
raise(`Unsupported component interaction type: ${interaction.type}`)
|
||||
}
|
||||
|
||||
function createReacordMessage(message: Discord.Message): Message {
|
||||
@@ -94,19 +111,33 @@ function getDiscordMessageOptions(
|
||||
embeds: options.embeds,
|
||||
components: options.actionRows.map((row) => ({
|
||||
type: "ACTION_ROW",
|
||||
components: row.map((component) => {
|
||||
if (component.type === "button") {
|
||||
return {
|
||||
type: "BUTTON",
|
||||
customId: component.customId,
|
||||
label: component.label ?? "",
|
||||
style: toUpper(component.style ?? "secondary"),
|
||||
disabled: component.disabled,
|
||||
emoji: component.emoji,
|
||||
components: row.map(
|
||||
(component): Discord.MessageActionRowComponentOptions => {
|
||||
if (component.type === "button") {
|
||||
return {
|
||||
type: "BUTTON",
|
||||
customId: component.customId,
|
||||
label: component.label ?? "",
|
||||
style: toUpper(component.style ?? "secondary"),
|
||||
disabled: component.disabled,
|
||||
emoji: component.emoji,
|
||||
}
|
||||
}
|
||||
}
|
||||
raise(`Unsupported component type: ${component.type}`)
|
||||
}),
|
||||
|
||||
if (component.type === "select") {
|
||||
return {
|
||||
...component,
|
||||
type: "SELECT_MENU",
|
||||
options: component.options.map((option) => ({
|
||||
...option,
|
||||
default: component.values?.includes(option.value),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
raise(`Unsupported component type: ${component.type}`)
|
||||
},
|
||||
),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
14
library/core/components/option-node.ts
Normal file
14
library/core/components/option-node.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { MessageSelectOptionOptions } from "../../internal/message"
|
||||
import { Node } from "../../internal/node"
|
||||
import type { OptionProps } from "./option"
|
||||
|
||||
export class OptionNode extends Node<OptionProps> {
|
||||
get options(): MessageSelectOptionOptions {
|
||||
return {
|
||||
label: this.props.children || this.props.label || this.props.value || "",
|
||||
value: this.props.value,
|
||||
description: this.props.description,
|
||||
emoji: this.props.emoji,
|
||||
}
|
||||
}
|
||||
}
|
||||
17
library/core/components/option.tsx
Normal file
17
library/core/components/option.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react"
|
||||
import { ReacordElement } from "../../internal/element"
|
||||
import { OptionNode } from "./option-node"
|
||||
|
||||
export type OptionProps = {
|
||||
label?: string
|
||||
children?: string
|
||||
value: string
|
||||
description?: string
|
||||
emoji?: string
|
||||
}
|
||||
|
||||
export function Option(props: OptionProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new OptionNode(props)} />
|
||||
)
|
||||
}
|
||||
89
library/core/components/select.tsx
Normal file
89
library/core/components/select.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { nanoid } from "nanoid"
|
||||
import type { ReactNode } from "react"
|
||||
import React from "react"
|
||||
import { isInstanceOf } from "../../../helpers/is-instance-of"
|
||||
import { ReacordElement } from "../../internal/element.js"
|
||||
import type { ComponentInteraction } from "../../internal/interaction"
|
||||
import type { MessageOptions } from "../../internal/message"
|
||||
import { getNextActionRow } from "../../internal/message"
|
||||
import { Node } from "../../internal/node.js"
|
||||
import { OptionNode } from "./option-node"
|
||||
|
||||
export type SelectProps = {
|
||||
children?: ReactNode
|
||||
value?: string
|
||||
values?: string[]
|
||||
placeholder?: string
|
||||
multiple?: boolean
|
||||
minValues?: number
|
||||
maxValues?: number
|
||||
disabled?: boolean
|
||||
onSelect?: (event: SelectEvent) => void
|
||||
onSelectValue?: (value: string) => void
|
||||
onSelectMultiple?: (values: string[]) => void
|
||||
}
|
||||
|
||||
export type SelectEvent = {
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export function Select(props: SelectProps) {
|
||||
return (
|
||||
<ReacordElement props={props} createNode={() => new SelectNode(props)}>
|
||||
{props.children}
|
||||
</ReacordElement>
|
||||
)
|
||||
}
|
||||
|
||||
class SelectNode extends Node<SelectProps> {
|
||||
readonly customId = nanoid()
|
||||
|
||||
override modifyMessageOptions(message: MessageOptions): void {
|
||||
const actionRow = getNextActionRow(message)
|
||||
|
||||
const options = [...this.children]
|
||||
.filter(isInstanceOf(OptionNode))
|
||||
.map((node) => node.options)
|
||||
|
||||
const {
|
||||
multiple,
|
||||
value,
|
||||
values,
|
||||
minValues = 0,
|
||||
maxValues = 25,
|
||||
children,
|
||||
onSelect,
|
||||
onSelectValue,
|
||||
onSelectMultiple,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
actionRow.push({
|
||||
...props,
|
||||
type: "select",
|
||||
customId: this.customId,
|
||||
options,
|
||||
values: [...(values || []), ...(value ? [value] : [])],
|
||||
minValues: multiple ? minValues : undefined,
|
||||
maxValues: multiple ? Math.max(minValues, maxValues) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
override handleComponentInteraction(
|
||||
interaction: ComponentInteraction,
|
||||
): boolean {
|
||||
if (
|
||||
interaction.type === "select" &&
|
||||
interaction.customId === this.customId &&
|
||||
!this.props.disabled
|
||||
) {
|
||||
this.props.onSelect?.({ values: interaction.values })
|
||||
this.props.onSelectMultiple?.(interaction.values)
|
||||
if (interaction.values[0]) {
|
||||
this.props.onSelectValue?.(interaction.values[0])
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,6 @@ export type SelectInteraction = {
|
||||
id: string
|
||||
channelId: string
|
||||
customId: string
|
||||
values: string[]
|
||||
update(options: MessageOptions): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { Except } from "type-fest"
|
||||
import { last } from "../../helpers/last"
|
||||
import type { EmbedOptions } from "../core/components/embed-options"
|
||||
import type { SelectProps } from "../core/components/select"
|
||||
|
||||
export type MessageOptions = {
|
||||
content: string
|
||||
embeds: EmbedOptions[]
|
||||
actionRows: Array<
|
||||
Array<MessageButtonOptions | MessageLinkOptions | MessageSelectOptions>
|
||||
>
|
||||
actionRows: ActionRow[]
|
||||
}
|
||||
|
||||
type ActionRow = Array<
|
||||
MessageButtonOptions | MessageLinkOptions | MessageSelectOptions
|
||||
>
|
||||
|
||||
export type MessageButtonOptions = {
|
||||
type: "button"
|
||||
customId: string
|
||||
@@ -25,12 +30,33 @@ export type MessageLinkOptions = {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type MessageSelectOptions = {
|
||||
export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
|
||||
type: "select"
|
||||
customId: string
|
||||
options: MessageSelectOptionOptions[]
|
||||
}
|
||||
|
||||
export type MessageSelectOptionOptions = {
|
||||
label: string
|
||||
value: string
|
||||
description?: string
|
||||
emoji?: string
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
edit(options: MessageOptions): Promise<void>
|
||||
disableComponents(): Promise<void>
|
||||
}
|
||||
|
||||
export function getNextActionRow(options: MessageOptions): ActionRow {
|
||||
let actionRow = last(options.actionRows)
|
||||
if (
|
||||
actionRow == undefined ||
|
||||
actionRow.length >= 5 ||
|
||||
actionRow[0]?.type === "select"
|
||||
) {
|
||||
actionRow = []
|
||||
options.actionRows.push(actionRow)
|
||||
}
|
||||
return actionRow
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ export * from "./core/components/embed-image"
|
||||
export * from "./core/components/embed-thumbnail"
|
||||
export * from "./core/components/embed-title"
|
||||
export * from "./core/components/link"
|
||||
export * from "./core/components/option"
|
||||
export * from "./core/components/select"
|
||||
export * from "./core/reacord"
|
||||
|
||||
@@ -7,11 +7,13 @@ import type {
|
||||
ButtonInteraction,
|
||||
CommandInteraction,
|
||||
ComponentInteraction,
|
||||
SelectInteraction,
|
||||
} from "./internal/interaction"
|
||||
import type {
|
||||
Message,
|
||||
MessageButtonOptions,
|
||||
MessageOptions,
|
||||
MessageSelectOptions,
|
||||
} from "./internal/message"
|
||||
|
||||
export class TestAdapter implements Adapter<TestCommandInteraction> {
|
||||
@@ -44,6 +46,20 @@ export class TestAdapter implements Adapter<TestCommandInteraction> {
|
||||
raise(`Couldn't find button with label "${label}"`)
|
||||
}
|
||||
|
||||
findSelectByPlaceholder(placeholder: string) {
|
||||
for (const message of this.messages) {
|
||||
for (const component of message.options.actionRows.flat()) {
|
||||
if (
|
||||
component.type === "select" &&
|
||||
component.placeholder === placeholder
|
||||
) {
|
||||
return this.createSelectActions(component, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
||||
}
|
||||
|
||||
private createButtonActions(
|
||||
button: MessageButtonOptions,
|
||||
message: TestMessage,
|
||||
@@ -56,6 +72,19 @@ export class TestAdapter implements Adapter<TestCommandInteraction> {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createSelectActions(
|
||||
component: MessageSelectOptions,
|
||||
message: TestMessage,
|
||||
) {
|
||||
return {
|
||||
select: (...values: string[]) => {
|
||||
this.componentInteractionListener(
|
||||
new TestSelectInteraction(component.customId, message, values),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestMessage implements Message {
|
||||
@@ -109,3 +138,19 @@ export class TestButtonInteraction implements ButtonInteraction {
|
||||
this.message.options = options
|
||||
}
|
||||
}
|
||||
|
||||
export class TestSelectInteraction implements SelectInteraction {
|
||||
readonly type = "select"
|
||||
readonly id = nanoid()
|
||||
readonly channelId = "test-channel-id"
|
||||
|
||||
constructor(
|
||||
readonly customId: string,
|
||||
readonly message: TestMessage,
|
||||
readonly values: string[],
|
||||
) {}
|
||||
|
||||
async update(options: MessageOptions): Promise<void> {
|
||||
this.message.options = options
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user