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
+22 -1
View File
@@ -1,17 +1,26 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { downloadProjectFile, readProjectFile, ProjectImportError } from '../../data/exportImport'
import { supabase } from '../../data/supabaseClient'
import { useProjectStore } from '../../state/projectStore'
import { useSubmissionStore } from '../../state/submissionStore'
import MySubmissionsModal from '../submissions/MySubmissionsModal'
import DiagramManagerModal from './DiagramManagerModal'
export default function TopBar() {
const project = useProjectStore((s) => s.project)
const renameProject = useProjectStore((s) => s.renameProject)
const importProject = useProjectStore((s) => s.importProject)
const mySubmissions = useSubmissionStore((s) => s.mySubmissions)
const fileInputRef = useRef<HTMLInputElement>(null)
const [diagramManagerOpen, setDiagramManagerOpen] = useState(false)
const [submissionsOpen, setSubmissionsOpen] = useState(false)
const [username, setUsername] = useState<string | null>(null)
const pendingSubmissionCount = useMemo(
() => mySubmissions.filter((s) => s.status === 'pending').length,
[mySubmissions],
)
// One-time fetch — nothing else in the app changes your own username after
// CompleteProfileScreen sets it, so there's no need to keep this live.
useEffect(() => {
@@ -60,6 +69,17 @@ export default function TopBar() {
>
Diagrams
</button>
<button
onClick={() => setSubmissionsOpen(true)}
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
>
My Submissions
{pendingSubmissionCount > 0 && (
<span className="ml-1 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
{pendingSubmissionCount}
</span>
)}
</button>
<button onClick={handleImportClick} className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
Import
</button>
@@ -79,6 +99,7 @@ export default function TopBar() {
</button>
</div>
{diagramManagerOpen && <DiagramManagerModal onClose={() => setDiagramManagerOpen(false)} />}
{submissionsOpen && <MySubmissionsModal onClose={() => setSubmissionsOpen(false)} />}
</header>
)
}