import { useState } from 'react' import { cableTypeToRow, deviceCategoryToRow, deviceTemplateToRow, portTypeToRow, rowToCableTypeFields, rowToDeviceTemplateFields, rowToPortTypeFields, } from '../../data/catalogRowMapping' import type { CatalogSubmission } from '../../data/SubmissionRepository' import { allPortTypes } from '../../domain/project' import type { CableType, Catalog, DeviceTemplate, PortType } from '../../domain/types' import { useCatalogStore } from '../../state/catalogStore' import { useSubmissionStore } from '../../state/submissionStore' import { resolveCableTypeFields } from '../palette/cableTypeFormFields' import { CableTypeForm } from '../palette/CableTypeManager' import DeviceTemplateEditor from '../palette/DeviceTemplateEditor' import { PortTypeForm } from '../palette/PortTypeManager' import Modal from '../common/Modal' const ENTITY_TYPE_LABELS: Record = { device_template: 'Device', port_type: 'Port type', cable_type: 'Cable type', device_category: 'Category', } /** The submission's entity, as it currently exists in the catalog (public * or private), if it still exists at all. */ function findEntity(catalog: Catalog, submission: CatalogSubmission) { switch (submission.entityType) { case 'port_type': return catalog.portTypes.find((pt) => pt.id === submission.entityId) case 'cable_type': return catalog.cableTypes.find((ct) => ct.id === submission.entityId) case 'device_template': return catalog.deviceTemplates.find((dt) => dt.id === submission.entityId) case 'device_category': return catalog.deviceCategories.find((c) => c.id === submission.entityId) } } /** Best-effort display name: the entity's current live name if it still * exists in the catalog, else whatever name was proposed (it always will * exist in this app's flow — entity_id always points at a real row — but * falling back is cheap insurance). */ function entityDisplayName(catalog: Catalog, submission: CatalogSubmission): string { const proposedName = typeof submission.proposedData.name === 'string' ? submission.proposedData.name : undefined return findEntity(catalog, submission)?.name ?? proposedName ?? submission.entityId } /** True when this submission is about an entry you still privately own — * i.e. "promote my own entry" rather than "suggest an edit to a public * entry you don't own". For that case the library entry (editable via the * normal palette/managers) is the single source of truth; catalogStore * keeps proposed_data synced to it automatically (see syncProposedData), * so there's no separate draft to edit here. */ function isOwnPrivateEntry(catalog: Catalog, submission: CatalogSubmission): boolean { return findEntity(catalog, submission)?.custom === true } /** Row-shaped snapshot of the live entity, for the "Resubmit" action on an * own-private-entry submission — re-confirms the (already-synced) * proposed_data and flips status back to pending in one explicit click. */ function liveEntityRow(catalog: Catalog, submission: CatalogSubmission): Record | undefined { const entity = findEntity(catalog, submission) if (!entity) return undefined switch (submission.entityType) { case 'port_type': return portTypeToRow(entity as PortType) case 'cable_type': return cableTypeToRow(entity as CableType) case 'device_template': return deviceTemplateToRow(entity as DeviceTemplate) case 'device_category': return deviceCategoryToRow(entity.name) } } function statusBadge(submission: CatalogSubmission) { switch (submission.status) { case 'pending': return Pending case 'approved': return Approved case 'rejected': return Rejected } } function formatDate(iso: string): string { return new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }) } export default function MySubmissionsModal({ onClose }: { onClose: () => void }) { const catalog = useCatalogStore((s) => s.catalog) const updateCustomPortType = useCatalogStore((s) => s.updateCustomPortType) const updateCustomCableType = useCatalogStore((s) => s.updateCustomCableType) const mySubmissions = useSubmissionStore((s) => s.mySubmissions) const withdraw = useSubmissionStore((s) => s.withdraw) const resubmit = useSubmissionStore((s) => s.resubmit) const [editingId, setEditingId] = useState(null) const [categoryDraft, setCategoryDraft] = useState('') const allPortTypesList = allPortTypes(catalog) const handleWithdraw = (submission: CatalogSubmission) => { const confirmed = window.confirm(`Withdraw this submission for "${entityDisplayName(catalog, submission)}"?`) if (confirmed) withdraw(submission.id) } const startEditing = (submission: CatalogSubmission) => { setCategoryDraft(typeof submission.proposedData.name === 'string' ? submission.proposedData.name : '') setEditingId(submission.id) } return (
{mySubmissions.length === 0 && (

No submissions yet — use "Submit" or "Suggest edit" on a catalog entry to propose it for the public catalog.

)} {mySubmissions.map((submission) => { const isEditing = editingId === submission.id const ownEntry = isOwnPrivateEntry(catalog, submission) // For an own-private-entry submission, the library entry is the // only place to edit — this "revise" form only ever applies to // suggesting an edit to a public entry you don't own. const canRevise = !ownEntry && (submission.status === 'pending' || submission.status === 'rejected') // Own-private-entry editing: the exact same form + save action as // the device library's own "Edit" button (direct catalogStore // update, not a submission-specific save path) — catalogStore's // update actions already keep this submission's proposed_data in // sync afterward (see syncProposedData), so nothing extra is // needed here beyond editing the live entry itself. if (isEditing && ownEntry && submission.entityType === 'port_type') { const entity = findEntity(catalog, submission) as PortType return ( setEditingId(null)} onSave={(fields) => { updateCustomPortType(submission.entityId, fields) setEditingId(null) }} /> ) } if (isEditing && ownEntry && submission.entityType === 'cable_type') { const entity = findEntity(catalog, submission) as CableType return ( setEditingId(null)} onSave={(fields) => { const resolved = resolveCableTypeFields(fields, allPortTypesList) if (!resolved) return updateCustomCableType(submission.entityId, resolved) setEditingId(null) }} /> ) } if (isEditing && ownEntry && submission.entityType === 'device_template') { const entity = findEntity(catalog, submission) as DeviceTemplate return setEditingId(null)} /> } // Suggest-an-edit-to-a-public-entry editing: a separate draft // (proposed_data) since there's no live private row to defer to — // saving here revises the submission itself. if (isEditing && submission.entityType === 'port_type') { return ( setEditingId(null)} onSave={(fields) => { resubmit(submission.id, portTypeToRow(fields)) setEditingId(null) }} /> ) } if (isEditing && submission.entityType === 'cable_type') { return ( setEditingId(null)} onSave={(fields) => { const resolved = resolveCableTypeFields(fields, allPortTypesList) if (!resolved) return resubmit(submission.id, cableTypeToRow(resolved)) setEditingId(null) }} /> ) } if (isEditing && submission.entityType === 'device_template') { return ( setEditingId(null)} onSaved={() => setEditingId(null)} /> ) } if (isEditing && submission.entityType === 'device_category') { return (
setCategoryDraft(e.target.value)} className="w-0 min-w-0 flex-1 rounded border border-slate-300 px-1.5 py-1 text-xs" />
) } return (
{entityDisplayName(catalog, submission)} {statusBadge(submission)}
{ENTITY_TYPE_LABELS[submission.entityType]} · submitted {formatDate(submission.createdAt)}
{ownEntry && (submission.status === 'pending' || submission.status === 'rejected') && ( )} {ownEntry && submission.status === 'rejected' && ( )} {canRevise && ( )} {submission.status === 'pending' && ( )}
{ownEntry && (submission.status === 'pending' || submission.status === 'rejected') && (

Tracks your library entry — editing it here (or from the palette) keeps this submission up to date automatically.

)} {submission.status === 'rejected' && submission.reviewReason && (

Reason: {submission.reviewReason}

)}
) })}
) }