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
@@ -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)