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,13 +1,17 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { deviceTemplateToRow } from '../../data/catalogRowMapping'
|
||||
import { allDeviceCategories, allDeviceTemplates, hiddenDeviceTemplates } from '../../domain/project'
|
||||
import type { DeviceTemplate } from '../../domain/types'
|
||||
import { useCatalogStore } from '../../state/catalogStore'
|
||||
import { useProjectStore } from '../../state/projectStore'
|
||||
import { useSubmissionStore } from '../../state/submissionStore'
|
||||
import { TEMPLATE_DRAG_MIME } from '../canvas/FlowCanvas'
|
||||
import Chevron from '../common/Chevron'
|
||||
import ConnectorLibraryModal from './ConnectorLibraryModal'
|
||||
import DeviceTemplateEditor from './DeviceTemplateEditor'
|
||||
|
||||
type EditorTarget = { mode: 'new' } | { mode: 'edit'; template: DeviceTemplate } | { mode: 'suggestEdit'; template: DeviceTemplate }
|
||||
|
||||
export default function DevicePalette() {
|
||||
const project = useProjectStore((s) => s.project)
|
||||
const addDeviceFromTemplate = useProjectStore((s) => s.addDeviceFromTemplate)
|
||||
@@ -16,7 +20,9 @@ export default function DevicePalette() {
|
||||
const removeCustomDeviceTemplate = useCatalogStore((s) => s.removeCustomDeviceTemplate)
|
||||
const hidePublicDeviceTemplate = useCatalogStore((s) => s.hidePublicDeviceTemplate)
|
||||
const restorePublicDeviceTemplate = useCatalogStore((s) => s.restorePublicDeviceTemplate)
|
||||
const [editorTarget, setEditorTarget] = useState<'new' | DeviceTemplate | null>(null)
|
||||
const mySubmissions = useSubmissionStore((s) => s.mySubmissions)
|
||||
const submitForReview = useSubmissionStore((s) => s.submitForReview)
|
||||
const [editorTarget, setEditorTarget] = useState<EditorTarget | null>(null)
|
||||
const [portTypesOpen, setPortTypesOpen] = useState(false)
|
||||
const [hiddenListOpen, setHiddenListOpen] = useState(false)
|
||||
// Collapsed by default — expanding on demand is the whole point (keeps a
|
||||
@@ -81,6 +87,18 @@ export default function DevicePalette() {
|
||||
|
||||
const hiddenDefaults = useMemo(() => hiddenDeviceTemplates(catalog, hiddenPublicIds), [catalog, hiddenPublicIds])
|
||||
|
||||
const pendingEntityIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
mySubmissions.filter((s) => s.entityType === 'device_template' && s.status === 'pending').map((s) => s.entityId),
|
||||
),
|
||||
[mySubmissions],
|
||||
)
|
||||
|
||||
const handleSubmitForPublic = (template: DeviceTemplate) => {
|
||||
submitForReview('device_template', template.id, deviceTemplateToRow(template))
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="flex h-full w-64 shrink-0 flex-col border-r border-slate-200 bg-slate-50">
|
||||
<div className="flex items-center justify-between border-b border-slate-200 px-3 py-2">
|
||||
@@ -94,7 +112,7 @@ export default function DevicePalette() {
|
||||
Connectors
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditorTarget('new')}
|
||||
onClick={() => setEditorTarget({ mode: 'new' })}
|
||||
className="rounded bg-indigo-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500"
|
||||
>
|
||||
+ Custom
|
||||
@@ -136,9 +154,33 @@ export default function DevicePalette() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 items-center gap-1 opacity-0 group-hover:opacity-100">
|
||||
{pendingEntityIds.has(template.id) ? (
|
||||
<span
|
||||
title="A submission for this device is pending Admin review"
|
||||
className="rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-600"
|
||||
>
|
||||
Pending
|
||||
</span>
|
||||
) : template.custom ? (
|
||||
<button
|
||||
onClick={() => handleSubmitForPublic(template)}
|
||||
title="Submit this to the public catalog for Admin review"
|
||||
className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-500 hover:bg-emerald-100 hover:text-emerald-700"
|
||||
>
|
||||
⇪
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditorTarget({ mode: 'suggestEdit', template })}
|
||||
title="Suggest an edit to this device for Admin review"
|
||||
className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-500 hover:bg-indigo-100 hover:text-indigo-700"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
)}
|
||||
{template.custom && (
|
||||
<button
|
||||
onClick={() => setEditorTarget(template)}
|
||||
onClick={() => setEditorTarget({ mode: 'edit', template })}
|
||||
title="Edit this device"
|
||||
className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-500 hover:bg-indigo-100 hover:text-indigo-700"
|
||||
>
|
||||
@@ -199,7 +241,8 @@ export default function DevicePalette() {
|
||||
)}
|
||||
{editorTarget && (
|
||||
<DeviceTemplateEditor
|
||||
template={editorTarget === 'new' ? undefined : editorTarget}
|
||||
template={editorTarget.mode === 'new' ? undefined : editorTarget.template}
|
||||
submissionMode={editorTarget.mode === 'suggestEdit'}
|
||||
onClose={() => setEditorTarget(null)}
|
||||
onSaved={(categoryId) => expandCategory(categoryId)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user