fix(branding): preserve customCss through preset switches + theme changes (#645)

Reporter @aemisrogers nailed the root cause: same #317 class of bug as
logoUrl. None of `GALLERY_THEME_PRESETS` (`theme.types.ts:125`) include
`customCss` in their `config` object, so any path that REPLACES
`currentTheme` with `preset.config` (or with a sparse `newTheme` that
came from `preset.config` upstream) silently dropped `customCss` from
React state. The persisted value in `theme_config` stayed correct (the
public gallery still rendered it), but the admin textarea showed
empty on reload — admin-UI display drift, not data loss.

Three surgical fixes, mirroring the #317 logoUrl pattern:

1. `BrandingPage.tsx` `handleThemeChange` — `customCss: newTheme.customCss
   ?? currentTheme.customCss` alongside the existing `logoUrl` fallback.
   Closes the propagation hole where the customizer's `handlePresetSelect`
   fires `onChange(preset.config)` (no customCss) and the parent wipes
   it from currentTheme.

2. `BrandingPage.tsx` `handlePresetChange` — preserve `customCss` from
   prev/currentTheme on preset switch, same shape as the existing
   `logoUrl: prev.logoUrl` preservation. Touches both the `setCurrentTheme`
   and the preview-mode `setTheme` paths.

3. `ThemeCustomizerEnhanced.tsx` `handlePresetSelect` — remove the
   `setCustomCss('')` that wiped the local textarea state on preset
   pick. The previous comment ("Clear custom CSS when selecting a preset")
   described the original intent but produced data drift across the
   preset round-trip. The sibling `ThemeCustomizer.tsx` already never
   cleared it; this aligns the two.

Verified against `v3.44.0` and `origin/beta`: identical code on both
branches, so the bug exists on stable + beta. Lint + tsc clean on the
two changed files.

Closes #645.
This commit is contained in:
Paul Nothaft
2026-06-20 23:17:34 +02:00
parent 38514ce60f
commit 7cf26795ec
2 changed files with 26 additions and 8 deletions
@@ -259,7 +259,13 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
if (preset) {
setSelectedPreset(presetKey);
setLocalTheme(preset.config);
setCustomCss(''); // Clear custom CSS when selecting a preset
// Don't wipe customCss on preset pick — preset configs carry no
// customCss, and the admin's persisted styling extras should
// survive a layout switch (#645). Matches ThemeCustomizer.tsx
// which never cleared it. The parent's handleThemeChange merges
// via `customCss: newTheme.customCss ?? currentTheme.customCss`,
// so propagating preset.config (no customCss) keeps the saved
// value intact end-to-end.
if (onPresetChange) {
onPresetChange(presetKey);
}
+19 -7
View File
@@ -186,12 +186,14 @@ export const BrandingPage: React.FC = () => {
};
const handleThemeChange = (newTheme: ThemeConfig) => {
// Preset configs don't carry a logoUrl, so a preset change inside the
// customizer arrives here with newTheme.logoUrl=undefined. Keep the
// existing logo instead of wiping branding_logo_url on save (#317).
// Preset configs don't carry a logoUrl or customCss, so a preset change
// inside the customizer arrives here with those fields undefined. Keep
// the existing values instead of wiping the persisted ones on save
// (#317 for logoUrl, #645 for customCss).
const mergedTheme: ThemeConfig = {
...newTheme,
logoUrl: newTheme.logoUrl ?? currentTheme.logoUrl
logoUrl: newTheme.logoUrl ?? currentTheme.logoUrl,
customCss: newTheme.customCss ?? currentTheme.customCss
};
setCurrentTheme(mergedTheme);
if (newTheme.logoUrl !== undefined && newTheme.logoUrl !== currentTheme.logoUrl) {
@@ -207,10 +209,20 @@ export const BrandingPage: React.FC = () => {
// Get the preset theme config
const preset = GALLERY_THEME_PRESETS[presetName];
if (preset) {
// Preserve the existing logo when switching presets (#317).
setCurrentTheme(prev => ({ ...preset.config, logoUrl: prev.logoUrl }));
// Preserve the existing logo + custom CSS when switching presets
// (#317 for logo, #645 for customCss). Presets define a look; they
// shouldn't silently drop the admin's persisted styling extras.
setCurrentTheme(prev => ({
...preset.config,
logoUrl: prev.logoUrl,
customCss: prev.customCss
}));
if (isPreviewMode) {
setTheme({ ...preset.config, logoUrl: currentTheme.logoUrl });
setTheme({
...preset.config,
logoUrl: currentTheme.logoUrl,
customCss: currentTheme.customCss
});
}
}
};