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 @@ -53,7 +53,7 @@ export function scheduleDeleteDialogOpen({
/**
* Coordinates opening the delete dialog from a ContextMenu item selection.
*
* When "Close Worktree" is selected, we wait for ContextMenu close and then:
* When "Close Workspace" is selected, we wait for ContextMenu close and then:
* 1) prevent Radix auto-focus from returning to the trigger
* 2) open the delete dialog
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
import { type ComponentPropsWithoutRef, forwardRef, useMemo } from "react";
import { HiMiniXMark } from "react-icons/hi2";
import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent";
import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput";
import type { DashboardSidebarWorkspace } from "../../../../types";
import type { WorkspaceRowMockData } from "../../utils";
Expand Down Expand Up @@ -179,7 +180,10 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
Close workspace
<HotkeyTooltipContent
label="Close workspace"
hotkeyId={isActive ? "CLOSE_WORKSPACE" : undefined}
/>
</TooltipContent>
</Tooltip>
</div>
Expand Down Expand Up @@ -258,7 +262,10 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
Close workspace
<HotkeyTooltipContent
label="Close workspace"
hotkeyId={isActive ? "CLOSE_WORKSPACE" : undefined}
/>
</TooltipContent>
</Tooltip>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
useNavigate,
} from "@tanstack/react-router";
import { useFeatureFlagEnabled } from "posthog-js/react";
import { useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar";
import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel";
import { WorkspaceSidebar } from "renderer/screens/main/components/WorkspaceSidebar";
import { DeleteWorkspaceDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
import { useAppHotkey } from "renderer/stores/hotkeys";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";
import {
Expand Down Expand Up @@ -93,6 +95,27 @@ function DashboardLayout() {
[openNewWorkspaceModal, currentWorkspace?.projectId],
);

const [deleteTarget, setDeleteTarget] = useState<{
workspaceId: string;
workspaceName: string;
workspaceType: "worktree" | "branch";
} | null>(null);

useAppHotkey(
"CLOSE_WORKSPACE",
() => {
if (currentWorkspaceId && currentWorkspace) {
setDeleteTarget({
workspaceId: currentWorkspaceId,
workspaceName: currentWorkspace.name,
workspaceType: currentWorkspace.type,
});
}
},
{ enabled: !!currentWorkspaceId },
[currentWorkspaceId, currentWorkspace],
);

return (
<div className="flex flex-col h-full w-full">
<TopBar />
Expand Down Expand Up @@ -125,6 +148,17 @@ function DashboardLayout() {
<div className="flex flex-1 min-h-0 min-w-0">
<Outlet />
</div>
{deleteTarget && (
<DeleteWorkspaceDialog
workspaceId={deleteTarget.workspaceId}
workspaceName={deleteTarget.workspaceName}
workspaceType={deleteTarget.workspaceType}
open={true}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null);
}}
/>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function CollapsedWorkspaceItem({
}}
>
<LuX className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
Close Worktree
Close Workspace
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test";
import { createContextMenuDeleteDialogCoordinator } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler";

describe("WorkspaceContextMenu - delete/close option (#2741)", () => {
test("coordinator calls onDelete when close auto-focus fires after request", () => {
let deleteCalled = false;
const coordinator = createContextMenuDeleteDialogCoordinator(() => {
deleteCalled = true;
});

coordinator.requestOpenDeleteDialog();

let preventDefaultCalled = false;
coordinator.handleCloseAutoFocus({
preventDefault: () => {
preventDefaultCalled = true;
},
});

expect(preventDefaultCalled).toBe(true);
expect(deleteCalled).toBe(true);
});

test("coordinator does not call onDelete if no request was made", () => {
let deleteCalled = false;
const coordinator = createContextMenuDeleteDialogCoordinator(() => {
deleteCalled = true;
});

coordinator.handleCloseAutoFocus({
preventDefault: () => {},
});

expect(deleteCalled).toBe(false);
});

test("coordinator resets after firing, so a second close does not re-trigger", () => {
let callCount = 0;
const coordinator = createContextMenuDeleteDialogCoordinator(() => {
callCount += 1;
});

coordinator.requestOpenDeleteDialog();
coordinator.handleCloseAutoFocus({ preventDefault: () => {} });
coordinator.handleCloseAutoFocus({ preventDefault: () => {} });

expect(callCount).toBe(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@superset/ui/hover-card";
import { useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import {
LuArrowRightLeft,
LuBellOff,
Expand All @@ -31,6 +31,7 @@ import {
useMoveWorkspacesToSection,
useMoveWorkspaceToSection,
} from "renderer/react-query/workspaces";
import { createContextMenuDeleteDialogCoordinator } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler";
import { useWorkspaceSelectionStore } from "renderer/stores/workspace-selection";
import { STROKE_WIDTH } from "../constants";
import { WorkspaceHoverCardContent } from "./components";
Expand All @@ -49,7 +50,7 @@ interface WorkspaceContextMenuProps {
onCopyPath: () => void;
onSetUnread: (isUnread: boolean) => void;
onResetStatus: () => void;
onClose: () => void;
onDelete: () => void;
children: React.ReactNode;
}

Expand All @@ -66,7 +67,7 @@ export function WorkspaceContextMenu({
onCopyPath,
onSetUnread,
onResetStatus,
onClose,
onDelete,
children,
}: WorkspaceContextMenuProps) {
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
Expand All @@ -75,6 +76,10 @@ export function WorkspaceContextMenu({
const moveToSection = useMoveWorkspaceToSection();
const bulkMoveToSection = useMoveWorkspacesToSection();
const createSectionFromWorkspaces = useCreateSectionFromWorkspaces();
const deleteDialogCoordinator = useMemo(
() => createContextMenuDeleteDialogCoordinator(onDelete),
[onDelete],
);

const handleContextMenuOpenChange = (open: boolean) => {
setIsContextMenuOpen(open);
Expand Down Expand Up @@ -176,23 +181,29 @@ export function WorkspaceContextMenu({
Clear Status
</ContextMenuItem>
)}
{!isBranchWorkspace && (
<>
<ContextMenuSeparator />
<ContextMenuItem onSelect={onClose}>
<LuX className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
Close Worktree
</ContextMenuItem>
</>
)}
<ContextMenuSeparator />
<ContextMenuItem
onSelect={() => {
deleteDialogCoordinator.requestOpenDeleteDialog();
}}
>
<LuX className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
{isBranchWorkspace ? "Close Workspace" : "Close Worktree"}
</ContextMenuItem>
</>
);

if (isBranchWorkspace) {
return (
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent>{commonContextMenuItems}</ContextMenuContent>
<ContextMenuContent
onCloseAutoFocus={(event) => {
deleteDialogCoordinator.handleCloseAutoFocus(event);
}}
>
{commonContextMenuItems}
</ContextMenuContent>
</ContextMenu>
);
}
Expand All @@ -207,7 +218,11 @@ export function WorkspaceContextMenu({
<HoverCardTrigger asChild>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
</HoverCardTrigger>
<ContextMenuContent>
<ContextMenuContent
onCloseAutoFocus={(event) => {
deleteDialogCoordinator.handleCloseAutoFocus(event);
}}
>
<ContextMenuItem onSelect={onRename}>
<LuPencil className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
Rename
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cn } from "@superset/ui/utils";
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useMemo, useRef, useState } from "react";
import { HiMiniXMark } from "react-icons/hi2";
import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent";
import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { getGitHubStatusQueryPolicy } from "renderer/lib/githubQueryPolicy";
Expand Down Expand Up @@ -416,7 +417,10 @@ export function WorkspaceListItem({
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
Close workspace
<HotkeyTooltipContent
label="Close workspace"
hotkeyId={isActive ? "CLOSE_WORKSPACE" : undefined}
/>
</TooltipContent>
</Tooltip>
)}
Expand Down Expand Up @@ -462,7 +466,7 @@ export function WorkspaceListItem({
onCopyPath={handleCopyPath}
onSetUnread={(unread) => setUnread.mutate({ id, isUnread: unread })}
onResetStatus={() => resetWorkspaceStatus(id)}
onClose={handleDeleteClick}
onDelete={handleDeleteClick}
>
{content}
</WorkspaceContextMenu>
Expand Down
56 changes: 56 additions & 0 deletions apps/desktop/src/shared/hotkeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
canonicalizeHotkey,
canonicalizeHotkeyForPlatform,
deriveNonMacDefault,
HOTKEYS,
hotkeyFromKeyboardEvent,
isTerminalReservedEvent,
matchesHotkeyEvent,
toElectronAccelerator,
} from "./hotkeys";

Expand Down Expand Up @@ -92,3 +94,57 @@ describe("isTerminalReservedEvent", () => {
).toBe(true);
});
});

describe("CLOSE_WORKSPACE hotkey", () => {
it("is defined in HOTKEYS with correct properties", () => {
expect(HOTKEYS.CLOSE_WORKSPACE).toBeDefined();
expect(HOTKEYS.CLOSE_WORKSPACE.label).toBe("Close Workspace");
expect(HOTKEYS.CLOSE_WORKSPACE.category).toBe("Workspace");
expect(HOTKEYS.CLOSE_WORKSPACE.defaults.darwin).toBe("meta+backspace");
});

it("matches ⌘+Backspace keyboard event", () => {
const matches = matchesHotkeyEvent(
{
key: "Backspace",
code: "Backspace",
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
},
HOTKEYS.CLOSE_WORKSPACE.defaults.darwin ?? "",
);
expect(matches).toBe(true);
});

it("does not match Backspace without meta", () => {
const matches = matchesHotkeyEvent(
{
key: "Backspace",
code: "Backspace",
metaKey: false,
ctrlKey: false,
altKey: false,
shiftKey: false,
},
HOTKEYS.CLOSE_WORKSPACE.defaults.darwin ?? "",
);
expect(matches).toBe(false);
});

it("does not conflict with existing workspace hotkeys", () => {
const closeDefaults = HOTKEYS.CLOSE_WORKSPACE.defaults;
const workspaceHotkeys = Object.entries(HOTKEYS)
.filter(
([key, def]) =>
def.category === "Workspace" && key !== "CLOSE_WORKSPACE",
)
.map(([key, def]) => ({ key, defaults: def.defaults }));

for (const hotkey of workspaceHotkeys) {
expect(hotkey.defaults.darwin).not.toBe(closeDefaults.darwin);
expect(hotkey.defaults.linux).not.toBe(closeDefaults.linux);
}
});
});
6 changes: 6 additions & 0 deletions apps/desktop/src/shared/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ export const HOTKEYS = {
label: "Next Workspace",
category: "Workspace",
}),
CLOSE_WORKSPACE: defineHotkey({
keys: "meta+backspace",
label: "Close Workspace",
category: "Workspace",
description: "Close or delete the current workspace",
}),

// Layout
TOGGLE_SIDEBAR: defineHotkey({
Expand Down
Loading