From 5b7fc27110bfd3f2d44a577bddff693f2656e05e Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sun, 12 Apr 2026 21:27:29 -0700 Subject: [PATCH 1/2] feat(desktop): focus neighbor workspace after v2 delete When the active v2 workspace is deleted, navigate to the previous workspace in visual order, falling back to the next, then to / if no neighbors exist. Mirrors v1 behavior (without the wrap-around). Also switches the delete flow to toast.promise for feedback. --- .../DashboardSidebarWorkspaceItem.tsx | 3 - ...useDashboardSidebarWorkspaceItemActions.ts | 42 +++++++---- .../getDeleteFocusTargetWorkspaceId.ts | 10 +++ .../getDeleteFocusTargetWorkspaceId/index.ts | 1 + .../getFlattenedV2WorkspaceIds.ts | 70 +++++++++++++++++++ .../utils/getFlattenedV2WorkspaceIds/index.ts | 1 + 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/index.ts 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 a825932302c..9b2b2b4e551 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({ handleOpenInFinder, isActive, isDeleteDialogOpen, - isDeleting, isRenaming, moveWorkspaceToSection, removeWorkspaceFromSidebar, @@ -129,7 +128,6 @@ export function DashboardSidebarWorkspaceItem({ onConfirm={handleDelete} title={`Delete "${name || branch}"?`} description="This will permanently delete the workspace." - isPending={isDeleting} /> )} @@ -191,7 +189,6 @@ export function DashboardSidebarWorkspaceItem({ onConfirm={handleDelete} title={`Delete "${name || branch}"?`} description="This will permanently delete the workspace." - isPending={isDeleting} /> )} 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 f38886a9a1d..55bd218e9e6 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 @@ -2,7 +2,11 @@ import { toast } from "@superset/ui/sonner"; import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { apiTrpcClient } from "renderer/lib/api-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 { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; interface UseDashboardSidebarWorkspaceItemActionsOptions { workspaceId: string; @@ -17,13 +21,13 @@ export function useDashboardSidebarWorkspaceItemActions({ }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); const matchRoute = useMatchRoute(); + const collections = useCollections(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = useDashboardSidebarState(); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(workspaceName); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); const isActive = !!matchRoute({ to: "/v2-workspace/$workspaceId", @@ -65,23 +69,34 @@ export function useDashboardSidebarWorkspaceItemActions({ } }; - const handleDelete = async () => { - setIsDeleting(true); - try { + const handleDelete = () => { + const focusTargetId = isActive + ? getDeleteFocusTargetWorkspaceId( + getFlattenedV2WorkspaceIds(collections), + workspaceId, + ) + : null; + + setIsDeleteDialogOpen(false); + + const deletePromise = (async () => { await apiTrpcClient.v2Workspace.delete.mutate({ id: workspaceId }); removeWorkspaceFromSidebar(workspaceId); - setIsDeleteDialogOpen(false); - toast.success("Workspace deleted"); if (isActive) { - navigate({ to: "/" }); + if (focusTargetId) { + await navigateToV2Workspace(focusTargetId, navigate); + } else { + await navigate({ to: "/" }); + } } - } catch (error) { - toast.error( + })(); + + toast.promise(deletePromise, { + loading: "Deleting workspace...", + success: "Workspace deleted", + error: (error) => `Failed to delete: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } finally { - setIsDeleting(false); - } + }); }; const handleCreateSection = () => { @@ -106,7 +121,6 @@ export function useDashboardSidebarWorkspaceItemActions({ handleOpenInFinder, isActive, isDeleteDialogOpen, - isDeleting, isRenaming, moveWorkspaceToSection, removeWorkspaceFromSidebar, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts new file mode 100644 index 00000000000..3ae3971fb90 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts @@ -0,0 +1,10 @@ +export function getDeleteFocusTargetWorkspaceId( + flattenedWorkspaceIds: readonly string[], + deletedWorkspaceId: string, +): string | null { + const index = flattenedWorkspaceIds.indexOf(deletedWorkspaceId); + if (index === -1) return null; + return ( + flattenedWorkspaceIds[index - 1] ?? flattenedWorkspaceIds[index + 1] ?? null + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts new file mode 100644 index 00000000000..dffd3b0fba9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts @@ -0,0 +1 @@ +export { getDeleteFocusTargetWorkspaceId } from "./getDeleteFocusTargetWorkspaceId"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts new file mode 100644 index 00000000000..f7004a62216 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts @@ -0,0 +1,70 @@ +import type { AppCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; + +type TopLevelItem = + | { kind: "workspace"; tabOrder: number; workspaceId: string } + | { kind: "section"; tabOrder: number; sectionId: string }; + +export function getFlattenedV2WorkspaceIds( + collections: Pick< + AppCollections, + "v2SidebarProjects" | "v2SidebarSections" | "v2WorkspaceLocalState" + >, +): string[] { + const projects = Array.from( + collections.v2SidebarProjects.state.values(), + ).sort((left, right) => left.tabOrder - right.tabOrder); + const allSections = Array.from(collections.v2SidebarSections.state.values()); + const allWorkspaces = Array.from( + collections.v2WorkspaceLocalState.state.values(), + ); + + const result: string[] = []; + + for (const project of projects) { + const projectWorkspaces = allWorkspaces.filter( + (workspace) => workspace.sidebarState.projectId === project.projectId, + ); + const projectSections = allSections.filter( + (section) => section.projectId === project.projectId, + ); + + const topLevelItems: TopLevelItem[] = []; + for (const workspace of projectWorkspaces) { + if (workspace.sidebarState.sectionId == null) { + topLevelItems.push({ + kind: "workspace", + tabOrder: workspace.sidebarState.tabOrder, + workspaceId: workspace.workspaceId, + }); + } + } + for (const section of projectSections) { + topLevelItems.push({ + kind: "section", + tabOrder: section.tabOrder, + sectionId: section.sectionId, + }); + } + topLevelItems.sort((left, right) => left.tabOrder - right.tabOrder); + + for (const item of topLevelItems) { + if (item.kind === "workspace") { + result.push(item.workspaceId); + continue; + } + const sectionWorkspaces = projectWorkspaces + .filter( + (workspace) => workspace.sidebarState.sectionId === item.sectionId, + ) + .sort( + (left, right) => + left.sidebarState.tabOrder - right.sidebarState.tabOrder, + ); + for (const workspace of sectionWorkspaces) { + result.push(workspace.workspaceId); + } + } + } + + return result; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/index.ts new file mode 100644 index 00000000000..c2ef10af52d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/index.ts @@ -0,0 +1 @@ +export { getFlattenedV2WorkspaceIds } from "./getFlattenedV2WorkspaceIds"; From 089f2c1e1d365f9cb3b39c4390ec6a160f5a8bfe Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sun, 12 Apr 2026 21:34:52 -0700 Subject: [PATCH 2/2] fix(desktop): address PR feedback on v2 delete focus - Isolate navigation from toast.promise so a router rejection after a successful delete doesn't surface a misleading "Failed to delete" toast (greptile). - Break sort ties in getFlattenedV2WorkspaceIds with section-before- workspace to match the sidebar's ordering when tabOrder collides (cubic). --- .../useDashboardSidebarWorkspaceItemActions.ts | 16 +++++++++------- .../getFlattenedV2WorkspaceIds.ts | 8 +++++++- 2 files changed, 16 insertions(+), 8 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 55bd218e9e6..629300efa25 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 @@ -82,13 +82,6 @@ export function useDashboardSidebarWorkspaceItemActions({ const deletePromise = (async () => { await apiTrpcClient.v2Workspace.delete.mutate({ id: workspaceId }); removeWorkspaceFromSidebar(workspaceId); - if (isActive) { - if (focusTargetId) { - await navigateToV2Workspace(focusTargetId, navigate); - } else { - await navigate({ to: "/" }); - } - } })(); toast.promise(deletePromise, { @@ -97,6 +90,15 @@ export function useDashboardSidebarWorkspaceItemActions({ error: (error) => `Failed to delete: ${error instanceof Error ? error.message : "Unknown error"}`, }); + + void deletePromise.then(() => { + if (!isActive) return; + if (focusTargetId) { + void navigateToV2Workspace(focusTargetId, navigate); + } else { + void navigate({ to: "/" }); + } + }); }; const handleCreateSection = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts index f7004a62216..04a8aad2735 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts @@ -45,7 +45,13 @@ export function getFlattenedV2WorkspaceIds( sectionId: section.sectionId, }); } - topLevelItems.sort((left, right) => left.tabOrder - right.tabOrder); + topLevelItems.sort((left, right) => { + if (left.tabOrder !== right.tabOrder) { + return left.tabOrder - right.tabOrder; + } + if (left.kind === right.kind) return 0; + return left.kind === "section" ? -1 : 1; + }); for (const item of topLevelItems) { if (item.kind === "workspace") {