Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/renderer/src/components/right-sidebar/SourceControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7974,6 +7974,7 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({
<SourceControlEntryContextMenu
currentWorktreeId={currentWorktreeId}
absolutePath={joinPath(worktreePath, entry.path)}
relativePath={entry.path}
connectionId={connectionId}
onView={() => onOpen(entry)}
onRevealInExplorer={onRevealInExplorer}
Expand Down Expand Up @@ -8229,6 +8230,7 @@ function BranchEntryRow({
<SourceControlEntryContextMenu
currentWorktreeId={currentWorktreeId}
absolutePath={joinPath(worktreePath, entry.path)}
relativePath={entry.path}
connectionId={connectionId}
onView={() => onOpen()}
onRevealInExplorer={onRevealInExplorer}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<SourceControlEntryContextMenu
currentWorktreeId="worktree-1"
absolutePath="/repo/src/example.ts"
relativePath="src/example.ts"
onRevealInExplorer={vi.fn()}
>
<div />
</SourceControlEntryContextMenu>
)

const copyRelativePathItem = items.list.find(
(item) => childrenText(item.children) === 'Copy Relative Path'
)

expect(copyRelativePathItem).toBeDefined()
copyRelativePathItem?.onSelect?.()
expect(writeClipboardText).toHaveBeenCalledWith('src/example.ts')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
type SourceControlEntryContextMenuProps = {
currentWorktreeId: string
absolutePath?: string
relativePath?: string
connectionId?: string | null
onView?: () => void
onRevealInExplorer: (worktreeId: string, absolutePath: string) => void
Expand All @@ -33,6 +34,7 @@ type SourceControlEntryContextMenuProps = {
export function SourceControlEntryContextMenu({
currentWorktreeId,
absolutePath,
relativePath,
connectionId,
onView,
onRevealInExplorer,
Expand All @@ -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
Expand Down Expand Up @@ -91,6 +100,13 @@ export function SourceControlEntryContextMenu({
<Copy className="size-3.5" />
{translate('auto.components.right.sidebar.FileExplorerRow.b5d436aa30', 'Copy Path')}
</ContextMenuItem>
<ContextMenuItem onSelect={handleCopyRelativePath} disabled={!relativePath}>
<Copy className="size-3.5" />
{translate(
'auto.components.right.sidebar.FileExplorerRow.66a29dde82',
'Copy Relative Path'
)}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger disabled={!absolutePath}>
Expand Down
Loading