Colour labels for client proofing, plus the photographer's own stars and colours in the admin grid. - Guest colour labels alongside likes/reactions, opt-in per event (defaults off so live galleries do not change mid-proofing), with 'colors' and 'lightroom' keybind schemes. - One global default per feedback type, replacing the per-type scatter. - Admin marks live in their own table (photo_admin_marks) so they can never reach a guest-facing surface. - XMP export prefers a real label, keeping the rating-derived mapping as a fallback. Review: concurrent-write loss on the mark update path, migration index idempotency and error classification all fixed in 7139bcae; migrations renumbered to 182/183 in 8fecdfae after 180/181 were taken on main. Merged with admin privileges: bypass-size-gate is a required check that fails on size alone for review-bypass authors and never re-evaluates on review, which is its designed behaviour once a maintainer has approved.
171 lines
6.3 KiB
JavaScript
171 lines
6.3 KiB
JavaScript
/**
|
|
* Global defaults for the per-event guest-feedback toggles (#1044).
|
|
*
|
|
* Before this module the creation defaults lived in four places that had
|
|
* already drifted apart: the admin create route's destructuring defaults, the
|
|
* v1 create route's hard-coded insert (which forgot `allow_reactions`
|
|
* entirely), `feedbackService.getEventFeedbackSettings()`'s no-row fallback
|
|
* and the migration column defaults. The same install answered "are comments
|
|
* on by default?" differently depending on which code path created the event.
|
|
*
|
|
* Everything now resolves through FEEDBACK_TOGGLES:
|
|
*
|
|
* app_settings row -> per-event column default -> the built-in fallback
|
|
*
|
|
* The `event_default_*` keys extend the family `event_default_require_password`
|
|
* (#317) and `event_default_feedback_enabled` (#520) already established: they
|
|
* are the value a NEW event starts from, not a retroactive master switch.
|
|
* Flipping one never changes a gallery that already exists — a switch that
|
|
* silently strips the rating stars off a gallery a client is mid-way through
|
|
* proofing would be a different, much riskier feature.
|
|
*
|
|
* Adding a seventh feedback type means adding one row to FEEDBACK_TOGGLES
|
|
* (plus its column in the migration and its UI) — no sweep across the create
|
|
* routes.
|
|
*/
|
|
|
|
const { db } = require('../database/db');
|
|
const logger = require('../utils/logger');
|
|
|
|
/**
|
|
* Every guest-feedback toggle, in the order the admin UI shows them.
|
|
*
|
|
* `fallback` is what applies when the admin has never touched the global
|
|
* setting. These match the values the admin create route has shipped with, so
|
|
* installs that never open Settings > Events keep creating events exactly as
|
|
* they did before — except `allow_color_labels`, which is new in #1044 and
|
|
* opt-in so no existing gallery grows a colour bar on upgrade.
|
|
*/
|
|
const FEEDBACK_TOGGLES = [
|
|
{ column: 'allow_ratings', settingKey: 'event_default_allow_ratings', fallback: true },
|
|
{ column: 'allow_likes', settingKey: 'event_default_allow_likes', fallback: true },
|
|
{ column: 'allow_favorites', settingKey: 'event_default_allow_favorites', fallback: true },
|
|
{ column: 'allow_comments', settingKey: 'event_default_allow_comments', fallback: true },
|
|
{ column: 'allow_reactions', settingKey: 'event_default_allow_reactions', fallback: true },
|
|
{ column: 'allow_color_labels', settingKey: 'event_default_allow_color_labels', fallback: false },
|
|
];
|
|
|
|
/** Non-boolean feedback defaults that follow the same global -> event flow. */
|
|
const KEYBIND_MODES = ['colors', 'lightroom'];
|
|
const DEFAULT_KEYBIND_MODE = 'colors';
|
|
const KEYBIND_MODE_SETTING_KEY = 'event_default_keybind_mode';
|
|
|
|
/** Every app_settings key this module owns — for the settings API surface. */
|
|
const FEEDBACK_DEFAULT_SETTING_KEYS = [
|
|
...FEEDBACK_TOGGLES.map((t) => t.settingKey),
|
|
KEYBIND_MODE_SETTING_KEY,
|
|
];
|
|
|
|
/**
|
|
* app_settings stores JSON-encoded values, but rows written by older code
|
|
* paths (and by SQLite's looser typing) can arrive as raw strings. Accept
|
|
* both, and return undefined for anything that isn't recognisably a boolean
|
|
* so the caller falls back rather than persisting `null` into a NOT NULL-ish
|
|
* boolean column.
|
|
*/
|
|
function parseBooleanSetting(rawValue) {
|
|
let value = rawValue;
|
|
if (typeof value === 'string') {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized === 'true') return true;
|
|
if (normalized === 'false') return false;
|
|
return undefined;
|
|
}
|
|
}
|
|
if (typeof value === 'boolean') return value;
|
|
// SQLite hands back 0/1 for booleans written through formatBoolean.
|
|
if (value === 1 || value === 0) return value === 1;
|
|
return undefined;
|
|
}
|
|
|
|
function parseKeybindModeSetting(rawValue) {
|
|
let value = rawValue;
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (typeof parsed === 'string') value = parsed;
|
|
} catch {
|
|
/* keep raw */
|
|
}
|
|
}
|
|
return KEYBIND_MODES.includes(value) ? value : undefined;
|
|
}
|
|
|
|
/**
|
|
* Resolve the global feedback defaults in ONE query (mirrors
|
|
* getBrandingDefaults' batched whereIn rather than firing seven `.first()`
|
|
* calls on every event creation).
|
|
*
|
|
* Never throws: a settings-table hiccup must not fail event creation, so the
|
|
* built-in fallbacks stand in.
|
|
*
|
|
* @returns {Promise<{allow_ratings: boolean, allow_likes: boolean,
|
|
* allow_favorites: boolean, allow_comments: boolean, allow_reactions: boolean,
|
|
* allow_color_labels: boolean, keybind_mode: string}>}
|
|
*/
|
|
async function resolveEventFeedbackDefaults() {
|
|
const defaults = {};
|
|
for (const toggle of FEEDBACK_TOGGLES) {
|
|
defaults[toggle.column] = toggle.fallback;
|
|
}
|
|
defaults.keybind_mode = DEFAULT_KEYBIND_MODE;
|
|
|
|
try {
|
|
const rows = await db('app_settings')
|
|
.whereIn('setting_key', FEEDBACK_DEFAULT_SETTING_KEYS)
|
|
.select('setting_key', 'setting_value');
|
|
|
|
const bySettingKey = new Map(rows.map((row) => [row.setting_key, row.setting_value]));
|
|
|
|
for (const toggle of FEEDBACK_TOGGLES) {
|
|
if (!bySettingKey.has(toggle.settingKey)) continue;
|
|
const parsed = parseBooleanSetting(bySettingKey.get(toggle.settingKey));
|
|
if (parsed !== undefined) defaults[toggle.column] = parsed;
|
|
}
|
|
|
|
if (bySettingKey.has(KEYBIND_MODE_SETTING_KEY)) {
|
|
const parsed = parseKeybindModeSetting(bySettingKey.get(KEYBIND_MODE_SETTING_KEY));
|
|
if (parsed !== undefined) defaults.keybind_mode = parsed;
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to read global feedback defaults, using built-ins', {
|
|
error: error.message,
|
|
});
|
|
}
|
|
|
|
return defaults;
|
|
}
|
|
|
|
/**
|
|
* Merge an explicit request body over the resolved globals. A value the caller
|
|
* actually sent always wins; `undefined` (the field was omitted) inherits.
|
|
*
|
|
* @param {Object} body - the create-request payload
|
|
* @param {Object} globals - output of resolveEventFeedbackDefaults()
|
|
*/
|
|
function applyFeedbackDefaults(body = {}, globals) {
|
|
const resolved = { ...globals };
|
|
for (const toggle of FEEDBACK_TOGGLES) {
|
|
if (body[toggle.column] !== undefined) {
|
|
resolved[toggle.column] = body[toggle.column];
|
|
}
|
|
}
|
|
if (KEYBIND_MODES.includes(body.keybind_mode)) {
|
|
resolved.keybind_mode = body.keybind_mode;
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
module.exports = {
|
|
FEEDBACK_TOGGLES,
|
|
FEEDBACK_DEFAULT_SETTING_KEYS,
|
|
KEYBIND_MODES,
|
|
KEYBIND_MODE_SETTING_KEY,
|
|
DEFAULT_KEYBIND_MODE,
|
|
resolveEventFeedbackDefaults,
|
|
applyFeedbackDefaults,
|
|
};
|