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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user