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
4 changes: 2 additions & 2 deletions src/features/app/components/WorktreeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type WorktreeCardProps = {
isActive: boolean;
isDeleting?: boolean;
onSelectWorkspace: (id: string) => void;
onShowWorktreeMenu: (event: MouseEvent, workspaceId: string) => void;
onShowWorktreeMenu: (event: MouseEvent, worktree: WorkspaceInfo) => void;
onToggleWorkspaceCollapse: (workspaceId: string, collapsed: boolean) => void;
onConnectWorkspace: (workspace: WorkspaceInfo) => void;
children?: React.ReactNode;
Expand Down Expand Up @@ -41,7 +41,7 @@ export function WorktreeCard({
}}
onContextMenu={(event) => {
if (!isDeleting) {
onShowWorktreeMenu(event, worktree.id);
onShowWorktreeMenu(event, worktree);
}
}}
onKeyDown={(event) => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/app/components/WorktreeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type WorktreeSectionProps = {
threadId: string,
canPin: boolean,
) => void;
onShowWorktreeMenu: (event: MouseEvent, workspaceId: string) => void;
onShowWorktreeMenu: (event: MouseEvent, worktree: WorkspaceInfo) => void;
onToggleExpanded: (workspaceId: string) => void;
onLoadOlderThreads: (workspaceId: string) => void;
};
Expand Down
101 changes: 101 additions & 0 deletions src/features/app/hooks/useSidebarMenus.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/** @vitest-environment jsdom */
import type { MouseEvent as ReactMouseEvent } from "react";
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import type { WorkspaceInfo } from "../../../types";
import { useSidebarMenus } from "./useSidebarMenus";

const menuNew = vi.hoisted(() =>
vi.fn(async ({ items }) => ({ popup: vi.fn(), items })),
);
const menuItemNew = vi.hoisted(() => vi.fn(async (options) => options));

vi.mock("@tauri-apps/api/menu", () => ({
Menu: { new: menuNew },
MenuItem: { new: menuItemNew },
}));

vi.mock("@tauri-apps/api/window", () => ({
getCurrentWindow: () => ({ scaleFactor: () => 1 }),
}));

vi.mock("@tauri-apps/api/dpi", () => ({
LogicalPosition: class LogicalPosition {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
},
}));

const revealItemInDir = vi.hoisted(() => vi.fn());

vi.mock("@tauri-apps/plugin-opener", () => ({
revealItemInDir: (...args: unknown[]) => revealItemInDir(...args),
}));

vi.mock("../../../services/toasts", () => ({
pushErrorToast: vi.fn(),
}));

describe("useSidebarMenus", () => {
it("adds a show in finder option for worktrees", async () => {
const onDeleteThread = vi.fn();
const onSyncThread = vi.fn();
const onPinThread = vi.fn();
const onUnpinThread = vi.fn();
const isThreadPinned = vi.fn(() => false);
const onRenameThread = vi.fn();
const onReloadWorkspaceThreads = vi.fn();
const onDeleteWorkspace = vi.fn();
const onDeleteWorktree = vi.fn();

const { result } = renderHook(() =>
useSidebarMenus({
onDeleteThread,
onSyncThread,
onPinThread,
onUnpinThread,
isThreadPinned,
onRenameThread,
onReloadWorkspaceThreads,
onDeleteWorkspace,
onDeleteWorktree,
}),
);

const worktree: WorkspaceInfo = {
id: "worktree-1",
name: "feature/test",
path: "/tmp/worktree-1",
kind: "worktree",
connected: true,
settings: {
sidebarCollapsed: false,
worktreeSetupScript: "",
},
worktree: { branch: "feature/test" },
};

const event = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
clientX: 12,
clientY: 34,
} as unknown as ReactMouseEvent;

await result.current.showWorktreeMenu(event, worktree);

const menuArgs = menuNew.mock.calls[0]?.[0];
const revealItem = menuArgs.items.find(
(item: { text: string }) => item.text === "Show in Finder",
);

expect(revealItem).toBeDefined();
await revealItem.action();
expect(revealItemInDir).toHaveBeenCalledWith("/tmp/worktree-1");
});
});
36 changes: 32 additions & 4 deletions src/features/app/hooks/useSidebarMenus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { LogicalPosition } from "@tauri-apps/api/dpi";
import { getCurrentWindow } from "@tauri-apps/api/window";

import type { WorkspaceInfo } from "../../../types";
import { pushErrorToast } from "../../../services/toasts";

type SidebarMenuHandlers = {
onDeleteThread: (workspaceId: string, threadId: string) => void;
onSyncThread: (workspaceId: string, threadId: string) => void;
Expand Down Expand Up @@ -110,18 +113,43 @@ export function useSidebarMenus({
);

const showWorktreeMenu = useCallback(
async (event: MouseEvent, workspaceId: string) => {
async (event: MouseEvent, worktree: WorkspaceInfo) => {
event.preventDefault();
event.stopPropagation();
const reloadItem = await MenuItem.new({
text: "Reload threads",
action: () => onReloadWorkspaceThreads(workspaceId),
action: () => onReloadWorkspaceThreads(worktree.id),
});
const revealItem = await MenuItem.new({
text: "Show in Finder",
action: async () => {
if (!worktree.path) {
return;
}
try {
const { revealItemInDir } = await import(
"@tauri-apps/plugin-opener"
);
await revealItemInDir(worktree.path);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
pushErrorToast({
title: "Couldn't show worktree in Finder",
message,
});
console.warn("Failed to reveal worktree", {
message,
workspaceId: worktree.id,
path: worktree.path,
});
}
},
});
const deleteItem = await MenuItem.new({
text: "Delete worktree",
action: () => onDeleteWorktree(workspaceId),
action: () => onDeleteWorktree(worktree.id),
});
const menu = await Menu.new({ items: [reloadItem, deleteItem] });
const menu = await Menu.new({ items: [reloadItem, revealItem, deleteItem] });
const window = getCurrentWindow();
const position = new LogicalPosition(event.clientX, event.clientY);
await menu.popup(position, window);
Expand Down