diff --git a/backend/migrations/core/152_hero_logo_visible_inherit.js b/backend/migrations/core/152_hero_logo_visible_inherit.js new file mode 100644 index 00000000..0fb1e74a --- /dev/null +++ b/backend/migrations/core/152_hero_logo_visible_inherit.js @@ -0,0 +1,52 @@ +/** + * Migration 152: make events.hero_logo_visible NULL-able so NULL means + * "inherit the global branding_logo_display_hero setting" (#756). + * + * Before: hero_logo_visible was `boolean NOT NULL DEFAULT true`, and every + * event got a concrete true/false snapshotted at creation. The global + * "Show logo in hero section" toggle (branding_logo_display_hero) was only a + * creation-time default and never affected existing galleries — so disabling + * it did nothing to already-published galleries. + * + * After: NULL = inherit. gallery read-resolution falls back to the global + * setting when the per-event value is NULL, so the global toggle controls + * every gallery that hasn't been deliberately overridden per-event. + * + * Data backfill: NULL out the DEFAULTED `true` rows so they start inheriting + * the global. A deliberate per-gallery hide (`false`) is kept — we can't tell a + * defaulted-true from a chosen-true, but `false` is almost always a conscious + * "hide it here", and nulling it could silently re-show a hidden logo. + */ +exports.up = async function (knex) { + if (!(await knex.schema.hasColumn('events', 'hero_logo_visible'))) return; + + const client = (knex.client.config.client || '').toLowerCase(); + if (client === 'pg' || client === 'postgresql') { + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_visible DROP DEFAULT'); + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_visible DROP NOT NULL'); + } else { + // SQLite (and others): knex recreates the table without the NOT NULL/default. + await knex.schema.alterTable('events', (t) => { + t.boolean('hero_logo_visible').nullable().alter(); + }); + } + + // Existing defaulted-`true` galleries now inherit the global toggle. + await knex('events').where('hero_logo_visible', true).update({ hero_logo_visible: null }); +}; + +exports.down = async function (knex) { + if (!(await knex.schema.hasColumn('events', 'hero_logo_visible'))) return; + // Re-materialise NULLs as the old default (true) before restoring NOT NULL. + await knex('events').whereNull('hero_logo_visible').update({ hero_logo_visible: true }); + + const client = (knex.client.config.client || '').toLowerCase(); + if (client === 'pg' || client === 'postgresql') { + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_visible SET DEFAULT true'); + await knex.raw('ALTER TABLE events ALTER COLUMN hero_logo_visible SET NOT NULL'); + } else { + await knex.schema.alterTable('events', (t) => { + t.boolean('hero_logo_visible').notNullable().defaultTo(true).alter(); + }); + } +}; diff --git a/backend/src/routes/adminEvents/crud.js b/backend/src/routes/adminEvents/crud.js index 67e985a0..0cdb4c1a 100644 --- a/backend/src/routes/adminEvents/crud.js +++ b/backend/src/routes/adminEvents/crud.js @@ -339,7 +339,13 @@ module.exports = (router) => { // Get branding defaults for hero logo settings (Feature 7: Branding Inheritance) const brandingDefaults = await getBrandingDefaults(); - const effectiveHeroLogoVisible = req.body.hero_logo_visible !== undefined ? hero_logo_visible : brandingDefaults.hero_logo_visible; + // hero_logo_visible: store NULL ("inherit") unless the admin explicitly + // set it, so the global branding_logo_display_hero toggle keeps + // controlling this gallery afterwards (#756). Only an explicit per-event + // choice overrides the global. + const effectiveHeroLogoVisible = req.body.hero_logo_visible !== undefined + ? formatBoolean(hero_logo_visible) + : null; const effectiveHeroLogoSize = req.body.hero_logo_size || brandingDefaults.hero_logo_size; const effectiveHeroLogoPosition = req.body.hero_logo_position || brandingDefaults.hero_logo_position; @@ -425,7 +431,8 @@ module.exports = (router) => { allow_presigned_download: formatBoolean(allow_presigned_download === true || allow_presigned_download === 'true'), require_password: formatBoolean(requirePassword), css_template_id: css_template_id || null, - hero_logo_visible: formatBoolean(effectiveHeroLogoVisible), + // Already formatBoolean-coerced above, or null = inherit global (#756). + hero_logo_visible: effectiveHeroLogoVisible, hero_logo_size: effectiveHeroLogoSize, hero_logo_position: effectiveHeroLogoPosition, header_style: effectiveHeaderStyle || 'standard', @@ -1424,9 +1431,13 @@ module.exports = (router) => { updates.expires_at = null; } - // Format hero logo settings if provided + // Format hero logo settings if provided. null = inherit the global + // branding_logo_display_hero toggle (#756); only an explicit true/false + // is a per-event override. if (Object.prototype.hasOwnProperty.call(updates, 'hero_logo_visible')) { - updates.hero_logo_visible = formatBoolean(updates.hero_logo_visible); + updates.hero_logo_visible = updates.hero_logo_visible === null + ? null + : formatBoolean(updates.hero_logo_visible); } // Per-event opt-in for hero-photo OG share image (#474). Coerce so diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index e24603a8..54ce3f2b 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -2,9 +2,21 @@ const express = require('express'); const jwt = require('jsonwebtoken'); const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); +const { getAppSetting } = require('../utils/appSettings'); const archiver = require('archiver'); const path = require('path'); const router = express.Router(); + +// #756: a NULL per-event hero_logo_visible means "inherit the global +// branding_logo_display_hero toggle". Only an explicit true/false is a +// per-gallery override. `globalDefault` is branding_logo_display_hero +// (defaults true when unset). +function resolveHeroLogoVisible(perEvent, globalDefault) { + if (perEvent === null || perEvent === undefined) { + return globalDefault !== false; + } + return perEvent !== false && perEvent !== 0 && perEvent !== '0'; +} const watermarkService = require('../services/watermarkService'); const watermarkGeneratorService = require('../services/watermarkGeneratorService'); const { verifyGalleryAccess, denySlideshowToken, isAdminPreview } = require('../middleware/gallery'); @@ -182,6 +194,7 @@ router.get('/:slug/info', async (req, res) => { } const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0'); + const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true); res.json({ event_name: event.event_name, @@ -199,7 +212,7 @@ router.get('/:slug/info', async (req, res) => { watermark_text: event.watermark_text, enable_devtools_protection: event.enable_devtools_protection === true || event.enable_devtools_protection === 1 || event.enable_devtools_protection === '1', use_canvas_rendering: event.use_canvas_rendering === true || event.use_canvas_rendering === 1 || event.use_canvas_rendering === '1', - hero_logo_visible: event.hero_logo_visible !== false && event.hero_logo_visible !== 0 && event.hero_logo_visible !== '0', + hero_logo_visible: resolveHeroLogoVisible(event.hero_logo_visible, globalHeroLogoVisible), hero_logo_size: event.hero_logo_size || 'medium', hero_logo_position: event.hero_logo_position || 'top', hero_logo_url: event.hero_logo_url || null, @@ -635,7 +648,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) // selection back to source files. Tied to the same toggle as downloads — // one switch controls both surfaces. const useOriginalFilenames = await getUseOriginalFilenames(); - + const globalHeroLogoVisible = await getAppSetting('branding_logo_display_hero', true); res.json({ event: { @@ -654,7 +667,7 @@ router.get('/:slug/photos', verifyGalleryAccess, resolveGuest, async (req, res) watermark_text: req.event.watermark_text, enable_devtools_protection: req.event.enable_devtools_protection === true, use_canvas_rendering: req.event.use_canvas_rendering === true, - hero_logo_visible: req.event.hero_logo_visible !== false && req.event.hero_logo_visible !== 0 && req.event.hero_logo_visible !== '0', + hero_logo_visible: resolveHeroLogoVisible(req.event.hero_logo_visible, globalHeroLogoVisible), hero_logo_size: req.event.hero_logo_size || 'medium', hero_logo_position: req.event.hero_logo_position || 'top', hero_logo_url: req.event.hero_logo_url || null, diff --git a/frontend/src/components/gallery/GalleryLayout.tsx b/frontend/src/components/gallery/GalleryLayout.tsx index 377ddfcb..c9e8ba26 100644 --- a/frontend/src/components/gallery/GalleryLayout.tsx +++ b/frontend/src/components/gallery/GalleryLayout.tsx @@ -25,6 +25,10 @@ interface GalleryLayoutProps { promo_mode?: 'inherit' | 'custom' | 'off'; promo_markdown?: string | null; }; + // Effective hero-logo visibility for THIS gallery, already resolved by the + // backend (per-event override, else the global branding toggle) (#756). + // When provided it wins over brandingSettings.logo_display_hero. + heroLogoVisible?: boolean; brandingSettings?: { company_name?: string; company_tagline?: string; @@ -108,6 +112,7 @@ const HeaderDownloadButton: React.FC<{ export const GalleryLayout: React.FC = ({ event, brandingSettings, + heroLogoVisible, showLogout = false, onLogout, showDownloadAll = false, @@ -202,6 +207,9 @@ export const GalleryLayout: React.FC = ({ if (context === 'header') { return brandingSettings?.logo_display_header !== false; } else { + // #756: prefer the backend-resolved per-event value (override, else + // global); fall back to the global branding toggle if not provided. + if (heroLogoVisible !== undefined) return heroLogoVisible; return brandingSettings?.logo_display_hero !== false; } }; diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 7ff8d5be..8d7201a8 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -820,6 +820,7 @@ export const GalleryView: React.FC = ({ slug, event }) => { promo_markdown: (data?.event as { promo_markdown?: string | null })?.promo_markdown, }} brandingSettings={brandingSettings} + heroLogoVisible={data?.event?.hero_logo_visible !== false} headerStyle={data?.event?.header_style || theme.headerStyle} showLogout={true} onLogout={logout} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 871607d3..971feefe 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1110,6 +1110,9 @@ "protectionLevelMaximum": "Maximum - DevTools-Erkennung & Canvas-Rendering", "heroLogoSettings": "Hero-Logo-Einstellungen", "heroLogoVisible": "Logo im Hero-Bereich anzeigen", + "heroLogoInherit": "Branding-Standard verwenden", + "heroLogoShow": "Immer anzeigen", + "heroLogoHide": "Immer ausblenden", "heroLogoSize": "Logo-Größe", "heroLogoSizeSmall": "Klein", "heroLogoSizeMedium": "Mittel", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f6f85e94..93df80e8 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -655,6 +655,9 @@ "protectionLevelMaximum": "Maximum - DevTools detection & canvas rendering", "heroLogoSettings": "Hero Logo Settings", "heroLogoVisible": "Display logo in hero section", + "heroLogoInherit": "Use branding default", + "heroLogoShow": "Always show", + "heroLogoHide": "Always hide", "heroLogoSize": "Logo Size", "heroLogoSizeSmall": "Small", "heroLogoSizeMedium": "Medium", diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 1d3a9363..84c852a1 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -306,8 +306,9 @@ export const EventDetailsPage: React.FC = () => { allow_presigned_download: (event as { allow_presigned_download?: boolean }).allow_presigned_download ?? false, enable_devtools_protection: event.enable_devtools_protection ?? true, use_canvas_rendering: event.use_canvas_rendering ?? false, - // Load hero logo settings from event - hero_logo_visible: event.hero_logo_visible ?? true, + // Load hero logo settings from event. Preserve null = "inherit global" + // (#756) — don't collapse it to true, or saving would snapshot an override. + hero_logo_visible: event.hero_logo_visible ?? null, hero_logo_size: event.hero_logo_size || 'medium', hero_logo_position: event.hero_logo_position || 'top', // Hero image anchor position (#162) diff --git a/frontend/src/pages/admin/event-details/EventInformationCard.tsx b/frontend/src/pages/admin/event-details/EventInformationCard.tsx index 51b25c00..3353f937 100644 --- a/frontend/src/pages/admin/event-details/EventInformationCard.tsx +++ b/frontend/src/pages/admin/event-details/EventInformationCard.tsx @@ -589,18 +589,31 @@ export const EventInformationCard: React.FC = ({
- +
+ + +
- {editForm.hero_logo_visible && ( + {editForm.hero_logo_visible !== false && ( <>