diff --git a/backend/migrations/core/137_add_slideshow_share.js b/backend/migrations/core/138_add_slideshow_share.js similarity index 100% rename from backend/migrations/core/137_add_slideshow_share.js rename to backend/migrations/core/138_add_slideshow_share.js diff --git a/backend/migrations/core/138_add_slideshow_styling.js b/backend/migrations/core/139_add_slideshow_styling.js similarity index 97% rename from backend/migrations/core/138_add_slideshow_styling.js rename to backend/migrations/core/139_add_slideshow_styling.js index 4a159d1e..df86c98a 100644 --- a/backend/migrations/core/138_add_slideshow_styling.js +++ b/backend/migrations/core/139_add_slideshow_styling.js @@ -1,6 +1,6 @@ const { addColumnIfNotExists } = require('../helpers'); -// Live Slideshow styling (migration 137 follow-up): +// Live Slideshow styling (migration 138 follow-up): // - a ZDF/ARD-ident-style watermark: a white, semi-transparent logo in a // corner of the projected slideshow (sourced from the site branding logo // or the event's own logo). @@ -54,5 +54,5 @@ exports.up = async function up(knex) { }; exports.down = async function down() { - // Safe rollback - intentionally no-op to avoid data loss (mirrors 074/137). + // Safe rollback - intentionally no-op to avoid data loss (mirrors 074/138). }; diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 507348e7..a1cc3f10 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -1882,7 +1882,7 @@ router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), r // --------------------------------------------------------------------------- // Live Slideshow ("Diashow") — a token-only fullscreen kiosk link for live -// events that auto-picks-up new uploads (migration 137). Mirrors the +// events that auto-picks-up new uploads (migration 138). Mirrors the // client-access second-token pattern: the link is minted on demand, rotatable // and disable-able, independent of the gallery password / share link. // --------------------------------------------------------------------------- diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 1eadc09d..3bb261a7 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -290,7 +290,7 @@ router.put('/accounting', adminAuth, requirePermission('settings.edit'), async ( } }); -// Global Live Slideshow defaults (migration 138). The per-event watermark is +// Global Live Slideshow defaults (migration 139). The per-event watermark is // tri-state (events.show_watermark NULL = inherit these). Read via the generic // GET /:type ('slideshow'); this is the typed write. router.put('/slideshow', adminAuth, requirePermission('settings.edit'), async (req, res) => { diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 888e8d70..1187185d 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -220,7 +220,7 @@ router.get('/:slug/info', async (req, res) => { // --------------------------------------------------------------------------- // Live Slideshow ("Diashow") — token-only fullscreen kiosk surface -// (migration 137). The token in the URL IS the secret (no gallery password), +// (migration 138). The token in the URL IS the secret (no gallery password), // so these routes are unauthenticated except for the token match itself. The // slideshow shows ALL public/visible, finished photos — exactly the guest // set — so once /session mints a short-lived `accessLevel:'slideshow'` JWT, @@ -586,7 +586,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) // Log view — but NOT for the Live Slideshow kiosk. A running projector // refetches this list on every new-upload poll, which would massively // inflate total_views / unique_visitors. The slideshow is explicitly - // excluded from real visitor analytics (migration 137 design). + // excluded from real visitor analytics (migration 138 design). if (req.accessLevel !== 'slideshow') { await db('access_logs').insert({ event_id: req.event.id, diff --git a/frontend/src/components/admin/SlideshowSettingsCard.tsx b/frontend/src/components/admin/SlideshowSettingsCard.tsx index b568dd14..ada57c3b 100644 --- a/frontend/src/components/admin/SlideshowSettingsCard.tsx +++ b/frontend/src/components/admin/SlideshowSettingsCard.tsx @@ -1,7 +1,7 @@ /** * * - * Per-event "Live Slideshow" ("Diashow") admin surface (migrations 137/138). + * Per-event "Live Slideshow" ("Diashow") admin surface (migrations 138/139). * A token-only fullscreen kiosk link for live events that auto-picks-up new * uploads while it runs. Mounted once on the EventDetailsPage; admin can: * - Generate the slideshow link on demand (mints show_share_token) diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 9b5acba0..079a8b73 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -2159,7 +2159,7 @@ export const EventDetailsPage: React.FC = () => { - {/* Live Slideshow ("Diashow") link + live display settings (migrations 137/138). + {/* Live Slideshow ("Diashow") link + live display settings (migrations 138/139). Gated behind the `slideshow` feature flag. */} {flags.slideshow && ( { + const actual = await vi.importActual('react-i18next'); + return { + ...actual, + useTranslation: () => ({ t: (k: string, fb?: unknown) => (typeof fb === 'string' ? fb : k) }) + }; +}); + +vi.mock('react-toastify', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +vi.mock('../../../services/settings.service', async () => { + const actual = await vi.importActual('../../../services/settings.service'); + return { + ...actual, + settingsService: { + ...actual.settingsService, + getSettingsByType: vi.fn(async (type: string) => + type === 'theme' ? { theme_config: savedTheme } : {} + ), + getAllSettings: vi.fn(async () => ({ thumbnail_width: '800', thumbnail_height: '800' })), + updateTheme: vi.fn(async () => undefined), + updateBranding: vi.fn(async () => undefined), + }, + }; +}); + +vi.mock('../../../services/fonts.service', () => ({ + fontsService: { list: vi.fn(async () => []) }, + extractFamilyName: (s: string) => s, +})); + +vi.mock('../../../services/businessProfile.service', () => ({ + businessProfileService: { get: vi.fn(async () => ({ profile: {} })), update: vi.fn() }, +})); + +vi.mock('../../../hooks/usePublicSettings', () => ({ + PUBLIC_SETTINGS_QUERY_KEY: ['public-settings'], + usePublicSettings: () => ({ data: { branding_force_color_mode: null } }), +})); + +vi.mock('../../../contexts/FeatureFlagsContext', () => ({ + useFeatureFlags: () => ({ flags: {} }), + useFeatureEnabled: () => false, +})); + +// Heavy/unrelated children — stub to keep the test focused on the CSS textarea. +vi.mock('../../../components/admin/CustomerDashboardBrandingCard', () => ({ + CustomerDashboardBrandingCard: () => null, +})); +vi.mock('../../../components/admin/PdfTypographyCard', () => ({ + PdfTypographyCard: () => null, +})); +vi.mock('../../../components/admin', async () => { + const actual = await vi.importActual('../../../components/admin'); + return { ...actual, GalleryPreview: () => null }; +}); + +import { BrandingPage } from '../BrandingPage'; +import { ThemeProvider } from '../../../contexts/ThemeContext'; + +describe('BrandingPage custom CSS persistence (#645)', () => { + beforeEach(() => localStorage.clear()); + + it('shows the saved custom CSS in the textarea on load for a Gallery Premium theme', async () => { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render( + + + + + + ); + + const textarea = (await screen.findByPlaceholderText( + '/* Add custom CSS here */' + )) as HTMLTextAreaElement; + + await waitFor(() => expect(textarea.value).toBe(CSS)); + }); +}); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ce5895f7..b84491bf 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -67,7 +67,7 @@ export interface Event { // Client access (#172) client_access_enabled?: boolean; client_share_token?: string; - // Live Slideshow / "Diashow" (migration 137). Token-only fullscreen kiosk + // Live Slideshow / "Diashow" (migration 138). Token-only fullscreen kiosk // link minted on demand; null token = disabled. Settings drive the running // projector and can be changed live. show_share_token?: string | null;