Wire the app to the shared Supabase catalog (public catalog read integration)
Moves port/cable/category/device-template data from per-diagram embedded storage to the global public/private catalog backed by Supabase, per organized-ideas.md's "live reference, not snapshot" decision. - domain/types.ts, project.ts, compatibility.ts, bom.ts: lookup functions now take an explicit Catalog parameter instead of deriving data from Project — Project is reduced to just diagram-scoped fields. - New CatalogRepository/SupabaseCatalogRepository (mirrors the DiagramRepository pattern) and catalogStore.ts, replacing the catalog-related actions that used to live in projectStore. - device_templates gets a plain-text manufacturer column for now (the normalized manufacturer catalog from organized-ideas.md §3 is its own future pass, not blocking this one). - domain/library.ts is no longer imported by the app — it's now only the source scripts/generate-seed.mjs reads to produce supabase/seed.sql. - Swept every UI call site via tsc -b until clean; oxlint clean; 23/23 pgTAP RLS tests still passing after a `supabase db reset`.
This commit is contained in:
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { allCableTypes } from './project'
|
||||
import type { CableType, Connection, Device, Project } from './types'
|
||||
import type { CableType, Catalog, Connection, Device, Project } from './types'
|
||||
|
||||
export interface BomCableLine {
|
||||
cableTypeId: string
|
||||
@@ -66,8 +66,8 @@ function connectionCost(connection: Connection, cableType: CableType | undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function computeBom(project: Project): Bom {
|
||||
const cableTypes = new Map(allCableTypes(project).map((ct) => [ct.id, ct]))
|
||||
export function computeBom(project: Project, catalog: Catalog): Bom {
|
||||
const cableTypes = new Map(allCableTypes(catalog).map((ct) => [ct.id, ct]))
|
||||
|
||||
const cableLines = new Map<string, BomCableLine>()
|
||||
for (const connection of project.connections) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { acceptedFamilies, getPortType, hasBridgingCableType } from './project'
|
||||
import type { Connection, Device, PortType, Project } from './types'
|
||||
import type { Catalog, Connection, Device, PortType, Project } from './types'
|
||||
|
||||
/**
|
||||
* Two ports connect if the sets of families they each accept (their own
|
||||
@@ -37,6 +37,7 @@ export interface ConnectionValidation {
|
||||
*/
|
||||
export function validateConnection(
|
||||
project: Project,
|
||||
catalog: Catalog,
|
||||
candidate: ConnectionCandidate,
|
||||
existingConnections: Connection[] = project.connections,
|
||||
): ConnectionValidation {
|
||||
@@ -62,15 +63,15 @@ export function validateConnection(
|
||||
return { valid: false, reason: 'Cannot connect two output ports.' }
|
||||
}
|
||||
|
||||
const sourcePortType = getPortType(project, sourcePort.portTypeId)
|
||||
const targetPortType = getPortType(project, targetPort.portTypeId)
|
||||
const sourcePortType = getPortType(catalog, sourcePort.portTypeId)
|
||||
const targetPortType = getPortType(catalog, targetPort.portTypeId)
|
||||
if (!sourcePortType || !targetPortType) {
|
||||
return { valid: false, reason: 'Unknown port type.' }
|
||||
}
|
||||
// Directly compatible port types, or bridgeable by a specific adapter
|
||||
// cable (different connectors on each end, e.g. XLR-to-TRS) even when the
|
||||
// port types themselves declare no general compatibility.
|
||||
if (!portTypesCompatible(sourcePortType, targetPortType) && !hasBridgingCableType(project, sourcePortType, targetPortType)) {
|
||||
if (!portTypesCompatible(sourcePortType, targetPortType) && !hasBridgingCableType(catalog, sourcePortType, targetPortType)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `${sourcePortType.name} is not compatible with ${targetPortType.name}.`,
|
||||
|
||||
+5
-12
@@ -1,9 +1,11 @@
|
||||
import type { CableType, DeviceCategoryDef, DeviceTemplate, PortType } from './types'
|
||||
|
||||
/**
|
||||
* Built-in connector/cable/device libraries. These ship read-only
|
||||
* (`builtIn`/`custom: false`-ish via omission) and are merged with each
|
||||
* project's custom entries at lookup time — see `domain/project.ts`.
|
||||
* Built-in connector/cable/device libraries — the source of truth for the
|
||||
* public catalog's seed data. Not imported by the app itself anymore (the
|
||||
* catalog is fetched from Supabase at runtime, see state/catalogStore.ts);
|
||||
* this file is read only by scripts/generate-seed.mjs, which turns it into
|
||||
* supabase/seed.sql. Re-run that script after editing anything here.
|
||||
*/
|
||||
|
||||
export const BUILT_IN_DEVICE_CATEGORIES: DeviceCategoryDef[] = [
|
||||
@@ -162,7 +164,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-display',
|
||||
name: 'Display (TV / Projector)',
|
||||
category: 'display',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'HDMI In 1', direction: 'input', portTypeId: 'pt-hdmi' },
|
||||
{ id: 'p2', name: 'HDMI In 2', direction: 'input', portTypeId: 'pt-hdmi' },
|
||||
@@ -173,7 +174,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-media-player',
|
||||
name: 'Media Player / Streamer',
|
||||
category: 'video-source',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'HDMI Out', direction: 'output', portTypeId: 'pt-hdmi' },
|
||||
{ id: 'p2', name: 'Power In', direction: 'input', portTypeId: 'pt-nema-power' },
|
||||
@@ -183,7 +183,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-avr',
|
||||
name: 'AV Receiver',
|
||||
category: 'amplifier',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'HDMI In 1', direction: 'input', portTypeId: 'pt-hdmi' },
|
||||
{ id: 'p2', name: 'HDMI In 2', direction: 'input', portTypeId: 'pt-hdmi' },
|
||||
@@ -198,14 +197,12 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-speaker',
|
||||
name: 'Speaker (Passive)',
|
||||
category: 'speaker',
|
||||
builtIn: true,
|
||||
ports: [{ id: 'p1', name: 'Speaker In', direction: 'input', portTypeId: 'pt-speaker-wire' }],
|
||||
},
|
||||
{
|
||||
id: 'dt-switch',
|
||||
name: 'Network Switch',
|
||||
category: 'network',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'Port 1', direction: 'bidirectional', portTypeId: 'pt-cat-rj45' },
|
||||
{ id: 'p2', name: 'Port 2', direction: 'bidirectional', portTypeId: 'pt-cat-rj45' },
|
||||
@@ -218,7 +215,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-camera',
|
||||
name: 'PTZ Camera',
|
||||
category: 'camera',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'SDI Out', direction: 'output', portTypeId: 'pt-sdi' },
|
||||
{ id: 'p2', name: 'Control (RJ45)', direction: 'bidirectional', portTypeId: 'pt-cat-rj45' },
|
||||
@@ -229,7 +225,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-computer',
|
||||
name: 'Computer / Laptop',
|
||||
category: 'computer',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: 'HDMI Out', direction: 'output', portTypeId: 'pt-hdmi' },
|
||||
{ id: 'p2', name: 'USB-C', direction: 'bidirectional', portTypeId: 'pt-usb-c' },
|
||||
@@ -245,7 +240,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-adapter-midi-trs-a-to-din',
|
||||
name: '3.5mm-to-DIN MIDI Adapter (Type A)',
|
||||
category: 'adapter',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: '3.5mm In (from MIDI Out)', direction: 'input', portTypeId: 'pt-midi-trs-a' },
|
||||
{ id: 'p2', name: 'DIN Out (to MIDI In)', direction: 'output', portTypeId: 'pt-midi-din' },
|
||||
@@ -255,7 +249,6 @@ export const BUILT_IN_DEVICE_TEMPLATES: DeviceTemplate[] = [
|
||||
id: 'dt-adapter-midi-trs-b-to-din',
|
||||
name: '3.5mm-to-DIN MIDI Adapter (Type B)',
|
||||
category: 'adapter',
|
||||
builtIn: true,
|
||||
ports: [
|
||||
{ id: 'p1', name: '3.5mm In (from MIDI Out)', direction: 'input', portTypeId: 'pt-midi-trs-b' },
|
||||
{ id: 'p2', name: 'DIN Out (to MIDI In)', direction: 'output', portTypeId: 'pt-midi-din' },
|
||||
|
||||
+30
-63
@@ -1,58 +1,46 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { BUILT_IN_CABLE_TYPES, BUILT_IN_DEVICE_CATEGORIES, BUILT_IN_DEVICE_TEMPLATES, BUILT_IN_PORT_TYPES } from './library'
|
||||
import type { CableType, Device, DeviceCategoryDef, DeviceTemplate, Port, PortType, Project } from './types'
|
||||
import type { CableType, Catalog, Device, DeviceCategoryDef, DeviceTemplate, PortType, Project } from './types'
|
||||
|
||||
/** Merges built-in + project-custom entries. Custom entries with a matching
|
||||
* id win, so a project could in principle override a built-in (not exposed
|
||||
* in the UI today, but keeps the merge rule simple and predictable). */
|
||||
function mergeById<T extends { id: string }>(builtIns: T[], custom: T[]): T[] {
|
||||
const byId = new Map(builtIns.map((item) => [item.id, item]))
|
||||
for (const item of custom) byId.set(item.id, item)
|
||||
return [...byId.values()]
|
||||
export function allPortTypes(catalog: Catalog): PortType[] {
|
||||
return catalog.portTypes
|
||||
}
|
||||
|
||||
export function allPortTypes(project: Project): PortType[] {
|
||||
return mergeById(BUILT_IN_PORT_TYPES, project.customPortTypes)
|
||||
export function allCableTypes(catalog: Catalog): CableType[] {
|
||||
return catalog.cableTypes
|
||||
}
|
||||
|
||||
export function allCableTypes(project: Project): CableType[] {
|
||||
return mergeById(BUILT_IN_CABLE_TYPES, project.customCableTypes)
|
||||
/** `hiddenPublicIds` is a per-user UI preference (see state/catalogStore.ts),
|
||||
* not part of the catalog data itself — you can hide any public template
|
||||
* from your own palette without affecting anyone else or the shared entry. */
|
||||
export function allDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet<string> = new Set()): DeviceTemplate[] {
|
||||
return catalog.deviceTemplates.filter((t) => t.custom || !hiddenPublicIds.has(t.id))
|
||||
}
|
||||
|
||||
export function allDeviceTemplates(project: Project): DeviceTemplate[] {
|
||||
const merged = mergeById(BUILT_IN_DEVICE_TEMPLATES, project.customDeviceTemplates)
|
||||
const hidden = new Set(project.hiddenBuiltInDeviceTemplateIds)
|
||||
return merged.filter((t) => !(t.builtIn && hidden.has(t.id)))
|
||||
/** Public templates the user has hidden, for a "restore" list. */
|
||||
export function hiddenDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet<string>): DeviceTemplate[] {
|
||||
return catalog.deviceTemplates.filter((t) => !t.custom && hiddenPublicIds.has(t.id))
|
||||
}
|
||||
|
||||
/** Built-in templates the user has hidden, for a "restore" list — deleting a
|
||||
* custom template is permanent, but hiding a built-in one isn't, since the
|
||||
* built-in still exists in code and can always be shown again. */
|
||||
export function hiddenBuiltInDeviceTemplates(project: Project): DeviceTemplate[] {
|
||||
const hidden = new Set(project.hiddenBuiltInDeviceTemplateIds)
|
||||
return BUILT_IN_DEVICE_TEMPLATES.filter((t) => hidden.has(t.id))
|
||||
export function allDeviceCategories(catalog: Catalog): DeviceCategoryDef[] {
|
||||
return catalog.deviceCategories
|
||||
}
|
||||
|
||||
export function allDeviceCategories(project: Project): DeviceCategoryDef[] {
|
||||
return mergeById(BUILT_IN_DEVICE_CATEGORIES, project.customDeviceCategories)
|
||||
}
|
||||
|
||||
export function getDeviceCategory(project: Project, categoryId: string): DeviceCategoryDef | undefined {
|
||||
return allDeviceCategories(project).find((c) => c.id === categoryId)
|
||||
export function getDeviceCategory(catalog: Catalog, categoryId: string): DeviceCategoryDef | undefined {
|
||||
return allDeviceCategories(catalog).find((c) => c.id === categoryId)
|
||||
}
|
||||
|
||||
/** Friendly label for a category id, falling back to the raw id if it's
|
||||
* somehow unknown (e.g. data from an older export) rather than crashing. */
|
||||
export function deviceCategoryName(project: Project, categoryId: string): string {
|
||||
return getDeviceCategory(project, categoryId)?.name ?? categoryId
|
||||
export function deviceCategoryName(catalog: Catalog, categoryId: string): string {
|
||||
return getDeviceCategory(catalog, categoryId)?.name ?? categoryId
|
||||
}
|
||||
|
||||
export function getPortType(project: Project, portTypeId: string): PortType | undefined {
|
||||
return allPortTypes(project).find((pt) => pt.id === portTypeId)
|
||||
export function getPortType(catalog: Catalog, portTypeId: string): PortType | undefined {
|
||||
return allPortTypes(catalog).find((pt) => pt.id === portTypeId)
|
||||
}
|
||||
|
||||
export function getCableType(project: Project, cableTypeId: string): CableType | undefined {
|
||||
return allCableTypes(project).find((ct) => ct.id === cableTypeId)
|
||||
export function getCableType(catalog: Catalog, cableTypeId: string): CableType | undefined {
|
||||
return allCableTypes(catalog).find((ct) => ct.id === cableTypeId)
|
||||
}
|
||||
|
||||
/** The set of connector families a port type will mate with: its own family
|
||||
@@ -82,9 +70,9 @@ function cableFits(cableType: CableType, sourceFamilies: Set<string>, targetFami
|
||||
/** Cable types usable with the given port type at (at least) one end — for
|
||||
* a plain cable that means its family matches; for an adapter cable it's
|
||||
* enough that either end matches. */
|
||||
export function cableTypesForPortType(project: Project, portType: PortType): CableType[] {
|
||||
export function cableTypesForPortType(catalog: Catalog, portType: PortType): CableType[] {
|
||||
const families = acceptedFamilies(portType)
|
||||
return allCableTypes(project).filter((ct) => {
|
||||
return allCableTypes(catalog).filter((ct) => {
|
||||
const [endA, endB] = cableEnds(ct)
|
||||
return families.has(endA) || families.has(endB)
|
||||
})
|
||||
@@ -102,21 +90,17 @@ export function cableTypesForPortType(project: Project, portType: PortType): Cab
|
||||
* aren't declared generally compatible still be wired with the right cable,
|
||||
* without changing either port type's own compatibility rules.
|
||||
*/
|
||||
export function cableTypesForConnection(
|
||||
project: Project,
|
||||
sourcePortType: PortType,
|
||||
targetPortType: PortType,
|
||||
): CableType[] {
|
||||
export function cableTypesForConnection(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): CableType[] {
|
||||
const sourceFamilies = acceptedFamilies(sourcePortType)
|
||||
const targetFamilies = acceptedFamilies(targetPortType)
|
||||
return allCableTypes(project).filter((ct) => cableFits(ct, sourceFamilies, targetFamilies))
|
||||
return allCableTypes(catalog).filter((ct) => cableFits(ct, sourceFamilies, targetFamilies))
|
||||
}
|
||||
|
||||
/** Whether any cable type (built-in or custom) could bridge these two port
|
||||
* types — used to allow a connection even when the port types aren't
|
||||
* declared directly compatible, as long as a specific adapter cable exists. */
|
||||
export function hasBridgingCableType(project: Project, sourcePortType: PortType, targetPortType: PortType): boolean {
|
||||
return cableTypesForConnection(project, sourcePortType, targetPortType).length > 0
|
||||
export function hasBridgingCableType(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): boolean {
|
||||
return cableTypesForConnection(catalog, sourcePortType, targetPortType).length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,20 +134,12 @@ export function createEmptyProject(name = 'Untitled Project'): Project {
|
||||
updatedAt: now,
|
||||
devices: [],
|
||||
connections: [],
|
||||
customDeviceTemplates: [],
|
||||
customPortTypes: [],
|
||||
customCableTypes: [],
|
||||
customDeviceCategories: [],
|
||||
hiddenBuiltInDeviceTemplateIds: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** Instantiates a device on the canvas from a template, deep-copying ports
|
||||
* (with fresh ids) so editing the instance never mutates the template. */
|
||||
export function createDeviceFromTemplate(
|
||||
template: DeviceTemplate,
|
||||
position: { x: number; y: number },
|
||||
): Device {
|
||||
export function createDeviceFromTemplate(template: DeviceTemplate, position: { x: number; y: number }): Device {
|
||||
return {
|
||||
id: uuid(),
|
||||
name: template.name,
|
||||
@@ -177,15 +153,6 @@ export function createDeviceFromTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankPort(overrides: Partial<Port> = {}): Port {
|
||||
return {
|
||||
id: uuid(),
|
||||
name: overrides.name ?? 'New Port',
|
||||
direction: overrides.direction ?? 'output',
|
||||
portTypeId: overrides.portTypeId ?? BUILT_IN_PORT_TYPES[0].id,
|
||||
}
|
||||
}
|
||||
|
||||
/** "CV Out 1" -> "CV Out 2", "Port 3" -> "Port 4", "Gate In" -> "Gate In 2".
|
||||
* Only increments a number that's the very last thing in the name (trailing
|
||||
* whitespace aside), so labels like `1/4" TS In` aren't misread as ending
|
||||
|
||||
+21
-10
@@ -66,7 +66,12 @@ export interface DeviceTemplate {
|
||||
/** Suggested default cost, copied onto each placed Device at creation time
|
||||
* (same as ports) and independently editable per instance afterward. */
|
||||
cost?: number
|
||||
builtIn?: boolean
|
||||
/** True for your own private template (is_public = false, owner = you).
|
||||
* Absent/false for anything public — whether an original seeded default
|
||||
* or a user submission that's since been approved — since both behave
|
||||
* the same way for you: not directly editable, only hideable from your
|
||||
* own palette (see catalogStore's hidePublicDeviceTemplate). */
|
||||
custom?: boolean
|
||||
}
|
||||
|
||||
/** A device placed on the canvas. Ports are copied from the template at
|
||||
@@ -136,13 +141,19 @@ export interface Project {
|
||||
updatedAt: string
|
||||
devices: Device[]
|
||||
connections: Connection[]
|
||||
/** User-defined additions to the built-in libraries, scoped to this project. */
|
||||
customDeviceTemplates: DeviceTemplate[]
|
||||
customPortTypes: PortType[]
|
||||
customCableTypes: CableType[]
|
||||
customDeviceCategories: DeviceCategoryDef[]
|
||||
/** Built-in device template ids the user has removed from their palette.
|
||||
* Hidden rather than deleted since built-ins are shared code, not
|
||||
* per-project data — this lets them be restored later. */
|
||||
hiddenBuiltInDeviceTemplateIds: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The full set of port types / cable types / device categories / device
|
||||
* templates visible to the current user — public entries plus their own
|
||||
* private ones, already merged (by Postgres RLS, at fetch time — see
|
||||
* state/catalogStore.ts), not scoped to any one diagram. Devices/ports/
|
||||
* connections reference these by id (a live reference, not a copy — see
|
||||
* organized-ideas.md §1/§3 for why that's the deliberate choice).
|
||||
*/
|
||||
export interface Catalog {
|
||||
portTypes: PortType[]
|
||||
cableTypes: CableType[]
|
||||
deviceCategories: DeviceCategoryDef[]
|
||||
deviceTemplates: DeviceTemplate[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user