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
+9 -30
View File
@@ -1,5 +1,6 @@
import { v4 as uuid } from 'uuid'
import type { Catalog, CableType, DeviceCategoryDef, DeviceTemplate, Port, PortType } from '../domain/types'
import { cableTypeToRow, deviceTemplateToRow, portTypeToRow } from './catalogRowMapping'
import type { CatalogRepository } from './CatalogRepository'
import { supabase } from './supabaseClient'
@@ -156,16 +157,9 @@ export class SupabaseCatalogRepository implements CatalogRepository {
async addPortType(portType: Omit<PortType, 'id' | 'custom'>): Promise<PortType> {
const ownerId = await this.currentUserId()
const id = uuid()
const { error } = await supabase.from('port_types').insert({
id,
name: portType.name,
category: portType.category,
family: portType.family,
compatible_family_ids: portType.compatibleFamilyIds ?? [],
max_connections: portType.maxConnections ?? null,
is_public: false,
owner_id: ownerId,
})
const { error } = await supabase
.from('port_types')
.insert({ id, ...portTypeToRow(portType), is_public: false, owner_id: ownerId })
if (error) console.error('Failed to add port type to Supabase', error)
return { ...portType, id, custom: true }
}
@@ -184,16 +178,9 @@ export class SupabaseCatalogRepository implements CatalogRepository {
async addCableType(cableType: Omit<CableType, 'id' | 'custom'>): Promise<CableType> {
const ownerId = await this.currentUserId()
const id = uuid()
const { error } = await supabase.from('cable_types').insert({
id,
name: cableType.name,
family: cableType.family,
family2: cableType.family2 ?? null,
unit: cableType.unit,
cost_per_unit: cableType.costPerUnit ?? null,
is_public: false,
owner_id: ownerId,
})
const { error } = await supabase
.from('cable_types')
.insert({ id, ...cableTypeToRow(cableType), is_public: false, owner_id: ownerId })
if (error) console.error('Failed to add cable type to Supabase', error)
return { ...cableType, id, custom: true }
}
@@ -212,16 +199,8 @@ export class SupabaseCatalogRepository implements CatalogRepository {
async addDeviceTemplate(template: Omit<DeviceTemplate, 'id' | 'custom'>): Promise<DeviceTemplate> {
const ownerId = await this.currentUserId()
const id = uuid()
const { error } = await supabase.from('device_templates').insert({
id,
name: template.name,
category_id: template.category,
manufacturer: template.manufacturer ?? null,
model: template.model ?? null,
cost: template.cost ?? null,
is_public: false,
owner_id: ownerId,
})
const { ports: _ports, ...templateRow } = deviceTemplateToRow(template)
const { error } = await supabase.from('device_templates').insert({ id, ...templateRow, is_public: false, owner_id: ownerId })
if (error) {
console.error('Failed to add device template to Supabase', error)
return { ...template, id, custom: true }