09 — Submissions Inbox (Screen 5, `/app/testimonials`)
Cursor-ready plan: admin list view for testimonial submissions with tabs, filters, row cards, and bulk moderation actions.
09 — Submissions Inbox (Screen 5)
Section titled “09 — Submissions Inbox (Screen 5)”Source: 02-Implementation-Blueprint.md — Screen 5 - Submissions Inbox (/app/testimonials).
Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — §6 MVP → Moderation & Publishing (pending queue, quick review).
This document is a build spec only. No code changes are implied until a task references this file.
Out of scope here: Screen 6 (submission detail, deep edit, full media metadata) — link to that route from each row; implement in a separate plan if needed.
Related: 08-security-compliance-and-privacy.md (audit log on actions), 06-public-submission-page-screen-13.md (data lands as pending).
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”Merchants open /app/testimonials in the embedded app, switch tabs by workflow state, filter the list, see row cards with thumbnail + key fields, open a detail page for one item, and run bulk approve / reject / archive on selected rows.
1) Route and file naming
Section titled “1) Route and file naming”| Blueprint §6 suggestion | Use this or equivalent |
|---|---|
app/routes/app.testimonial-submissions.jsx | Recommended — file name matches “submissions” language. |
| URL path | /app/testimonials (align with Screen 5 title; set path in route config if using folder-based routes). |
Auth: every loader and action must call authenticate.admin(request) and scope all Prisma queries by shopId resolved from the session (same pattern as app.support.jsx / app.knowledge.jsx in the boilerplate).
Navigation: add an entry in the embedded app nav (e.g. app/routes/app.jsx s-link) when the testimonial section ships — exact label: “Submissions” or “Inbox”; link href="/app/testimonials".
2) Data model (read side)
Section titled “2) Data model (read side)”2.1 Primary entity
Section titled “2.1 Primary entity”TestimonialSubmission(blueprint §5.7):status(pending|approved|rejected|archived),mediaType,submittedAt,rating,displayName,shopifyProductId,requestId, etc.
2.2 Joins for display
Section titled “2.2 Joins for display”-
Product title: join
Producton(shopId, shopifyProductId)when a local mirror exists; if missing, show “Unknown product” or fetch title via Admin API in the loader (cache per request — avoid N+1 GraphQL if listing many rows; prefer denormalizedproductTitleSnapshoton submission in a later migration if needed). -
Thumbnail: from
TestimonialMediaAsset.thumbnailUrl(§5.8); fallback placeholder image if null while processing.
2.3 Indexes
Section titled “2.3 Indexes”Ensure queries use indexed fields: (shopId, status), (shopId, submittedAt). Add @@index([shopId, status, submittedAt]) in Prisma if not present.
3) URL state (tabs + filters)
Section titled “3) URL state (tabs + filters)”Use search params so the view is bookmarkable and shareable:
| Param | Values | Default |
|---|---|---|
tab | pending, approved, rejected, archived | pending |
media | all, video, photo | all |
productId | internal uuid or shopifyProductId string | empty |
ratingMin / ratingMax | integer 1–5 | empty |
from / to | ISO date (UTC, date-only) | empty |
page | integer | 1 |
pageSize | integer (e.g. 20, max 100) | 20 |
sort | submittedAt_desc (v1 only; expand later) | submittedAt_desc |
Loader parses new URL(request.url).searchParams and builds a single where clause for Prisma.
4) UI structure (Polaris)
Section titled “4) UI structure (Polaris)”4.1 Page shell
Section titled “4.1 Page shell”Pagewithtitle="Submissions"(or “Testimonial submissions”).- Primary action (optional v1): “Refresh” or rely on React Router revalidation after actions.
- Tabs component or ButtonGroup mapping to
tabparam — counts badge per tab (see §5).
4.2 Filters bar (collapsible on mobile)
Section titled “4.2 Filters bar (collapsible on mobile)”- Media type —
Select/ button group: All, Video, Photo. - Product —
AutocompleteorSelectpopulated from distinct products that have submissions for this shop (queryTestimonialSubmissiongroupBy/ distinctshopifyProductId+ join titles). - Rating — range or min select (only if
ratingis non-null in data). - Date range — two date fields or
DatePickerrange (usedate-fns+ shop § date format from env if already in app). - Clear filters — resets params except
tab.
4.3 Row cards / list
Section titled “4.3 Row cards / list”For each submission in the current page:
- Thumbnail (left)
- Stack: customer display name (or “Anonymous” if empty), product title, media type badge, submitted at (formatted with
date-formathelper) - Status badge matching
tabor rawstatus - Kebab / secondary actions (optional on row): “Open” →
/app/testimonials/${id}(Screen 6)
Layout: use ResourceList with ResourceItem or IndexTable with selection checkboxes for bulk actions. IndexTable is better for bulk select.
4.4 Empty states
Section titled “4.4 Empty states”- No rows: EmptyState with copy like “No pending submissions” + link to help doc or campaign setup (optional).
4.5 Pagination
Section titled “4.5 Pagination”- Pagination control at bottom;
count()+skip/takeor cursor-based (offset is fine for v1 under ~10k rows per shop).
5) Tab counts (badges)
Section titled “5) Tab counts (badges)”Run an efficient aggregate in the loader (one query per shop, not per tab N+1):
groupByonstatuswhereshopId = X, or fourcountqueries inPromise.all.
Pass counts to the tab UI: pending: 12, approved: 40, etc.
6) Bulk actions
Section titled “6) Bulk actions”6.1 Selection
Section titled “6.1 Selection”- Select all on page checkbox in
IndexTable. - Bulk action bar appears when
selectedIds.length > 0.
6.2 Actions (blueprint)
Section titled “6.2 Actions (blueprint)”-
Approve — set
statustoapproved(and optionallypublished/ visibility per product rules — if Screen 7 owns “published”, v1 can only approve here and leavepublished=falseuntil merchant publishes from Screen 7 — pick one product rule and document it).Recommended v1: Approve =
status=approved+published=false; merchant uses Screen 7 to go live. Alternative: Approve + auto-publish to PDP — simpler for merchants but couples screens; state the choice in the action handler comment. -
Reject — modal with required reason string; set
status=rejected, store reason on submission or inTestimonialModerationLogonly (08 — prefer log + optionalrejectionReasonon submission for admin list). -
Archive — set
status=archived(or soft-delete pattern if you usearchivedboolean — align with §5.7statusenum).
6.3 Action implementation
Section titled “6.3 Action implementation”- POST
actionon the same route (Remix/React Routeractionexport) withintent: "bulk_approve" | "bulk_reject" | "bulk_archive"andids: string[]. - Validate every id belongs to
shopIdbefore update. - Transaction:
$transactionupdating many rows + insertingTestimonialModerationLogrows per id (08). - Return toast success + revalidate.
6.4 Limits
Section titled “6.4 Limits”- Cap bulk size (e.g. max 50 ids per request) to avoid timeout.
7) Single-row navigation
Section titled “7) Single-row navigation”- Row click or “View” navigates to
/app/testimonials/:id(Screen 6 — not implemented in this plan file).
8) Performance and limits
Section titled “8) Performance and limits”- Default page size 20; max 100.
- Avoid loading full
reviewTextor large JSON in list query —selectonly fields needed for cards. - For video thumbnail, use
thumbnailUrlonly; do not loadplaybackUrlfor every row if unnecessary.
9) Testing checklist (acceptance criteria)
Section titled “9) Testing checklist (acceptance criteria)”- Non-admin / wrong shop cannot access another shop’s submissions (verify session shop filter).
- Each tab shows only rows matching
status. - Filters combine correctly (media + product + date + rating).
- Pagination changes
pageparam and preserves filters. - Bulk approve updates all selected rows + creates moderation log entries (08).
- Bulk reject requires reason; rejected rows appear under Rejected tab.
- Empty states show for each tab when count is zero.
- Deep link with query params restores the same view after reload.
10) Implementation order (for Cursor)
Section titled “10) Implementation order (for Cursor)”- Prisma: confirm
TestimonialSubmission+TestimonialMediaAssetexist; add list indexes if missing. app.testimonial-submissions.jsx(or chosen filename): loader with filters + pagination + tab counts.- Polaris layout: tabs, filters,
IndexTable, pagination. action: bulk approve / reject / archive + moderation log + toast.- Wire nav link in
app.jsx. - Stub link to detail route
/app/testimonials/:id(Screen 6 — separate task).
11) References
Section titled “11) References”02-Implementation-Blueprint.md— Screen 5, §5.7–5.8, §5.10, §6 admin routes list.01-Post-Purchase-Video-Testimonial-Collector-Plan.md— §6 Moderation & Publishing.
12) Note on numbering
Section titled “12) Note on numbering”Existing plans through 08-security-compliance-and-privacy.md. This file is 09-….