all repos — stealth-developers @ 1b5781040ce72dcd1506a4f4058e2ad8fe61abdf

wip: impl bug editing modal
willow hai@wlo.moe
Sun, 09 Nov 2025 18:01:32 +0000
commit

1b5781040ce72dcd1506a4f4058e2ad8fe61abdf

parent

22bd268c32fa05c64444c6abe2514e95c0ed99cd

3 files changed, 360 insertions(+), 18 deletions(-)

jump to
A comps.json

@@ -0,0 +1,106 @@

+[ + { + "type": 17, + "spoiler": false, + "id": 1, + "accent_color": null, + "components": [ + { + "type": 9, + "id": 2, + "accessory": { + "type": 11, + "spoiler": false, + "id": 4, + "description": null, + "media": { + "width": 150, + "url": "https://tr.rbxcdn.com/180DAY-43ff702ae43b081ec8db160d1a1d6636/150/150/Image/Webp/noFilter", + "proxy_url": "https://images-ext-1.discordapp.net/external/zQ4qoZCXOORfncY77jeSQp90ZZcixPfRcgeR35WIaGs/https/tr.rbxcdn.com/180DAY-43ff702ae43b081ec8db160d1a1d6636/150/150/Image/Webp/noFilter", + "placeholder_version": 1, + "placeholder": "l0oHFwpYWkOHaHiWY4hlC7W1g2z52JYP", + "loading_state": 2, + "id": "1437134752179163326", + "height": 150, + "content_type": "image/webp", + "content_scan_metadata": { + "version": 2, + "flags": 0 + } + } + }, + "components": [ + { + "type": 10, + "id": 3, + "content": "### dasd\naasdasd" + } + ] + }, + { + "type": 12, + "id": 5, + "items": [ + { + "spoiler": false, + "description": null, + "media": { + "width": 1536, + "url": "https://cdn.discordapp.com/ephemeral-attachments/1437134703646998558/1437134732696490045/2025-11-08_21-27-02.mp4?ex=69122384&is=6910d204&hm=6830fc0ef6ab2c4d96ffa814430fcb2d5afc2a94ea1c9bd9d554c0ac780f47da&", + "proxy_url": "https://media.discordapp.net/ephemeral-attachments/1437134703646998558/1437134732696490045/2025-11-08_21-27-02.mp4?ex=69122384&is=6910d204&hm=6830fc0ef6ab2c4d96ffa814430fcb2d5afc2a94ea1c9bd9d554c0ac780f47da&", + "placeholder_version": 1, + "placeholder": "yvcFBIDU08ePd5h+Z31mcjAKBA==", + "loading_state": 2, + "id": "1437134752179163327", + "height": 864, + "content_type": "video/mp4", + "content_scan_metadata": { + "version": 2, + "flags": 0 + } + } + } + ] + }, + { + "type": 10, + "id": 6, + "content": "-# #236 • Airsoft Battles • Reported by <@1357056975812301013>" + } + ] + }, + { + "type": 1, + "id": 7, + "components": [ + { + "type": 2, + "style": 2, + "label": "close", + "id": 8, + "custom_id": "bug:close:236" + }, + { + "type": 2, + "style": 1, + "label": "edit", + "id": 9, + "custom_id": "bug:edit:236" + }, + { + "type": 2, + "style": 4, + "label": "delete", + "id": 10, + "custom_id": "bug:delete:236" + }, + { + "url": "https://trello.com/addCard?name=dasd&url=https%3A%2F%2Fdiscord.com%2Fchannels%2F1359315933797285948%2F1378648691543380079%2F1437134745480728698&idBoard=95bcpKcU", + "type": 2, + "style": 5, + "label": "add to trello", + "id": 11 + } + ] + } +]
M src/interactions/commands/bug/buttons.tssrc/interactions/commands/bug/buttons.ts

@@ -10,7 +10,9 @@ ButtonStyle,

ChannelType, type Client, type GuildMember, + type Interaction, } from "discord.js"; +import { showEditModal } from "./report.ts"; const logger = lily.child("bugButtons");

@@ -70,8 +72,8 @@

return row; } -async function canManageBug( - interaction: ButtonInteraction, +export async function canManageBug( + interaction: Interaction, bugUserId: string, ): Promise<boolean> { if (!interaction.guild || !interaction.member) return false;

@@ -205,11 +207,15 @@ });

