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:
@@ -0,0 +1,102 @@
|
||||
import type { CableType, DeviceTemplate, PortType } from '../domain/types'
|
||||
|
||||
/**
|
||||
* Maps domain objects to the shape of their Supabase table row (snake_case
|
||||
* columns, no id/is_public/owner_id). Shared by two call sites that both
|
||||
* need a row-shaped payload: SupabaseCatalogRepository's inserts, and
|
||||
* catalog submissions' `proposed_data` (see SubmissionRepository) — a
|
||||
* submission's proposed_data is deliberately row-shaped so approving it is
|
||||
* just writing that object onto the row at `entity_id`, not a translation
|
||||
* step an Admin's approval action would otherwise have to duplicate.
|
||||
*/
|
||||
|
||||
export function portTypeToRow(portType: Omit<PortType, 'id' | 'custom'>): Record<string, unknown> {
|
||||
return {
|
||||
name: portType.name,
|
||||
category: portType.category,
|
||||
family: portType.family,
|
||||
compatible_family_ids: portType.compatibleFamilyIds ?? [],
|
||||
max_connections: portType.maxConnections ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function cableTypeToRow(cableType: Omit<CableType, 'id' | 'custom'>): Record<string, unknown> {
|
||||
return {
|
||||
name: cableType.name,
|
||||
family: cableType.family,
|
||||
family2: cableType.family2 ?? null,
|
||||
unit: cableType.unit,
|
||||
cost_per_unit: cableType.costPerUnit ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function deviceCategoryToRow(name: string): Record<string, unknown> {
|
||||
return { name }
|
||||
}
|
||||
|
||||
export function deviceTemplateToRow(template: Omit<DeviceTemplate, 'id' | 'custom'>): Record<string, unknown> {
|
||||
return {
|
||||
name: template.name,
|
||||
category_id: template.category,
|
||||
manufacturer: template.manufacturer ?? null,
|
||||
model: template.model ?? null,
|
||||
cost: template.cost ?? null,
|
||||
ports: template.ports.map((port, index) => ({
|
||||
name: port.name,
|
||||
direction: port.direction,
|
||||
port_type_id: port.portTypeId,
|
||||
sort_order: index,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The reverse direction: turns a `catalog_submissions.proposed_data` row
|
||||
* back into domain-shaped fields, so a rejected/pending submission can be
|
||||
* reopened in the same form component that created it (see MySubmissionsModal)
|
||||
* instead of needing a bespoke "edit a raw JSON blob" UI. Safe because
|
||||
* proposed_data is always a *complete* row projection (every field of the
|
||||
* type, per the `*ToRow` functions above), never a partial patch.
|
||||
*/
|
||||
|
||||
export function rowToPortTypeFields(row: Record<string, unknown>): Omit<PortType, 'id' | 'custom'> {
|
||||
const compatibleFamilyIds = row.compatible_family_ids as string[] | undefined
|
||||
return {
|
||||
name: row.name as string,
|
||||
category: row.category as PortType['category'],
|
||||
family: row.family as string,
|
||||
compatibleFamilyIds: compatibleFamilyIds && compatibleFamilyIds.length > 0 ? compatibleFamilyIds : undefined,
|
||||
maxConnections: (row.max_connections as number | null) ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function rowToCableTypeFields(row: Record<string, unknown>): Omit<CableType, 'id' | 'custom'> {
|
||||
return {
|
||||
name: row.name as string,
|
||||
family: row.family as string,
|
||||
family2: (row.family2 as string | null) ?? undefined,
|
||||
unit: row.unit as CableType['unit'],
|
||||
costPerUnit: (row.cost_per_unit as number | null) ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function rowToDeviceTemplateFields(row: Record<string, unknown>): Omit<DeviceTemplate, 'id' | 'custom'> {
|
||||
const ports = (row.ports as Array<{ name: string; direction: DeviceTemplate['ports'][number]['direction']; port_type_id: string }>) ?? []
|
||||
return {
|
||||
name: row.name as string,
|
||||
category: row.category_id as string,
|
||||
manufacturer: (row.manufacturer as string | null) ?? undefined,
|
||||
model: (row.model as string | null) ?? undefined,
|
||||
cost: (row.cost as number | null) ?? undefined,
|
||||
ports: ports.map((port, index) => ({
|
||||
id: `draft-port-${index}`,
|
||||
name: port.name,
|
||||
direction: port.direction,
|
||||
portTypeId: port.port_type_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function rowToDeviceCategoryName(row: Record<string, unknown>): string {
|
||||
return row.name as string
|
||||
}
|
||||
Reference in New Issue
Block a user