From e6837fd200edcb3e6270eb04a8e63832ebc5532e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 15:33:11 -0700 Subject: [PATCH 01/11] fix(desktop): toast and switch workspace when deleting in v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Destroy can take 10–20s; showing no feedback and leaving the user on the workspace being torn down felt broken. Show a loading toast that resolves to success/error, and navigate to a sibling workspace (or home) the moment destroy kicks off instead of waiting for it to finish. --- .../DashboardSidebarDeleteDialog.tsx | 4 +++ .../useDestroyDialogState.ts | 25 ++++++++++----- .../DashboardSidebarWorkspaceItem.tsx | 2 ++ ...useDashboardSidebarWorkspaceItemActions.ts | 31 +++++++++++-------- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 2dea5a47ecc..028961183c2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,6 +9,8 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; + /** Fires the moment destroy kicks off, so callers can navigate away. */ + onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -24,6 +26,7 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, + onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -37,6 +40,7 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 413a8489ee7..de8948609bc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -10,6 +10,7 @@ interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; + onDeleting?: () => void; onDeleted?: () => void; } @@ -18,21 +19,23 @@ interface UseDestroyDialogStateOptions { * * UX pattern: * - On confirm, close the dialog immediately, mark the workspace as - * deleting (sidebar row hides optimistically), and run destroy in - * the background silently. No loading toast — destroy can take - * 10–20s and a persistent toast across that window feels bad. The - * hidden row is the feedback. - * - On success, `onDeleted` removes the row from sidebar state. + * deleting (sidebar row hides optimistically), show a loading toast, + * and fire `onDeleting` so the caller can immediately navigate off + * the deleted workspace (don't leave the user staring at a workspace + * that's being torn down). Destroy itself runs in the background. + * - On success, the loading toast resolves to success and `onDeleted` + * removes the row from sidebar state. * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) * we reopen the dialog in the matching error pane so the user can * force-retry with full context. The branch opt-in is preserved. - * - For unknown errors we just toast.error — no reopen. + * - For unknown errors the loading toast resolves to error — no reopen. */ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); @@ -68,19 +71,26 @@ export function useDestroyDialogState({ setError(null); onOpenChange(false); markDeleting(workspaceId); + onDeleting?.(); + + const toastId = toast.loading(`Deleting "${workspaceName}"...`); try { const result = await destroy({ deleteBranch, force }); + toast.success(`Deleted "${workspaceName}"`, { id: toastId }); for (const warning of result.warnings) toast.warning(warning); setDeleteBranch(false); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; if (e.kind === "conflict" || e.kind === "teardown-failed") { + toast.dismiss(toastId); setError(e); onOpenChange(true); } else { - toast.error(`Failed to delete ${workspaceName}: ${e.message}`); + toast.error(`Failed to delete ${workspaceName}: ${e.message}`, { + id: toastId, + }); } } finally { clearDeleting(workspaceId); @@ -93,6 +103,7 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, + onDeleting, onDeleted, markDeleting, clearDeleting, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 750c00c58c8..db7826b396a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -41,6 +41,7 @@ export function DashboardSidebarWorkspaceItem({ handleCopyPath, handleCopyBranchName, handleCreateSection, + handleDeleting, handleDeleted, handleOpenInFinder, isActive, @@ -139,6 +140,7 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} + onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 3eb9f4f059f..a1d4d20a58b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -78,21 +78,17 @@ export function useDashboardSidebarWorkspaceItemActions({ }; /** - * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from - * the sidebar and, if we were viewing the deleted workspace, navigates - * to the next sibling or home. + * Runs the moment destroy kicks off. If we were viewing the deleted + * workspace, navigate immediately to a sibling (or home) so the user + * isn't stuck on a workspace that's being torn down. Destroy itself + * can take 10–20s. */ - const handleDeleted = () => { - const focusTargetId = isActive - ? getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ) - : null; - - removeWorkspaceFromSidebar(workspaceId); - + const handleDeleting = () => { if (!isActive) return; + const focusTargetId = getDeleteFocusTargetWorkspaceId( + getFlattenedV2WorkspaceIds(collections), + workspaceId, + ); if (focusTargetId) { void navigateToV2Workspace(focusTargetId, navigate); } else { @@ -100,6 +96,14 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; + /** + * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from + * sidebar state (the row was already hidden optimistically). + */ + const handleDeleted = () => { + removeWorkspaceFromSidebar(workspaceId); + }; + const handleCreateSection = () => { createSection(projectId, { insertAfterWorkspaceId: workspaceId, @@ -167,6 +171,7 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyPath, handleCopyBranchName, handleCreateSection, + handleDeleting, handleDeleted, handleOpenInFinder, isActive, From 1df79601f117843df52c5da75a112eed9907e3e9 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 21:38:36 -0700 Subject: [PATCH 02/11] fix(desktop): simplify to fire-and-forget info toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A loading toast that persists for 10–20s and then resolves to success is noisier than the problem warrants. Just fire an info toast at start; success is already conveyed by the row disappearing. --- .../useDestroyDialogState.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index de8948609bc..d097a611cd4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -19,17 +19,16 @@ interface UseDestroyDialogStateOptions { * * UX pattern: * - On confirm, close the dialog immediately, mark the workspace as - * deleting (sidebar row hides optimistically), show a loading toast, + * deleting (sidebar row hides optimistically), fire an info toast, * and fire `onDeleting` so the caller can immediately navigate off * the deleted workspace (don't leave the user staring at a workspace * that's being torn down). Destroy itself runs in the background. - * - On success, the loading toast resolves to success and `onDeleted` - * removes the row from sidebar state. + * - On success, `onDeleted` removes the row from sidebar state. * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) * we reopen the dialog in the matching error pane so the user can * force-retry with full context. The branch opt-in is preserved. - * - For unknown errors the loading toast resolves to error — no reopen. + * - For unknown errors we just toast.error — no reopen. */ export function useDestroyDialogState({ workspaceId, @@ -72,25 +71,20 @@ export function useDestroyDialogState({ onOpenChange(false); markDeleting(workspaceId); onDeleting?.(); - - const toastId = toast.loading(`Deleting "${workspaceName}"...`); + toast(`Deleting "${workspaceName}"...`); try { const result = await destroy({ deleteBranch, force }); - toast.success(`Deleted "${workspaceName}"`, { id: toastId }); for (const warning of result.warnings) toast.warning(warning); setDeleteBranch(false); onDeleted?.(); } catch (err) { const e = err as DestroyWorkspaceError; if (e.kind === "conflict" || e.kind === "teardown-failed") { - toast.dismiss(toastId); setError(e); onOpenChange(true); } else { - toast.error(`Failed to delete ${workspaceName}: ${e.message}`, { - id: toastId, - }); + toast.error(`Failed to delete ${workspaceName}: ${e.message}`); } } finally { clearDeleting(workspaceId); From 854c6e45c06d6185d5cc1a3b89cfd2f8e7fe373d Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 21:54:37 -0700 Subject: [PATCH 03/11] revert: drop workspace-switch-on-delete, keep only the deleting toast Reverts the handleDeleting split and onDeleting plumbing introduced in e6837fd20 / 1df79601f. Only change now is a single toast fired at the start of the destroy flow. --- .../DashboardSidebarDeleteDialog.tsx | 4 --- .../useDestroyDialogState.ts | 12 +++---- .../DashboardSidebarWorkspaceItem.tsx | 2 -- ...useDashboardSidebarWorkspaceItemActions.ts | 31 ++++++++----------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 028961183c2..2dea5a47ecc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,8 +9,6 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; - /** Fires the moment destroy kicks off, so callers can navigate away. */ - onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -26,7 +24,6 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, - onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -40,7 +37,6 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index d097a611cd4..00e6e958869 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -10,7 +10,6 @@ interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; - onDeleting?: () => void; onDeleted?: () => void; } @@ -19,10 +18,10 @@ interface UseDestroyDialogStateOptions { * * UX pattern: * - On confirm, close the dialog immediately, mark the workspace as - * deleting (sidebar row hides optimistically), fire an info toast, - * and fire `onDeleting` so the caller can immediately navigate off - * the deleted workspace (don't leave the user staring at a workspace - * that's being torn down). Destroy itself runs in the background. + * deleting (sidebar row hides optimistically), and run destroy in + * the background silently. No loading toast — destroy can take + * 10–20s and a persistent toast across that window feels bad. The + * hidden row is the feedback. * - On success, `onDeleted` removes the row from sidebar state. * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) @@ -34,7 +33,6 @@ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); @@ -70,7 +68,6 @@ export function useDestroyDialogState({ setError(null); onOpenChange(false); markDeleting(workspaceId); - onDeleting?.(); toast(`Deleting "${workspaceName}"...`); try { @@ -97,7 +94,6 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, - onDeleting, onDeleted, markDeleting, clearDeleting, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index db7826b396a..750c00c58c8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -41,7 +41,6 @@ export function DashboardSidebarWorkspaceItem({ handleCopyPath, handleCopyBranchName, handleCreateSection, - handleDeleting, handleDeleted, handleOpenInFinder, isActive, @@ -140,7 +139,6 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} - onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index a1d4d20a58b..3eb9f4f059f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -78,17 +78,21 @@ export function useDashboardSidebarWorkspaceItemActions({ }; /** - * Runs the moment destroy kicks off. If we were viewing the deleted - * workspace, navigate immediately to a sibling (or home) so the user - * isn't stuck on a workspace that's being torn down. Destroy itself - * can take 10–20s. + * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from + * the sidebar and, if we were viewing the deleted workspace, navigates + * to the next sibling or home. */ - const handleDeleting = () => { + const handleDeleted = () => { + const focusTargetId = isActive + ? getDeleteFocusTargetWorkspaceId( + getFlattenedV2WorkspaceIds(collections), + workspaceId, + ) + : null; + + removeWorkspaceFromSidebar(workspaceId); + if (!isActive) return; - const focusTargetId = getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ); if (focusTargetId) { void navigateToV2Workspace(focusTargetId, navigate); } else { @@ -96,14 +100,6 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; - /** - * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from - * sidebar state (the row was already hidden optimistically). - */ - const handleDeleted = () => { - removeWorkspaceFromSidebar(workspaceId); - }; - const handleCreateSection = () => { createSection(projectId, { insertAfterWorkspaceId: workspaceId, @@ -171,7 +167,6 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyPath, handleCopyBranchName, handleCreateSection, - handleDeleting, handleDeleted, handleOpenInFinder, isActive, From 53f487c91d20437ef576002fb47a562f141af9ba Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 21:57:48 -0700 Subject: [PATCH 04/11] fix(desktop): navigate off workspace early on delete or hide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete takes 10–20s; hide is immediate. In both cases we don't want to leave the user staring at a row that's being torn down or hidden. Fires `onDeleting` at the start of destroy (before teardown completes) and wraps `removeWorkspaceFromSidebar` to nav first, then remove. --- .../DashboardSidebarDeleteDialog.tsx | 4 ++ .../useDestroyDialogState.ts | 4 ++ .../DashboardSidebarWorkspaceItem.tsx | 8 ++-- ...useDashboardSidebarWorkspaceItemActions.ts | 40 ++++++++++++------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 2dea5a47ecc..028961183c2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,6 +9,8 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; + /** Fires the moment destroy kicks off, so callers can navigate away. */ + onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -24,6 +26,7 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, + onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -37,6 +40,7 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 00e6e958869..1cb7d518496 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -10,6 +10,7 @@ interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; + onDeleting?: () => void; onDeleted?: () => void; } @@ -33,6 +34,7 @@ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); @@ -68,6 +70,7 @@ export function useDestroyDialogState({ setError(null); onOpenChange(false); markDeleting(workspaceId); + onDeleting?.(); toast(`Deleting "${workspaceName}"...`); try { @@ -94,6 +97,7 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, + onDeleting, onDeleted, markDeleting, clearDeleting, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 750c00c58c8..7a9b32fc43d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -41,13 +41,14 @@ export function DashboardSidebarWorkspaceItem({ handleCopyPath, handleCopyBranchName, handleCreateSection, + handleDeleting, handleDeleted, handleOpenInFinder, + handleRemoveFromSidebar, isActive, isDeleteDialogOpen, isRenaming, moveWorkspaceToSection, - removeWorkspaceFromSidebar, renameValue, setIsDeleteDialogOpen, setRenameValue, @@ -124,7 +125,7 @@ export function DashboardSidebarWorkspaceItem({ onOpenInFinder={handleOpenInFinder} onCopyPath={handleCopyPath} onCopyBranchName={handleCopyBranchName} - onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} + onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > @@ -139,6 +140,7 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} + onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} @@ -189,7 +191,7 @@ export function DashboardSidebarWorkspaceItem({ onOpenInFinder={handleOpenInFinder} onCopyPath={handleCopyPath} onCopyBranchName={handleCopyBranchName} - onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} + onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 3eb9f4f059f..e8ce6a3b97a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -78,21 +78,16 @@ export function useDashboardSidebarWorkspaceItemActions({ }; /** - * Runs after `workspaceCleanup.destroy` succeeds. Removes the row from - * the sidebar and, if we were viewing the deleted workspace, navigates - * to the next sibling or home. + * If the user is currently viewing this workspace, jump to a sibling + * (or home if none). Shared by delete + hide — in both cases we must + * move the user off the row before it disappears. */ - const handleDeleted = () => { - const focusTargetId = isActive - ? getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ) - : null; - - removeWorkspaceFromSidebar(workspaceId); - + const navigateAwayIfActive = () => { if (!isActive) return; + const focusTargetId = getDeleteFocusTargetWorkspaceId( + getFlattenedV2WorkspaceIds(collections), + workspaceId, + ); if (focusTargetId) { void navigateToV2Workspace(focusTargetId, navigate); } else { @@ -100,6 +95,22 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; + /** + * Fires the moment destroy kicks off, before the 10–20s teardown, so + * the user isn't stuck staring at a workspace that's being removed. + */ + const handleDeleting = navigateAwayIfActive; + + /** Fires after `workspaceCleanup.destroy` succeeds. */ + const handleDeleted = () => { + removeWorkspaceFromSidebar(workspaceId); + }; + + const handleRemoveFromSidebar = () => { + navigateAwayIfActive(); + removeWorkspaceFromSidebar(workspaceId); + }; + const handleCreateSection = () => { createSection(projectId, { insertAfterWorkspaceId: workspaceId, @@ -167,13 +178,14 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyPath, handleCopyBranchName, handleCreateSection, + handleDeleting, handleDeleted, handleOpenInFinder, + handleRemoveFromSidebar, isActive, isDeleteDialogOpen, isRenaming, moveWorkspaceToSection, - removeWorkspaceFromSidebar, renameValue, setIsDeleteDialogOpen, setRenameValue, From c54ad9cedae68e8c52ae25eb732142c7f54b8528 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 22:42:24 -0700 Subject: [PATCH 05/11] fix(desktop): switch isActive to useParams so early-nav actually fires matchRoute with params + fuzzy was returning false for this item's hook when the user was viewing it, so navigateAwayIfActive no-op'd. useParams matches v1's pattern (useDeleteWorkspace) which is known to work. --- .../useDashboardSidebarWorkspaceItemActions.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index e8ce6a3b97a..aafe0ee60bf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -1,5 +1,5 @@ import { toast } from "@superset/ui/sonner"; -import { useMatchRoute, useNavigate } from "@tanstack/react-router"; +import { useNavigate, useParams } from "@tanstack/react-router"; import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; @@ -26,7 +26,7 @@ export function useDashboardSidebarWorkspaceItemActions({ branch, }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); - const matchRoute = useMatchRoute(); + const params = useParams({ strict: false }); const collections = useCollections(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); @@ -37,11 +37,7 @@ export function useDashboardSidebarWorkspaceItemActions({ const [renameValue, setRenameValue] = useState(workspaceName); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const isActive = !!matchRoute({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId }, - fuzzy: true, - }); + const isActive = params.workspaceId === workspaceId; const handleClick = () => { if (isRenaming) return; From 5077051de8d573ee01cba1a4056bce27bf5097f9 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 22:47:50 -0700 Subject: [PATCH 06/11] fix(desktop): pick the next visible sidebar workspace directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous attempts threaded an onDeleting callback through dialog props and relied on the sidebar item's isActive closure, which wasn't firing. Replace all that with useNavigateAwayFromWorkspace: reads the current URL's workspaceId via useParams, reads the sidebar list from collections, picks the first sibling that isn't the one being removed. Delete and Hide both call it directly — no callback plumbing. --- .../DashboardSidebarDeleteDialog.tsx | 4 --- .../useDestroyDialogState.ts | 8 ++--- .../DashboardSidebarWorkspaceItem.tsx | 2 -- ...useDashboardSidebarWorkspaceItemActions.ts | 35 ++----------------- .../useNavigateAwayFromWorkspace/index.ts | 1 + .../useNavigateAwayFromWorkspace.ts | 27 ++++++++++++++ 6 files changed, 35 insertions(+), 42 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 028961183c2..2dea5a47ecc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,8 +9,6 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; - /** Fires the moment destroy kicks off, so callers can navigate away. */ - onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -26,7 +24,6 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, - onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -40,7 +37,6 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 1cb7d518496..5f0a0aff9af 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -4,13 +4,13 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; - onDeleting?: () => void; onDeleted?: () => void; } @@ -34,11 +34,11 @@ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); + const navigateAway = useNavigateAwayFromWorkspace(); const [deleteBranch, setDeleteBranch] = useState(false); const [error, setError] = useState(null); @@ -70,7 +70,7 @@ export function useDestroyDialogState({ setError(null); onOpenChange(false); markDeleting(workspaceId); - onDeleting?.(); + navigateAway(workspaceId); toast(`Deleting "${workspaceName}"...`); try { @@ -97,10 +97,10 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, - onDeleting, onDeleted, markDeleting, clearDeleting, + navigateAway, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 7a9b32fc43d..0ba0972232c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -41,7 +41,6 @@ export function DashboardSidebarWorkspaceItem({ handleCopyPath, handleCopyBranchName, handleCreateSection, - handleDeleting, handleDeleted, handleOpenInFinder, handleRemoveFromSidebar, @@ -140,7 +139,6 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} - onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index aafe0ee60bf..f684f062d06 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -5,11 +5,8 @@ import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; -import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; -import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; interface UseDashboardSidebarWorkspaceItemActionsOptions { @@ -27,7 +24,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); const params = useParams({ strict: false }); - const collections = useCollections(); + const navigateAway = useNavigateAwayFromWorkspace(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = @@ -73,37 +70,12 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; - /** - * If the user is currently viewing this workspace, jump to a sibling - * (or home if none). Shared by delete + hide — in both cases we must - * move the user off the row before it disappears. - */ - const navigateAwayIfActive = () => { - if (!isActive) return; - const focusTargetId = getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ); - if (focusTargetId) { - void navigateToV2Workspace(focusTargetId, navigate); - } else { - void navigate({ to: "/" }); - } - }; - - /** - * Fires the moment destroy kicks off, before the 10–20s teardown, so - * the user isn't stuck staring at a workspace that's being removed. - */ - const handleDeleting = navigateAwayIfActive; - - /** Fires after `workspaceCleanup.destroy` succeeds. */ const handleDeleted = () => { removeWorkspaceFromSidebar(workspaceId); }; const handleRemoveFromSidebar = () => { - navigateAwayIfActive(); + navigateAway(workspaceId); removeWorkspaceFromSidebar(workspaceId); }; @@ -174,7 +146,6 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyPath, handleCopyBranchName, handleCreateSection, - handleDeleting, handleDeleted, handleOpenInFinder, handleRemoveFromSidebar, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts new file mode 100644 index 00000000000..debafab68e8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts @@ -0,0 +1 @@ +export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts new file mode 100644 index 00000000000..50ef86d8081 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts @@ -0,0 +1,27 @@ +import { useNavigate, useParams } from "@tanstack/react-router"; +import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { getFlattenedV2WorkspaceIds } from "../../utils/getFlattenedV2WorkspaceIds"; + +/** + * Returns a function that, given a workspaceId about to disappear from + * the sidebar (delete or hide), navigates the user off it to the next + * visible sibling — or home if none remain. No-op if the user isn't + * currently viewing the workspace being removed. + */ +export function useNavigateAwayFromWorkspace() { + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const collections = useCollections(); + + return (workspaceId: string) => { + if (params.workspaceId !== workspaceId) return; + const ids = getFlattenedV2WorkspaceIds(collections); + const next = ids.find((id) => id !== workspaceId); + if (next) { + void navigateToV2Workspace(next, navigate); + } else { + void navigate({ to: "/" }); + } + }; +} From 86e271271c42eeb5651a8309c61e86ad7e2b14d0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 22:55:00 -0700 Subject: [PATCH 07/11] refactor(desktop): use onDeleting callback instead of extracted hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the useNavigateAwayFromWorkspace hook. Nav logic stays inline in the sidebar item hook (where it already lived for the post-destroy path) and is shared between handleDeleting and handleRemoveFromSidebar. The dialog gets the nav via a new onDeleting callback mirroring onDeleted — same pattern, no hook extraction, zero new files. --- .../DashboardSidebarDeleteDialog.tsx | 3 ++ .../useDestroyDialogState.ts | 8 ++--- .../DashboardSidebarWorkspaceItem.tsx | 2 ++ ...useDashboardSidebarWorkspaceItemActions.ts | 33 +++++++++++++++---- .../useNavigateAwayFromWorkspace/index.ts | 1 - .../useNavigateAwayFromWorkspace.ts | 27 --------------- 6 files changed, 36 insertions(+), 38 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index 2dea5a47ecc..fb0dc5ad6c0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,6 +9,7 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; + onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -24,6 +25,7 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, + onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -37,6 +39,7 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 5f0a0aff9af..1cb7d518496 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -4,13 +4,13 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; -import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; + onDeleting?: () => void; onDeleted?: () => void; } @@ -34,11 +34,11 @@ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, + onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); - const navigateAway = useNavigateAwayFromWorkspace(); const [deleteBranch, setDeleteBranch] = useState(false); const [error, setError] = useState(null); @@ -70,7 +70,7 @@ export function useDestroyDialogState({ setError(null); onOpenChange(false); markDeleting(workspaceId); - navigateAway(workspaceId); + onDeleting?.(); toast(`Deleting "${workspaceName}"...`); try { @@ -97,10 +97,10 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, + onDeleting, onDeleted, markDeleting, clearDeleting, - navigateAway, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 0ba0972232c..e15160267c1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -42,6 +42,7 @@ export function DashboardSidebarWorkspaceItem({ handleCopyBranchName, handleCreateSection, handleDeleted, + handleDeleting, handleOpenInFinder, handleRemoveFromSidebar, isActive, @@ -139,6 +140,7 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} + onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index f684f062d06..618375a5cc9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -1,12 +1,15 @@ import { toast } from "@superset/ui/sonner"; -import { useNavigate, useParams } from "@tanstack/react-router"; +import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; -import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; +import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; +import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; +import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; interface UseDashboardSidebarWorkspaceItemActionsOptions { @@ -23,8 +26,8 @@ export function useDashboardSidebarWorkspaceItemActions({ branch, }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); - const params = useParams({ strict: false }); - const navigateAway = useNavigateAwayFromWorkspace(); + const matchRoute = useMatchRoute(); + const collections = useCollections(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = @@ -34,7 +37,11 @@ export function useDashboardSidebarWorkspaceItemActions({ const [renameValue, setRenameValue] = useState(workspaceName); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const isActive = params.workspaceId === workspaceId; + const isActive = !!matchRoute({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId }, + fuzzy: true, + }); const handleClick = () => { if (isRenaming) return; @@ -70,12 +77,25 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; + const navigateAway = () => { + if (!isActive) return; + const focusTargetId = getDeleteFocusTargetWorkspaceId( + getFlattenedV2WorkspaceIds(collections), + workspaceId, + ); + if (focusTargetId) { + void navigateToV2Workspace(focusTargetId, navigate); + } else { + void navigate({ to: "/" }); + } + }; + const handleDeleted = () => { removeWorkspaceFromSidebar(workspaceId); }; const handleRemoveFromSidebar = () => { - navigateAway(workspaceId); + navigateAway(); removeWorkspaceFromSidebar(workspaceId); }; @@ -147,6 +167,7 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyBranchName, handleCreateSection, handleDeleted, + handleDeleting: navigateAway, handleOpenInFinder, handleRemoveFromSidebar, isActive, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts deleted file mode 100644 index debafab68e8..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts deleted file mode 100644 index 50ef86d8081..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useNavigate, useParams } from "@tanstack/react-router"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { getFlattenedV2WorkspaceIds } from "../../utils/getFlattenedV2WorkspaceIds"; - -/** - * Returns a function that, given a workspaceId about to disappear from - * the sidebar (delete or hide), navigates the user off it to the next - * visible sibling — or home if none remain. No-op if the user isn't - * currently viewing the workspace being removed. - */ -export function useNavigateAwayFromWorkspace() { - const navigate = useNavigate(); - const params = useParams({ strict: false }); - const collections = useCollections(); - - return (workspaceId: string) => { - if (params.workspaceId !== workspaceId) return; - const ids = getFlattenedV2WorkspaceIds(collections); - const next = ids.find((id) => id !== workspaceId); - if (next) { - void navigateToV2Workspace(next, navigate); - } else { - void navigate({ to: "/" }); - } - }; -} From 694c892093b04efd13bc8b8367dc6c1bf4c50042 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 23:16:46 -0700 Subject: [PATCH 08/11] fix(desktop): use useParams for isActive so nav actually fires matchRoute with params + fuzzy returns false for this hook's isActive check in the early-nav callback path, even when the user is viewing the workspace. useParams is what v1's useDeleteWorkspace uses and is known to work. --- .../useDashboardSidebarWorkspaceItemActions.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 618375a5cc9..30c79c92f4d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -1,5 +1,5 @@ import { toast } from "@superset/ui/sonner"; -import { useMatchRoute, useNavigate } from "@tanstack/react-router"; +import { useNavigate, useParams } from "@tanstack/react-router"; import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; @@ -26,7 +26,7 @@ export function useDashboardSidebarWorkspaceItemActions({ branch, }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); - const matchRoute = useMatchRoute(); + const params = useParams({ strict: false }); const collections = useCollections(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); @@ -37,11 +37,7 @@ export function useDashboardSidebarWorkspaceItemActions({ const [renameValue, setRenameValue] = useState(workspaceName); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const isActive = !!matchRoute({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId }, - fuzzy: true, - }); + const isActive = params.workspaceId === workspaceId; const handleClick = () => { if (isRenaming) return; From 1824883bffff847c4b3366ea5a1d6bdde788eb50 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 23:46:51 -0700 Subject: [PATCH 09/11] fix(desktop): fire onDeleting before dialog close + markDeleting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nav was never landing — dialog close and markDeleting state thrash were swallowing it. Move onDeleting to be the first thing run() does so the navigate call goes out before any other state update can interfere. --- .../hooks/useDestroyDialogState/useDestroyDialogState.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 1cb7d518496..2e4369038ff 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -65,12 +65,14 @@ export function useDestroyDialogState({ if (inFlight.current) return; inFlight.current = true; - // Optimistic close. State (deleteBranch) preserved in case we re-open - // on a decision-required error. + // Navigate off the doomed workspace FIRST, before dialog close / + // markDeleting / any other state thrash. Closing the dialog and + // hiding the row were swallowing the nav otherwise. + onDeleting?.(); + setError(null); onOpenChange(false); markDeleting(workspaceId); - onDeleting?.(); toast(`Deleting "${workspaceName}"...`); try { From d6d4a4bd48eb2b29f4170bf37e3af71d31f6805f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 23:52:14 -0700 Subject: [PATCH 10/11] fix(desktop): restore useNavigateAwayFromWorkspace hook so nav fires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onDeleting callback route was silently dropping the navigation. The hook-extracted version was the only one that actually worked — call navigateAway(id) directly inside useDestroyDialogState and the sidebar item hook, no prop plumbing. --- .../DashboardSidebarDeleteDialog.tsx | 3 --- .../useDestroyDialogState.ts | 8 +++--- .../DashboardSidebarWorkspaceItem.tsx | 2 -- ...useDashboardSidebarWorkspaceItemActions.ts | 23 +++------------- .../useNavigateAwayFromWorkspace/index.ts | 1 + .../useNavigateAwayFromWorkspace.ts | 27 +++++++++++++++++++ 6 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx index fb0dc5ad6c0..2dea5a47ecc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx @@ -9,7 +9,6 @@ interface DashboardSidebarDeleteDialogProps { workspaceName: string; open: boolean; onOpenChange: (open: boolean) => void; - onDeleting?: () => void; /** Fires after a successful destroy (any warnings reported via toast). */ onDeleted?: () => void; } @@ -25,7 +24,6 @@ export function DashboardSidebarDeleteDialog({ workspaceName, open, onOpenChange, - onDeleting, onDeleted, }: DashboardSidebarDeleteDialogProps) { const { @@ -39,7 +37,6 @@ export function DashboardSidebarDeleteDialog({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 2e4369038ff..d5fdddb5766 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -4,13 +4,13 @@ import { type DestroyWorkspaceError, useDestroyWorkspace, } from "renderer/hooks/host-service/useDestroyWorkspace"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { workspaceId: string; workspaceName: string; onOpenChange: (open: boolean) => void; - onDeleting?: () => void; onDeleted?: () => void; } @@ -34,11 +34,11 @@ export function useDestroyDialogState({ workspaceId, workspaceName, onOpenChange, - onDeleting, onDeleted, }: UseDestroyDialogStateOptions) { const { destroy } = useDestroyWorkspace(workspaceId); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); + const navigateAway = useNavigateAwayFromWorkspace(); const [deleteBranch, setDeleteBranch] = useState(false); const [error, setError] = useState(null); @@ -68,7 +68,7 @@ export function useDestroyDialogState({ // Navigate off the doomed workspace FIRST, before dialog close / // markDeleting / any other state thrash. Closing the dialog and // hiding the row were swallowing the nav otherwise. - onDeleting?.(); + navigateAway(workspaceId); setError(null); onOpenChange(false); @@ -99,10 +99,10 @@ export function useDestroyDialogState({ workspaceName, workspaceId, onOpenChange, - onDeleting, onDeleted, markDeleting, clearDeleting, + navigateAway, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index e15160267c1..0ba0972232c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -42,7 +42,6 @@ export function DashboardSidebarWorkspaceItem({ handleCopyBranchName, handleCreateSection, handleDeleted, - handleDeleting, handleOpenInFinder, handleRemoveFromSidebar, isActive, @@ -140,7 +139,6 @@ export function DashboardSidebarWorkspaceItem({ workspaceName={name || branch} open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen} - onDeleting={handleDeleting} onDeleted={handleDeleted} /> )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 30c79c92f4d..f684f062d06 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -5,11 +5,8 @@ import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; -import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; -import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; interface UseDashboardSidebarWorkspaceItemActionsOptions { @@ -27,7 +24,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); const params = useParams({ strict: false }); - const collections = useCollections(); + const navigateAway = useNavigateAwayFromWorkspace(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = @@ -73,25 +70,12 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; - const navigateAway = () => { - if (!isActive) return; - const focusTargetId = getDeleteFocusTargetWorkspaceId( - getFlattenedV2WorkspaceIds(collections), - workspaceId, - ); - if (focusTargetId) { - void navigateToV2Workspace(focusTargetId, navigate); - } else { - void navigate({ to: "/" }); - } - }; - const handleDeleted = () => { removeWorkspaceFromSidebar(workspaceId); }; const handleRemoveFromSidebar = () => { - navigateAway(); + navigateAway(workspaceId); removeWorkspaceFromSidebar(workspaceId); }; @@ -163,7 +147,6 @@ export function useDashboardSidebarWorkspaceItemActions({ handleCopyBranchName, handleCreateSection, handleDeleted, - handleDeleting: navigateAway, handleOpenInFinder, handleRemoveFromSidebar, isActive, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts new file mode 100644 index 00000000000..debafab68e8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/index.ts @@ -0,0 +1 @@ +export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts new file mode 100644 index 00000000000..6915878a0f6 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts @@ -0,0 +1,27 @@ +import { useNavigate, useParams } from "@tanstack/react-router"; +import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { getFlattenedV2WorkspaceIds } from "../../utils/getFlattenedV2WorkspaceIds"; + +/** + * If the user is viewing the workspace about to be removed, jump to the + * next visible sidebar sibling (or home). No-op otherwise. Called + * directly at the callsite — not via a callback prop — because + * plumbing this through dialog onDeleting was silently dropping the nav. + */ +export function useNavigateAwayFromWorkspace() { + const navigate = useNavigate(); + const params = useParams({ strict: false }); + const collections = useCollections(); + + return (workspaceId: string) => { + if (params.workspaceId !== workspaceId) return; + const ids = getFlattenedV2WorkspaceIds(collections); + const next = ids.find((id) => id !== workspaceId); + if (next) { + void navigateToV2Workspace(next, navigate); + } else { + void navigate({ to: "/" }); + } + }; +} From 1c343e3c9cfb5601ec5d5e249615dd74ebcd6062 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 22 Apr 2026 23:58:26 -0700 Subject: [PATCH 11/11] chore(desktop): clean stale docs + minimize diff before merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refresh useDestroyDialogState JSDoc: it now navigates first and fires a one-shot toast (was "run destroy silently, no toast"). - Restore the "deleteBranch preserved on optimistic close" comment that was accidentally dropped. - Revert the isActive matchRoute→useParams swap in the sidebar item hook — isActive is only used for styling now, so the change was noise. --- .../useDestroyDialogState.ts | 17 +++++++++-------- .../useDashboardSidebarWorkspaceItemActions.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index d5fdddb5766..10f39af2279 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -18,11 +18,11 @@ interface UseDestroyDialogStateOptions { * Drives the delete flow for `DashboardSidebarDeleteDialog`. * * UX pattern: - * - On confirm, close the dialog immediately, mark the workspace as - * deleting (sidebar row hides optimistically), and run destroy in - * the background silently. No loading toast — destroy can take - * 10–20s and a persistent toast across that window feels bad. The - * hidden row is the feedback. + * - On confirm, navigate off the workspace first (if viewing it), + * close the dialog, mark the workspace deleting (row hides + * optimistically), fire a one-shot "Deleting..." toast, and let + * destroy run in the background. A loading toast across the 10–20s + * teardown feels worse than fire-and-forget + hidden row. * - On success, `onDeleted` removes the row from sidebar state. * - On error, `clearDeleting` runs in the `finally` block so the row * reappears. For decision-required errors (CONFLICT, TEARDOWN_FAILED) @@ -65,11 +65,12 @@ export function useDestroyDialogState({ if (inFlight.current) return; inFlight.current = true; - // Navigate off the doomed workspace FIRST, before dialog close / - // markDeleting / any other state thrash. Closing the dialog and - // hiding the row were swallowing the nav otherwise. + // Navigate off the doomed workspace FIRST. Closing the dialog + // and hiding the row were swallowing the nav otherwise. navigateAway(workspaceId); + // Optimistic close. `deleteBranch` preserved in case we re-open + // on a decision-required error. setError(null); onOpenChange(false); markDeleting(workspaceId); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index f684f062d06..7c0dc64ce0e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -1,5 +1,5 @@ import { toast } from "@superset/ui/sonner"; -import { useNavigate, useParams } from "@tanstack/react-router"; +import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; @@ -23,7 +23,7 @@ export function useDashboardSidebarWorkspaceItemActions({ branch, }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); - const params = useParams({ strict: false }); + const matchRoute = useMatchRoute(); const navigateAway = useNavigateAwayFromWorkspace(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); @@ -34,7 +34,11 @@ export function useDashboardSidebarWorkspaceItemActions({ const [renameValue, setRenameValue] = useState(workspaceName); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const isActive = params.workspaceId === workspaceId; + const isActive = !!matchRoute({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId }, + fuzzy: true, + }); const handleClick = () => { if (isRenaming) return;