From 7cf26795ec12845743a30d48956381f25c6e181c Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 20 Jun 2026 23:17:34 +0200 Subject: [PATCH] fix(branding): preserve customCss through preset switches + theme changes (#645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../admin/ThemeCustomizerEnhanced.tsx | 8 +++++- frontend/src/pages/admin/BrandingPage.tsx | 26 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index 9a08c407..b0901fdd 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -259,7 +259,13 @@ export const ThemeCustomizerEnhanced: React.FC = ( 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); } diff --git a/frontend/src/pages/admin/BrandingPage.tsx b/frontend/src/pages/admin/BrandingPage.tsx index d0fa47ea..dc3e0371 100644 --- a/frontend/src/pages/admin/BrandingPage.tsx +++ b/frontend/src/pages/admin/BrandingPage.tsx @@ -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 + }); } } };