diff --git a/source/Groups/ConfigurationMenuHandler.ts b/source/Groups/ConfigurationMenuHandler.ts index 65f6415..85fedf9 100644 --- a/source/Groups/ConfigurationMenuHandler.ts +++ b/source/Groups/ConfigurationMenuHandler.ts @@ -13,7 +13,8 @@ import { inlineCode, italic, Snowflake, - StringSelectMenuBuilder, StringSelectMenuOptionBuilder + StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, + TextInputStyle } from "discord.js"; import {ChannelId} from "../types/DiscordTypes"; import {MessageActionRowComponentBuilder} from "@discordjs/builders"; @@ -80,7 +81,24 @@ export class ConfigurationMenuHandler { 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 { - this.configuration.saveConfiguration(context.path.join('.'), value.join('; ')); + private getStringValue(context: FieldMenuItemContext): string { + 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); } } \ No newline at end of file diff --git a/source/Groups/GroupConfigurationHandler.ts b/source/Groups/GroupConfigurationHandler.ts index bcfdfc7..ae7efac 100644 --- a/source/Groups/GroupConfigurationHandler.ts +++ b/source/Groups/GroupConfigurationHandler.ts @@ -15,6 +15,9 @@ export class GroupConfigurationHandler { locale: new Intl.Locale('en-GB'), permissions: { allowMemberManagingPlaydates: false + }, + calendar: { + title: null } } diff --git a/source/Groups/GroupConfigurationTransformers.ts b/source/Groups/GroupConfigurationTransformers.ts index 4fd59c0..e2017f8 100644 --- a/source/Groups/GroupConfigurationTransformers.ts +++ b/source/Groups/GroupConfigurationTransformers.ts @@ -7,6 +7,7 @@ export enum TransformerType { Locale, Channel, PermissionBoolean, + String } type GroupConfigurationTransformer = { @@ -15,7 +16,7 @@ type GroupConfigurationTransformer = { } export type GroupConfigurationResult = - ChannelId | Intl.Locale | boolean + ChannelId | Intl.Locale | boolean | string export class GroupConfigurationTransformers { static TRANSFORMERS: GroupConfigurationTransformer[] = [ @@ -34,6 +35,10 @@ export class GroupConfigurationTransformers { { path: ['permissions', 'allowMemberManagingPlaydates'], type: TransformerType.PermissionBoolean + }, + { + path: ['calendar', 'title'], + type: TransformerType.String } ]; @@ -50,6 +55,8 @@ export class GroupConfigurationTransformers { return configValue.value; case TransformerType.PermissionBoolean: return configValue.value === '1'; + case TransformerType.String: + return configValue.value; } } diff --git a/source/Groups/RuntimeGroupConfiguration.d.ts b/source/Groups/RuntimeGroupConfiguration.d.ts index 5e6e10a..2909dde 100644 --- a/source/Groups/RuntimeGroupConfiguration.d.ts +++ b/source/Groups/RuntimeGroupConfiguration.d.ts @@ -4,7 +4,8 @@ import {Nullable} from "../types/Nullable"; export type RuntimeGroupConfiguration = { channels: Nullable, locale: Intl.Locale, - permissions: PermissionRuntimeGroupConfiguration + permissions: PermissionRuntimeGroupConfiguration, + calendar: CalendarRuntimeGroupConfiguration }; export type ChannelRuntimeGroupConfiguration = { @@ -14,4 +15,8 @@ export type ChannelRuntimeGroupConfiguration = { export type PermissionRuntimeGroupConfiguration = { allowMemberManagingPlaydates: boolean +} + +export type CalendarRuntimeGroupConfiguration = { + title: null|string } \ No newline at end of file diff --git a/source/Menu/MenuRenderer.ts b/source/Menu/MenuRenderer.ts index a678542..96a16be 100644 --- a/source/Menu/MenuRenderer.ts +++ b/source/Menu/MenuRenderer.ts @@ -2,12 +2,21 @@ import {EventHandler} from "../Events/EventHandler"; import {Container} from "../Container/Container"; import {AnyMenuItem, MenuAction, MenuItemType, TraversalPath} from "./MenuRenderer.types"; 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 {IconCache} from "../Icons/IconCache"; import {MessageActionRowComponentBuilder} from "@discordjs/builders"; import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent"; import {MenuTraversal} from "./MenuTraversal"; +import {Prompt} from "./Modals/Prompt"; export class MenuRenderer { private readonly menuId: string; @@ -15,6 +24,10 @@ export class MenuRenderer { private exitButton: 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( private readonly traversal: MenuTraversal, @@ -62,26 +75,38 @@ export class MenuRenderer { } const rows = [ - this.getComponentForMenuItem(this.traversal.currentMenuItem), + ...this.getComponentForMenuItem(this.traversal.currentMenuItem), navigation ]; return rows.filter(row => row.components.length > 0); } - private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder { + private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder[] { if (menuItem.type === MenuItemType.Collection) { - const navigation = new ActionRowBuilder(); - navigation.setComponents( - ...menuItem.children.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") ?? '') - ) - ) + const rowCount = Math.ceil(menuItem.children.length / MenuRenderer.MAX_BUTTON_PER_ROW); + if (rowCount > MenuRenderer.MAX_USER_ROW_COUNT) { + throw new TypeError( + `A collection can only have a max of ${MenuRenderer.MAX_USER_ROW_COUNT * MenuRenderer.MAX_BUTTON_PER_ROW} entries!` + ); + } - 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() + .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) { @@ -89,10 +114,10 @@ export class MenuRenderer { path: this.traversal.path }) action.setCustomId(this.getInteractionId("SET", this.traversal.stringifiedPath)); - return new ActionRowBuilder().setComponents([action]); + return [new ActionRowBuilder().setComponents([action])]; } - return new ActionRowBuilder(); + return [new ActionRowBuilder()]; } private getEmbed(): EmbedBuilder { @@ -120,7 +145,7 @@ export class MenuRenderer { return `${this.menuId};${action};${parameter ?? ''}` } - private handleUIEvents(ev: ComponentInteractionEvent) { + private async handleUIEvents(ev: ComponentInteractionEvent) { if (!ev.interaction.customId.startsWith(this.menuId)) { 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(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(), embeds: [ this.getEmbed() ] }) diff --git a/source/Menu/Modals/Prompt.ts b/source/Menu/Modals/Prompt.ts index 26ee246..602c89e 100644 --- a/source/Menu/Modals/Prompt.ts +++ b/source/Menu/Modals/Prompt.ts @@ -1,17 +1,44 @@ 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 { + constructor(eventHandler: EventHandler) { + super(eventHandler); + } + public async requestValue( label: string, field: TextInputBuilder, interaction: MessageComponentInteraction - ): Promise { + ): Promise { const modal = this.getBuilder() .setTitle(label); + field.setCustomId("value"); + const actionRow = new ActionRowBuilder(); + actionRow.setComponents([field]) + modal.setComponents(actionRow); + await interaction.showModal(modal); const responseInteraction = await this.awaitResponse(); - responseInteraction.fields.getTextInputValue() + const value = responseInteraction.fields.getTextInputValue("value"); + + return { + value, + interaction: responseInteraction + }; + } } \ No newline at end of file