Normalizes manufacturer as a shared catalog entity (like device categories) instead of free text on each device, giving the admin duplicate-detection nudge a reliable signal. Adds full CRUD (including Admin direct-publish, bypassing the submission queue) for categories, manufacturers, port types, and cable types, plus a Categories & Manufacturers library modal and a browse-by-manufacturer/search view in the device palette. Adds Port.builtInCable so a captive/permanently-attached cable (a keyboard's USB lead, a budget AVR's power cord) can be flagged and excluded from the BOM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
import type { CatalogEntityType } from './SubmissionRepository'
|
|
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.
|
|
*/
|
|
|
|
/** Which table backs each catalog entity type — shared by
|
|
* SupabaseAdminSubmissionRepository (approving into the right table) and
|
|
* SupabaseCatalogRepository (direct Admin unpublish). */
|
|
export const CATALOG_TABLE_BY_ENTITY_TYPE: Record<CatalogEntityType, string> = {
|
|
device_template: 'device_templates',
|
|
port_type: 'port_types',
|
|
cable_type: 'cable_types',
|
|
device_category: 'device_categories',
|
|
manufacturer: 'manufacturers',
|
|
}
|
|
|
|
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 manufacturerToRow(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_id: template.manufacturerId ?? 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,
|
|
built_in_cable: port.builtInCable ?? false,
|
|
})),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
built_in_cable?: boolean
|
|
}>) ?? []
|
|
return {
|
|
name: row.name as string,
|
|
category: row.category_id as string,
|
|
manufacturerId: (row.manufacturer_id 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,
|
|
builtInCable: port.built_in_cable ?? false,
|
|
})),
|
|
}
|
|
}
|
|
|
|
export function rowToDeviceCategoryName(row: Record<string, unknown>): string {
|
|
return row.name as string
|
|
}
|