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:
co-authored by
Claude Sonnet 4.6
parent
1166ea5b3f
commit
d14d767c75
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user