feat(gallery): colour labels for client proofing, and one global default per feedback type (#1044) (#1137)

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.
This commit is contained in:
Luca
2026-08-23 11:15:01 +02:00
committed by GitHub
parent 7b77bbf243
commit e2844d1909
60 changed files with 3912 additions and 182 deletions
+14 -1
View File
@@ -2,6 +2,8 @@ const { body, param, validationResult } = require('express-validator');
const validator = require('validator');
const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('./emailNormalization');
const { REACTION_EMOJIS } = require('../constants/reactions');
const { COLOR_LABELS } = require('../constants/colorLabels');
const { KEYBIND_MODES } = require('../services/feedbackDefaults');
/**
* Validation rules for feedback submission
@@ -134,7 +136,7 @@ function getValidationRules(feedbackType) {
*/
const validateFeedbackSubmission = [
body('feedback_type')
.isIn(['rating', 'like', 'comment', 'favorite', 'reaction'])
.isIn(['rating', 'like', 'comment', 'favorite', 'reaction', 'color_label'])
.withMessage('Invalid feedback type'),
// Conditional validation based on feedback type. 0 clears the guest's
@@ -151,6 +153,14 @@ const validateFeedbackSubmission = [
.if(body('feedback_type').equals('reaction'))
.custom((value) => REACTION_EMOJIS.includes(value))
.withMessage('Invalid reaction'),
// Colour labels (#1044): Lightroom's five colours only — the value ends up
// in an XMP field Lightroom parses, so free-form strings are rejected here
// rather than sanitised later.
body('color_label')
.if(body('feedback_type').equals('color_label'))
.custom((value) => COLOR_LABELS.includes(value))
.withMessage('Invalid color label'),
body('comment_text')
.if(body('feedback_type').equals('comment'))
@@ -194,6 +204,9 @@ const validateFeedbackSettings = [
body('allow_comments').optional().isBoolean(),
body('allow_favorites').optional().isBoolean(),
body('allow_reactions').optional().isBoolean(),
body('allow_color_labels').optional().isBoolean(),
body('keybind_mode').optional().isIn(KEYBIND_MODES)
.withMessage(`keybind_mode must be one of: ${KEYBIND_MODES.join(', ')}`),
body('require_name_email').optional().isBoolean(),
body('moderate_comments').optional().isBoolean(),
body('show_feedback_to_guests').optional().isBoolean(),