Add multi-diagram management
- DiagramRepository interface redesigned around multiple diagrams: list()/loadById()/deleteById() replace the old single-diagram load()/clear(). Both implementations updated to match. - SupabaseDiagramRepository.save() now does an explicit update-or-insert instead of a blind upsert, so owner_id is only ever set at creation -- an upsert would resend it on every save and let whoever saves last silently reassign ownership. Not reachable yet (no collaborator UI), but a real landmine once diagram sharing (organized-ideas.md §8) lands, and cheap to avoid now. - LocalStorageDiagramRepository now stores diagrams keyed by id (was a single fixed key), keeping it a genuine working fallback rather than a stale reference implementing an old interface. - Store: loadInitialDiagram (renamed from loadFromStorage) opens the last diagram you had open (tracked in localStorage -- a UI preference, not app data), falling back to the most recently updated one, falling back to a fresh empty diagram. New actions: refreshDiagramList, switchToDiagram, deleteDiagram. newProject and importProject now persist immediately (not just via the debounced autosave) so a new/imported diagram shows up in the list right away; importProject also assigns a fresh id so it can't collide with an existing diagram. - New DiagramManagerModal (list/open/delete/+New), opened from a "Diagrams" button in TopBar that replaces the old single-diagram "New" button and its now-unnecessary confirmation dialog -- nothing is lost by creating a new diagram anymore, since the old one stays saved and reachable from the list. Verified insert/list/update(-preserves-owner)/loadById/delete against the real local stack; RLS test suite still 23/23 after a fresh reset. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useProjectStore } from '../../state/projectStore'
|
||||
import Modal from '../common/Modal'
|
||||
|
||||
function formatUpdatedAt(iso: string): string {
|
||||
const date = new Date(iso)
|
||||
return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
refreshDiagramList()
|
||||
}, [refreshDiagramList])
|
||||
|
||||
const handleOpen = async (id: string) => {
|
||||
if (id === project.id) return
|
||||
await switchToDiagram(id)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleNew = async () => {
|
||||
await newProject()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string, name: string) => {
|
||||
const confirmed = window.confirm(
|
||||
diagramList.length === 1
|
||||
? `Delete "${name}"? This is your only diagram, so a fresh empty one will replace it.`
|
||||
: `Delete "${name}"? This can't be undone.`,
|
||||
)
|
||||
if (!confirmed) return
|
||||
await deleteDiagram(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="Diagrams" onClose={onClose} width="max-w-lg">
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleNew}
|
||||
className="w-full rounded bg-indigo-600 py-1.5 text-xs font-medium text-white hover:bg-indigo-500"
|
||||
>
|
||||
+ New Diagram
|
||||
</button>
|
||||
|
||||
<ul className="space-y-1">
|
||||
{diagramList.map((d) => {
|
||||
const isCurrent = d.id === project.id
|
||||
return (
|
||||
<li
|
||||
key={d.id}
|
||||
className={`group flex items-center justify-between gap-2 rounded border px-3 py-2 text-xs ${
|
||||
isCurrent ? 'border-indigo-300 bg-indigo-50' : 'border-slate-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleOpen(d.id)}
|
||||
disabled={isCurrent}
|
||||
className="min-w-0 flex-1 text-left disabled:cursor-default"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium text-slate-700">{d.name}</span>
|
||||
{isCurrent && (
|
||||
<span className="shrink-0 rounded bg-indigo-100 px-1 text-[10px] font-medium text-indigo-700">
|
||||
current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[10px] text-slate-400">Updated {formatUpdatedAt(d.updatedAt)}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(d.id, d.name)}
|
||||
title="Delete this diagram"
|
||||
className="shrink-0 rounded px-1.5 py-0.5 text-slate-400 opacity-0 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{diagramList.length === 0 && (
|
||||
<p className="py-2 text-center text-xs italic text-slate-400">No diagrams yet.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user