From 4489a3ffb7214355b18216b3fb6f69e9ec773a18 Mon Sep 17 00:00:00 2001 From: biast12 <53872542+biast12@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:58:40 +0200 Subject: [PATCH] Add multipanel embed fields and URL support Extends multipanel embed editing and rendering across the stack by adding embed field support, a title URL input, and updated typings in the frontend. The preview now displays embed fields in Discord-like inline rows, and announcement channels are now selectable where channel type 5 is valid. On the backend, multipanel get/message-data handlers now use the shared CustomEmbed type and safely handle missing embed data with defaults, and the update endpoint now returns the updated multipanel object in the response. --- .../http/endpoints/api/panel/multipanelget.go | 47 +++--------------- .../api/panel/multipanelmessagedata.go | 7 ++- .../endpoints/api/panel/multipanelupdate.go | 2 +- frontend/src/components/PanelPreview.tsx | 49 +++++++++++++++++++ frontend/src/lib/guild-channels.ts | 3 +- .../src/pages/manage/multipanels/create.tsx | 25 ++++++++++ .../src/pages/manage/multipanels/edit.tsx | 25 ++++++++++ frontend/src/types.d.ts | 5 ++ 8 files changed, 119 insertions(+), 44 deletions(-) diff --git a/backend/app/http/endpoints/api/panel/multipanelget.go b/backend/app/http/endpoints/api/panel/multipanelget.go index 30128c0b..ff784c5e 100644 --- a/backend/app/http/endpoints/api/panel/multipanelget.go +++ b/backend/app/http/endpoints/api/panel/multipanelget.go @@ -8,6 +8,7 @@ import ( "github.com/ticketsbot-cloud/dashboard/backend/app" dbclient "github.com/ticketsbot-cloud/dashboard/backend/database" "github.com/ticketsbot-cloud/dashboard/backend/utils" + "github.com/ticketsbot-cloud/dashboard/backend/utils/types" ) func MultiPanelGet(ctx *gin.Context) { @@ -19,28 +20,6 @@ func MultiPanelGet(ctx *gin.Context) { CustomEmojiId *uint64 `json:"custom_emoji_id,omitempty,string"` } - type embedAuthor struct { - Name *string `json:"name"` - IconUrl *string `json:"icon_url"` - Url *string `json:"url"` - } - - type embedFooter struct { - Text *string `json:"text"` - IconUrl *string `json:"icon_url"` - } - - type embedResponse struct { - Title *string `json:"title"` - Description *string `json:"description"` - Url *string `json:"url"` - Colour uint32 `json:"colour"` - Author embedAuthor `json:"author"` - ImageUrl *string `json:"image_url"` - ThumbnailUrl *string `json:"thumbnail_url"` - Footer embedFooter `json:"footer"` - } - type multiPanelResponse struct { Id int `json:"id"` MessageId uint64 `json:"message_id,string"` @@ -48,7 +27,7 @@ func MultiPanelGet(ctx *gin.Context) { GuildId uint64 `json:"guild_id,string"` SelectMenu bool `json:"select_menu"` SelectMenuPlaceholder *string `json:"select_menu_placeholder"` - Embed *embedResponse `json:"embed"` + Embed *types.CustomEmbed `json:"embed"` Panels []panelConfiguration `json:"panels"` } @@ -76,25 +55,11 @@ func MultiPanelGet(ctx *gin.Context) { return } - var transformedEmbed *embedResponse - if multiPanel.Embed != nil { - e := multiPanel.Embed.CustomEmbed - transformedEmbed = &embedResponse{ - Title: e.Title, - Description: e.Description, - Url: e.Url, - Colour: e.Colour, - Author: embedAuthor{Name: e.AuthorName, IconUrl: e.AuthorIconUrl, Url: e.AuthorUrl}, - ImageUrl: e.ImageUrl, - ThumbnailUrl: e.ThumbnailUrl, - Footer: embedFooter{Text: e.FooterText, IconUrl: e.FooterIconUrl}, - } + var transformedEmbed *types.CustomEmbed + if multiPanel.Embed != nil && multiPanel.Embed.CustomEmbed != nil { + transformedEmbed = types.NewCustomEmbed(multiPanel.Embed.CustomEmbed, multiPanel.Embed.Fields) } else { - transformedEmbed = &embedResponse{ - Colour: 0x5865f2, - Author: embedAuthor{}, - Footer: embedFooter{}, - } + transformedEmbed = &types.CustomEmbed{Colour: 0x5865f2} } panels, err := dbclient.Client.MultiPanelTargets.GetPanels(ctx, multiPanelId) diff --git a/backend/app/http/endpoints/api/panel/multipanelmessagedata.go b/backend/app/http/endpoints/api/panel/multipanelmessagedata.go index 0cff3522..cb1c5288 100644 --- a/backend/app/http/endpoints/api/panel/multipanelmessagedata.go +++ b/backend/app/http/endpoints/api/panel/multipanelmessagedata.go @@ -43,6 +43,11 @@ func multiPanelDiscordSubPanelError(action, detail string) string { } func multiPanelIntoMessageData(panel database.MultiPanel, footer footerPolicy) multiPanelMessageData { + custom := &types.CustomEmbed{Colour: 0x5865f2} + if panel.Embed != nil && panel.Embed.CustomEmbed != nil { + custom = types.NewCustomEmbed(panel.Embed.CustomEmbed, panel.Embed.Fields) + } + return multiPanelMessageData{ Footer: footer, @@ -50,7 +55,7 @@ func multiPanelIntoMessageData(panel database.MultiPanel, footer footerPolicy) m SelectMenu: panel.SelectMenu, SelectMenuPlaceholder: panel.SelectMenuPlaceholder, - Embed: types.NewCustomEmbed(panel.Embed.CustomEmbed, panel.Embed.Fields).IntoDiscordEmbed(), + Embed: custom.IntoDiscordEmbed(), } } diff --git a/backend/app/http/endpoints/api/panel/multipanelupdate.go b/backend/app/http/endpoints/api/panel/multipanelupdate.go index 0baf7e9f..3b9932db 100644 --- a/backend/app/http/endpoints/api/panel/multipanelupdate.go +++ b/backend/app/http/endpoints/api/panel/multipanelupdate.go @@ -295,6 +295,6 @@ func MultiPanelUpdate(c *gin.Context) { }) c.JSON(200, gin.H{ "success": true, - "data": multiPanel, + "data": updated, }) } diff --git a/frontend/src/components/PanelPreview.tsx b/frontend/src/components/PanelPreview.tsx index b78ab293..24e55f94 100644 --- a/frontend/src/components/PanelPreview.tsx +++ b/frontend/src/components/PanelPreview.tsx @@ -14,6 +14,8 @@ function resolveEmoteName(emote: Panel["emote"] | undefined): string { return ""; } +type PreviewField = { name: string; value: string; inline?: boolean }; + type MultiPanelPreviewRequest = Omit & Partial>; @@ -122,6 +124,30 @@ const PanelPreview: FC = ({ type, data, brandingFooter }) => const displayFooterText = brandingFooter ? BRANDING_FOOTER_TEXT : footerText; const displayFooterIconUrl = brandingFooter ? BRANDING_FOOTER_ICON : footerIconUrl; + // Discord packs at most 3 inline fields per row. + const fieldRows: PreviewField[][] = []; + if (embedData?.fields?.length) { + let currentRow: PreviewField[] = []; + for (const field of embedData.fields) { + if (field.inline) { + currentRow.push(field); + if (currentRow.length === 3) { + fieldRows.push(currentRow); + currentRow = []; + } + } else { + if (currentRow.length > 0) { + fieldRows.push(currentRow); + currentRow = []; + } + fieldRows.push([field]); + } + } + if (currentRow.length > 0) { + fieldRows.push(currentRow); + } + } + const thumbnailUrl = resolveTicketAvatar(embedData?.thumbnail_url); const imageUrl = resolveTicketAvatar(embedData?.image_url); const authorIconUrl = resolveTicketAvatar(embedData?.author?.icon_url); @@ -180,6 +206,29 @@ const PanelPreview: FC = ({ type, data, brandingFooter }) => className="text-sm mt-1 text-[#dbdee1] leading-snug" /> )} + {fieldRows.length > 0 && ( +
+ {fieldRows.map((row, ri) => ( +
1 ? `repeat(${row.length}, 1fr)` : "1fr", + }} + > + {row.map((field, fi) => ( +
+ + +
+ ))} +
+ ))} +
+ )} {thumbnailUrl && (
diff --git a/frontend/src/lib/guild-channels.ts b/frontend/src/lib/guild-channels.ts index 46425db7..86e4e6d1 100644 --- a/frontend/src/lib/guild-channels.ts +++ b/frontend/src/lib/guild-channels.ts @@ -12,7 +12,8 @@ export const sortGuildChannels = (channels: GuildChannel[]) => { const toOption = (channel: GuildChannel) => ({ key: String(channel.id), label: `${channel.type == 2 ? "🔈" : "#"} ${channel.name}`, - disabled: channel.type !== 0, + // Type 5 = announcement, which the API accepts. + disabled: channel.type !== 0 && channel.type !== 5, }); return sorted.reduce( diff --git a/frontend/src/pages/manage/multipanels/create.tsx b/frontend/src/pages/manage/multipanels/create.tsx index affc9c8d..7d564477 100644 --- a/frontend/src/pages/manage/multipanels/create.tsx +++ b/frontend/src/pages/manage/multipanels/create.tsx @@ -32,6 +32,7 @@ import { PANEL_MESSAGE_INFO } from "@/constants/panelChannelInfo"; import MultiPanelInfoModal from "@/components/modals/MultiPanelInfoModal"; import { EMBED_LIMITS } from "@/constants/embedLimits"; import EmbedCharacterTotal from "@/components/EmbedCharacterTotal"; +import EmbedFieldsEditor from "@/components/EmbedFieldsEditor"; import { useApiErrorHandler } from "@/hooks/useApiErrorHandler"; type MultiPanelDraft = Omit & { @@ -91,6 +92,7 @@ const MultiPanelsPage: FC = () => { author: {}, colour: 0x5865f2, description: "", + fields: [], footer: {}, }, panels: [] as MultiPanelPanelEntry[], @@ -351,6 +353,19 @@ const MultiPanelsPage: FC = () => { } />
+
+ + setMultiPanel((prev) => + prev ? { ...prev, embed: { ...prev.embed, url: e } } : prev, + ) + } + maxLength={EMBED_LIMITS.URL} + /> +