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:
@@ -1,8 +1,10 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { deviceTemplateToRow } from '../../data/catalogRowMapping'
|
||||
import { PORT_CATEGORIES, PORT_DIRECTIONS } from '../../domain/constants'
|
||||
import { allPortTypes, expandCompatibleFamilyIds, incrementPortName } from '../../domain/project'
|
||||
import type { DeviceTemplate, PortDirection } from '../../domain/types'
|
||||
import { useCatalogStore } from '../../state/catalogStore'
|
||||
import { useSubmissionStore } from '../../state/submissionStore'
|
||||
import CategorySelect from '../common/CategorySelect'
|
||||
import Modal from '../common/Modal'
|
||||
|
||||
@@ -21,11 +23,22 @@ function nextDraftKey() {
|
||||
|
||||
export default function DeviceTemplateEditor({
|
||||
template,
|
||||
submissionMode = false,
|
||||
resubmitId,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: {
|
||||
/** When set, edits this existing custom template instead of creating a new one. */
|
||||
/** When set, edits/prefills from this existing template instead of creating a new one. */
|
||||
template?: DeviceTemplate
|
||||
/** When true, `template` is a public entry you don't own — saving submits
|
||||
* a suggested edit for Admin review instead of mutating it directly
|
||||
* (which RLS wouldn't allow anyway). */
|
||||
submissionMode?: boolean
|
||||
/** When set, saving revises this existing submission (of your own,
|
||||
* pending or rejected) instead of creating a new one — see
|
||||
* MySubmissionsModal's "Edit & resubmit". Takes precedence over
|
||||
* `submissionMode` if both are somehow set. */
|
||||
resubmitId?: string
|
||||
onClose: () => void
|
||||
/** Called with the template's category id right before closing, so the
|
||||
* palette can expand that category and reveal what was just created/edited. */
|
||||
@@ -35,6 +48,8 @@ export default function DeviceTemplateEditor({
|
||||
const addCustomDeviceTemplate = useCatalogStore((s) => s.addCustomDeviceTemplate)
|
||||
const updateCustomDeviceTemplate = useCatalogStore((s) => s.updateCustomDeviceTemplate)
|
||||
const addCustomPortType = useCatalogStore((s) => s.addCustomPortType)
|
||||
const submitForReview = useSubmissionStore((s) => s.submitForReview)
|
||||
const resubmit = useSubmissionStore((s) => s.resubmit)
|
||||
|
||||
const portTypes = useMemo(
|
||||
() => [...allPortTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
@@ -124,7 +139,11 @@ export default function DeviceTemplateEditor({
|
||||
cost: cost.trim() === '' ? undefined : Number(cost),
|
||||
ports: ports.map((p) => ({ id: p.key, name: p.name.trim() || 'Port', direction: p.direction, portTypeId: p.portTypeId })),
|
||||
}
|
||||
if (template) {
|
||||
if (resubmitId) {
|
||||
resubmit(resubmitId, deviceTemplateToRow(fields))
|
||||
} else if (submissionMode && template) {
|
||||
submitForReview('device_template', template.id, deviceTemplateToRow(fields))
|
||||
} else if (template) {
|
||||
updateCustomDeviceTemplate(template.id, fields)
|
||||
} else {
|
||||
addCustomDeviceTemplate(fields)
|
||||
@@ -134,7 +153,11 @@ export default function DeviceTemplateEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={template ? 'Edit Custom Device' : 'New Custom Device'} onClose={onClose} width="max-w-xl">
|
||||
<Modal
|
||||
title={resubmitId ? 'Revise Submission' : submissionMode ? 'Suggest an Edit' : template ? 'Edit Custom Device' : 'New Custom Device'}
|
||||
onClose={onClose}
|
||||
width="max-w-xl"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="col-span-2 text-xs font-medium text-slate-600">
|
||||
@@ -327,7 +350,7 @@ export default function DeviceTemplateEditor({
|
||||
disabled={!canSave}
|
||||
className="rounded bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{template ? 'Save changes' : 'Save device'}
|
||||
{resubmitId ? 'Resubmit' : submissionMode ? 'Submit for review' : template ? 'Save changes' : 'Save device'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user