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' interface DraftPort { key: string name: string direction: PortDirection portTypeId: string } let draftKeySeq = 0 function nextDraftKey() { draftKeySeq += 1 return `draft-${draftKeySeq}` } export default function DeviceTemplateEditor({ template, submissionMode = false, resubmitId, onClose, onSaved, }: { /** 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. */ onSaved?: (categoryId: string) => void }) { const catalog = useCatalogStore((s) => s.catalog) 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)), [catalog], ) const [name, setName] = useState(template?.name ?? '') const [category, setCategory] = useState(template?.category ?? 'other') const [manufacturer, setManufacturer] = useState(template?.manufacturer ?? '') const [model, setModel] = useState(template?.model ?? '') const [cost, setCost] = useState(template?.cost?.toString() ?? '') const [ports, setPorts] = useState( () => template?.ports.map((p) => ({ key: p.id, name: p.name, direction: p.direction, portTypeId: p.portTypeId })) ?? [ { key: nextDraftKey(), name: 'Port 1', direction: 'output', portTypeId: portTypes[0]?.id ?? '' }, ], ) const [newPortTypeOpen, setNewPortTypeOpen] = useState(false) const [newPortTypeName, setNewPortTypeName] = useState('') const [newPortTypeFamily, setNewPortTypeFamily] = useState('') const [newPortTypeCategory, setNewPortTypeCategory] = useState<(typeof PORT_CATEGORIES)[number]['value']>('other') const [alsoCompatibleWith, setAlsoCompatibleWith] = useState>(new Set()) const [focusKey, setFocusKey] = useState(null) const toggleAlsoCompatible = (portTypeId: string) => { setAlsoCompatibleWith((prev) => { const next = new Set(prev) if (next.has(portTypeId)) next.delete(portTypeId) else next.add(portTypeId) return next }) } const updatePort = (key: string, patch: Partial) => { setPorts((prev) => prev.map((p) => (p.key === key ? { ...p, ...patch } : p))) } const addPortRow = () => { const key = nextDraftKey() setPorts((prev) => [ ...prev, { key, name: `Port ${prev.length + 1}`, direction: 'output', portTypeId: portTypes[0]?.id ?? '' }, ]) setFocusKey(key) } const clonePortRow = (sourceKey: string) => { const key = nextDraftKey() setPorts((prev) => { const index = prev.findIndex((p) => p.key === sourceKey) if (index === -1) return prev const source = prev[index] const clone: DraftPort = { ...source, key, name: incrementPortName(source.name) } return [...prev.slice(0, index + 1), clone, ...prev.slice(index + 1)] }) setFocusKey(key) } const removePortRow = (key: string) => { setPorts((prev) => prev.filter((p) => p.key !== key)) } const handleCreatePortType = () => { if (!newPortTypeName.trim()) return const compatibleFamilyIds = expandCompatibleFamilyIds([...alsoCompatibleWith], portTypes) addCustomPortType({ name: newPortTypeName.trim(), category: newPortTypeCategory, family: (newPortTypeFamily.trim() || newPortTypeName.trim()).toLowerCase().replace(/\s+/g, '-'), ...(compatibleFamilyIds.length > 0 ? { compatibleFamilyIds } : {}), }) setNewPortTypeName('') setNewPortTypeFamily('') setAlsoCompatibleWith(new Set()) setNewPortTypeOpen(false) } const canSave = name.trim().length > 0 && ports.length > 0 && ports.every((p) => p.portTypeId) const handleSave = () => { if (!canSave) return const fields = { name: name.trim(), category, manufacturer: manufacturer.trim() || undefined, model: model.trim() || undefined, 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 (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) } onSaved?.(category) onClose() } return (

Ports

{ports.map((port) => (
{ if (el && focusKey === port.key) { el.focus() setFocusKey(null) } }} value={port.name} onChange={(e) => updatePort(port.key, { name: e.target.value })} className="w-28 min-w-0 flex-1 rounded border border-slate-300 px-1.5 py-1 text-xs" placeholder="Port name" />
))} {ports.length === 0 &&

No ports yet.

}
{!newPortTypeOpen ? ( ) : (
setNewPortTypeName(e.target.value)} placeholder="Connector name (e.g. Zigbee)" className="flex-1 rounded border border-slate-300 px-1.5 py-1 text-xs" />
setNewPortTypeFamily(e.target.value)} placeholder="Compatibility family (optional, defaults to name)" className="w-full rounded border border-slate-300 px-1.5 py-1 text-xs" />

Two ports can be wired together only if their connector type shares the same family.

Also compatible with (for jacks that accept more than one kind — e.g. a patch bay that's balanced or unbalanced):

{portTypes.map((pt) => ( ))}
)}
) }