feat(string-prompt): Adds string prompt to menu item
This commit is contained in:
parent
1d73ee8a78
commit
5c7c9c9f87
6 changed files with 164 additions and 26 deletions
|
|
@ -13,7 +13,8 @@ import {
|
||||||
inlineCode,
|
inlineCode,
|
||||||
italic,
|
italic,
|
||||||
Snowflake,
|
Snowflake,
|
||||||
StringSelectMenuBuilder, StringSelectMenuOptionBuilder
|
StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder,
|
||||||
|
TextInputStyle
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import {ChannelId} from "../types/DiscordTypes";
|
import {ChannelId} from "../types/DiscordTypes";
|
||||||
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||||
|
|
@ -80,7 +81,24 @@ export class ConfigurationMenuHandler {
|
||||||
setValue: this.setValue.bind(this)
|
setValue: this.setValue.bind(this)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
traversalKey: "calendar",
|
||||||
|
label: "Calendar",
|
||||||
|
description: "Provides settings for the metadata contained in the playdate exports.",
|
||||||
|
type: MenuItemType.Collection,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
traversalKey: "title",
|
||||||
|
label: "Title",
|
||||||
|
description: "Defines how the calendar entry should be called.",
|
||||||
|
type: MenuItemType.Prompt,
|
||||||
|
getCurrentValue: this.getStringValue.bind(this),
|
||||||
|
getActionRowBuilder: this.getStringBuilder.bind(this),
|
||||||
|
setValue: this.setValue.bind(this)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +176,29 @@ export class ConfigurationMenuHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private setValue(value: FieldMenuItemSaveValue[], context: FieldMenuItemContext): void {
|
private getStringValue(context: FieldMenuItemContext): string {
|
||||||
this.configuration.saveConfiguration(context.path.join('.'), value.join('; '));
|
const value = this.configuration.getConfigurationByPath(context.path.join('.'));
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new TypeError(`Value of type ${typeof value} can't be used for a string value!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStringBuilder(context: FieldMenuItemContext): TextInputBuilder {
|
||||||
|
return new TextInputBuilder()
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setValue(value: FieldMenuItemSaveValue[]|string, context: FieldMenuItemContext): void {
|
||||||
|
const savedValue = typeof value !== 'string' ?
|
||||||
|
value.join('; ') :
|
||||||
|
value;
|
||||||
|
|
||||||
|
this.configuration.saveConfiguration(context.path.join('.'), savedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,9 @@ export class GroupConfigurationHandler {
|
||||||
locale: new Intl.Locale('en-GB'),
|
locale: new Intl.Locale('en-GB'),
|
||||||
permissions: {
|
permissions: {
|
||||||
allowMemberManagingPlaydates: false
|
allowMemberManagingPlaydates: false
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
title: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export enum TransformerType {
|
||||||
Locale,
|
Locale,
|
||||||
Channel,
|
Channel,
|
||||||
PermissionBoolean,
|
PermissionBoolean,
|
||||||
|
String
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupConfigurationTransformer = {
|
type GroupConfigurationTransformer = {
|
||||||
|
|
@ -15,7 +16,7 @@ type GroupConfigurationTransformer = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupConfigurationResult =
|
export type GroupConfigurationResult =
|
||||||
ChannelId | Intl.Locale | boolean
|
ChannelId | Intl.Locale | boolean | string
|
||||||
|
|
||||||
export class GroupConfigurationTransformers {
|
export class GroupConfigurationTransformers {
|
||||||
static TRANSFORMERS: GroupConfigurationTransformer[] = [
|
static TRANSFORMERS: GroupConfigurationTransformer[] = [
|
||||||
|
|
@ -34,6 +35,10 @@ export class GroupConfigurationTransformers {
|
||||||
{
|
{
|
||||||
path: ['permissions', 'allowMemberManagingPlaydates'],
|
path: ['permissions', 'allowMemberManagingPlaydates'],
|
||||||
type: TransformerType.PermissionBoolean
|
type: TransformerType.PermissionBoolean
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ['calendar', 'title'],
|
||||||
|
type: TransformerType.String
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -50,6 +55,8 @@ export class GroupConfigurationTransformers {
|
||||||
return <ChannelId>configValue.value;
|
return <ChannelId>configValue.value;
|
||||||
case TransformerType.PermissionBoolean:
|
case TransformerType.PermissionBoolean:
|
||||||
return configValue.value === '1';
|
return configValue.value === '1';
|
||||||
|
case TransformerType.String:
|
||||||
|
return configValue.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
7
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
|
|
@ -4,7 +4,8 @@ import {Nullable} from "../types/Nullable";
|
||||||
export type RuntimeGroupConfiguration = {
|
export type RuntimeGroupConfiguration = {
|
||||||
channels: Nullable<ChannelRuntimeGroupConfiguration>,
|
channels: Nullable<ChannelRuntimeGroupConfiguration>,
|
||||||
locale: Intl.Locale,
|
locale: Intl.Locale,
|
||||||
permissions: PermissionRuntimeGroupConfiguration
|
permissions: PermissionRuntimeGroupConfiguration,
|
||||||
|
calendar: CalendarRuntimeGroupConfiguration
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelRuntimeGroupConfiguration = {
|
export type ChannelRuntimeGroupConfiguration = {
|
||||||
|
|
@ -14,4 +15,8 @@ export type ChannelRuntimeGroupConfiguration = {
|
||||||
|
|
||||||
export type PermissionRuntimeGroupConfiguration = {
|
export type PermissionRuntimeGroupConfiguration = {
|
||||||
allowMemberManagingPlaydates: boolean
|
allowMemberManagingPlaydates: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CalendarRuntimeGroupConfiguration = {
|
||||||
|
title: null|string
|
||||||
}
|
}
|
||||||
|
|
@ -2,12 +2,21 @@ import {EventHandler} from "../Events/EventHandler";
|
||||||
import {Container} from "../Container/Container";
|
import {Container} from "../Container/Container";
|
||||||
import {AnyMenuItem, MenuAction, MenuItemType, TraversalPath} from "./MenuRenderer.types";
|
import {AnyMenuItem, MenuAction, MenuItemType, TraversalPath} from "./MenuRenderer.types";
|
||||||
import {randomUUID} from "node:crypto";
|
import {randomUUID} from "node:crypto";
|
||||||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, MessageFlags} from "discord.js";
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
CommandInteraction,
|
||||||
|
EmbedBuilder,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
MessageFlags
|
||||||
|
} from "discord.js";
|
||||||
import {Nullable} from "../types/Nullable";
|
import {Nullable} from "../types/Nullable";
|
||||||
import {IconCache} from "../Icons/IconCache";
|
import {IconCache} from "../Icons/IconCache";
|
||||||
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||||
import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent";
|
import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent";
|
||||||
import {MenuTraversal} from "./MenuTraversal";
|
import {MenuTraversal} from "./MenuTraversal";
|
||||||
|
import {Prompt} from "./Modals/Prompt";
|
||||||
|
|
||||||
export class MenuRenderer {
|
export class MenuRenderer {
|
||||||
private readonly menuId: string;
|
private readonly menuId: string;
|
||||||
|
|
@ -15,6 +24,10 @@ export class MenuRenderer {
|
||||||
|
|
||||||
private exitButton: ButtonBuilder;
|
private exitButton: ButtonBuilder;
|
||||||
private backButton: ButtonBuilder;
|
private backButton: ButtonBuilder;
|
||||||
|
private static MAX_BUTTON_PER_ROW = 5;
|
||||||
|
private static MAX_ROWS = 5;
|
||||||
|
private static SYSTEM_ROW_COUNT = 1;
|
||||||
|
private static MAX_USER_ROW_COUNT = MenuRenderer.MAX_ROWS - MenuRenderer.SYSTEM_ROW_COUNT;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly traversal: MenuTraversal,
|
private readonly traversal: MenuTraversal,
|
||||||
|
|
@ -62,26 +75,38 @@ export class MenuRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = [
|
const rows = [
|
||||||
this.getComponentForMenuItem(this.traversal.currentMenuItem),
|
...this.getComponentForMenuItem(this.traversal.currentMenuItem),
|
||||||
navigation
|
navigation
|
||||||
];
|
];
|
||||||
|
|
||||||
return rows.filter(row => row.components.length > 0);
|
return rows.filter(row => row.components.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder<MessageActionRowComponentBuilder> {
|
private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||||
if (menuItem.type === MenuItemType.Collection) {
|
if (menuItem.type === MenuItemType.Collection) {
|
||||||
const navigation = new ActionRowBuilder<ButtonBuilder>();
|
const rowCount = Math.ceil(menuItem.children.length / MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||||
navigation.setComponents(
|
if (rowCount > MenuRenderer.MAX_USER_ROW_COUNT) {
|
||||||
...menuItem.children.map(item => new ButtonBuilder()
|
throw new TypeError(
|
||||||
.setLabel(item.label)
|
`A collection can only have a max of ${MenuRenderer.MAX_USER_ROW_COUNT * MenuRenderer.MAX_BUTTON_PER_ROW} entries!`
|
||||||
.setStyle(ButtonStyle.Primary)
|
);
|
||||||
.setCustomId(this.getInteractionId("MOVE", item.traversalKey))
|
}
|
||||||
.setEmoji(this.iconCache?.get(item.type === MenuItemType.Field ? 'pen_solid' : "folder_solid") ?? '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return navigation
|
return Array.from(Array(rowCount).keys())
|
||||||
|
.map((index) => {
|
||||||
|
const childStart = index * MenuRenderer.MAX_BUTTON_PER_ROW;
|
||||||
|
return menuItem.children.toSpliced(childStart, MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||||
|
})
|
||||||
|
.reverse()
|
||||||
|
.map((items) => new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.setComponents(
|
||||||
|
...items.map(item => new ButtonBuilder()
|
||||||
|
.setLabel(item.label)
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setCustomId(this.getInteractionId("MOVE", item.traversalKey))
|
||||||
|
.setEmoji(this.iconCache?.get(item.type === MenuItemType.Field ? 'pen_solid' : "folder_solid") ?? '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuItem.type === MenuItemType.Field) {
|
if (menuItem.type === MenuItemType.Field) {
|
||||||
|
|
@ -89,10 +114,10 @@ export class MenuRenderer {
|
||||||
path: this.traversal.path
|
path: this.traversal.path
|
||||||
})
|
})
|
||||||
action.setCustomId(this.getInteractionId("SET", this.traversal.stringifiedPath));
|
action.setCustomId(this.getInteractionId("SET", this.traversal.stringifiedPath));
|
||||||
return new ActionRowBuilder<MessageActionRowComponentBuilder>().setComponents([action]);
|
return [new ActionRowBuilder<MessageActionRowComponentBuilder>().setComponents([action])];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActionRowBuilder();
|
return [new ActionRowBuilder()];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEmbed(): EmbedBuilder {
|
private getEmbed(): EmbedBuilder {
|
||||||
|
|
@ -120,7 +145,7 @@ export class MenuRenderer {
|
||||||
return `${this.menuId};${action};${parameter ?? ''}`
|
return `${this.menuId};${action};${parameter ?? ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleUIEvents(ev: ComponentInteractionEvent) {
|
private async handleUIEvents(ev: ComponentInteractionEvent) {
|
||||||
if (!ev.interaction.customId.startsWith(this.menuId)) {
|
if (!ev.interaction.customId.startsWith(this.menuId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +179,38 @@ export class MenuRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.interaction.update({
|
let updateInteraction: unknown = ev.interaction;
|
||||||
|
|
||||||
|
if (this.traversal.currentMenuItem.type === MenuItemType.Prompt) {
|
||||||
|
const prompt = new Prompt(<EventHandler>this.eventHandler);
|
||||||
|
|
||||||
|
const currentValue = this.traversal.currentMenuItem;
|
||||||
|
const currentValuePath = [...this.traversal.path];
|
||||||
|
|
||||||
|
const row = currentValue.getActionRowBuilder({
|
||||||
|
path: currentValuePath
|
||||||
|
});
|
||||||
|
row.setLabel(currentValue.label);
|
||||||
|
row.setValue(currentValue.getCurrentValue({path: currentValuePath}));
|
||||||
|
row.setPlaceholder(currentValue.description ?? '');
|
||||||
|
|
||||||
|
this.traversal.travelBack();
|
||||||
|
const value = await prompt.requestValue(
|
||||||
|
this.traversal.currentMenuItem.label,
|
||||||
|
row,
|
||||||
|
ev.interaction
|
||||||
|
);
|
||||||
|
|
||||||
|
currentValue.setValue(
|
||||||
|
value.value,
|
||||||
|
{
|
||||||
|
path: currentValuePath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
updateInteraction = value.interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInteraction.update({
|
||||||
components: this.getActionRows(),
|
components: this.getActionRows(),
|
||||||
embeds: [ this.getEmbed() ]
|
embeds: [ this.getEmbed() ]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,44 @@
|
||||||
import {Modal} from "./Modal";
|
import {Modal} from "./Modal";
|
||||||
import {Interaction, MessageComponentInteraction, TextInputBuilder} from "discord.js";
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
Interaction,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
TextInputBuilder
|
||||||
|
} from "discord.js";
|
||||||
|
import {EventHandler} from "../../Events/EventHandler";
|
||||||
|
|
||||||
|
export type RequestResponse = {
|
||||||
|
interaction: ModalSubmitInteraction,
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
export class Prompt extends Modal {
|
export class Prompt extends Modal {
|
||||||
|
constructor(eventHandler: EventHandler) {
|
||||||
|
super(eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
public async requestValue(
|
public async requestValue(
|
||||||
label: string,
|
label: string,
|
||||||
field: TextInputBuilder,
|
field: TextInputBuilder,
|
||||||
interaction: MessageComponentInteraction
|
interaction: MessageComponentInteraction
|
||||||
): Promise<string> {
|
): Promise<RequestResponse> {
|
||||||
const modal = this.getBuilder()
|
const modal = this.getBuilder()
|
||||||
.setTitle(label);
|
.setTitle(label);
|
||||||
|
|
||||||
|
field.setCustomId("value");
|
||||||
|
const actionRow = new ActionRowBuilder<TextInputBuilder>();
|
||||||
|
actionRow.setComponents([field])
|
||||||
|
modal.setComponents(actionRow);
|
||||||
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
const responseInteraction = await this.awaitResponse();
|
const responseInteraction = await this.awaitResponse();
|
||||||
responseInteraction.fields.getTextInputValue()
|
const value = responseInteraction.fields.getTextInputValue("value");
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
interaction: responseInteraction
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue