Add the user-submission flow for the catalog

Lets users submit a private catalog entry (port type, cable type, device
template) for promotion to the public catalog, or suggest an edit to an
existing public entry — both go into the catalog_submissions review queue
per organized-ideas.md §3. No Admin review UI yet (next sub-phase); this
covers the submitter's side only.

- data/SubmissionRepository + SupabaseSubmissionRepository: submit,
  resubmit, withdraw, list-mine, backed by the existing catalog_submissions
  RLS policies (no schema changes needed).
- data/catalogRowMapping.ts: shared domain<->row mappers, in both
  directions, so a submission's proposed_data is always shaped like the
  underlying table row (what an eventual admin-approval would write
  directly) and can be turned back into form-editable fields for revision.
- state/submissionStore.ts: mySubmissions + submit/resubmit/withdraw, plus
  syncProposedData — called from catalogStore's updateCustom* actions so a
  submission about your own still-private entry never goes stale relative
  to it (edits from the library and from My Submissions are the same
  action and always agree).
- UI: "Submit"/"Suggest edit" wired into PortTypeManager, CableTypeManager,
  DevicePalette/DeviceTemplateEditor; new MySubmissionsModal (opened from
  TopBar, with a pending-count badge) shows status, rejection reasons, and
  lets you edit/resubmit or withdraw.
- Deliberately deferred: device_category submissions (no listing UI to
  hang a button on yet) and the normalized manufacturer catalog.

Verified: tsc -b and oxlint clean; supabase db reset + 23/23 pgTAP RLS
tests still pass (no schema changes this round); manually tested submit,
suggest-edit, edit-from-either-side sync, reject/resubmit, and withdraw.
This commit is contained in:
2026-09-08 10:37:38 -05:00
parent a03987c36b
commit a0c598cb1b
14 changed files with 968 additions and 119 deletions
+59
View File
@@ -0,0 +1,59 @@
/** Which catalog table a submission is about. Mirrors `catalog_submissions.entity_type`'s
* check constraint — `manufacturer` is omitted since the app doesn't wire up the
* normalized manufacturer catalog yet (see organized-ideas.md §3, deferred alongside
* device_templates.manufacturer staying free text for now). */
export type CatalogEntityType = 'device_template' | 'port_type' | 'cable_type' | 'device_category'
/**
* A user's request to change the public catalog — either promoting one of
* their own private entries to public, or proposing an edit to an existing
* public entry. Regular users can never write a public catalog row directly
* (RLS only allows Admins to), so this is the only path either kind of
* change can take; an Admin review queue (not yet built — organized-ideas.md
* §9's next sub-phase) is what actually applies an approved one.
*/
export interface CatalogSubmission {
id: string
entityType: CatalogEntityType
/** The row this submission is about — always an existing row in this
* app's flow: either your own private entry (being promoted, possibly
* with edits) or a public entry (you're suggesting an edit to). */
entityId: string
/** Proposed field values, shaped like the underlying table's row (see
* data/catalogRowMapping.ts) — so applying an approval is just writing
* this object onto the row at `entityId`, not a separate translation
* step future Admin-approval logic would otherwise have to redo. */
proposedData: Record<string, unknown>
submitterId: string
status: 'pending' | 'approved' | 'rejected'
reviewerId?: string
/** Set by an Admin on rejection — organized-ideas.md §3's "notified either
* way, with a reason attached on rejection rather than a silent disappearance." */
reviewReason?: string
createdAt: string
updatedAt: string
}
/** Storage abstraction for the catalog submission/review queue — mirrors
* DiagramRepository/CatalogRepository's role for this concern. Only covers
* the submitter's-eye view (create, revise, withdraw your own) for now; an
* Admin's view (list all pending, approve/reject) is the next sub-phase. */
export interface SubmissionRepository {
/** Every submission the current user has made, most recent first. */
listMine(): Promise<CatalogSubmission[]>
/** Submits `entityId` (a row you own privately, or a public row you're
* proposing an edit to) for Admin review. */
submit(entityType: CatalogEntityType, entityId: string, proposedData: Record<string, unknown>): Promise<CatalogSubmission>
/** Revises a still-pending or previously-rejected submission of your own
* and puts it (back) into the pending queue. */
resubmit(id: string, proposedData: Record<string, unknown>): Promise<void>
/** Updates only `proposed_data`, leaving status/reviewer/reason untouched.
* Used to keep a submission's snapshot in sync with the live private
* entry it's about (see catalogStore) — a plain edit to your library
* entry shouldn't silently pull a rejected submission back into the
* pending queue, so status changes stay a separate, explicit action
* (`resubmit`), while the data itself is kept fresh automatically. */
updateProposedData(id: string, proposedData: Record<string, unknown>): Promise<void>
/** Withdraws your own still-pending submission. */
withdraw(id: string): Promise<void>
}