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:
@@ -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. */
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user