return; } - // TODO)) implement edit modal - await interaction.reply({ - content: "❌ Editing bugs is not implemented yet.", - flags: ["Ephemeral"], - }); + const projectKey = bug.projects[0] || ""; + + await showEditModal( + bugId, + bug.title, + bug.description, + projectKey, + interaction, + ); } export async function handleDeleteButton(
M src/interactions/commands/bug/report.tssrc/interactions/commands/bug/report.ts

@@ -12,6 +12,7 @@ type Attachment,

AttachmentBuilder, type ChatInputCommandInteraction, type Client, + ComponentType, ContainerBuilder, FileUploadBuilder, LabelBuilder,

@@ -29,7 +30,7 @@ TextInputStyle,

ThumbnailBuilder, } from "discord.js"; -import { buildButtonRow } from "./buttons.ts"; +import { buildButtonRow, canManageBug } from "./buttons.ts"; import { PROJECT_MAP, logger as lily } from "./shared.ts"; const logger = lily.child("report");

@@ -57,7 +58,7 @@

// types type ProjectChoice<T extends "name" | "label" = "name"> = T extends "name" ? { name: string; value: string } - : { label: string; value: string }; + : { label: string; value: string; default: boolean }; interface DownloadedMediaResult { media: MediaType[];

@@ -138,11 +139,16 @@

// project choices export function getProjectChoices<T extends "name" | "label" = "name">( label: T = "name" as T, + defaultProjectKey?: string, ): ProjectChoice<T>[] { return Object.entries(PROJECT_MAP).map(([key, project]) => label === "name" ? { name: project.displayName, value: key } - : { label: project.displayName, value: key }, + : { + label: project.displayName, + value: key, + default: key === defaultProjectKey, + }, ) as ProjectChoice<T>[]; }

@@ -203,6 +209,63 @@ .setTitle("Report a Bug")

.setLabelComponents(gameLabel, titleLabel, descriptionLabel, mediaLabel); } +function buildEditModal( + bugId: number, + currentTitle: string, + currentDescription: string, + currentProject: string, +): ModalBuilder { + const terminology = config.data.terminology; + const capitalizedTerminology = + terminology.charAt(0).toUpperCase() + terminology.slice(1); + + const projectOptions = getProjectChoices("label", currentProject); + const selectedProject = projectOptions.find( + (opt) => opt.value === currentProject, + ); + + const gameLabel = new LabelBuilder() + .setLabel(capitalizedTerminology) + .setDescription(`The ${terminology} affected by this bug`) + .setStringSelectMenuComponent( + new StringSelectMenuBuilder() + .setMaxValues(1) + .setMinValues(1) + .setCustomId(INPUT_IDS.AFFECTED) + .setPlaceholder(selectedProject?.label || `Select the ${terminology}`) + .addOptions(projectOptions), + ); + + const titleLabel = new LabelBuilder() + .setLabel("Bug Title") + .setDescription("Update the bug title") + .setTextInputComponent( + new TextInputBuilder() + .setCustomId(INPUT_IDS.TITLE) + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(VALIDATION.TITLE_MAX_LENGTH) + .setValue(currentTitle), + ); + + const descriptionLabel = new LabelBuilder() + .setLabel("Bug Description") + .setDescription("Update the bug description") + .setTextInputComponent( + new TextInputBuilder() + .setCustomId(INPUT_IDS.DESCRIPTION) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setMaxLength(VALIDATION.DESCRIPTION_MAX_LENGTH) + .setValue(currentDescription), + ); + + return new ModalBuilder() + .setCustomId(`${MODAL_IDS.EDIT}:${bugId}`) + .setTitle(`Edit Bug #${bugId}`) + .setLabelComponents(gameLabel, titleLabel, descriptionLabel); +} + // validation async function validateGuildSetup(interaction: ModalSubmitInteraction) { if (!interaction.guild) {

@@ -225,12 +288,19 @@

return { guildData, channel }; } -function parseModalCustomId(customId: string): { type: string } { +function parseModalCustomId(customId: string): { + type: string; + bugId?: number; +} { const parts = customId.split(":"); - if (parts.length !== 2 || parts[0] !== "bug") { + if (parts.length < 2 || parts[0] !== "bug") { throw new Error("Invalid modal submission format."); } - return { type: parts[1] }; + + const type = parts[1]; + const bugId = parts[2] ? Number.parseInt(parts[2]) : undefined; + + return { type, bugId }; } // containers

@@ -277,6 +347,145 @@

return container; } +// modals handlers +async function handleEditModal( + client: Client, + interaction: ModalSubmitInteraction, + bugId: number, +): Promise<void> { + if (!interaction.guild) { + await interaction.reply({ + content: "❌ This command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + + const bug = await BugModel.findOne({ bug_id: bugId }); + if (!bug) { + await interaction.reply({ + content: "❌ Bug report not found.", + flags: ["Ephemeral"], + }); + return; + } + + const canManage = await canManageBug(interaction, bug.user_id); + if (!canManage) { + await interaction.reply({ + content: "❌ You don't have permission to edit this bug report.", + flags: ["Ephemeral"], + }); + return; + } + + await interaction.deferReply({ flags: ["Ephemeral"] }); + + try { + // collect inputs + const newTitle = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); + const newDescription = interaction.fields.getTextInputValue( + INPUT_IDS.DESCRIPTION, + ); + const newProject = interaction.fields + .getStringSelectValues(INPUT_IDS.AFFECTED) + .join(""); + + // track changes + const changes: string[] = []; + if (bug.title !== newTitle) changes.push("title"); + if (bug.description !== newDescription) changes.push("description"); + if (bug.projects.join(",") !== newProject) changes.push("project"); + + if (changes.length === 0) { + await interaction.editReply({ + content: "❌ No changes were made to the bug report.", + }); + return; + } + + bug.title = newTitle; + bug.description = newDescription; + bug.projects = [newProject]; + await bug.save(); + + if (bug.message_id && bug.thread_id) { + const thread = await client.channels.fetch(bug.thread_id); + if (thread?.isThread()) { + const message = await thread.fetchStarterMessage(); + if (message) { + const projectKey = newProject.split(",")[0]; + + const existingMediaUrls = Array.from(message.components.values()) + .filter((comp) => comp.type === ComponentType.Container) + .flatMap((comp) => Array.from(comp.components.values())) + .filter((item) => item.type === ComponentType.MediaGallery) + .flatMap((item) => Array.from(item.items.values())) + .map((mediaItem) => mediaItem.media.url); + + // build updated container & edit message + const storedMedia = await MediaModel.find({ bug_id: bugId }); + const container = buildBugContainer( + bug, + projectKey, + bug.user_id, + existingMediaUrls, + ); + + const buttonRow = buildButtonRow( + bugId, + message.url, + bug.title, + bug.status === "closed", + ); + + const mediaAttachments = storedMedia.map((media) => + new AttachmentBuilder(media.data).setName( + `bug_${bugId}_media.${media.extension}`, + ), + ); + + await message.edit({ + components: [container, buttonRow], + files: mediaAttachments.length > 0 ? mediaAttachments : undefined, + }); + + // update thread name if title changed + if (changes.includes("title")) { + const newThreadName = `#${bugId} ${truncateTitle(bug.title, VALIDATION.THREAD_NAME_MAX_LENGTH)}`; + await thread.setName(newThreadName); + } + + const editLogMessage = `Bug report edited by <@${interaction.user.id}> • **Changes:** ${changes.join(", ")} • <t:${Math.floor(Date.now() / 1000)}:R>`; + await thread.send({ + content: editLogMessage, + allowedMentions: { users: [] }, + }); + } + } + } + + await interaction.editReply({ + content: `✅ Bug report #${bugId} has been updated successfully!\n**Changed:** ${changes.join(", ")}`, + }); + + logger.info("Bug report edited successfully", { + bugId, + editedBy: interaction.user.id, + changes, + }); + } catch (error) { + logger.error("Failed to edit bug report", { error, bugId }); + + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred."; + + await interaction.editReply({ + content: `❌ ${errorMessage}`, + }); + } +} + // main export async function execute( _client: Client,

@@ -300,13 +509,17 @@ });

return; } - const { type } = parseModalCustomId(interaction.customId); + const { type, bugId: _bugId } = parseModalCustomId(interaction.customId); if (type === "edit") { - await interaction.reply({ - content: "❌ Editing bugs is not implemented yet.", - flags: ["Ephemeral"], - }); + if (!_bugId) { + await interaction.reply({ + content: "❌ Invalid bug ID.", + flags: ["Ephemeral"], + }); + return; + } + await handleEditModal(client, interaction, _bugId); return; }

@@ -445,8 +658,25 @@ }

} } +export async function showEditModal( + bugId: number, + currentTitle: string, + currentDescription: string, + currentProject: string, + interaction: { showModal: (modal: ModalBuilder) => Promise<void> }, +): Promise<void> { + const modal = buildEditModal( + bugId, + currentTitle, + currentDescription, + currentProject, + ); + await interaction.showModal(modal); +} + export const reportCommand = { execute, modalExecute, getProjectChoices, + showEditModal, };