feat(tenants): add workspace delete UI and fix project switch element loading (#11)

- Add DELETE /api/tenants/:id endpoint with cascade (projects, elements, snapshots)
- Add delete buttons with inline confirm in workspace switcher panel
- Fix project switch not loading elements: switchProjectUI now directly clears
  canvas and calls loadExistingElements() instead of relying on WS roundtrip

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxime Roy (new.blacc)
2026-04-06 14:37:11 +02:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 1166ea5b3f
commit d14d767c75
7 changed files with 113 additions and 15 deletions
+6
View File
@@ -502,6 +502,12 @@
}
.menu-create-btn:hover:not(:disabled) { background: #4c2fa8; }
.menu-create-btn:disabled { opacity: 0.5; cursor: default; }
.tenant-row {
position: relative;
display: flex;
align-items: stretch;
}
.tenant-row:hover .project-delete-btn { opacity: 0.5; }
.project-row {
position: relative;
display: flex;
+63 -13
View File
@@ -133,6 +133,7 @@ function App(): JSX.Element {
const [isCreatingProject, setIsCreatingProject] = useState<boolean>(false)
const newProjectInputRef = useRef<HTMLInputElement | null>(null)
const [confirmDeleteProjectId, setConfirmDeleteProjectId] = useState<string | null>(null)
const [confirmDeleteTenantId, setConfirmDeleteTenantId] = useState<string | null>(null)
// Keep refs in sync so closures (WebSocket handlers) always see latest values
useEffect(() => {
@@ -1241,6 +1242,16 @@ function App(): JSX.Element {
if (data.success) {
setActiveProject({ id: data.project.id, name: data.project.name })
setProjectMenuOpen(false)
// Clear canvas and load the new project's elements directly
// (don't rely on WS roundtrip which can race)
const api = excalidrawAPIRef.current
if (api) {
api.updateScene({ elements: [], captureUpdate: CaptureUpdateAction.NEVER })
lastSyncedHashRef.current = ''
lastSeenHashRef.current = ''
lastSyncedElementsRef.current = new Map()
}
await loadExistingElements()
showToast(`Switched to "${data.project.name}"`)
}
} catch (err) {
@@ -1293,6 +1304,27 @@ function App(): JSX.Element {
}
}
const deleteTenantUI = async (tenantId: string) => {
try {
const res = await fetch(`/api/tenants/${tenantId}`, {
method: 'DELETE',
headers: tenantHeaders()
})
const data = await res.json()
if (data.success) {
setConfirmDeleteTenantId(null)
setTenantList(prev => prev.filter(t => t.id !== tenantId))
showToast('Workspace deleted')
} else {
showToast(data.error ?? 'Delete failed', 4000)
setConfirmDeleteTenantId(null)
}
} catch (err) {
console.error('Failed to delete tenant:', err)
setConfirmDeleteTenantId(null)
}
}
const syncToBackend = async (): Promise<void> => {
if (!excalidrawAPI || isSyncingRef.current) return
@@ -1553,19 +1585,37 @@ function App(): JSX.Element {
</div>
<div className="menu-list">
{filtered.map(t => (
<button
key={t.id}
className={`menu-item ${activeTenant?.id === t.id ? 'menu-item-active' : ''}`}
onClick={() => switchTenant(t.id)}
>
<span className="menu-item-name">{t.name}</span>
<span className="menu-item-path" title={t.workspace_path}>
{t.workspace_path.length > 40
? '...' + t.workspace_path.slice(-37)
: t.workspace_path}
</span>
{activeTenant?.id === t.id && <span className="menu-item-check"></span>}
</button>
<div key={t.id} className="tenant-row">
{confirmDeleteTenantId === t.id ? (
<div className="project-delete-confirm">
<span className="project-delete-msg">Delete "{t.name}"?</span>
<button className="project-delete-yes" onClick={() => deleteTenantUI(t.id)}>Delete</button>
<button className="project-delete-no" onClick={() => setConfirmDeleteTenantId(null)}>Cancel</button>
</div>
) : (
<>
<button
className={`menu-item ${activeTenant?.id === t.id ? 'menu-item-active' : ''}`}
onClick={() => switchTenant(t.id)}
>
<span className="menu-item-name">{t.name}</span>
<span className="menu-item-path" title={t.workspace_path}>
{t.workspace_path.length > 40
? '...' + t.workspace_path.slice(-37)
: t.workspace_path}
</span>
{activeTenant?.id === t.id && <span className="menu-item-check"></span>}
</button>
{activeTenant?.id !== t.id && (
<button
className="project-delete-btn"
title="Delete workspace"
onClick={e => { e.stopPropagation(); setConfirmDeleteTenantId(t.id) }}
>×</button>
)}
</>
)}
</div>
))}
{filtered.length === 0 && <div className="menu-empty">No matching workspaces</div>}
</div>