Skip to content

feat(desktop): focus neighbor workspace after v2 delete#3401

Merged
saddlepaddle merged 2 commits into
mainfrom
brainy-epoxy
Apr 13, 2026
Merged

feat(desktop): focus neighbor workspace after v2 delete#3401
saddlepaddle merged 2 commits into
mainfrom
brainy-epoxy

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 13, 2026

Summary

  • When the active v2 workspace is deleted, navigate to the previous workspace in visual order; fall back to the next, then to / if no neighbors exist. Matches v1 behavior (minus the wrap-around).
  • Switches the delete flow to toast.promise so the user gets a loading → success/error toast instead of a manual try/catch.
  • Drops the now-redundant isDeleting state since the dialog is dismissed immediately and toast owns the feedback.

Neighbor lookup reads directly from TanStack DB / ElectricSQL collection snapshots (`v2SidebarProjects`, `v2SidebarSections`, `v2WorkspaceLocalState`) at click time via the new `getFlattenedV2WorkspaceIds` util — no React context or prop drilling.

Test plan

  • Focus the middle workspace in a project with 3+ workspaces, delete → previous workspace becomes active
  • Focus the first workspace in the sidebar, delete → next workspace becomes active
  • Have only one workspace total, delete → navigates to `/`
  • Focus workspace A, delete a different workspace B from the context menu → focus stays on A
  • Two projects: focus the first workspace of project 2 and delete → focus jumps to the last workspace of project 1 (cross-project visual order)
  • Delete the active workspace, then immediately try ⌘[ / ⌘] → prev/next hotkeys still work on the new list
  • Toast shows "Deleting workspace..." → "Workspace deleted" on success, error message on failure

Summary by cubic

Focuses the nearest neighbor workspace after deleting the active v2 workspace, falling back to / if none. Also switches the delete flow to toast.promise for clear loading/success/error feedback.

  • New Features

    • After deleting the active v2 workspace: go to the previous in visual order; if none, the next; else /. No wrap-around.
    • Visual order computed at click time from v2SidebarProjects, v2SidebarSections, and v2WorkspaceLocalState via getFlattenedV2WorkspaceIds/getDeleteFocusTargetWorkspaceId. Delete dialog closes immediately; isDeleting removed. Deleting a non-active workspace doesn’t change focus.
  • Bug Fixes

    • Navigation runs after the delete promise (outside toast.promise) to avoid false failure toasts; tie-breaker sets section-before-workspace when tabOrder matches to mirror sidebar order.

Written for commit 089f2c1. Summary will update on new commits.

Summary by CodeRabbit

  • Improvements

    • Workspace deletion now shows consolidated toast notifications for loading, success, and error states.
    • Post-deletion navigation automatically redirects to an appropriate remaining workspace for continuity.
    • Delete confirmation dialog no longer displays a component-level pending/disabled state.
  • New Features

    • Added utilities to determine the best workspace to focus next and to derive an ordered list of workspace IDs for sidebar navigation.

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b277ce0e-9fa8-4d2e-8402-1910ae990d35

📥 Commits

Reviewing files that changed from the base of the PR and between 5b7fc27 and 089f2c1.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts

📝 Walkthrough

Walkthrough

Removed component-level delete pending state and moved deletion flow into the hook: delete now closes the dialog immediately, performs the TRPC delete with toast.promise, computes a focus target from flattened workspace IDs, and navigates to a neighboring workspace when the deleted workspace was active.

Changes

Cohort / File(s) Summary
Sidebar item
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
Removed use of isDeleting from hook return and no longer passes isPending={isDeleting} into the delete dialog for both collapsed and expanded variants.
Delete action hook
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
Refactored delete flow: removed isDeleting state, compute focusTargetId when needed, close dialog immediately, run TRPC delete and sidebar removal via a promise, show toast.promise for statuses, and navigate to computed focus workspace (or /) after success.
Focus-target util
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts, .../getDeleteFocusTargetWorkspaceId/index.ts
Added getDeleteFocusTargetWorkspaceId(flattenedWorkspaceIds, deletedWorkspaceId) and an index re-export; returns preceding or following workspace ID (or null) for post-delete focus.
Flattening util
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts, .../getFlattenedV2WorkspaceIds/index.ts
Added getFlattenedV2WorkspaceIds and re-export: builds an ordered flattened list of v2 workspace IDs by sorting projects, assembling top-level items (workspaces/sections), and ordering section workspaces by sidebar tabOrder for traversal.

Sequence Diagram

sequenceDiagram
    actor User
    participant Dialog as Delete Dialog
    participant Hook as Delete Hook
    participant Collections as Collections/State
    participant API as TRPC Delete
    participant Nav as Navigation

    User->>Dialog: confirm delete
    Dialog->>Hook: handleDelete()
    Hook->>Collections: getFlattenedV2WorkspaceIds()
    Collections-->>Hook: flattened list
    Hook->>Hook: getDeleteFocusTargetWorkspaceId(flattened, deletedId)
    Hook->>Dialog: close dialog
    Hook->>API: trigger delete mutation (promise)
    Hook->>Collections: remove workspace from sidebar
    API-->>Hook: delete result
    alt deleted workspace was active
        Hook->>Nav: navigateToV2Workspace(focusTargetId or "/")
    else
        Hook->>Nav: no navigation
    end
    Hook-->>User: toast.promise shows status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through lists and orders new,

I nudged the focus when a workspace flew,
The dialog closed, the toast sang clear,
Neighboring tabs now draw us near,
Little rabbit cheers—no loading fear! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: focusing on the neighbor workspace after deleting a v2 workspace, which matches the PR's primary objective.
Description check ✅ Passed The description is comprehensive, covering the summary, test plan, and auto-generated summary with clear details about the changes and rationale. It covers the required sections from the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brainy-epoxy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR improves the v2 workspace delete UX in two ways: it navigates to the visually adjacent workspace (previous → next → /) when the active workspace is deleted, and it replaces the manual try/catch/finally + isDeleting state with toast.promise for cleaner loading/success/error feedback.

Key implementation details:

  • getFlattenedV2WorkspaceIds reads directly from three TanStack DB / ElectricSQL collection snapshots (v2SidebarProjects, v2SidebarSections, v2WorkspaceLocalState) to reproduce the exact visual order of the sidebar — projects sorted by tabOrder, top-level items (workspaces + sections) interleaved by tabOrder, and in-section workspaces sorted by their own tabOrder.
  • getDeleteFocusTargetWorkspaceId finds the previous neighbor via array index arithmetic; correctly handles the first-workspace edge case by relying on JavaScript returning undefined for array[-1], which falls through the ?? chain to the next neighbor.
  • The focus target is snapshotted at the moment the user confirms deletion (before the async mutation), so the deleted workspace is still present in the list when the neighbor is computed — this is intentional and correct.
  • isDeleting state and isPending on the ConfirmDialog are cleanly removed since the dialog is dismissed synchronously and toast.promise owns all user feedback.
  • A P2 edge-case: if the router navigation rejects after the server delete already succeeded, toast.promise will surface a misleading "Failed to delete" message (see inline comment).

Confidence Score: 5/5

Safe to merge — the core neighbor-navigation logic is correct across all edge cases and the refactor to toast.promise is clean.

The getFlattenedV2WorkspaceIds and getDeleteFocusTargetWorkspaceId utilities correctly handle all described scenarios (middle, first, last, only workspace, cross-project). The pre-deletion snapshot approach is intentional and sound. The only concern is a P2: if an in-app navigation unexpectedly rejects after a successful server delete, the toast message is misleading. This is an extremely unlikely scenario that does not affect the primary delete path.

No files require special attention; the one P2 suggestion is in useDashboardSidebarWorkspaceItemActions.ts around the navigation error propagation.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts Refactored handleDelete from async/await+isDeleting state to a synchronous initiator that fires a toast.promise-wrapped IIFE. Focus target is correctly snapshotted pre-deletion; isActive closure is accurate at call time. Minor: if navigation throws after a successful server delete the error toast misleadingly says "Failed to delete".
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts New utility that flattens all v2 workspace IDs into a single ordered list mirroring sidebar visual order — projects sorted by tabOrder, then top-level workspaces and sections interleaved by tabOrder, then in-section workspaces sorted by tabOrder. Schema confirms every workspace entry has a required sidebarState.projectId, so the filter logic is safe.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts Tiny utility that returns the previous neighbor first, falling back to the next, then null. Correctly exploits JS array[-1] === undefined so the first-workspace edge case is handled implicitly by the nullish coalescing chain.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx Removes isPending prop from both DashboardSidebarDeleteDialog usages — correct since the dialog is now dismissed before the async work begins and toast owns the loading/success/error feedback.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant D as DeleteDialog
    participant H as handleDelete
    participant C as collections (snapshot)
    participant T as toast.promise
    participant API as apiTrpcClient
    participant S as useDashboardSidebarState
    participant R as TanStack Router

    U->>D: Click "Confirm Delete"
    D->>H: handleDelete()
    H->>C: getFlattenedV2WorkspaceIds(collections)
    C-->>H: [w1, w2, ..., wN] (pre-deletion snapshot)
    H->>H: getDeleteFocusTargetWorkspaceId(ids, workspaceId) → focusTargetId
    H->>D: setIsDeleteDialogOpen(false) — dialog closes immediately
    H->>T: toast.promise(deletePromise, { loading, success, error })
    T-->>U: "Deleting workspace..." toast
    H->>API: v2Workspace.delete.mutate({ id })
    API-->>H: success
    H->>S: removeWorkspaceFromSidebar(workspaceId)
    alt isActive && focusTargetId
        H->>R: navigateToV2Workspace(focusTargetId)
    else isActive && no neighbor
        H->>R: navigate({ to: "/" })
    else not active
        Note over H: no navigation needed
    end
    H-->>T: promise resolves
    T-->>U: "Workspace deleted" toast
Loading

Reviews (1): Last reviewed commit: "feat(desktop): focus neighbor workspace ..." | Re-trigger Greptile

Comment on lines +82 to +99
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);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading error toast if navigation rejects after a successful delete

The deletePromise IIFE chains removeWorkspaceFromSidebar and the navigation call after the server mutate. If the mutate succeeds but navigateToV2Workspace (or navigate({ to: "/" })) throws, deletePromise rejects and toast.promise will display:

"Failed to delete: <navigation error>"

…even though the workspace was already deleted on the server and removed from the sidebar. In practice, in-app TanStack Router navigations almost never throw, so the risk is very low — but if it does happen the user could believe the workspace still exists and retry the delete unnecessarily.

One option is to isolate the navigation so its failure doesn't propagate to the promise used by toast.promise:

Suggested change
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 deletePromise = (async () => {
await apiTrpcClient.v2Workspace.delete.mutate({ id: workspaceId });
removeWorkspaceFromSidebar(workspaceId);
})();
toast.promise(deletePromise, {
loading: "Deleting workspace...",
success: "Workspace deleted",
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: "/" });
}
});

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts:48">
P2: Sort top-level items with the same section-before-workspace tie-breaker as the sidebar, otherwise delete can focus the wrong neighbor when `tabOrder` values collide.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- 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).
@saddlepaddle saddlepaddle merged commit 6ff4ac8 into main Apr 13, 2026
7 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 13, 2026
…3401)

* 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.

* 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).
@Kitenite Kitenite deleted the brainy-epoxy branch April 13, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant