fix(branding): make 'Show logo in hero' a true global toggle with per-event override (#756)

Before: the global branding_logo_display_hero toggle was only a
creation-time default — snapshotted into each event's hero_logo_visible
column at creation and never consulted again. Disabling it did nothing
to existing galleries (the reporter's bug), and the two gallery render
paths disagreed (GalleryLayout read the global, HeroHeader read the
per-event snapshot).

Now: NULL per-event hero_logo_visible = 'inherit the global toggle';
an explicit true/false is a per-gallery override.

- Migration 152: make events.hero_logo_visible nullable and NULL out the
  defaulted  rows so existing galleries follow the global going
  forward. Deliberate per-gallery hides () are preserved.
- Creation stores NULL unless the admin explicitly sets it; the update
  path preserves NULL.
- gallery.js resolves per-event ?? global (branding_logo_display_hero,
  default true) and sends the EFFECTIVE value on both gallery responses.
- Both frontend render paths now consume that resolved value
  (GalleryLayout gets it via a new heroLogoVisible prop).
- Admin per-event control is now tri-state: Use branding default /
  Always show / Always hide (en + de).

Verified: SQLite migration + live resolution (inherit follows global
both ways; override wins both ways); PG migration SQL dry-run; the admin
tri-state renders 'Use branding default' for an inherited event; tsc
clean, 106 adminEvents+gallery tests pass, build green.
This commit is contained in:
Paul Nothaft
2026-07-06 11:20:36 +02:00
parent f15e104702
commit 96fe478bf8
11 changed files with 131 additions and 26 deletions
@@ -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<GalleryLayoutProps> = ({
event,
brandingSettings,
heroLogoVisible,
showLogout = false,
onLogout,
showDownloadAll = false,
@@ -202,6 +207,9 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
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;
}
};
@@ -820,6 +820,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ 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}
+3
View File
@@ -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",
+3
View File
@@ -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",
@@ -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)
@@ -589,18 +589,31 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
</h3>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={editForm.hero_logo_visible}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_visible: e.target.checked }))}
className="w-4 h-4 text-accent border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.heroLogoVisible', 'Display logo in hero section')}</span>
</label>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1 flex items-center gap-1">
<Image className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
{t('events.heroLogoVisible', 'Display logo in hero section')}
</label>
<select
// Tri-state (#756): "inherit" = null = follow the global
// Branding → "Show logo in hero" toggle; show/hide override it
// for just this gallery.
value={editForm.hero_logo_visible === null || editForm.hero_logo_visible === undefined
? 'inherit'
: editForm.hero_logo_visible ? 'show' : 'hide'}
onChange={(e) => setEditForm(prev => ({
...prev,
hero_logo_visible: e.target.value === 'inherit' ? null : e.target.value === 'show'
}))}
className="w-full sm:w-64 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md shadow-sm focus:ring-primary-500 focus:border-accent-dark text-sm"
>
<option value="inherit">{t('events.heroLogoInherit', 'Use branding default')}</option>
<option value="show">{t('events.heroLogoShow', 'Always show')}</option>
<option value="hide">{t('events.heroLogoHide', 'Always hide')}</option>
</select>
</div>
{editForm.hero_logo_visible && (
{editForm.hero_logo_visible !== false && (
<>
<div className="ml-6">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
@@ -24,8 +24,8 @@ export type EditFormState = {
allow_presigned_download: boolean;
enable_devtools_protection: boolean;
use_canvas_rendering: boolean;
// Hero logo settings
hero_logo_visible: boolean;
// Hero logo settings. null = inherit the global branding toggle (#756).
hero_logo_visible: boolean | null;
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge';
hero_logo_position: 'top' | 'center' | 'bottom';
// Hero image anchor position (#162) keyword or "X% Y%" focal point
@@ -72,8 +72,8 @@ export const INITIAL_EDIT_FORM: EditFormState = {
allow_presigned_download: false,
enable_devtools_protection: true,
use_canvas_rendering: false,
// Hero logo settings
hero_logo_visible: true,
// Hero logo settings — null = inherit global branding toggle (#756)
hero_logo_visible: null,
hero_logo_size: 'medium',
hero_logo_position: 'top',
// Hero image anchor position (#162)
+2 -2
View File
@@ -43,7 +43,7 @@ export interface Event {
enable_devtools_protection?: boolean;
use_canvas_rendering?: boolean;
// Hero logo customization fields
hero_logo_visible?: boolean;
hero_logo_visible?: boolean | null;
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
hero_logo_position?: 'top' | 'center' | 'bottom';
hero_logo_url?: string | null;
@@ -188,7 +188,7 @@ export interface GalleryData {
fragmentation_level?: number;
overlay_protection?: boolean;
// Hero logo customization fields
hero_logo_visible?: boolean;
hero_logo_visible?: boolean | null;
hero_logo_size?: 'small' | 'medium' | 'large' | 'xlarge';
hero_logo_position?: 'top' | 'center' | 'bottom';
hero_logo_url?: string | null;