From 0e83ab73afba7d9c2a6311db874ed8e47aacfb3e Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 11 Aug 2026 11:47:46 +0800 Subject: [PATCH] fix(webui): keep unsaved system config edits across branding uploads Uploading or restoring a logo/favicon revalidated /api/system/config, which re-ran the dialog's sync effect and replaced the whole draft, silently wiping unsaved wordmark/login-terms edits. Now only the draft's file field is updated from the response and only the public config key is revalidated. Also fix the favicon fallback preview src from the nonexistent /favicon.ico to /icon.png. Co-Authored-By: Claude Opus 4.7 --- .../components/system-config/GeneralTab.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/components/system-config/GeneralTab.tsx b/src/app/components/system-config/GeneralTab.tsx index 3951eb2..2ab9375 100644 --- a/src/app/components/system-config/GeneralTab.tsx +++ b/src/app/components/system-config/GeneralTab.tsx @@ -17,27 +17,36 @@ function BrandingUpload({ kind, label, currentSrc, + setDraft, }: { kind: "logo" | "favicon"; label: string; currentSrc: string; + setDraft: TabProps["setDraft"]; }) { const inputRef = useRef(null); const [preview, setPreview] = useState(null); const [busy, setBusy] = useState(false); + const fileField = kind === "logo" ? "logoFile" : "faviconFile"; const call = async (url: string, init: RequestInit, fallback: string) => { setBusy(true); try { const response = await fetch(url, init); const body = (await response.json().catch(() => null)) as - | { message?: string } + | { message?: string; file?: string } | null; if (!response.ok) throw new Error(body?.message || fallback); + // Update only this kind's file field in the draft; leave other unsaved + // edits intact. DELETE implies the file field is now null. + const nextFile = init.method === "DELETE" ? null : (body?.file ?? null); + setDraft((config) => ({ + ...config, + branding: { ...config.branding, [fileField]: nextFile }, + })); setPreview(null); if (inputRef.current) inputRef.current.value = ""; void globalMutate(PUBLIC_SYSTEM_CONFIG_KEY); - void globalMutate("/api/system/config"); } catch (error) { errorToast(`branding.${kind}`, error instanceof Error ? error.message : fallback); } finally { @@ -114,15 +123,16 @@ export function GeneralTab({ draft, setDraft }: TabProps) { } /> - + 0 ? `/api/system/branding/asset/favicon?v=${publicConfig.faviconVersion}` - : "/favicon.ico" + : "/icon.png" } + setDraft={setDraft} />

PNG, JPEG or SVG, up to 512KB. The favicon also accepts ICO. Changes