From 26f41b3d4a3f9941d332f2c680a2a6fc2eba74db Mon Sep 17 00:00:00 2001 From: aarbit Date: Fri, 11 Sep 2026 12:10:11 -0500 Subject: [PATCH] Rename Project to Diagram throughout the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per organized-ideas.md §8: the storage layer (DiagramRepository etc.) was already renamed in an earlier phase; this finishes it everywhere else. - domain/types.ts: Project -> Diagram. domain/project.ts -> domain/diagram.ts (createEmptyProject -> createEmptyDiagram, default name "Untitled Diagram"). - domain/compatibility.ts, domain/bom.ts: Project param/type -> Diagram. - data/exportImport.ts: ProjectImportError -> DiagramImportError, projectToJson/downloadProjectFile/readProjectFile/normalizeProject -> their Diagram equivalents. - state/projectStore.ts -> state/diagramStore.ts: useProjectStore -> useDiagramStore, the `project` field -> `diagram`, newProject/renameProject/ importProject/applyRestoredProject -> *Diagram, restoredProjectUpdatedAt -> restoredDiagramUpdatedAt. - Every component updated to match, compiler-guided (tsc -b enumerated each remaining call site after the core rename, the same approach used for the earlier catalog-parameter refactor). - README updated for the terminology, and to match the repository class names (which had already been renamed but the README hadn't caught up). No backend/schema changes: the JSON shape stored in diagrams.data never changed, only TypeScript-side identifiers, so existing diagrams are unaffected. One SQL comment fixed for accuracy (no migration needed). Verified: tsc -b and oxlint clean; grepped src/ for any remaining Project/project reference (none) after the sweep. --- README.md | 27 +-- src/components/bom/BomPanel.tsx | 6 +- src/components/canvas/FlowCanvas.tsx | 46 ++--- src/components/common/CategorySelect.tsx | 2 +- .../inspector/ConnectionInspector.tsx | 18 +- src/components/inspector/DeviceInspector.tsx | 24 +-- src/components/layout/AppShell.tsx | 6 +- .../layout/ConnectionErrorBanner.tsx | 6 +- src/components/layout/DiagramManagerModal.tsx | 20 +-- src/components/layout/RightPanel.tsx | 6 +- src/components/layout/TopBar.tsx | 24 +-- src/components/layout/ViewOnlyBanner.tsx | 4 +- src/components/palette/CableTypeManager.tsx | 2 +- src/components/palette/DevicePalette.tsx | 12 +- .../palette/DeviceTemplateEditor.tsx | 2 +- src/components/palette/PortTypeManager.tsx | 2 +- .../sharing/DiagramSharingModal.tsx | 10 +- .../sharing/VersionHistoryModal.tsx | 10 +- .../submissions/MySubmissionsModal.tsx | 2 +- src/data/DiagramRepository.ts | 24 +-- src/data/DiagramSnapshotRepository.ts | 6 +- src/data/LocalStorageDiagramRepository.ts | 23 +-- src/data/SupabaseDiagramRepository.ts | 24 +-- src/data/SupabaseDiagramSnapshotRepository.ts | 8 +- src/data/exportImport.ts | 44 ++--- src/domain/bom.ts | 10 +- src/domain/compatibility.ts | 12 +- src/domain/constants.ts | 4 +- src/domain/{project.ts => diagram.ts} | 4 +- src/domain/types.ts | 7 +- src/state/catalogStore.ts | 2 +- src/state/diagramSnapshotStore.ts | 18 +- .../{projectStore.ts => diagramStore.ts} | 168 +++++++++--------- .../20260910010000_admin_review_queue.sql | 2 +- 34 files changed, 292 insertions(+), 293 deletions(-) rename src/domain/{project.ts => diagram.ts} (97%) rename src/state/{projectStore.ts => diagramStore.ts} (75%) diff --git a/README.md b/README.md index 3ff36d9..5270f2f 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ src/ domain/ Framework-agnostic types + logic (compatibility rules, BOM math, the built-in connector/cable/device library). No React, no React Flow — safe to unit test or reuse from a future backend. - data/ Storage abstraction. `ProjectRepository` is the interface the - rest of the app codes against; `LocalStorageProjectRepository` - is the only implementation today. Swapping in a real backend - later (REST/GraphQL) means adding one new implementation of - that interface, not touching state/UI code. - state/ Zustand store (`projectStore`) — the single source of truth for - the current project. Auto-saves to the repository (debounced) + data/ Storage abstraction. `DiagramRepository` is the interface the + rest of the app codes against; `SupabaseDiagramRepository` is + the active implementation, with `LocalStorageDiagramRepository` + kept as a reference/fallback. Swapping storage means adding + one new implementation of that interface, not touching + state/UI code. + state/ Zustand store (`diagramStore`) — the single source of truth for + the current diagram. Auto-saves to the repository (debounced) on every change. components/ canvas/ React Flow wiring surface: custom device node, custom cable @@ -40,17 +41,19 @@ src/ - **PortType** — a connector kind (HDMI, XLR, Cat5e/6, ...). Two ports can be wired together only if their PortTypes share a compatibility `family`. - **DeviceTemplate** — a reusable device shape (its ports), shown in the - palette. Built-ins live in `domain/library.ts`; custom ones are saved on the - project. + palette. Built-ins live in `domain/library.ts` and seed the public catalog; + custom ones are your own private catalog entries (or submitted for public + review). - **Device** — an instance placed on the canvas, with its own copy of ports (editing an instance never mutates its template). - **CableType** — a physical cable spec, scoped to a PortType family. - **Connection** — a wire between two ports, referencing a CableType and an optional user-entered distance (there's no floor plan / scale model, so length is a manual estimate per connection). -- **Project** — devices + connections + any custom library entries. This is - the one object persisted (auto-saved to `localStorage`, or explicitly - exported/imported as JSON). +- **Diagram** — devices + connections. This is the one object persisted + (auto-saved to Supabase, or explicitly exported/imported as JSON); the + catalog (PortType/CableType/DeviceTemplate/category) is shared and global, + not part of any one diagram. ## Development diff --git a/src/components/bom/BomPanel.tsx b/src/components/bom/BomPanel.tsx index bd2574d..8d97968 100644 --- a/src/components/bom/BomPanel.tsx +++ b/src/components/bom/BomPanel.tsx @@ -1,7 +1,7 @@ import { Fragment, useMemo, useState } from 'react' import { computeBom } from '../../domain/bom' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Chevron from '../common/Chevron' function formatCost(value: number): string { @@ -9,9 +9,9 @@ function formatCost(value: number): string { } export default function BomPanel() { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const bom = useMemo(() => computeBom(project, catalog), [project, catalog]) + const bom = useMemo(() => computeBom(diagram, catalog), [diagram, catalog]) // Collapsed by default, per cable type — most diagrams only need the // per-type totals; the length breakdown is there when you're ready to // actually go buy cables. diff --git a/src/components/canvas/FlowCanvas.tsx b/src/components/canvas/FlowCanvas.tsx index 6732138..5883508 100644 --- a/src/components/canvas/FlowCanvas.tsx +++ b/src/components/canvas/FlowCanvas.tsx @@ -15,9 +15,9 @@ import { import '@xyflow/react/dist/style.css' import { useCallback, useMemo } from 'react' import { validateConnection } from '../../domain/compatibility' -import { allCableTypes, allPortTypes, cableTypesForConnection, deviceCategoryName, getPortType } from '../../domain/project' +import { allCableTypes, allPortTypes, cableTypesForConnection, deviceCategoryName, getPortType } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import CableEdge, { type CableFlowEdge } from './CableEdge' import DeviceNode, { type DeviceFlowNode, type PortInfo } from './DeviceNode' import { buildHandleId, parsePortId } from './handleIds' @@ -28,19 +28,19 @@ const nodeTypes: NodeTypes = { device: DeviceNode } const edgeTypes: EdgeTypes = { cable: CableEdge } function FlowCanvasInner() { - const project = useProjectStore((s) => s.project) - const selectedDeviceId = useProjectStore((s) => s.selectedDeviceId) - const selectedConnectionId = useProjectStore((s) => s.selectedConnectionId) - const updateDevicePosition = useProjectStore((s) => s.updateDevicePosition) - const removeDevice = useProjectStore((s) => s.removeDevice) - const removeConnection = useProjectStore((s) => s.removeConnection) - const addConnection = useProjectStore((s) => s.addConnection) - const addDeviceFromTemplate = useProjectStore((s) => s.addDeviceFromTemplate) - const selectDevice = useProjectStore((s) => s.selectDevice) - const selectConnection = useProjectStore((s) => s.selectConnection) + const diagram = useDiagramStore((s) => s.diagram) + const selectedDeviceId = useDiagramStore((s) => s.selectedDeviceId) + const selectedConnectionId = useDiagramStore((s) => s.selectedConnectionId) + const updateDevicePosition = useDiagramStore((s) => s.updateDevicePosition) + const removeDevice = useDiagramStore((s) => s.removeDevice) + const removeConnection = useDiagramStore((s) => s.removeConnection) + const addConnection = useDiagramStore((s) => s.addConnection) + const addDeviceFromTemplate = useDiagramStore((s) => s.addDeviceFromTemplate) + const selectDevice = useDiagramStore((s) => s.selectDevice) + const selectConnection = useDiagramStore((s) => s.selectConnection) const { screenToFlowPosition } = useReactFlow() const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) // Owner/edit-collaborator/(a Super Admin, who getAccess always reports as // 'edit') can edit; a view-only collaborator can look but not touch — // organized-ideas.md §8's per-collaborator permissions. `access` is only @@ -53,10 +53,10 @@ function FlowCanvasInner() { const nodes: DeviceFlowNode[] = useMemo( () => - project.devices.map((device) => { + diagram.devices.map((device) => { const portInfos: PortInfo[] = device.ports.map((port) => { const portType = portTypes.find((pt) => pt.id === port.portTypeId) - const connectionCount = project.connections.filter( + const connectionCount = diagram.connections.filter( (c) => c.sourcePortId === port.id || c.targetPortId === port.id, ).length const max = portType?.maxConnections ?? 1 @@ -70,12 +70,12 @@ function FlowCanvasInner() { data: { device, categoryName: deviceCategoryName(catalog, device.category), portInfos }, } }), - [project, catalog, portTypes, selectedDeviceId], + [diagram, catalog, portTypes, selectedDeviceId], ) const edges: CableFlowEdge[] = useMemo( () => - project.connections.map((connection) => { + diagram.connections.map((connection) => { const cableType = cableTypes.find((ct) => ct.id === connection.cableTypeId) return { id: connection.id, @@ -92,7 +92,7 @@ function FlowCanvasInner() { }, } }), - [project.connections, cableTypes, selectedConnectionId], + [diagram.connections, cableTypes, selectedConnectionId], ) const isValidConnection = useCallback( @@ -101,14 +101,14 @@ function FlowCanvasInner() { const sourcePortId = parsePortId(c.sourceHandle) const targetPortId = parsePortId(c.targetHandle) if (!canEdit || !c.source || !c.target || !sourcePortId || !targetPortId) return false - return validateConnection(project, catalog, { + return validateConnection(diagram, catalog, { sourceDeviceId: c.source, sourcePortId, targetDeviceId: c.target, targetPortId, }).valid }, - [project, catalog, canEdit], + [diagram, catalog, canEdit], ) const onConnect = useCallback( @@ -117,8 +117,8 @@ function FlowCanvasInner() { const sourcePortId = parsePortId(connection.sourceHandle) const targetPortId = parsePortId(connection.targetHandle) if (!connection.source || !connection.target || !sourcePortId || !targetPortId) return - const sourceDevice = project.devices.find((d) => d.id === connection.source) - const targetDevice = project.devices.find((d) => d.id === connection.target) + const sourceDevice = diagram.devices.find((d) => d.id === connection.source) + const targetDevice = diagram.devices.find((d) => d.id === connection.target) const sourcePort = sourceDevice?.ports.find((p) => p.id === sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === targetPortId) const sourcePortType = sourcePort && getPortType(catalog, sourcePort.portTypeId) @@ -134,7 +134,7 @@ function FlowCanvasInner() { cableTypeId: defaultCableType.id, }) }, - [project, catalog, canEdit, addConnection], + [diagram, catalog, canEdit, addConnection], ) const onNodesChange = useCallback( diff --git a/src/components/common/CategorySelect.tsx b/src/components/common/CategorySelect.tsx index 1bcd5bd..f1560b3 100644 --- a/src/components/common/CategorySelect.tsx +++ b/src/components/common/CategorySelect.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { allDeviceCategories } from '../../domain/project' +import { allDeviceCategories } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' const NEW_CATEGORY_VALUE = '__new__' diff --git a/src/components/inspector/ConnectionInspector.tsx b/src/components/inspector/ConnectionInspector.tsx index a717ea8..a111688 100644 --- a/src/components/inspector/ConnectionInspector.tsx +++ b/src/components/inspector/ConnectionInspector.tsx @@ -1,19 +1,19 @@ import { useMemo } from 'react' -import { cableTypesForConnection, getPortType } from '../../domain/project' +import { cableTypesForConnection, getPortType } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' export default function ConnectionInspector({ connectionId }: { connectionId: string }) { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const updateConnection = useProjectStore((s) => s.updateConnection) - const removeConnection = useProjectStore((s) => s.removeConnection) + const updateConnection = useDiagramStore((s) => s.updateConnection) + const removeConnection = useDiagramStore((s) => s.removeConnection) - const connection = project.connections.find((c) => c.id === connectionId) - const sourceDevice = project.devices.find((d) => d.id === connection?.sourceDeviceId) - const targetDevice = project.devices.find((d) => d.id === connection?.targetDeviceId) + const connection = diagram.connections.find((c) => c.id === connectionId) + const sourceDevice = diagram.devices.find((d) => d.id === connection?.sourceDeviceId) + const targetDevice = diagram.devices.find((d) => d.id === connection?.targetDeviceId) const sourcePort = sourceDevice?.ports.find((p) => p.id === connection?.sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === connection?.targetPortId) diff --git a/src/components/inspector/DeviceInspector.tsx b/src/components/inspector/DeviceInspector.tsx index c38ff61..6452611 100644 --- a/src/components/inspector/DeviceInspector.tsx +++ b/src/components/inspector/DeviceInspector.tsx @@ -1,25 +1,25 @@ import { useMemo, useState } from 'react' import { PORT_DIRECTIONS } from '../../domain/constants' -import { allPortTypes } from '../../domain/project' +import { allPortTypes } from '../../domain/diagram' import type { PortDirection } from '../../domain/types' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import CategorySelect from '../common/CategorySelect' export default function DeviceInspector({ deviceId }: { deviceId: string }) { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const updateDevice = useProjectStore((s) => s.updateDevice) - const removeDevice = useProjectStore((s) => s.removeDevice) - const addPort = useProjectStore((s) => s.addPort) - const clonePort = useProjectStore((s) => s.clonePort) - const updatePort = useProjectStore((s) => s.updatePort) - const removePort = useProjectStore((s) => s.removePort) + const updateDevice = useDiagramStore((s) => s.updateDevice) + const removeDevice = useDiagramStore((s) => s.removeDevice) + const addPort = useDiagramStore((s) => s.addPort) + const clonePort = useDiagramStore((s) => s.clonePort) + const updatePort = useDiagramStore((s) => s.updatePort) + const removePort = useDiagramStore((s) => s.removePort) const [focusPortId, setFocusPortId] = useState(null) - const device = project.devices.find((d) => d.id === deviceId) + const device = diagram.devices.find((d) => d.id === deviceId) const portTypes = useMemo( () => [...allPortTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)), [catalog], @@ -28,7 +28,7 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) { if (!device) return null const connectionCountForPort = (portId: string) => - project.connections.filter((c) => c.sourcePortId === portId || c.targetPortId === portId).length + diagram.connections.filter((c) => c.sourcePortId === portId || c.targetPortId === portId).length return (
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 8136213..3ad5f88 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useAdminReviewStore } from '../../state/adminReviewStore' import { useAuthStore } from '../../state/authStore' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import { useSubmissionStore } from '../../state/submissionStore' import FlowCanvas from '../canvas/FlowCanvas' import DevicePalette from '../palette/DevicePalette' @@ -12,8 +12,8 @@ import TopBar from './TopBar' import ViewOnlyBanner from './ViewOnlyBanner' export default function AppShell() { - const isLoaded = useProjectStore((s) => s.isLoaded) - const loadInitialDiagram = useProjectStore((s) => s.loadInitialDiagram) + const isLoaded = useDiagramStore((s) => s.isLoaded) + const loadInitialDiagram = useDiagramStore((s) => s.loadInitialDiagram) const isCatalogLoaded = useCatalogStore((s) => s.isLoaded) const loadCatalog = useCatalogStore((s) => s.loadCatalog) const loadMySubmissions = useSubmissionStore((s) => s.loadMySubmissions) diff --git a/src/components/layout/ConnectionErrorBanner.tsx b/src/components/layout/ConnectionErrorBanner.tsx index 73559f8..c591fbb 100644 --- a/src/components/layout/ConnectionErrorBanner.tsx +++ b/src/components/layout/ConnectionErrorBanner.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' export default function ConnectionErrorBanner() { - const error = useProjectStore((s) => s.lastConnectionError) - const clear = useProjectStore((s) => s.clearConnectionError) + const error = useDiagramStore((s) => s.lastConnectionError) + const clear = useDiagramStore((s) => s.clearConnectionError) useEffect(() => { if (!error) return diff --git a/src/components/layout/DiagramManagerModal.tsx b/src/components/layout/DiagramManagerModal.tsx index 5c1a6b0..a274a9d 100644 --- a/src/components/layout/DiagramManagerModal.tsx +++ b/src/components/layout/DiagramManagerModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Modal from '../common/Modal' function formatUpdatedAt(iso: string): string { @@ -8,25 +8,25 @@ function formatUpdatedAt(iso: string): string { } export default function DiagramManagerModal({ onClose }: { onClose: () => void }) { - const project = useProjectStore((s) => s.project) - const diagramList = useProjectStore((s) => s.diagramList) - const refreshDiagramList = useProjectStore((s) => s.refreshDiagramList) - const newProject = useProjectStore((s) => s.newProject) - const switchToDiagram = useProjectStore((s) => s.switchToDiagram) - const deleteDiagram = useProjectStore((s) => s.deleteDiagram) + const diagram = useDiagramStore((s) => s.diagram) + const diagramList = useDiagramStore((s) => s.diagramList) + const refreshDiagramList = useDiagramStore((s) => s.refreshDiagramList) + const newDiagram = useDiagramStore((s) => s.newDiagram) + const switchToDiagram = useDiagramStore((s) => s.switchToDiagram) + const deleteDiagram = useDiagramStore((s) => s.deleteDiagram) useEffect(() => { refreshDiagramList() }, [refreshDiagramList]) const handleOpen = async (id: string) => { - if (id === project.id) return + if (id === diagram.id) return await switchToDiagram(id) onClose() } const handleNew = async () => { - await newProject() + await newDiagram() onClose() } @@ -52,7 +52,7 @@ export default function DiagramManagerModal({ onClose }: { onClose: () => void }
    {diagramList.map((d) => { - const isCurrent = d.id === project.id + const isCurrent = d.id === diagram.id return (
  • s.selectedDeviceId) - const selectedConnectionId = useProjectStore((s) => s.selectedConnectionId) + const selectedDeviceId = useDiagramStore((s) => s.selectedDeviceId) + const selectedConnectionId = useDiagramStore((s) => s.selectedConnectionId) const [tab, setTab] = useState('inspector') return ( diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 5b0877b..79d8a58 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -1,9 +1,9 @@ import { useMemo, useRef, useState } from 'react' -import { downloadProjectFile, readProjectFile, ProjectImportError } from '../../data/exportImport' +import { downloadDiagramFile, readDiagramFile, DiagramImportError } from '../../data/exportImport' import { supabase } from '../../data/supabaseClient' import { useAdminReviewStore } from '../../state/adminReviewStore' import { useAuthStore } from '../../state/authStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import { useSubmissionStore } from '../../state/submissionStore' import AdminUsersModal from '../admin/AdminUsersModal' import DiagramSharingModal from '../sharing/DiagramSharingModal' @@ -33,11 +33,11 @@ function setLastSeenSubmissionsAt(iso: string): void { } export default function TopBar() { - const project = useProjectStore((s) => s.project) - const access = useProjectStore((s) => s.access) + const diagram = useDiagramStore((s) => s.diagram) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const renameProject = useProjectStore((s) => s.renameProject) - const importProject = useProjectStore((s) => s.importProject) + const renameDiagram = useDiagramStore((s) => s.renameDiagram) + const importDiagram = useDiagramStore((s) => s.importDiagram) const mySubmissions = useSubmissionStore((s) => s.mySubmissions) const username = useAuthStore((s) => s.username) const role = useAuthStore((s) => s.role) @@ -66,7 +66,7 @@ export default function TopBar() { setLastSeenSubmissionsAt(new Date().toISOString()) } - const handleExport = () => downloadProjectFile(project) + const handleExport = () => downloadDiagramFile(diagram) const handleImportClick = () => fileInputRef.current?.click() @@ -75,10 +75,10 @@ export default function TopBar() { event.target.value = '' if (!file) return try { - const imported = await readProjectFile(file) - await importProject(imported) + const imported = await readDiagramFile(file) + await importDiagram(imported) } catch (err) { - const message = err instanceof ProjectImportError ? err.message : 'Could not read that file.' + const message = err instanceof DiagramImportError ? err.message : 'Could not read that file.' window.alert(`Import failed: ${message}`) } } @@ -88,8 +88,8 @@ export default function TopBar() {
    AV Planner renameProject(e.target.value)} + value={diagram.name} + onChange={(e) => renameDiagram(e.target.value)} disabled={!canEdit} className="rounded border border-transparent px-2 py-1 text-sm text-slate-700 hover:border-slate-200 focus:border-slate-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60" /> diff --git a/src/components/layout/ViewOnlyBanner.tsx b/src/components/layout/ViewOnlyBanner.tsx index d7f8132..f3a0ce7 100644 --- a/src/components/layout/ViewOnlyBanner.tsx +++ b/src/components/layout/ViewOnlyBanner.tsx @@ -1,4 +1,4 @@ -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' /** Shown when the current user only has view access to the open diagram * (organized-ideas.md §8's per-collaborator permissions) — the canvas and @@ -6,7 +6,7 @@ import { useProjectStore } from '../../state/projectStore' * FlowCanvas/DeviceInspector/ConnectionInspector), this just makes that * visible instead of leaving it to be discovered by a control not working. */ export default function ViewOnlyBanner() { - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) if (access?.myPermission !== 'view') return null return ( diff --git a/src/components/palette/CableTypeManager.tsx b/src/components/palette/CableTypeManager.tsx index b39bfc8..7113bef 100644 --- a/src/components/palette/CableTypeManager.tsx +++ b/src/components/palette/CableTypeManager.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { allCableTypes, allPortTypes } from '../../domain/project' +import { allCableTypes, allPortTypes } from '../../domain/diagram' import type { CableType, PortType } from '../../domain/types' import { cableTypeToRow } from '../../data/catalogRowMapping' import { useAdminReviewStore } from '../../state/adminReviewStore' diff --git a/src/components/palette/DevicePalette.tsx b/src/components/palette/DevicePalette.tsx index a1272bf..cf6b679 100644 --- a/src/components/palette/DevicePalette.tsx +++ b/src/components/palette/DevicePalette.tsx @@ -1,11 +1,11 @@ import { useMemo, useState } from 'react' import { deviceTemplateToRow } from '../../data/catalogRowMapping' -import { allDeviceCategories, allDeviceTemplates, hiddenDeviceTemplates } from '../../domain/project' +import { allDeviceCategories, allDeviceTemplates, hiddenDeviceTemplates } from '../../domain/diagram' import type { DeviceTemplate } from '../../domain/types' import { useAdminReviewStore } from '../../state/adminReviewStore' import { useAuthStore } from '../../state/authStore' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import { useSubmissionStore } from '../../state/submissionStore' import { TEMPLATE_DRAG_MIME } from '../canvas/FlowCanvas' import Chevron from '../common/Chevron' @@ -19,10 +19,10 @@ type EditorTarget = | { mode: 'adminEdit'; template: DeviceTemplate } export default function DevicePalette() { - const project = useProjectStore((s) => s.project) - const access = useProjectStore((s) => s.access) + const diagram = useDiagramStore((s) => s.diagram) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const addDeviceFromTemplate = useProjectStore((s) => s.addDeviceFromTemplate) + const addDeviceFromTemplate = useDiagramStore((s) => s.addDeviceFromTemplate) const catalog = useCatalogStore((s) => s.catalog) const hiddenPublicIds = useCatalogStore((s) => s.hiddenPublicDeviceTemplateIds) const removeCustomDeviceTemplate = useCatalogStore((s) => s.removeCustomDeviceTemplate) @@ -81,7 +81,7 @@ export default function DevicePalette() { const handleQuickAdd = (template: DeviceTemplate) => { if (!canEdit) return // Cascade placement so repeated quick-adds don't stack exactly on top of each other. - const offset = (project.devices.length % 8) * 24 + const offset = (diagram.devices.length % 8) * 24 addDeviceFromTemplate(catalog, template.id, { x: 80 + offset, y: 80 + offset }) } diff --git a/src/components/palette/DeviceTemplateEditor.tsx b/src/components/palette/DeviceTemplateEditor.tsx index 5ecb5f5..13450e6 100644 --- a/src/components/palette/DeviceTemplateEditor.tsx +++ b/src/components/palette/DeviceTemplateEditor.tsx @@ -1,7 +1,7 @@ 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 { allPortTypes, expandCompatibleFamilyIds, incrementPortName } from '../../domain/diagram' import type { DeviceTemplate, PortDirection } from '../../domain/types' import { useCatalogStore } from '../../state/catalogStore' import { useSubmissionStore } from '../../state/submissionStore' diff --git a/src/components/palette/PortTypeManager.tsx b/src/components/palette/PortTypeManager.tsx index 6ae6a36..34adfb1 100644 --- a/src/components/palette/PortTypeManager.tsx +++ b/src/components/palette/PortTypeManager.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { PORT_CATEGORIES } from '../../domain/constants' -import { allPortTypes, expandCompatibleFamilyIds } from '../../domain/project' +import { allPortTypes, expandCompatibleFamilyIds } from '../../domain/diagram' import type { PortType } from '../../domain/types' import { portTypeToRow } from '../../data/catalogRowMapping' import { useAdminReviewStore } from '../../state/adminReviewStore' diff --git a/src/components/sharing/DiagramSharingModal.tsx b/src/components/sharing/DiagramSharingModal.tsx index fca58be..31f24ba 100644 --- a/src/components/sharing/DiagramSharingModal.tsx +++ b/src/components/sharing/DiagramSharingModal.tsx @@ -2,14 +2,14 @@ import { useEffect, useState } from 'react' import type { CollaboratorPermission } from '../../data/DiagramCollaboratorRepository' import { SelfCollaboratorError, UnknownUsernameError } from '../../data/DiagramCollaboratorRepository' import { useDiagramCollaboratorStore } from '../../state/diagramCollaboratorStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Modal from '../common/Modal' export default function DiagramSharingModal({ onClose }: { onClose: () => void }) { - const project = useProjectStore((s) => s.project) - const access = useProjectStore((s) => s.access) + const diagram = useDiagramStore((s) => s.diagram) + const access = useDiagramStore((s) => s.access) const isOwner = access?.myPermission === 'owner' - const diagramId = project.id + const diagramId = diagram.id const collaborators = useDiagramCollaboratorStore((s) => s.collaborators) const isLoaded = useDiagramCollaboratorStore((s) => s.isLoaded) @@ -52,7 +52,7 @@ export default function DiagramSharingModal({ onClose }: { onClose: () => void } } return ( - +
    {isOwner && (
    diff --git a/src/components/sharing/VersionHistoryModal.tsx b/src/components/sharing/VersionHistoryModal.tsx index 92fa2f4..1f79253 100644 --- a/src/components/sharing/VersionHistoryModal.tsx +++ b/src/components/sharing/VersionHistoryModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useDiagramSnapshotStore } from '../../state/diagramSnapshotStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Modal from '../common/Modal' function formatDateTime(iso: string): string { @@ -8,10 +8,10 @@ function formatDateTime(iso: string): string { } export default function VersionHistoryModal({ onClose }: { onClose: () => void }) { - const project = useProjectStore((s) => s.project) - const access = useProjectStore((s) => s.access) + const diagram = useDiagramStore((s) => s.diagram) + const access = useDiagramStore((s) => s.access) const canRestore = access?.myPermission === 'owner' || access?.myPermission === 'edit' - const diagramId = project.id + const diagramId = diagram.id const snapshots = useDiagramSnapshotStore((s) => s.snapshots) const isLoaded = useDiagramSnapshotStore((s) => s.isLoaded) @@ -37,7 +37,7 @@ export default function VersionHistoryModal({ onClose }: { onClose: () => void } } return ( - +

    Checkpoints saved periodically while editing (up to the 50 most recent). diff --git a/src/components/submissions/MySubmissionsModal.tsx b/src/components/submissions/MySubmissionsModal.tsx index f100d45..0d921ec 100644 --- a/src/components/submissions/MySubmissionsModal.tsx +++ b/src/components/submissions/MySubmissionsModal.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { cableTypeToRow, portTypeToRow, rowToCableTypeFields, rowToDeviceTemplateFields, rowToPortTypeFields } from '../../data/catalogRowMapping' import type { CatalogSubmission } from '../../data/SubmissionRepository' -import { allPortTypes } from '../../domain/project' +import { allPortTypes } from '../../domain/diagram' import type { CableType, Catalog, DeviceTemplate, PortType } from '../../domain/types' import { useCatalogStore } from '../../state/catalogStore' import { useSubmissionStore } from '../../state/submissionStore' diff --git a/src/data/DiagramRepository.ts b/src/data/DiagramRepository.ts index eb15cbe..7115ce5 100644 --- a/src/data/DiagramRepository.ts +++ b/src/data/DiagramRepository.ts @@ -1,4 +1,4 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' /** Lightweight metadata for listing diagrams without fetching each one's * full (potentially large) data payload. */ @@ -8,17 +8,6 @@ export interface DiagramSummary { updatedAt: string } -/** - * Storage abstraction the rest of the app codes against. `LocalStorageDiagramRepository` - * and `SupabaseDiagramRepository` both implement it — the store/UI layers don't need to - * know or care which one is active. - * - * Named for the "Diagram" terminology decided in organized-ideas.md §8 (renamed from - * "Project"). This is a deliberately scoped rename: only the storage layer's naming - * changed here. The domain type (`Project`), the Zustand store (`useProjectStore`), and - * user-facing copy still say "Project" — that's a separate, larger mechanical rename - * across the whole app, tracked as its own task rather than bundled into this one. - */ /** Your relationship to a diagram — who owns it, and what you personally * can do with it (organized-ideas.md §8's per-collaborator view/edit * permissions). Drives the frontend's own view-only lockdown, since RLS @@ -29,13 +18,18 @@ export interface DiagramAccess { myPermission: 'owner' | 'edit' | 'view' } +/** + * Storage abstraction the rest of the app codes against. `LocalStorageDiagramRepository` + * and `SupabaseDiagramRepository` both implement it — the store/UI layers don't need to + * know or care which one is active. + */ export interface DiagramRepository { /** Every diagram visible to the current user, most recently updated first. */ list(): Promise /** One diagram's full data by id, or null if it doesn't exist / isn't visible. */ - loadById(id: string): Promise - /** Upsert a diagram, matched by the project's own id. */ - save(project: Project): Promise + loadById(id: string): Promise + /** Upsert a diagram, matched by its own id. */ + save(diagram: Diagram): Promise /** Permanently delete one diagram by id. */ deleteById(id: string): Promise /** Who owns this diagram and what the current user can do with it. */ diff --git a/src/data/DiagramSnapshotRepository.ts b/src/data/DiagramSnapshotRepository.ts index a7997dc..b25000a 100644 --- a/src/data/DiagramSnapshotRepository.ts +++ b/src/data/DiagramSnapshotRepository.ts @@ -1,4 +1,4 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' export interface DiagramSnapshotSummary { id: string @@ -20,7 +20,7 @@ export interface DiagramSnapshotRepository { * it already knows the live state is an unmodified copy of a snapshot * that still exists (e.g. restoring again right after an earlier * restore, with no edits in between), so a second copy of it would just - * be a duplicate. Returns the restored Project so the caller can update + * be a duplicate. Returns the restored Diagram so the caller can update * the open diagram in place. */ - restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise + restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise } diff --git a/src/data/LocalStorageDiagramRepository.ts b/src/data/LocalStorageDiagramRepository.ts index 7ba5fc1..9b24a17 100644 --- a/src/data/LocalStorageDiagramRepository.ts +++ b/src/data/LocalStorageDiagramRepository.ts @@ -1,10 +1,10 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' import type { DiagramAccess, DiagramRepository, DiagramSummary } from './DiagramRepository' -import { normalizeProject } from './exportImport' +import { normalizeDiagram } from './exportImport' const STORAGE_KEY = 'av-planner:diagrams:v1' -type StoredDiagrams = Record +type StoredDiagrams = Record function readAll(): StoredDiagrams { try { @@ -20,10 +20,11 @@ function writeAll(diagrams: StoredDiagrams): void { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(diagrams)) } -/** Kept around (no longer the active repository — see projectStore.ts) as a - * reference implementation and a safe fallback if Supabase is unreachable. - * Multiple diagrams are stored as one JSON object keyed by id, matching the - * shape the Supabase-backed repository presents to the rest of the app. */ +/** Kept around (no longer the active repository — see state/diagramStore.ts) + * as a reference implementation and a safe fallback if Supabase is + * unreachable. Multiple diagrams are stored as one JSON object keyed by + * id, matching the shape the Supabase-backed repository presents to the + * rest of the app. */ export class LocalStorageDiagramRepository implements DiagramRepository { async list(): Promise { const diagrams = Object.values(readAll()) @@ -32,14 +33,14 @@ export class LocalStorageDiagramRepository implements DiagramRepository { .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) } - async loadById(id: string): Promise { + async loadById(id: string): Promise { const diagram = readAll()[id] - return diagram ? normalizeProject(diagram) : null + return diagram ? normalizeDiagram(diagram) : null } - async save(project: Project): Promise { + async save(diagram: Diagram): Promise { const all = readAll() - all[project.id] = project + all[diagram.id] = diagram writeAll(all) } diff --git a/src/data/SupabaseDiagramRepository.ts b/src/data/SupabaseDiagramRepository.ts index 2e73646..1606427 100644 --- a/src/data/SupabaseDiagramRepository.ts +++ b/src/data/SupabaseDiagramRepository.ts @@ -1,11 +1,11 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' import type { DiagramAccess, DiagramRepository, DiagramSummary } from './DiagramRepository' -import { normalizeProject } from './exportImport' +import { normalizeDiagram } from './exportImport' import { supabase } from './supabaseClient' /** * Backs the app with the local (or eventually production) Supabase - * `diagrams` table instead of localStorage. The whole `Project` domain + * `diagrams` table instead of localStorage. The whole `Diagram` domain * object is stored as-is in the `data` JSONB column — see the schema * migration's header comment for why diagrams are a document, not * normalized relational rows. @@ -27,7 +27,7 @@ export class SupabaseDiagramRepository implements DiagramRepository { return (data ?? []).map((row) => ({ id: row.id, name: row.name, updatedAt: row.updated_at })) } - async loadById(id: string): Promise { + async loadById(id: string): Promise { const { data: row, error } = await supabase.from('diagrams').select('data').eq('id', id).maybeSingle() if (error) { @@ -35,10 +35,10 @@ export class SupabaseDiagramRepository implements DiagramRepository { return null } if (!row) return null - return normalizeProject(row.data as Project) + return normalizeDiagram(row.data as Diagram) } - async save(project: Project): Promise { + async save(diagram: Diagram): Promise { const { data: { user }, } = await supabase.auth.getUser() @@ -52,15 +52,15 @@ export class SupabaseDiagramRepository implements DiagramRepository { // to whoever saves last — once diagram sharing (§8) is in play, an // edit-collaborator saving changes must never be able to overwrite who // owns the diagram. - const { data: existing } = await supabase.from('diagrams').select('id, data').eq('id', project.id).maybeSingle() + const { data: existing } = await supabase.from('diagrams').select('id, data').eq('id', diagram.id).maybeSingle() if (existing) { - await this.maybeWriteSnapshot(project.id, existing.data as Project, user.id) + await this.maybeWriteSnapshot(diagram.id, existing.data as Diagram, user.id) } - const payload = { name: project.name, data: project, updated_at: new Date().toISOString() } + const payload = { name: diagram.name, data: diagram, updated_at: new Date().toISOString() } const { error } = existing - ? await supabase.from('diagrams').update(payload).eq('id', project.id) - : await supabase.from('diagrams').insert({ id: project.id, owner_id: user.id, ...payload }) + ? await supabase.from('diagrams').update(payload).eq('id', diagram.id) + : await supabase.from('diagrams').insert({ id: diagram.id, owner_id: user.id, ...payload }) if (error) { console.error('Failed to save diagram to Supabase', error) @@ -73,7 +73,7 @@ export class SupabaseDiagramRepository implements DiagramRepository { // keystroke-adjacent change instead of meaningful recovery points. private static readonly SNAPSHOT_INTERVAL_MS = 5 * 60 * 1000 - private async maybeWriteSnapshot(diagramId: string, previousData: Project, userId: string): Promise { + private async maybeWriteSnapshot(diagramId: string, previousData: Diagram, userId: string): Promise { const { data: lastSnapshot } = await supabase .from('diagram_snapshots') .select('created_at') diff --git a/src/data/SupabaseDiagramSnapshotRepository.ts b/src/data/SupabaseDiagramSnapshotRepository.ts index 2fb0120..7dd05c0 100644 --- a/src/data/SupabaseDiagramSnapshotRepository.ts +++ b/src/data/SupabaseDiagramSnapshotRepository.ts @@ -1,6 +1,6 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' import type { DiagramSnapshotRepository, DiagramSnapshotSummary } from './DiagramSnapshotRepository' -import { normalizeProject } from './exportImport' +import { normalizeDiagram } from './exportImport' import { supabase } from './supabaseClient' interface SnapshotRow { @@ -36,7 +36,7 @@ export class SupabaseDiagramSnapshotRepository implements DiagramSnapshotReposit })) } - async restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise { + async restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise { const { data: snapshot, error } = await supabase.from('diagram_snapshots').select('data').eq('id', snapshotId).maybeSingle() if (error) console.error('Failed to load snapshot from Supabase', error) if (!snapshot) return null @@ -59,7 +59,7 @@ export class SupabaseDiagramSnapshotRepository implements DiagramSnapshotReposit } } - const restored = normalizeProject(snapshot.data as Project) + const restored = normalizeDiagram(snapshot.data as Diagram) const { error: updateError } = await supabase .from('diagrams') .update({ data: restored, name: restored.name, updated_at: new Date().toISOString() }) diff --git a/src/data/exportImport.ts b/src/data/exportImport.ts index d95b554..5e96fce 100644 --- a/src/data/exportImport.ts +++ b/src/data/exportImport.ts @@ -1,15 +1,15 @@ -import type { Project } from '../domain/types' +import type { Diagram } from '../domain/types' -export function projectToJson(project: Project): string { - return JSON.stringify(project, null, 2) +export function diagramToJson(diagram: Diagram): string { + return JSON.stringify(diagram, null, 2) } -export function downloadProjectFile(project: Project): void { - const json = projectToJson(project) +export function downloadDiagramFile(diagram: Diagram): void { + const json = diagramToJson(diagram) const blob = new Blob([json], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') - const safeName = project.name.trim().replace(/[^a-z0-9-_]+/gi, '_') || 'project' + const safeName = diagram.name.trim().replace(/[^a-z0-9-_]+/gi, '_') || 'diagram' a.href = url a.download = `${safeName}.avplan.json` document.body.appendChild(a) @@ -18,44 +18,44 @@ export function downloadProjectFile(project: Project): void { URL.revokeObjectURL(url) } -class ProjectImportError extends Error {} +class DiagramImportError extends Error {} /** Minimal structural check — enough to catch "wrong file" without pulling * in a schema validation library for a single call site. */ -function assertLooksLikeProject(value: unknown): asserts value is Project { +function assertLooksLikeDiagram(value: unknown): asserts value is Diagram { if (typeof value !== 'object' || value === null) { - throw new ProjectImportError('File does not contain a JSON object.') + throw new DiagramImportError('File does not contain a JSON object.') } - const p = value as Partial - if (typeof p.id !== 'string' || typeof p.name !== 'string') { - throw new ProjectImportError('File is missing required project fields.') + const d = value as Partial + if (typeof d.id !== 'string' || typeof d.name !== 'string') { + throw new DiagramImportError('File is missing required diagram fields.') } - if (!Array.isArray(p.devices) || !Array.isArray(p.connections)) { - throw new ProjectImportError('File is missing devices/connections arrays.') + if (!Array.isArray(d.devices) || !Array.isArray(d.connections)) { + throw new DiagramImportError('File is missing devices/connections arrays.') } } -export async function readProjectFile(file: File): Promise { +export async function readDiagramFile(file: File): Promise { const text = await file.text() let parsed: unknown try { parsed = JSON.parse(text) } catch { - throw new ProjectImportError('File is not valid JSON.') + throw new DiagramImportError('File is not valid JSON.') } - assertLooksLikeProject(parsed) - return normalizeProject(parsed) + assertLooksLikeDiagram(parsed) + return normalizeDiagram(parsed) } -/** Strips any legacy per-project catalog fields (customDeviceTemplates, +/** Strips any legacy per-diagram catalog fields (customDeviceTemplates, * customPortTypes, etc.) that might still be present in an older export * file or a localStorage diagram saved before the catalog moved to * Supabase (see state/catalogStore.ts) — that data now lives in the shared * catalog, not the diagram, so it's just discarded here rather than * crashing on an unexpected shape. */ -export function normalizeProject(project: Project): Project { - const { id, name, createdAt, updatedAt, devices, connections } = project +export function normalizeDiagram(diagram: Diagram): Diagram { + const { id, name, createdAt, updatedAt, devices, connections } = diagram return { id, name, createdAt, updatedAt, devices, connections } } -export { ProjectImportError } +export { DiagramImportError } diff --git a/src/domain/bom.ts b/src/domain/bom.ts index 6bb1b39..f69aa57 100644 --- a/src/domain/bom.ts +++ b/src/domain/bom.ts @@ -1,5 +1,5 @@ -import { allCableTypes } from './project' -import type { CableType, Catalog, Connection, Device, Project } from './types' +import { allCableTypes } from './diagram' +import type { CableType, Catalog, Connection, Device, Diagram } from './types' /** One length within a cable type's group — e.g. "3 × 10ft". `distance` * is undefined for the bucket of connections that have no length entered @@ -82,7 +82,7 @@ function connectionCost(connection: Connection, cableType: CableType | undefined return undefined } -export function computeBom(project: Project, catalog: Catalog): Bom { +export function computeBom(diagram: Diagram, catalog: Catalog): Bom { const cableTypes = new Map(allCableTypes(catalog).map((ct) => [ct.id, ct])) const cableLines = new Map() @@ -92,7 +92,7 @@ export function computeBom(project: Project, catalog: Catalog): Bom { // addressable by (cableTypeId, distance) while building them up. const lengthLines = new Map>() - for (const connection of project.connections) { + for (const connection of diagram.connections) { const cableType = cableTypes.get(connection.cableTypeId) const name = cableType?.name ?? 'Unknown Cable' const unit = cableType?.unit ?? 'ft' @@ -155,7 +155,7 @@ export function computeBom(project: Project, catalog: Catalog): Bom { } const deviceLines = new Map() - for (const device of project.devices) { + for (const device of diagram.devices) { const key = deviceGroupKey(device) const existing = deviceLines.get(key) ?? { key, diff --git a/src/domain/compatibility.ts b/src/domain/compatibility.ts index 4725df7..aaeda98 100644 --- a/src/domain/compatibility.ts +++ b/src/domain/compatibility.ts @@ -1,5 +1,5 @@ -import { acceptedFamilies, getPortType, hasBridgingCableType } from './project' -import type { Catalog, Connection, Device, PortType, Project } from './types' +import { acceptedFamilies, getPortType, hasBridgingCableType } from './diagram' +import type { Catalog, Connection, Device, Diagram, PortType } from './types' /** * Two ports connect if the sets of families they each accept (their own @@ -36,10 +36,10 @@ export interface ConnectionValidation { * server-side later since it only depends on plain domain data. */ export function validateConnection( - project: Project, + diagram: Diagram, catalog: Catalog, candidate: ConnectionCandidate, - existingConnections: Connection[] = project.connections, + existingConnections: Connection[] = diagram.connections, ): ConnectionValidation { const { sourceDeviceId, sourcePortId, targetDeviceId, targetPortId } = candidate @@ -47,8 +47,8 @@ export function validateConnection( return { valid: false, reason: 'A port cannot connect to itself.' } } - const sourceDevice = project.devices.find((d) => d.id === sourceDeviceId) - const targetDevice = project.devices.find((d) => d.id === targetDeviceId) + const sourceDevice = diagram.devices.find((d) => d.id === sourceDeviceId) + const targetDevice = diagram.devices.find((d) => d.id === targetDeviceId) const sourcePort = sourceDevice?.ports.find((p) => p.id === sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === targetPortId) diff --git a/src/domain/constants.ts b/src/domain/constants.ts index 7e58d13..a06a3cd 100644 --- a/src/domain/constants.ts +++ b/src/domain/constants.ts @@ -1,7 +1,7 @@ import type { PortCategory, PortDirection } from './types' -// Device categories are no longer a fixed list — see `domain/project.ts`'s -// `allDeviceCategories` (built-ins + project-custom, merged). +// Device categories are no longer a fixed list — see `domain/diagram.ts`'s +// `allDeviceCategories` (public catalog + your own private ones, merged). export const PORT_DIRECTIONS: { value: PortDirection; label: string }[] = [ { value: 'input', label: 'Input' }, diff --git a/src/domain/project.ts b/src/domain/diagram.ts similarity index 97% rename from src/domain/project.ts rename to src/domain/diagram.ts index 5ee9fe7..f9b4df4 100644 --- a/src/domain/project.ts +++ b/src/domain/diagram.ts @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid' -import type { CableType, Catalog, Device, DeviceCategoryDef, DeviceTemplate, PortType, Project } from './types' +import type { CableType, Catalog, Device, DeviceCategoryDef, Diagram, DeviceTemplate, PortType } from './types' export function allPortTypes(catalog: Catalog): PortType[] { return catalog.portTypes @@ -125,7 +125,7 @@ export function expandCompatibleFamilyIds(selectedPortTypeIds: string[], allType return [...families] } -export function createEmptyProject(name = 'Untitled Project'): Project { +export function createEmptyDiagram(name = 'Untitled Diagram'): Diagram { const now = new Date().toISOString() return { id: uuid(), diff --git a/src/domain/types.ts b/src/domain/types.ts index a331b90..e50a94a 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -16,11 +16,12 @@ export type PortCategory = | 'other' /** A device category id (references a DeviceCategoryDef). Kept as a plain - * string rather than a fixed union so projects can define their own. */ + * string rather than a fixed union so users can define their own. */ export type DeviceCategory = string /** A device grouping shown in the palette and category pickers. Built-ins - * ship in `library.ts`; users can add their own, scoped to the project. */ + * ship in `library.ts`; users can add their own private ones via the + * shared catalog (see state/catalogStore.ts), not scoped to any one diagram. */ export interface DeviceCategoryDef { id: string name: string @@ -134,7 +135,7 @@ export interface Connection { notes?: string } -export interface Project { +export interface Diagram { id: string name: string createdAt: string diff --git a/src/state/catalogStore.ts b/src/state/catalogStore.ts index 6d188e2..8b25951 100644 --- a/src/state/catalogStore.ts +++ b/src/state/catalogStore.ts @@ -10,7 +10,7 @@ const repository: CatalogRepository = new SupabaseCatalogRepository() // Which public device templates you've hidden from your own palette — a // per-user UI preference, not catalog data, so it's not a Supabase row: hiding -// a public entry only affects your view of it (see domain/project.ts's +// a public entry only affects your view of it (see domain/diagram.ts's // allDeviceTemplates/hiddenDeviceTemplates), never the shared entry itself. const HIDDEN_PUBLIC_TEMPLATES_KEY = 'av-planner:hiddenPublicDeviceTemplateIds' function loadHiddenPublicIds(): Set { diff --git a/src/state/diagramSnapshotStore.ts b/src/state/diagramSnapshotStore.ts index 3ff7d0e..37670db 100644 --- a/src/state/diagramSnapshotStore.ts +++ b/src/state/diagramSnapshotStore.ts @@ -1,8 +1,8 @@ import { create } from 'zustand' import type { DiagramSnapshotRepository, DiagramSnapshotSummary } from '../data/DiagramSnapshotRepository' import { SupabaseDiagramSnapshotRepository } from '../data/SupabaseDiagramSnapshotRepository' -import type { Project } from '../domain/types' -import { useProjectStore } from './projectStore' +import type { Diagram } from '../domain/types' +import { useDiagramStore } from './diagramStore' const repository: DiagramSnapshotRepository = new SupabaseDiagramSnapshotRepository() @@ -12,12 +12,12 @@ interface DiagramSnapshotStoreState { load: (diagramId: string) => Promise /** Restores a snapshot and applies it to the open diagram (via - * projectStore.applyRestoredProject) in one step. Skips writing a + * diagramStore.applyRestoredDiagram) in one step. Skips writing a * pointless duplicate safety snapshot when the live diagram is already * an unmodified copy of a snapshot still sitting in the list — see - * projectStore's restoredFromSnapshotId/restoredProjectUpdatedAt. - * Returns the restored Project, or null if the restore failed. */ - restore: (diagramId: string, snapshotId: string) => Promise + * diagramStore's restoredFromSnapshotId/restoredDiagramUpdatedAt. + * Returns the restored Diagram, or null if the restore failed. */ + restore: (diagramId: string, snapshotId: string) => Promise } export const useDiagramSnapshotStore = create((set) => ({ @@ -30,13 +30,13 @@ export const useDiagramSnapshotStore = create((set) = }, restore: async (diagramId, snapshotId) => { - const { project, restoredFromSnapshotId, restoredProjectUpdatedAt } = useProjectStore.getState() + const { diagram, restoredFromSnapshotId, restoredDiagramUpdatedAt } = useDiagramStore.getState() const knownUnmodifiedSinceLastRestore = - restoredFromSnapshotId !== null && project.updatedAt === restoredProjectUpdatedAt + restoredFromSnapshotId !== null && diagram.updatedAt === restoredDiagramUpdatedAt const restored = await repository.restore(diagramId, snapshotId, !knownUnmodifiedSinceLastRestore) if (restored) { - useProjectStore.getState().applyRestoredProject(restored, snapshotId) + useDiagramStore.getState().applyRestoredDiagram(restored, snapshotId) const snapshots = await repository.list(diagramId) set({ snapshots }) } diff --git a/src/state/projectStore.ts b/src/state/diagramStore.ts similarity index 75% rename from src/state/projectStore.ts rename to src/state/diagramStore.ts index 1186e06..e4a7194 100644 --- a/src/state/projectStore.ts +++ b/src/state/diagramStore.ts @@ -1,8 +1,8 @@ import { v4 as uuid } from 'uuid' import { create } from 'zustand' import { validateConnection } from '../domain/compatibility' -import { allDeviceTemplates, createDeviceFromTemplate, createEmptyProject, incrementPortName } from '../domain/project' -import type { Catalog, Connection, Device, DeviceCategory, Port, Project } from '../domain/types' +import { allDeviceTemplates, createDeviceFromTemplate, createEmptyDiagram, incrementPortName } from '../domain/diagram' +import type { Catalog, Connection, Device, DeviceCategory, Diagram, Port } from '../domain/types' import type { DiagramAccess, DiagramRepository, DiagramSummary } from '../data/DiagramRepository' import { SupabaseDiagramRepository } from '../data/SupabaseDiagramRepository' @@ -34,14 +34,14 @@ function setLastDiagramId(id: string): void { } } -async function loadMostRecent(): Promise { +async function loadMostRecent(): Promise { const list = await repository.list() if (list.length === 0) return null return repository.loadById(list[0].id) } -interface ProjectStoreState { - project: Project +interface DiagramStoreState { + diagram: Diagram isLoaded: boolean /** Every diagram the current user can see, most recently updated first. * Populated by refreshDiagramList — not kept live-synced on every store @@ -57,8 +57,8 @@ interface ProjectStoreState { access: DiagramAccess | null /** Set to a snapshot's id right after restoring it, meaning the live * diagram data is verbatim identical to that already-existing snapshot - * row — see applyRestoredProject. `restoredProjectUpdatedAt` is the - * project's own `updatedAt` at that moment; touch() always advances + * row — see applyRestoredDiagram. `restoredDiagramUpdatedAt` is the + * diagram's own `updatedAt` at that moment; touch() always advances * `updatedAt` on a genuine edit, so comparing the two later is a free * way to tell "has anything changed since this restore" without * hooking every mutating action. Both null once anything invalidates @@ -67,7 +67,7 @@ interface ProjectStoreState { * snapshot when the user restores several old versions in a row without * editing in between. */ restoredFromSnapshotId: string | null - restoredProjectUpdatedAt: string | null + restoredDiagramUpdatedAt: string | null selectedDeviceId: string | null selectedConnectionId: string | null lastConnectionError: string | null @@ -79,17 +79,17 @@ interface ProjectStoreState { refreshDiagramList: () => Promise /** Creates a new diagram, makes it current, and persists it immediately * (not just via the debounced autosave) so it shows up in the list right away. */ - newProject: (name?: string) => Promise - renameProject: (name: string) => void + newDiagram: (name?: string) => Promise + renameDiagram: (name: string) => void /** Imports diagram data under a fresh id, so it's added as a new diagram * rather than risking a collision with (or silently overwriting) an * existing one that happens to share the file's id. */ - importProject: (project: Project) => Promise + importDiagram: (diagram: Diagram) => Promise /** Applies a diagram that was already persisted by something else (the * version-history "Restore" action already wrote the restored data to * Supabase) — just reflects it in the currently open canvas, without * re-triggering an import-style "treat this as a brand new diagram". */ - applyRestoredProject: (project: Project, snapshotId: string) => void + applyRestoredDiagram: (diagram: Diagram, snapshotId: string) => void /** Switches the active diagram to another one of the user's own. */ switchToDiagram: (id: string) => Promise /** Permanently deletes a diagram. If it's the currently open one, falls @@ -132,17 +132,17 @@ interface ProjectStoreState { removeConnection: (id: string) => void } -function touch(project: Project): Project { - return { ...project, updatedAt: new Date().toISOString() } +function touch(diagram: Diagram): Diagram { + return { ...diagram, updatedAt: new Date().toISOString() } } -export const useProjectStore = create((set, get) => ({ - project: createEmptyProject(), +export const useDiagramStore = create((set, get) => ({ + diagram: createEmptyDiagram(), isLoaded: false, diagramList: [], access: null, restoredFromSnapshotId: null, - restoredProjectUpdatedAt: null, + restoredDiagramUpdatedAt: null, selectedDeviceId: null, selectedConnectionId: null, lastConnectionError: null, @@ -151,10 +151,10 @@ export const useProjectStore = create((set, get) => ({ loadInitialDiagram: async () => { const lastId = getLastDiagramId() const loaded = lastId ? await repository.loadById(lastId) : null - const project = loaded ?? (await loadMostRecent()) ?? createEmptyProject() - setLastDiagramId(project.id) - const access = await repository.getAccess(project.id) - set({ project, access, isLoaded: true, restoredFromSnapshotId: null, restoredProjectUpdatedAt: null }) + const diagram = loaded ?? (await loadMostRecent()) ?? createEmptyDiagram() + setLastDiagramId(diagram.id) + const access = await repository.getAccess(diagram.id) + set({ diagram, access, isLoaded: true, restoredFromSnapshotId: null, restoredDiagramUpdatedAt: null }) get().refreshDiagramList() }, @@ -163,45 +163,45 @@ export const useProjectStore = create((set, get) => ({ set({ diagramList }) }, - newProject: async (name = 'Untitled Project') => { - const project = createEmptyProject(name) - setLastDiagramId(project.id) + newDiagram: async (name = 'Untitled Diagram') => { + const diagram = createEmptyDiagram(name) + setLastDiagramId(diagram.id) set({ - project, + diagram, selectedDeviceId: null, selectedConnectionId: null, restoredFromSnapshotId: null, - restoredProjectUpdatedAt: null, + restoredDiagramUpdatedAt: null, }) - await repository.save(project) - const access = await repository.getAccess(project.id) + await repository.save(diagram) + const access = await repository.getAccess(diagram.id) set({ access }) get().refreshDiagramList() }, - renameProject: (name) => { - set((state) => ({ project: touch({ ...state.project, name }) })) + renameDiagram: (name) => { + set((state) => ({ diagram: touch({ ...state.diagram, name }) })) }, - applyRestoredProject: (project, snapshotId) => { + applyRestoredDiagram: (diagram, snapshotId) => { set({ - project, + diagram, selectedDeviceId: null, selectedConnectionId: null, restoredFromSnapshotId: snapshotId, - restoredProjectUpdatedAt: project.updatedAt, + restoredDiagramUpdatedAt: diagram.updatedAt, }) }, - importProject: async (project) => { - const fresh = touch({ ...project, id: uuid() }) + importDiagram: async (diagram) => { + const fresh = touch({ ...diagram, id: uuid() }) setLastDiagramId(fresh.id) set({ - project: fresh, + diagram: fresh, selectedDeviceId: null, selectedConnectionId: null, restoredFromSnapshotId: null, - restoredProjectUpdatedAt: null, + restoredDiagramUpdatedAt: null, }) await repository.save(fresh) const access = await repository.getAccess(fresh.id) @@ -210,34 +210,34 @@ export const useProjectStore = create((set, get) => ({ }, switchToDiagram: async (id) => { - if (id === get().project.id) return + if (id === get().diagram.id) return const loaded = await repository.loadById(id) if (!loaded) return setLastDiagramId(loaded.id) const access = await repository.getAccess(loaded.id) set({ - project: loaded, + diagram: loaded, access, selectedDeviceId: null, selectedConnectionId: null, restoredFromSnapshotId: null, - restoredProjectUpdatedAt: null, + restoredDiagramUpdatedAt: null, }) }, deleteDiagram: async (id) => { await repository.deleteById(id) - if (get().project.id === id) { - const next = (await loadMostRecent()) ?? createEmptyProject() + if (get().diagram.id === id) { + const next = (await loadMostRecent()) ?? createEmptyDiagram() setLastDiagramId(next.id) const access = await repository.getAccess(next.id) set({ - project: next, + diagram: next, access, selectedDeviceId: null, selectedConnectionId: null, restoredFromSnapshotId: null, - restoredProjectUpdatedAt: null, + restoredDiagramUpdatedAt: null, }) } get().refreshDiagramList() @@ -247,37 +247,37 @@ export const useProjectStore = create((set, get) => ({ selectConnection: (id) => set({ selectedConnectionId: id, selectedDeviceId: null }), addDeviceFromTemplate: (catalog, templateId, position) => { - const { project } = get() + const { diagram } = get() const template = allDeviceTemplates(catalog).find((t) => t.id === templateId) if (!template) return const device = createDeviceFromTemplate(template, position) - set({ project: touch({ ...project, devices: [...project.devices, device] }), selectedDeviceId: device.id }) + set({ diagram: touch({ ...diagram, devices: [...diagram.devices, device] }), selectedDeviceId: device.id }) }, updateDevicePosition: (id, position) => { set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => (d.id === id ? { ...d, position } : d)), + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => (d.id === id ? { ...d, position } : d)), }), })) }, updateDevice: (id, patch) => { set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => (d.id === id ? { ...d, ...patch } : d)), + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => (d.id === id ? { ...d, ...patch } : d)), }), })) }, removeDevice: (id) => { set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.filter((d) => d.id !== id), - connections: state.project.connections.filter( + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.filter((d) => d.id !== id), + connections: state.diagram.connections.filter( (c) => c.sourceDeviceId !== id && c.targetDeviceId !== id, ), }), @@ -288,9 +288,9 @@ export const useProjectStore = create((set, get) => ({ addPort: (deviceId, port) => { const newId = uuid() set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => d.id === deviceId ? { ...d, ports: [...d.ports, { ...port, id: newId }] } : d, ), }), @@ -300,9 +300,9 @@ export const useProjectStore = create((set, get) => ({ updatePort: (deviceId, portId, patch) => { set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => d.id === deviceId ? { ...d, ports: d.ports.map((p) => (p.id === portId ? { ...p, ...patch } : p)) } : d, @@ -314,9 +314,9 @@ export const useProjectStore = create((set, get) => ({ clonePort: (deviceId, portId) => { const newId = uuid() set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => { + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => { if (d.id !== deviceId) return d const index = d.ports.findIndex((p) => p.id === portId) if (index === -1) return d @@ -331,12 +331,12 @@ export const useProjectStore = create((set, get) => ({ removePort: (deviceId, portId) => { set((state) => ({ - project: touch({ - ...state.project, - devices: state.project.devices.map((d) => + diagram: touch({ + ...state.diagram, + devices: state.diagram.devices.map((d) => d.id === deviceId ? { ...d, ports: d.ports.filter((p) => p.id !== portId) } : d, ), - connections: state.project.connections.filter( + connections: state.diagram.connections.filter( (c) => c.sourcePortId !== portId && c.targetPortId !== portId, ), }), @@ -344,8 +344,8 @@ export const useProjectStore = create((set, get) => ({ }, addConnection: (catalog, { sourceDeviceId, sourcePortId, targetDeviceId, targetPortId, cableTypeId }) => { - const { project } = get() - const validation = validateConnection(project, catalog, { + const { diagram } = get() + const validation = validateConnection(diagram, catalog, { sourceDeviceId, sourcePortId, targetDeviceId, @@ -364,7 +364,7 @@ export const useProjectStore = create((set, get) => ({ cableTypeId, } set({ - project: touch({ ...project, connections: [...project.connections, connection] }), + diagram: touch({ ...diagram, connections: [...diagram.connections, connection] }), lastConnectionError: null, selectedConnectionId: connection.id, selectedDeviceId: null, @@ -374,18 +374,18 @@ export const useProjectStore = create((set, get) => ({ updateConnection: (id, patch) => { set((state) => ({ - project: touch({ - ...state.project, - connections: state.project.connections.map((c) => (c.id === id ? { ...c, ...patch } : c)), + diagram: touch({ + ...state.diagram, + connections: state.diagram.connections.map((c) => (c.id === id ? { ...c, ...patch } : c)), }), })) }, removeConnection: (id) => { set((state) => ({ - project: touch({ - ...state.project, - connections: state.project.connections.filter((c) => c.id !== id), + diagram: touch({ + ...state.diagram, + connections: state.diagram.connections.filter((c) => c.id !== id), }), selectedConnectionId: state.selectedConnectionId === id ? null : state.selectedConnectionId, })) @@ -393,22 +393,22 @@ export const useProjectStore = create((set, get) => ({ })) -// Debounced autosave: fires whenever `project` changes (skipped until the +// Debounced autosave: fires whenever `diagram` changes (skipped until the // initial load completes, so we don't immediately overwrite storage with a -// blank project on first mount). +// blank diagram on first mount). let saveTimeout: ReturnType | undefined -useProjectStore.subscribe((state, prevState) => { +useDiagramStore.subscribe((state, prevState) => { // Skip the isLoaded:false -> true transition itself (that's the initial - // load, not a user edit) and any tick where `project` didn't change. + // load, not a user edit) and any tick where `diagram` didn't change. if (!prevState.isLoaded) return - if (state.project === prevState.project) return + if (state.diagram === prevState.diagram) return // Defense in depth: the UI shouldn't expose any mutating control to a // view-only collaborator in the first place (see FlowCanvas/inspectors), // but skip the write here too rather than relying on RLS's silent no-op. if (state.access?.myPermission === 'view') return if (saveTimeout) clearTimeout(saveTimeout) saveTimeout = setTimeout(() => { - repository.save(state.project).catch((err) => console.error('Autosave failed', err)) + repository.save(state.diagram).catch((err) => console.error('Autosave failed', err)) }, 400) }) diff --git a/supabase/migrations/20260910010000_admin_review_queue.sql b/supabase/migrations/20260910010000_admin_review_queue.sql index e25ad2f..4c863f6 100644 --- a/supabase/migrations/20260910010000_admin_review_queue.sql +++ b/supabase/migrations/20260910010000_admin_review_queue.sql @@ -26,7 +26,7 @@ create policy "catalog_submissions_insert" on public.catalog_submissions for ins -- ---------------------------------------------------------------------- -- Usage-impact aggregate function. -- --- A diagram's `data` JSONB mirrors the exported Project shape (see +-- A diagram's `data` JSONB mirrors the exported Diagram shape (see -- data/exportImport.ts / domain/types.ts): devices[].templateId, -- devices[].category, devices[].ports[].portTypeId, and -- connections[].cableTypeId are the four places a catalog entity id can be