fix(gallery): make per-event banner overrides actually work, both banners (#440, #932) (#1064)

* fix(gallery): make per-event banner overrides actually work, both banners (#440, #932)

The promo banner shipped with a per-event inherit/custom/off override that
never reached a guest. GalleryView reads promo_mode from the /photos payload,
and /photos never sent it — so every gallery resolved to 'inherit'. Setting a
gallery's promo banner to "Off" did nothing; the global banner kept rendering.
The info banner (#932) mirrored that shape and inherited the same gaps.

Four places dropped the fields; all four now carry both banners:

1. GET /gallery/:slug/photos — send promo_mode/promo_markdown alongside the
   info fields. This is the fix that makes "Off" mean off.
2. POST /admin/events — the validators accepted both banners and the insert
   discarded them, so an API client could POST info_mode:'off', get 201, and
   find the row on 'inherit'. Markdown is stored only for 'custom', matching
   the PUT rule.
3. POST /admin/events/:id/duplicate — copy both from the source row. The
   dialog promises the copy "inherits the branding, behaviour, feedback, and
   category configuration"; a muted gallery un-muting on duplication is the
   opposite of that.
4. PUT /admin/events/:id — resolve the effective mode from the STORED row when
   a partial update sends only the markdown. Previously updates.promo_mode was
   undefined on such a request and the text was parked on an inherit/off
   gallery, then resurfaced when someone later switched it to 'custom'. The
   lookup is lazy: one extra query, only on that path.

The two normalisation blocks are now one loop over both banners, so the pair
can't drift apart again.

Verified in a browser, both directions against the same global banner:
promo_mode='off' -> not rendered; 'inherit' -> rendered. The /photos payload
went from promo_mode ABSENT to carrying the value.

* fix(gallery): thread promo into the reveal view, drop stale markdown on duplicate

External review, round 1 on this PR. Two gaps in the plumbing it introduced:

- The reveal-hidden branch copied only the info fields from /photos. Now that
  /photos carries promo too, a reveal-hidden gallery with promo_mode 'off'
  still fell back to 'inherit' and showed the global banner on the first load
  after login. Thread both banners there.

- The duplicate copied markdown verbatim. A row written before the PUT
  normalisation landed can hold text while its mode is 'inherit'/'off', so the
  copy inherited hidden text that would resurface the moment someone switched
  it to 'custom' — violating the very invariant this PR establishes. Copy
  markdown only when the source mode is 'custom'.

Test covers the stale-markdown source explicitly.

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-08-16 21:37:17 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 180f19d70d
commit 52db982661
4 changed files with 273 additions and 25 deletions
@@ -765,11 +765,13 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
<GalleryLayout
event={{
...event,
// The reveal-hidden view still renders the chrome, so the info
// banner resolves here too. The context `event` comes from the
// The reveal-hidden view still renders the chrome, so BOTH
// banners resolve here too. The context `event` comes from the
// gallery login response and carries no banner fields, which would
// silently downgrade a per-event 'off' to 'inherit' and show the
// global banner on a gallery the admin muted (#932).
// global banner on a gallery the admin muted (#440 promo / #932 info).
promo_mode: (data?.event as { promo_mode?: 'inherit' | 'custom' | 'off' })?.promo_mode,
promo_markdown: (data?.event as { promo_markdown?: string | null })?.promo_markdown,
info_mode: (data?.event as { info_mode?: 'inherit' | 'custom' | 'off' })?.info_mode,
info_markdown: (data?.event as { info_markdown?: string | null })?.info_markdown,
}}