From 04c3bc6eb13bd0a9eebdb6a263e7f5d0994d6f18 Mon Sep 17 00:00:00 2001 From: "molecule-ai[bot]" <276602405+molecule-ai[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:25:12 +0000 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20cascade-delete=20UX=20=E2=80=94?= =?UTF-8?q?=20warn=20before=20deleting=20workspace=20with=20children=20(PR?= =?UTF-8?q?=20#1252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Store: pendingDelete now carries `hasChildren: boolean` (computed from nodes.some(parentId === nodeId)) - ContextMenu: passes hasChildren into setPendingDelete - Canvas: dialog title changes to "Delete Workspace and Children" with ⚠️ message when hasChildren; confirms with "Delete All" Refs: #1137 Co-authored-by: Molecule AI Fullstack (floater) --- canvas/src/components/Canvas.tsx | 14 +++++++++++--- canvas/src/components/ContextMenu.tsx | 2 +- canvas/src/store/canvas.ts | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/canvas/src/components/Canvas.tsx b/canvas/src/components/Canvas.tsx index 16335608..c194e08f 100644 --- a/canvas/src/components/Canvas.tsx +++ b/canvas/src/components/Canvas.tsx @@ -117,6 +117,12 @@ function CanvasInner() { } }, [pendingDelete, setPendingDelete, removeNode]); + // Cascade guard: include child count in the warning message when the workspace + // has children, so the user understands the blast radius before clicking Delete All. + const cascadeMessage = pendingDelete?.hasChildren + ? `⚠️ Deleting "${pendingDelete.name}" will permanently delete all child workspaces and their data. This cannot be undone.` + : null; + const onNodeDragStop: OnNodeDrag> = useCallback( (_event, node) => { const { dragOverNodeId, nodes: allNodes } = useCanvasStore.getState(); @@ -381,9 +387,11 @@ function CanvasInner() { {/* Confirmation dialog for workspace delete — driven by store */} setPendingDelete(null)} diff --git a/canvas/src/components/ContextMenu.tsx b/canvas/src/components/ContextMenu.tsx index 34c17b01..3b54cc17 100644 --- a/canvas/src/components/ContextMenu.tsx +++ b/canvas/src/components/ContextMenu.tsx @@ -164,7 +164,7 @@ export function ContextMenu() { // it survives ContextMenu unmount. Closing the menu here avoids the // prior race where the portal dialog's Confirm click was treated as // "outside" by the menu's outside-click handler. - setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name }); + setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name, hasChildren }); closeContextMenu(); }, [contextMenu, setPendingDelete, closeContextMenu]); diff --git a/canvas/src/store/canvas.ts b/canvas/src/store/canvas.ts index 7c7cc314..692b5bdb 100644 --- a/canvas/src/store/canvas.ts +++ b/canvas/src/store/canvas.ts @@ -72,8 +72,8 @@ interface CanvasState { // handler: clicking Confirm registered as "outside", closed the menu, and // unmounted the dialog before its onClick fired. Hoisting the state fixes // that — see fix/context-menu-delete-race. - pendingDelete: { id: string; name: string } | null; - setPendingDelete: (v: { id: string; name: string } | null) => void; + pendingDelete: { id: string; name: string; hasChildren: boolean } | null; + setPendingDelete: (v: { id: string; name: string; hasChildren: boolean } | null) => void; searchOpen: boolean; setSearchOpen: (open: boolean) => void; viewport: { x: number; y: number; zoom: number };