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 <noreply@anthropic.com>
This commit is contained in:
m4
2026-08-11 11:47:46 +08:00
parent 5634abdbd5
commit 0e83ab73af
@@ -17,27 +17,36 @@ function BrandingUpload({
kind,
label,
currentSrc,
setDraft,
}: {
kind: "logo" | "favicon";
label: string;
currentSrc: string;
setDraft: TabProps["setDraft"];
}) {
const inputRef = useRef<HTMLInputElement>(null);
const [preview, setPreview] = useState<string | null>(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) {
}
/>
</div>
<BrandingUpload kind="logo" label="Logo" currentSrc={logoSrc(publicConfig)} />
<BrandingUpload kind="logo" label="Logo" currentSrc={logoSrc(publicConfig)} setDraft={setDraft} />
<BrandingUpload
kind="favicon"
label="Favicon"
currentSrc={
publicConfig && publicConfig.faviconVersion > 0
? `/api/system/branding/asset/favicon?v=${publicConfig.faviconVersion}`
: "/favicon.ico"
: "/icon.png"
}
setDraft={setDraft}
/>
<p className="text-xs text-muted-foreground">
PNG, JPEG or SVG, up to 512KB. The favicon also accepts ICO. Changes