10 — Submission Detail & Moderation (Screen 6, `/app/testimonials/:id`)
Cursor-ready plan: single-submission admin view with media preview, metadata, consent, actions, publish toggles, and merchandising controls.
10 — Submission Detail & Moderation (Screen 6)
Section titled “10 — Submission Detail & Moderation (Screen 6)”Source: 02-Implementation-Blueprint.md — Screen 6 - Submission Detail (/app/testimonials/:id).
Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — §6 MVP (moderation queue, edit before publish, product mapping).
This document is a build spec only. No code changes are implied until a task references this file.
Prerequisite UI: 09-submissions-inbox-screen-5.md (list + navigation to this page).
Related: 08-security-compliance-and-privacy.md (moderation log, PII masking, consent), 05-storefront-widgets-and-read-api.md (only published content is public).
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”For one TestimonialSubmission id, the merchant sees full media preview, technical metadata, customer and order context, editable text/rating, consent evidence, runs approve / reject / flag / publish actions, and adjusts merchandising (product links, featured, sort) before content goes or stays live on the storefront.
1) Route and file
Section titled “1) Route and file”| Item | Value |
|---|---|
| Suggested file | app/routes/app.testimonial-submission.$id.jsx (matches blueprint §6 “app.testimonial-submission.$id.jsx”). |
| URL | /app/testimonials/:id where :id is TestimonialSubmission.id (UUID). |
Auth: authenticate.admin(request); every loader/action must verify submission.shopId === currentShop.id → 404 if mismatch (do not leak existence across shops).
Navigation: list page (09) links here; optional breadcrumbs “Submissions → Detail”.
2) Loader: data to fetch
Section titled “2) Loader: data to fetch”2.1 Core
Section titled “2.1 Core”TestimonialSubmissionbyid+shopId, including:status,mediaType,rating,headline,reviewText,displayName,displayEmailconsentAccepted,consentAcceptedAt,consentVersionpublished,publishedAt(if columns exist per §5.7)featured,sortOrdershopifyProductId,shopifyVariantId,shopifyCustomerId,shopifyOrderId,requestId
2.2 Media
Section titled “2.2 Media”TestimonialMediaAssetfor this submission (includeplaybackUrl,thumbnailUrl,durationSec,width,height,fileSizeBytes,mimeType,processingStatus).- If multiple assets (future): list them; v1 assumes one primary asset per submission.
2.3 Source (upload vs record)
Section titled “2.3 Source (upload vs record)”- If not stored explicitly, derive from request payload metadata or add optional
captureSourceon submission/media in a migration — document chosen source. Placeholder: infervideo+ user agent from submission meta JSON if you addsubmission.metadata Json.
2.4 Order / request context
Section titled “2.4 Order / request context”- Join
TestimonialRequestviarequestIdwhen present to show human-readable order reference (Shopify order name/number if stored, orshopifyOrderId).
2.5 Optional product links
Section titled “2.5 Optional product links”TestimonialProductLinkrows (§5.9) for “Assign products (multi)” secondary merchandising.
2.6 Moderation history
Section titled “2.6 Moderation history”- Recent
TestimonialModerationLogrows for thissubmissionId(orderedcreatedAtdesc), for an audit sidebar (08).
3) UI layout (Polaris)
Section titled “3) UI layout (Polaris)”Use a two-column layout on desktop; stack on mobile.
3.1 Left column — Media
Section titled “3.1 Left column — Media”- Video: HTML5
<video>withcontrols,playsInline,poster={thumbnailUrl}; src =playbackUrlwhenprocessingStatus === ready; otherwise show processingBanner+ thumbnail only. - Photo:
<img>withsrc={playbackUrl || thumbnailUrl}. - Metadata card (from §5.8 + submission):
- Duration (format
mm:ss), resolutionwidth×height, file size (human-readable), MIME type, processing status, Source (upload|record).
- Duration (format
3.2 Right column — Content & customer
Section titled “3.2 Right column — Content & customer”- Customer: display name, email with optional masking (e.g.
s***@domain.com) — configurable boolean from env or shop setting (08). - Order: string from request/order join; if missing, “—”.
- Rating:
Ratingcomponent (read-only in v1 or editable — see §4). - Headline / Quote:
TextFieldmultiline for quote; single line for headline. - Consent block (read-only):
- Checkbox state (visual), Accepted at datetime, Consent version string.
3.3 Page actions (primary/secondary)
Section titled “3.3 Page actions (primary/secondary)”Group Moderation actions in a Page primaryAction / secondaryActions or ActionList:
- Approve
- Reject (opens modal; reason required text)
- Flag — set internal state for follow-up: either
statustransition to aflaggedvalue orsubmission.flaggedAt+flagReason(add columns if not in §5.7; document migration). - Publish / Unpublish — toggles
published+publishedAt(only allowed whenstatus === approvedif you enforce a state machine).
State machine (document in code):
- Suggested:
pending→approved|rejected|flagged|archived; onlyapprovedcan be published.
4) Forms and inline edit
Section titled “4) Forms and inline edit”4.1 Editable before approve
Section titled “4.1 Editable before approve”- Headline, review text, rating — merchants may fix typos before approval.
- Pattern: single “Save draft” secondary action that PATCHes only text fields while keeping
status=pending, or auto-save with debounce (Phase 2). v1: explicit Save button.
4.2 Validation
Section titled “4.2 Validation”- Max lengths for headline/quote (align with public 06 limits).
- Rating nullable vs required — match Screen 9 global rules when that exists; else optional.
5) Actions (server action intents)
Section titled “5) Actions (server action intents)”Each destructive or state-changing intent must:
- Re-fetch submission with
shopIdlock. - Apply transition rules (no illegal jumps).
- Create
TestimonialModerationLogrow (§5.10, 08):action,reason(reject),actorType=merchant,actorEmailfrom session.
| Intent | Behavior |
|---|---|
approve | Set status=approved; optionally set published per product rule (see §6 in 09 doc — recommend not auto-publish here if Screen 7 owns go-live). |
reject | Set status=rejected; require reason; optional rejectionReason on submission for inbox display. |
flag | Set flagged state (see §3.3). |
toggle_publish | Flip published; when turning on, set publishedAt=now(); when off, clear publishedAt or keep history — pick one (usually keep publishedAt for analytics). |
save_content | Update headline, reviewText, rating only. |
save_merchandising | Update featured, sortOrder, primary shopifyProductId if allowed, and sync TestimonialProductLink (replace set). |
Return toast + redirect or revalidate.
6) Merchandising controls (blueprint)
Section titled “6) Merchandising controls (blueprint)”6.1 Primary product
Section titled “6.1 Primary product”- Default binding is
TestimonialSubmission.shopifyProductIdfrom the token (06). Allow reassign only if blueprint permits override — Screen 6 lists Assign products (multi).
Implementation:
- Multi-select of products: use Shopify Resource Picker (App Bridge) or admin search against local
Producttable. - Persist:
- Primary: keep first selected or explicit “primary” radio — store on submission
shopifyProductId. - Additional:
TestimonialProductLinkrows (§5.9).
- Primary: keep first selected or explicit “primary” radio — store on submission
6.2 Featured & sort
Section titled “6.2 Featured & sort”- Featured — boolean
featuredon submission. - Sort priority —
sortOrderinteger; lower first or higher first — document sort direction used by 05 public API (align).
7) Integrations with other surfaces
Section titled “7) Integrations with other surfaces”- Storefront API (05): only shows submissions where
published === true(and processing ready). Changing Publish here affects live site after cache/revalidation (usually immediate). - Screen 7 (Published Content): if you implement Screen 7 separately, avoid duplicate truth — either Screen 7 is the only place for placement toggles (PDP/home) or duplicate fields on submission — blueprint shows both; recommended: placement visibility lives on Screen 7 only; Screen 6 handles approve + publish + product assignment + featured.
Document in ticket: single source of truth for “visibility by placement” (Screen 7 vs fields on submission).
8) Errors and edge cases
Section titled “8) Errors and edge cases”- 404 — wrong id or other shop.
- Processing media — disable Publish until
TestimonialMediaAsset.processingStatus === ready(or allow publish with poster only — product decision). - Concurrent edits — optional
updatedAtoptimistic check; v1 can last-write-wins.
9) Testing checklist (acceptance criteria)
Section titled “9) Testing checklist (acceptance criteria)”- Cross-shop access returns 404.
- Video plays when
playbackUrlpresent; processing state shows banner when not ready. - Reject requires reason; creates moderation log; status becomes
rejected. - Approve creates log; optional publish toggle works and affects 05 API visibility.
- Save content updates text without changing status incorrectly.
- Merchandising save updates
featured,sortOrder, and product links. - Email masking honors setting when displaying
displayEmail.
10) Implementation order (for Cursor)
Section titled “10) Implementation order (for Cursor)”- Route file + loader with all includes (submission, media, request, links, logs).
- Page layout: media + metadata + customer/order + consent (read-only).
- Text fields + save_content action.
- Approve / reject / flag actions + modals + moderation logs.
- Publish toggle + merchandising (picker + featured + sort).
- Polish: breadcrumbs, back link to 09 list with preserved query string.
11) References
Section titled “11) References”02-Implementation-Blueprint.md— Screen 6, §5.7–5.10, §6 route map.09-submissions-inbox-screen-5.md— list navigation.08-security-compliance-and-privacy.md— audit log, PII.
12) Note on numbering
Section titled “12) Note on numbering”Prior plans in this folder include 05–09. This file is 10-….