* feat(gallery): info banner above the photo grid (#932) A short informational note rendered at the TOP of a gallery, above the photos. Distinct from the promotional banner (#440), which stays by the footer for marketing copy — the reporter's case is an onboarding hint ("use the menu button to filter"), which is useless below a gallery the guest has to scroll past first. Mirrors the promo feature's shape rather than inventing a second one: a global default in Settings → Branding (branding_info_markdown) plus a per-event inherit/custom/off override. Markdown via the existing MarkdownContent sanitiser — no raw HTML, no CSS injection. Empty global default means nothing renders, so upgrading changes nothing visible. Deliberately NOT included: an alignment knob (this is short helper copy, not marketing layout) and guest dismissal — the issue lists dismissal as a nice-to-have, and it needs per-guest persistence that is its own decision. Migration 176 is idempotent (hasColumn / existing-key guarded). Note on the payload plumbing: the per-event fields travel in the /photos response, not just /info. GalleryAuthContext seeds its cached event from the gallery LOGIN response — a small identity subset — so anything absent there is undefined right after a guest signs in. /photos is the payload that refreshes on every gallery load, which is why the fields were added there and why GalleryView reads them from `data.event`. Verified in a browser across all three modes; reading them from the context event instead silently collapsed every override back to 'inherit'. * fix(branding): map branding_info_markdown on read so saving can't wipe it (#932) External review caught this. BrandingSettings declared no info_markdown and formatBrandingSettings never mapped branding_info_markdown, so BrandingPage's hydration — setBrandingSettings(prev => ({ ...prev, ...formatted })) — kept the empty-string initializer instead of the persisted value. The form loaded blank and the next Save posted '' back, wiping a configured banner. Silently: the gallery keeps rendering the old copy until that save lands. This is the same bug the footer/promo fields hit in #441 + #440 / #460, which the read mapper still carries a comment about. Add the field to the interface and the mapper, and pin the round-trip for the whole editable branding set so the next field added is caught by a test rather than by a user losing copy. Verified: the new test fails 3/4 with the mapper line removed. * fix(gallery): honour the info-banner override in the reveal-hidden view (#932) External review, round 2. The hidden-until-reveal branch renders GalleryLayout with the context `event`, which is seeded from the gallery login response and carries no banner fields — so while a gallery was hidden, a per-event 'off' silently resolved to 'inherit' and the global banner appeared on a gallery the admin had muted. Resolve the fields there the same way the main render path does. The two full-page layouts (gallery-premium, gallery-story) are deliberately left alone: they return before GalleryLayout and render no header, footer or promo banner either — injecting a wrapper into layouts documented as having 'their own integrated UI' would be a design change, not a fix. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
ab6ca6f82f
commit
b48fa62eea
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Migration: gallery info banner (#932)
|
||||
*
|
||||
* A short informational message rendered ABOVE the photo grid, so guests see
|
||||
* it on load. Distinct from the promotional banner (#440), which sits by the
|
||||
* footer and stays there for marketing/CTA copy — the reporter's case is an
|
||||
* onboarding hint ("use the menu button to filter"), which is useless below a
|
||||
* gallery the guest has to scroll past first.
|
||||
*
|
||||
* Deliberately mirrors the promo columns rather than inventing a second
|
||||
* shape: a global default in branding settings plus a per-event
|
||||
* inherit/custom/off override. Same semantics, same normalisation, so the
|
||||
* two banners stay predictable next to each other.
|
||||
*
|
||||
* Idempotent (hasColumn / existing-key guarded) so a re-run or a
|
||||
* partially-applied state is safe.
|
||||
*/
|
||||
|
||||
exports.up = async function (knex) {
|
||||
// 1. events.info_mode — 'inherit' (use the global default) | 'custom'
|
||||
// (this event's own copy) | 'off' (no banner for this gallery).
|
||||
const hasInfoMode = await knex.schema.hasColumn('events', 'info_mode');
|
||||
if (!hasInfoMode) {
|
||||
await knex.schema.alterTable('events', (table) => {
|
||||
table.string('info_mode', 16).notNullable().defaultTo('inherit');
|
||||
});
|
||||
console.log(' added events.info_mode (default "inherit")');
|
||||
} else {
|
||||
console.log(' events.info_mode already exists, skipping');
|
||||
}
|
||||
|
||||
// 2. events.info_markdown — per-event copy, only read when mode = custom.
|
||||
const hasInfoMarkdown = await knex.schema.hasColumn('events', 'info_markdown');
|
||||
if (!hasInfoMarkdown) {
|
||||
await knex.schema.alterTable('events', (table) => {
|
||||
table.text('info_markdown').nullable();
|
||||
});
|
||||
console.log(' added events.info_markdown (nullable text)');
|
||||
} else {
|
||||
console.log(' events.info_markdown already exists, skipping');
|
||||
}
|
||||
|
||||
// 3. The global default, following the existing `branding_<name>`
|
||||
// convention used by the promo rows this mirrors. Empty string = the
|
||||
// banner is off everywhere until an admin fills it in, so upgrading
|
||||
// changes nothing visible for existing installs.
|
||||
const infoSetting = {
|
||||
setting_key: 'branding_info_markdown',
|
||||
setting_value: JSON.stringify(''),
|
||||
setting_type: 'branding',
|
||||
};
|
||||
const exists = await knex('app_settings').where('setting_key', infoSetting.setting_key).first();
|
||||
if (!exists) {
|
||||
await knex('app_settings').insert({ ...infoSetting, updated_at: knex.fn.now() });
|
||||
console.log(' added branding_info_markdown (empty = banner off)');
|
||||
} else {
|
||||
console.log(' branding_info_markdown already exists, skipping');
|
||||
}
|
||||
|
||||
console.log('Migration 176_gallery_info_banner completed');
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
console.log('Rollback: 176_gallery_info_banner');
|
||||
|
||||
if (await knex.schema.hasTable('events')) {
|
||||
if (await knex.schema.hasColumn('events', 'info_markdown')) {
|
||||
await knex.schema.alterTable('events', (table) => table.dropColumn('info_markdown'));
|
||||
}
|
||||
if (await knex.schema.hasColumn('events', 'info_mode')) {
|
||||
await knex.schema.alterTable('events', (table) => table.dropColumn('info_mode'));
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable('app_settings')) {
|
||||
await knex('app_settings').where('setting_key', 'branding_info_markdown').del();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user