From ea39bdb964302d3ef3ed51fc41afb7fcc6562b34 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Thu, 16 Jul 2026 19:16:08 +0900 Subject: [PATCH] feat(source-control): add copy relative path --- .../right-sidebar/SourceControl.tsx | 2 + ...source-control-entry-context-menu.test.tsx | 94 +++++++++++++++++++ .../source-control-entry-context-menu.tsx | 16 ++++ 3 files changed, 112 insertions(+) create mode 100644 src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 45a9e212c99..51ea1fe0e6f 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -7974,6 +7974,7 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ onOpen(entry)} onRevealInExplorer={onRevealInExplorer} @@ -8229,6 +8230,7 @@ function BranchEntryRow({ onOpen()} onRevealInExplorer={onRevealInExplorer} diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx new file mode 100644 index 00000000000..05c4c788409 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { SourceControlEntryContextMenu } from './source-control-entry-context-menu' + +type ItemProps = { onSelect?: () => void; children?: React.ReactNode } + +const items = vi.hoisted(() => ({ list: [] as ItemProps[] })) + +vi.mock('@/components/ui/context-menu', async () => { + const React_ = await import('react') + const passthrough = ({ children }: { children?: React.ReactNode }) => + React_.createElement(React_.Fragment, null, children) + + return { + ContextMenu: passthrough, + ContextMenuContent: passthrough, + ContextMenuItem: (props: ItemProps) => { + items.list.push(props) + return React_.createElement(React_.Fragment, null, props.children) + }, + ContextMenuSeparator: () => null, + ContextMenuSub: passthrough, + ContextMenuSubContent: passthrough, + ContextMenuSubTrigger: passthrough, + ContextMenuTrigger: passthrough + } +}) + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: { settings: { openInApplications: never[] } }) => unknown) => + selector({ settings: { openInApplications: [] } }) +})) + +vi.mock('@/i18n/i18n', () => ({ + translate: (_key: string, fallback: string) => fallback +})) + +vi.mock('@/lib/local-file-manager-label', () => ({ + getLocalFileManagerLabel: () => 'Finder' +})) + +vi.mock('@/lib/open-in-app-catalog', () => ({ + OpenInApplicationIcon: () => null +})) + +vi.mock('@/components/sidebar/WorktreeOpenInMenu', () => ({ + getWorktreeOpenInEntries: () => [], + openOpenInAppsSettings: vi.fn(), + openWorktreePath: vi.fn() +})) + +function childrenText(children: React.ReactNode): string { + return React.Children.toArray(children) + .filter((child): child is string => typeof child === 'string') + .join('') +} + +describe('SourceControlEntryContextMenu', () => { + const writeClipboardText = vi.fn() + + beforeEach(() => { + items.list = [] + writeClipboardText.mockReset() + vi.stubGlobal('window', { + api: { ui: { writeClipboardText } } + }) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('copies the supplied relative path', () => { + renderToStaticMarkup( + +
+ + ) + + const copyRelativePathItem = items.list.find( + (item) => childrenText(item.children) === 'Copy Relative Path' + ) + + expect(copyRelativePathItem).toBeDefined() + copyRelativePathItem?.onSelect?.() + expect(writeClipboardText).toHaveBeenCalledWith('src/example.ts') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx index b333d887587..940667d1f0b 100644 --- a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.tsx @@ -23,6 +23,7 @@ import { type SourceControlEntryContextMenuProps = { currentWorktreeId: string absolutePath?: string + relativePath?: string connectionId?: string | null onView?: () => void onRevealInExplorer: (worktreeId: string, absolutePath: string) => void @@ -33,6 +34,7 @@ type SourceControlEntryContextMenuProps = { export function SourceControlEntryContextMenu({ currentWorktreeId, absolutePath, + relativePath, connectionId, onView, onRevealInExplorer, @@ -53,6 +55,13 @@ export function SourceControlEntryContextMenu({ void window.api.ui.writeClipboardText(absolutePath) }, [absolutePath]) + const handleCopyRelativePath = useCallback(() => { + if (!relativePath) { + return + } + void window.api.ui.writeClipboardText(relativePath) + }, [relativePath]) + const handleRevealInOrcaExplorer = useCallback(() => { if (!absolutePath) { return @@ -91,6 +100,13 @@ export function SourceControlEntryContextMenu({ {translate('auto.components.right.sidebar.FileExplorerRow.b5d436aa30', 'Copy Path')} + + + {translate( + 'auto.components.right.sidebar.FileExplorerRow.66a29dde82', + 'Copy Relative Path' + )} +