Rename Project to Diagram throughout the app

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.
This commit is contained in:
2026-09-11 12:10:11 -05:00
parent cbbe96f575
commit 26f41b3d4a
34 changed files with 292 additions and 293 deletions
+9 -15
View File
@@ -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<DiagramSummary[]>
/** One diagram's full data by id, or null if it doesn't exist / isn't visible. */
loadById(id: string): Promise<Project | null>
/** Upsert a diagram, matched by the project's own id. */
save(project: Project): Promise<void>
loadById(id: string): Promise<Diagram | null>
/** Upsert a diagram, matched by its own id. */
save(diagram: Diagram): Promise<void>
/** Permanently delete one diagram by id. */
deleteById(id: string): Promise<void>
/** Who owns this diagram and what the current user can do with it. */
+3 -3
View File
@@ -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<Project | null>
restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise<Diagram | null>
}
+12 -11
View File
@@ -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<string, Project>
type StoredDiagrams = Record<string, Diagram>
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<DiagramSummary[]> {
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<Project | null> {
async loadById(id: string): Promise<Diagram | null> {
const diagram = readAll()[id]
return diagram ? normalizeProject(diagram) : null
return diagram ? normalizeDiagram(diagram) : null
}
async save(project: Project): Promise<void> {
async save(diagram: Diagram): Promise<void> {
const all = readAll()
all[project.id] = project
all[diagram.id] = diagram
writeAll(all)
}
+12 -12
View File
@@ -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<Project | null> {
async loadById(id: string): Promise<Diagram | null> {
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<void> {
async save(diagram: Diagram): Promise<void> {
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<void> {
private async maybeWriteSnapshot(diagramId: string, previousData: Diagram, userId: string): Promise<void> {
const { data: lastSnapshot } = await supabase
.from('diagram_snapshots')
.select('created_at')
@@ -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<Project | null> {
async restore(diagramId: string, snapshotId: string, writeSafetySnapshot: boolean): Promise<Diagram | null> {
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() })
+22 -22
View File
@@ -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<Project>
if (typeof p.id !== 'string' || typeof p.name !== 'string') {
throw new ProjectImportError('File is missing required project fields.')
const d = value as Partial<Diagram>
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<Project> {
export async function readDiagramFile(file: File): Promise<Diagram> {
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 }