refactor(menu): Made sure the menu can be used for more than group
This commit is contained in:
parent
a79898b2e9
commit
1d73ee8a78
16 changed files with 650 additions and 406 deletions
164
source/Groups/ConfigurationMenuHandler.ts
Normal file
164
source/Groups/ConfigurationMenuHandler.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import {
|
||||
AnyMenuItem, FieldMenuItem,
|
||||
FieldMenuItemContext, FieldMenuItemSaveValue,
|
||||
MenuItemType,
|
||||
RowBuilderFieldMenuItemContext
|
||||
} from "../Menu/MenuRenderer.types";
|
||||
import {GroupConfigurationTransformers} from "./GroupConfigurationTransformers";
|
||||
import {GroupConfigurationHandler} from "./GroupConfigurationHandler";
|
||||
import {
|
||||
channelMention,
|
||||
ChannelSelectMenuBuilder,
|
||||
ChannelType,
|
||||
inlineCode,
|
||||
italic,
|
||||
Snowflake,
|
||||
StringSelectMenuBuilder, StringSelectMenuOptionBuilder
|
||||
} from "discord.js";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||
|
||||
export class ConfigurationMenuHandler {
|
||||
|
||||
constructor(
|
||||
private readonly configuration: GroupConfigurationHandler,
|
||||
private readonly transformer: GroupConfigurationTransformers
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public getMenuItems(): AnyMenuItem[] {
|
||||
return [
|
||||
{
|
||||
traversalKey: "channels",
|
||||
label: "Channels",
|
||||
description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.",
|
||||
type: MenuItemType.Collection,
|
||||
children: [
|
||||
{
|
||||
traversalKey: "newPlaydates",
|
||||
label: "New Playdates",
|
||||
description: "Sets the channel, where the group gets notified, when new Playdates are set.",
|
||||
type: MenuItemType.Field,
|
||||
getCurrentValue: this.getChannelValue.bind(this),
|
||||
getActionRowBuilder: this.getChannelMenuBuilder.bind(this),
|
||||
setValue: this.setValue.bind(this)
|
||||
},
|
||||
{
|
||||
traversalKey: "playdateReminders",
|
||||
label: 'Playdate Reminders',
|
||||
description: "Sets the channel, where the group gets reminded of upcoming playdates.",
|
||||
type: MenuItemType.Field,
|
||||
getCurrentValue: this.getChannelValue.bind(this),
|
||||
getActionRowBuilder: this.getChannelMenuBuilder.bind(this),
|
||||
setValue: this.setValue.bind(this)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
traversalKey: "locale",
|
||||
label: "Locale",
|
||||
description: "Provides locale to be used for this group.",
|
||||
type: MenuItemType.Field,
|
||||
getCurrentValue: this.getLocaleValue.bind(this),
|
||||
getActionRowBuilder: this.getLocaleMenuBuilder.bind(this),
|
||||
setValue: this.setValue.bind(this)
|
||||
},
|
||||
{
|
||||
traversalKey: "permissions",
|
||||
label: "Permissions",
|
||||
description: "Allows customization, how the members are allowed to interact with the data stored in the group.",
|
||||
type: MenuItemType.Collection,
|
||||
children: [
|
||||
{
|
||||
traversalKey: "allowMemberManagingPlaydates",
|
||||
label: "Manage Playdates",
|
||||
description: "Defines if the members are allowed to manage playdates like adding or deleting them.",
|
||||
type: MenuItemType.Field,
|
||||
getCurrentValue: this.getPermissionBooleanValue.bind(this),
|
||||
getActionRowBuilder: this.getPermissionBooleanBuilder.bind(this),
|
||||
setValue: this.setValue.bind(this)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private getChannelValue(context: FieldMenuItemContext): string {
|
||||
const value = this.configuration.getConfigurationByPath(context.path.join('.'));
|
||||
if (value === undefined) {
|
||||
return italic("None");
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return inlineCode("None");
|
||||
}
|
||||
return channelMention(<ChannelId>value);
|
||||
}
|
||||
|
||||
private getChannelMenuBuilder(context: FieldMenuItemContext): MessageActionRowComponentBuilder {
|
||||
return new ChannelSelectMenuBuilder()
|
||||
.setChannelTypes(ChannelType.GuildText)
|
||||
.setPlaceholder("New Value");
|
||||
}
|
||||
|
||||
private getLocaleValue(context: FieldMenuItemContext): string {
|
||||
const value = this.configuration.getConfigurationByPath(context.path.join('.'));
|
||||
if (value === undefined) {
|
||||
return italic("None");
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return inlineCode("Default");
|
||||
}
|
||||
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
return displaynames.of((<Intl.Locale>value)?.baseName) ?? "Unknown";
|
||||
}
|
||||
|
||||
private getLocaleMenuBuilder(context: FieldMenuItemContext): MessageActionRowComponentBuilder {
|
||||
const options = [
|
||||
'en-US',
|
||||
'fr-FR',
|
||||
'it-IT',
|
||||
'de-DE'
|
||||
]
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
return new StringSelectMenuBuilder()
|
||||
.setOptions(
|
||||
options.map(intl => new StringSelectMenuOptionBuilder()
|
||||
.setLabel(displaynames.of(intl) ?? '')
|
||||
.setValue(intl)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private getPermissionBooleanValue(context: FieldMenuItemContext) {
|
||||
const value = this.configuration.getConfigurationByPath(context.path.join('.'));
|
||||
if (value === undefined) {
|
||||
return italic("None");
|
||||
}
|
||||
|
||||
return value ? 'Allowed' : "Disallowed";
|
||||
}
|
||||
|
||||
private getPermissionBooleanBuilder(context: FieldMenuItemContext) {
|
||||
return new StringSelectMenuBuilder()
|
||||
.setOptions(
|
||||
[
|
||||
{
|
||||
label: "Allow",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
label: "Disallow",
|
||||
value: "0"
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private setValue(value: FieldMenuItemSaveValue[], context: FieldMenuItemContext): void {
|
||||
this.configuration.saveConfiguration(context.path.join('.'), value.join('; '));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,372 +0,0 @@
|
|||
import {GroupConfigurationTransformers, TransformerType} from "./GroupConfigurationTransformers";
|
||||
import {GroupConfigurationHandler} from "./GroupConfigurationHandler";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
AnySelectMenuInteraction,
|
||||
ButtonBuilder,
|
||||
ButtonStyle, channelMention,
|
||||
ChannelSelectMenuBuilder, ChannelType,
|
||||
ChatInputCommandInteraction, EmbedBuilder, inlineCode, Interaction,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions, italic, MessageFlags,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder, UserSelectMenuBuilder
|
||||
} from "discord.js";
|
||||
import {Logger} from "log4js";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {
|
||||
MentionableSelectMenuBuilder,
|
||||
MessageActionRowComponentBuilder,
|
||||
RoleSelectMenuBuilder
|
||||
} from "@discordjs/builders";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {IconCache} from "../Icons/IconCache";
|
||||
|
||||
type UIElementCollection = Record<string, UIElement>;
|
||||
type UIElement = {
|
||||
label: string,
|
||||
key: string,
|
||||
description: string,
|
||||
childrenElements?: UIElementCollection,
|
||||
isConfiguration?: true
|
||||
}
|
||||
|
||||
export class GroupConfigurationRenderer {
|
||||
private static MOVETO_COMMAND = 'moveto_';
|
||||
private static SETVALUE_COMMAND = 'setvalue_';
|
||||
private static MOVEBACK_COMMAND = 'back';
|
||||
|
||||
private breadcrumbs: string[] = [];
|
||||
|
||||
private static UI_ELEMENTS: UIElementCollection = {
|
||||
channels: {
|
||||
label: 'Channels',
|
||||
key: 'channels',
|
||||
description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.",
|
||||
childrenElements: {
|
||||
newPlaydates: {
|
||||
label: 'New Playdates',
|
||||
key: 'newPlaydates',
|
||||
description: "Sets the channel, where the group get notified when new Playdates are set.",
|
||||
isConfiguration: true
|
||||
},
|
||||
playdateReminders: {
|
||||
label: 'Playdate Reminders',
|
||||
key: 'playdateReminders',
|
||||
description: "Sets the channel, where the group gets reminded of upcoming playdates.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
label: "Locale",
|
||||
key: 'locale',
|
||||
description: "Provides locale to be used for this group. This mostly sets how the dates are displayed, but this can also be later used for translations.",
|
||||
isConfiguration: true
|
||||
},
|
||||
permissions: {
|
||||
label: "Permissions",
|
||||
key: "permissions",
|
||||
description: "Allows customization, how the members are allowed to interact with the data stored in the group.",
|
||||
childrenElements: {
|
||||
allowMemberManagingPlaydates: {
|
||||
label: "Manage Playdates",
|
||||
key: "allowMemberManagingPlaydates",
|
||||
description: "Defines if the members are allowed to manage playdates like adding or deleting them.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly configurationHandler: GroupConfigurationHandler,
|
||||
private readonly transformers: GroupConfigurationTransformers,
|
||||
) {
|
||||
}
|
||||
|
||||
public async setup(interaction: ChatInputCommandInteraction) {
|
||||
let response = await interaction.reply(this.getReplyOptions());
|
||||
let exit = false;
|
||||
let eventResponse;
|
||||
const filter = (i: Interaction) => i.user.id === interaction.user.id;
|
||||
do {
|
||||
|
||||
if (eventResponse) {
|
||||
response = await eventResponse.update(this.getReplyOptions());
|
||||
}
|
||||
|
||||
try {
|
||||
eventResponse = await response.resource?.message?.awaitMessageComponent({
|
||||
dispose: true,
|
||||
filter: filter,
|
||||
time: 60_000
|
||||
});
|
||||
} catch (_: unknown) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!eventResponse || eventResponse.customId === 'exit') {
|
||||
exit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId === GroupConfigurationRenderer.MOVEBACK_COMMAND) {
|
||||
this.breadcrumbs.pop()
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.MOVETO_COMMAND)) {
|
||||
this.breadcrumbs.push(
|
||||
eventResponse.customId.substring(GroupConfigurationRenderer.MOVETO_COMMAND.length)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.SETVALUE_COMMAND)) {
|
||||
this.handleSelection(<AnySelectMenuInteraction>eventResponse);
|
||||
continue;
|
||||
}
|
||||
|
||||
} while (!exit);
|
||||
|
||||
if (eventResponse) {
|
||||
try {
|
||||
await eventResponse.update(
|
||||
this.getReplyOptions()
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
await eventResponse.deleteReply();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const message = response.resource?.message
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deletable) {
|
||||
await message.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } {
|
||||
const embed = this.createEmbed();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
embed.setAuthor({
|
||||
name: "/ " + this.breadcrumbs.join(" / ")
|
||||
});
|
||||
|
||||
const exitButton = new ButtonBuilder()
|
||||
.setLabel("Exit")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setCustomId("exit")
|
||||
.setEmoji(icons.get("door_open_solid_white") ?? '');
|
||||
|
||||
const actionrow = new ActionRowBuilder<ButtonBuilder>()
|
||||
|
||||
if (this.breadcrumbs.length > 0) {
|
||||
const backButton = new ButtonBuilder()
|
||||
.setLabel("Back")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND)
|
||||
.setEmoji(icons.get("angle_left_solid") ?? '');
|
||||
|
||||
actionrow.addComponents(backButton)
|
||||
}
|
||||
actionrow.addComponents(exitButton)
|
||||
|
||||
return {
|
||||
content: "",
|
||||
embeds: [embed],
|
||||
components: [...this.createActionRowBuildersForMenu(), actionrow],
|
||||
withResponse: true,
|
||||
flags: MessageFlags.Ephemeral
|
||||
};
|
||||
}
|
||||
|
||||
private createEmbed(): EmbedBuilder {
|
||||
const {currentElement} = this.findCurrentUI();
|
||||
|
||||
if (currentElement === null) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle("Group Configuration")
|
||||
.setDescription("This UI allows you to change settings for your group.")
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(currentElement?.label ?? '')
|
||||
.setDescription(currentElement?.description ?? '');
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
embed.addFields(
|
||||
{name: "Current Value", value: this.getCurrentValueAsUI(), inline: false}
|
||||
)
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
private getCurrentValueAsUI(): string {
|
||||
const path = this.breadcrumbs.join(".");
|
||||
const value = this.configurationHandler.getConfigurationByPath(path);
|
||||
|
||||
if (value === undefined) return italic("None");
|
||||
|
||||
const type = this.transformers.getTransformerType(path);
|
||||
|
||||
if (type === undefined) {
|
||||
throw new Error("Could not find the type for " + path);
|
||||
}
|
||||
|
||||
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
|
||||
switch (type) {
|
||||
case TransformerType.Locale:
|
||||
if (!value) {
|
||||
return inlineCode("Default");
|
||||
}
|
||||
return displaynames.of((<Intl.Locale>value)?.baseName) ?? "Unknown";
|
||||
case TransformerType.Channel:
|
||||
if (!value) {
|
||||
return inlineCode("None");
|
||||
}
|
||||
return channelMention(<ChannelId>value);
|
||||
case TransformerType.PermissionBoolean:
|
||||
return value ? "Allowed" : "Disallowed"
|
||||
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
|
||||
private createActionRowBuildersForMenu(): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||
const {currentCollection, currentElement} = this.findCurrentUI();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
return [
|
||||
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
|
||||
.addComponents(this.getSelectForBreadcrumbs())
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
new ActionRowBuilder<ButtonBuilder>()
|
||||
.setComponents(
|
||||
...Object.values(currentCollection).map(elem => new ButtonBuilder()
|
||||
.setLabel(` ${elem.label}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
|
||||
.setEmoji(icons.get(elem.isConfiguration ? 'pen_solid' : "folder_solid") ?? '')
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private getSelectForBreadcrumbs(): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder {
|
||||
const breadcrumbPath = this.breadcrumbs.join('.')
|
||||
const transformerType = this.transformers.getTransformerType(breadcrumbPath);
|
||||
if (transformerType === undefined) {
|
||||
throw new Error(`Can not find transformer type for ${breadcrumbPath}`)
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
const options = [
|
||||
'en-US',
|
||||
'fr-FR',
|
||||
'it-IT',
|
||||
'de-DE'
|
||||
]
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
options.map(intl => new StringSelectMenuOptionBuilder()
|
||||
.setLabel(displaynames.of(intl) ?? '')
|
||||
.setValue(intl)
|
||||
)
|
||||
)
|
||||
case TransformerType.Channel:
|
||||
return new ChannelSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setChannelTypes(ChannelType.GuildText)
|
||||
.setPlaceholder("New Value");
|
||||
case TransformerType.PermissionBoolean:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
[
|
||||
{
|
||||
label: "Allow",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
label: "Disallow",
|
||||
value: "0"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId("...")
|
||||
.setOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Nothing to see here")
|
||||
.setValue("0")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelection(interaction: AnySelectMenuInteraction) {
|
||||
const path = interaction.customId.substring(GroupConfigurationRenderer.SETVALUE_COMMAND.length);
|
||||
|
||||
const savingValue = this.getSaveValue(interaction, path);
|
||||
Container.get<Logger>("logger").debug(`Saving '${savingValue}' to '${path}'`);
|
||||
|
||||
this.configurationHandler.saveConfiguration(path, savingValue);
|
||||
}
|
||||
|
||||
private getSaveValue(interaction: AnySelectMenuInteraction, path: string): string {
|
||||
const transformerType = this.transformers.getTransformerType(path);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can not find transformer type for ${path}`)
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
case TransformerType.Channel:
|
||||
case TransformerType.PermissionBoolean:
|
||||
return interaction.values.join('; ');
|
||||
default:
|
||||
throw new Error("Unhandled select menu");
|
||||
}
|
||||
}
|
||||
|
||||
private findCurrentUI(): { currentElement: Nullable<UIElement>, currentCollection: UIElementCollection } {
|
||||
let currentCollection: UIElementCollection = GroupConfigurationRenderer.UI_ELEMENTS;
|
||||
let currentElement: Nullable<UIElement> = null;
|
||||
|
||||
for (const breadcrumb of this.breadcrumbs) {
|
||||
currentElement = currentCollection[breadcrumb];
|
||||
|
||||
if (currentElement.isConfiguration ?? false) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentCollection = currentElement.childrenElements ?? {};
|
||||
}
|
||||
|
||||
return {
|
||||
currentElement,
|
||||
currentCollection,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue