Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 {
Expand All @@ -17,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)
Expand All @@ -37,6 +38,7 @@ export function useDestroyDialogState({
}: UseDestroyDialogStateOptions) {
const { destroy } = useDestroyWorkspace(workspaceId);
const { markDeleting, clearDeleting } = useDeletingWorkspaces();
const navigateAway = useNavigateAwayFromWorkspace();

const [deleteBranch, setDeleteBranch] = useState(false);
const [error, setError] = useState<DestroyWorkspaceError | null>(null);
Expand All @@ -63,11 +65,16 @@ export function useDestroyDialogState({
if (inFlight.current) return;
inFlight.current = true;

// Optimistic close. State (deleteBranch) preserved in case we re-open
// 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);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
onOpenChange(false);
markDeleting(workspaceId);
toast(`Deleting "${workspaceName}"...`);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

try {
const result = await destroy({ deleteBranch, force });
Expand Down Expand Up @@ -96,6 +103,7 @@ export function useDestroyDialogState({
onDeleted,
markDeleting,
clearDeleting,
navigateAway,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export function DashboardSidebarWorkspaceItem({
handleCreateSection,
handleDeleted,
handleOpenInFinder,
handleRemoveFromSidebar,
isActive,
isDeleteDialogOpen,
isRenaming,
moveWorkspaceToSection,
removeWorkspaceFromSidebar,
renameValue,
setIsDeleteDialogOpen,
setRenameValue,
Expand Down Expand Up @@ -124,7 +124,7 @@ export function DashboardSidebarWorkspaceItem({
onOpenInFinder={handleOpenInFinder}
onCopyPath={handleCopyPath}
onCopyBranchName={handleCopyBranchName}
onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)}
onRemoveFromSidebar={handleRemoveFromSidebar}
onRename={startRename}
onDelete={() => setIsDeleteDialogOpen(true)}
>
Expand Down Expand Up @@ -189,7 +189,7 @@ export function DashboardSidebarWorkspaceItem({
onOpenInFinder={handleOpenInFinder}
onCopyPath={handleCopyPath}
onCopyBranchName={handleCopyBranchName}
onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)}
onRemoveFromSidebar={handleRemoveFromSidebar}
onRename={startRename}
onDelete={() => setIsDeleteDialogOpen(true)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,7 +24,7 @@ export function useDashboardSidebarWorkspaceItemActions({
}: UseDashboardSidebarWorkspaceItemActionsOptions) {
const navigate = useNavigate();
const matchRoute = useMatchRoute();
const collections = useCollections();
const navigateAway = useNavigateAwayFromWorkspace();
const { activeHostUrl } = useLocalHostService();
const { copyToClipboard } = useCopyToClipboard();
const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } =
Expand Down Expand Up @@ -77,27 +74,13 @@ 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.
*/
const handleDeleted = () => {
const focusTargetId = isActive
? getDeleteFocusTargetWorkspaceId(
getFlattenedV2WorkspaceIds(collections),
workspaceId,
)
: null;

removeWorkspaceFromSidebar(workspaceId);
};

if (!isActive) return;
if (focusTargetId) {
void navigateToV2Workspace(focusTargetId, navigate);
} else {
void navigate({ to: "/" });
}
const handleRemoveFromSidebar = () => {
navigateAway(workspaceId);
removeWorkspaceFromSidebar(workspaceId);
};

const handleCreateSection = () => {
Expand Down Expand Up @@ -169,11 +152,11 @@ export function useDashboardSidebarWorkspaceItemActions({
handleCreateSection,
handleDeleted,
handleOpenInFinder,
handleRemoveFromSidebar,
isActive,
isDeleteDialogOpen,
isRenaming,
moveWorkspaceToSection,
removeWorkspaceFromSidebar,
renameValue,
setIsDeleteDialogOpen,
setRenameValue,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useNavigateAwayFromWorkspace } from "./useNavigateAwayFromWorkspace";
Original file line number Diff line number Diff line change
@@ -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: "/" });
}
};
}
Loading