feat: copy note git URLs

This commit is contained in:
lucaronin
2026-06-06 14:24:51 +02:00
parent 3a88724d8f
commit 191066a3bf
39 changed files with 853 additions and 7 deletions

View File

@@ -249,6 +249,24 @@ describe('BreadcrumbBar — file actions', () => {
expect(onCopyDeepLink).toHaveBeenCalledWith(baseEntry)
})
it('copies the current note git URL from the overflow menu when available', async () => {
const onCopyGitUrl = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onCopyGitUrl={onCopyGitUrl} />)
const menu = await openOverflowMenu()
fireEvent.click(within(menu).getByRole('menuitem', { name: 'Copy git URL' }))
expect(onCopyGitUrl).toHaveBeenCalledWith(baseEntry)
})
it('does not show the note git URL action without a remote-backed handler', async () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
const menu = await openOverflowMenu()
expect(within(menu).queryByRole('menuitem', { name: 'Copy git URL' })).not.toBeInTheDocument()
})
it('exports the current note as PDF from the overflow menu', async () => {
const onExportPdf = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onExportPdf={onExportPdf} />)

View File

@@ -60,6 +60,7 @@ interface BreadcrumbBarProps {
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onExportPdf?: () => void
onDelete?: () => void
onArchive?: () => void
@@ -819,6 +820,7 @@ function BreadcrumbActions({
onRevealFile,
onCopyFilePath,
onCopyDeepLink,
onCopyGitUrl,
onExportPdf,
onDelete,
onArchive,
@@ -868,6 +870,7 @@ function BreadcrumbActions({
onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath}
onCopyDeepLink={onCopyDeepLink}
onCopyGitUrl={onCopyGitUrl}
onExportPdf={onExportPdf}
onArchive={onArchive}
onUnarchive={onUnarchive}
@@ -892,6 +895,7 @@ function BreadcrumbOverflowMenu({
onRevealFile,
onCopyFilePath,
onCopyDeepLink,
onCopyGitUrl,
onExportPdf,
onArchive,
onUnarchive,
@@ -911,6 +915,7 @@ function BreadcrumbOverflowMenu({
| 'onRevealFile'
| 'onCopyFilePath'
| 'onCopyDeepLink'
| 'onCopyGitUrl'
| 'onExportPdf'
| 'onArchive'
| 'onUnarchive'
@@ -986,6 +991,7 @@ function BreadcrumbOverflowMenu({
<Link size={16} />
{translate(locale, 'editor.toolbar.copyNoteDeepLink')}
</DropdownMenuItem>
<CopyGitUrlMenuItem action={entryAction(onCopyGitUrl, entry)} locale={locale} />
<DropdownMenuItem disabled={!runArchiveAction} onSelect={runArchiveAction}>
<ArchiveMenuIcon archived={entry.archived} />
{archiveLabel}
@@ -999,6 +1005,22 @@ function BreadcrumbOverflowMenu({
)
}
function CopyGitUrlMenuItem({
action,
locale,
}: {
action: (() => void) | undefined
locale: AppLocale
}) {
if (!action) return null
return (
<DropdownMenuItem onSelect={action}>
<GitBranch size={16} />
{translate(locale, 'editor.toolbar.copyNoteGitUrl')}
</DropdownMenuItem>
)
}
function BreadcrumbSeparator() {
return <span aria-hidden="true" className="shrink-0 text-border"></span>
}

View File

@@ -96,6 +96,7 @@ interface EditorProps {
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onOpenExternalFile?: (path: string) => void
onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void
@@ -367,6 +368,7 @@ function EditorLayout({
onRevealFile,
onCopyFilePath,
onCopyDeepLink,
onCopyGitUrl,
onExportPdf,
onOpenExternalFile,
onDeleteNote,
@@ -441,6 +443,7 @@ function EditorLayout({
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onOpenExternalFile?: (path: string) => void
onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void
@@ -533,6 +536,7 @@ function EditorLayout({
onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath}
onCopyDeepLink={onCopyDeepLink}
onCopyGitUrl={onCopyGitUrl}
onExportPdf={() => onExportPdf?.('breadcrumb')}
onDeleteNote={onDeleteNote}
onArchiveNote={onArchiveNote}

View File

@@ -14,6 +14,8 @@ describe('NoteList context menu', () => {
const onToggleOrganized = vi.fn()
const onRevealFile = vi.fn()
const onCopyFilePath = vi.fn()
const canCopyGitUrl = vi.fn(() => true)
const onCopyGitUrl = vi.fn()
renderNoteList({
onOpenInNewWindow,
@@ -25,6 +27,8 @@ describe('NoteList context menu', () => {
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
})
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
@@ -58,6 +62,11 @@ describe('NoteList context menu', () => {
fireEvent.click(screen.getByText('Copy file path'))
expect(onCopyFilePath).toHaveBeenCalledWith(mockEntries[0].path)
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
fireEvent.click(screen.getByText('Copy git URL'))
expect(canCopyGitUrl).toHaveBeenCalledWith(mockEntries[0])
expect(onCopyGitUrl).toHaveBeenCalledWith(mockEntries[0])
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
fireEvent.click(screen.getByText('Export note as PDF'))
expect(onExportPdf).toHaveBeenCalledWith(mockEntries[0])
@@ -90,4 +99,16 @@ describe('NoteList context menu', () => {
expect(screen.getByText('Remove from Favorites')).toBeInTheDocument()
expect(screen.getByText('Mark as Unorganized')).toBeInTheDocument()
})
it('hides the git URL action for notes without a remote', () => {
renderNoteList({
canCopyGitUrl: () => false,
onCopyGitUrl: vi.fn(),
onCopyFilePath: vi.fn(),
})
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
expect(screen.queryByText('Copy git URL')).not.toBeInTheDocument()
})
})

View File

@@ -35,6 +35,7 @@ type BreadcrumbActions = Pick<
| 'onRevealFile'
| 'onCopyFilePath'
| 'onCopyDeepLink'
| 'onCopyGitUrl'
| 'onExportPdf'
| 'onDeleteNote'
| 'onArchiveNote'
@@ -204,6 +205,7 @@ function ActiveTabBreadcrumb({
onRevealFile={actions.onRevealFile}
onCopyFilePath={actions.onCopyFilePath}
onCopyDeepLink={actions.onCopyDeepLink}
onCopyGitUrl={actions.onCopyGitUrl}
onExportPdf={actions.onExportPdf}
onDelete={bindPath(actions.onDeleteNote, path)}
onArchive={bindPath(actions.onArchiveNote, path)}
@@ -271,6 +273,7 @@ function buildBreadcrumbActions(model: EditorContentModel): BreadcrumbActions {
onRevealFile: model.onRevealFile,
onCopyFilePath: model.onCopyFilePath,
onCopyDeepLink: model.onCopyDeepLink,
onCopyGitUrl: model.onCopyGitUrl,
onExportPdf: model.onExportPdf,
onDeleteNote: model.onDeleteNote,
onArchiveNote: model.onArchiveNote,

View File

@@ -43,6 +43,7 @@ export interface EditorContentProps {
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onExportPdf?: () => void
onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void

View File

@@ -28,6 +28,8 @@ interface NoteListContextMenuParams {
onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
}
function hasNoteListContextActions({
@@ -41,6 +43,8 @@ function hasNoteListContextActions({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}: NoteListContextMenuParams & { entry: VaultEntry }) {
return [
onOpenInNewWindow,
@@ -52,6 +56,7 @@ function hasNoteListContextActions({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
onCopyGitUrl && canCopyGitUrl?.(entry),
].some(Boolean)
}
@@ -66,6 +71,8 @@ export function useNoteListContextMenu({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}: NoteListContextMenuParams) {
const [ctxMenu, setCtxMenu] = useState<NoteListContextMenuState | null>(null)
const ctxMenuRef = useRef<HTMLDivElement>(null)
@@ -101,6 +108,8 @@ export function useNoteListContextMenu({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
})) return
event.preventDefault()
event.stopPropagation()
@@ -109,10 +118,12 @@ export function useNoteListContextMenu({
}, [
onArchivePaths,
onCopyFilePath,
canCopyGitUrl,
onDeletePaths,
onEnterNeighborhood,
onExportPdf,
onOpenInNewWindow,
onCopyGitUrl,
onRevealFile,
onToggleFavorite,
onToggleOrganized,
@@ -132,6 +143,8 @@ export function useNoteListContextMenu({
onToggleOrganized={onToggleOrganized}
onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath}
canCopyGitUrl={canCopyGitUrl}
onCopyGitUrl={onCopyGitUrl}
onClose={closeContextMenu}
/>
)

View File

@@ -6,6 +6,7 @@ import {
ClipboardText,
FilePdf,
FolderOpen,
GitBranch,
MapTrifold,
Star,
Trash,
@@ -43,6 +44,8 @@ interface NoteListContextMenuNodeProps {
onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onClose: () => void
}
@@ -58,6 +61,8 @@ type BuildContextMenuItemsParams = Pick<
| 'onToggleOrganized'
| 'onRevealFile'
| 'onCopyFilePath'
| 'canCopyGitUrl'
| 'onCopyGitUrl'
>
function openWindowItem(
@@ -149,6 +154,21 @@ function copyFilePathItem(
}]
}
function copyGitUrlItem(
entry: VaultEntry,
locale: AppLocale,
canCopyGitUrl: ((entry: VaultEntry) => boolean) | undefined,
onCopyGitUrl: ((entry: VaultEntry) => void) | undefined,
selectAction: SelectContextAction,
) {
if (!onCopyGitUrl || !canCopyGitUrl?.(entry)) return []
return [{
icon: GitBranch,
label: translate(locale, 'editor.toolbar.copyNoteGitUrl'),
onSelect: () => selectAction('copy_git_url', () => onCopyGitUrl(entry)),
}]
}
function exportPdfItem(
entry: VaultEntry,
locale: AppLocale,
@@ -206,6 +226,7 @@ function buildContextMenuItems(
...neighborhoodItem(entry, props.locale, props.onEnterNeighborhood, selectAction),
...revealFileItem(entry, props.locale, props.onRevealFile, selectAction),
...copyFilePathItem(entry, props.locale, props.onCopyFilePath, selectAction),
...copyGitUrlItem(entry, props.locale, props.canCopyGitUrl, props.onCopyGitUrl, selectAction),
...exportPdfItem(entry, props.locale, props.onExportPdf, selectAction),
...archiveItem(entry, props.locale, props.onArchivePaths, selectAction),
...deleteItem(entry, props.locale, props.onDeletePaths, selectAction),
@@ -241,6 +262,8 @@ export function NoteListContextMenuNode({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onClose,
}: NoteListContextMenuNodeProps) {
if (!ctxMenu) return null
@@ -262,6 +285,8 @@ export function NoteListContextMenuNode({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}, entry, selectAction)
return (

View File

@@ -345,6 +345,8 @@ interface UseNoteListInteractionStateParams {
onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onAutoTriggerDiff?: () => void
onDiscardFile?: (relativePath: string) => Promise<void>
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
@@ -374,6 +376,8 @@ function useNoteListInteractionState({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onAutoTriggerDiff,
onDiscardFile,
onCreateNote,
@@ -393,6 +397,8 @@ function useNoteListInteractionState({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
})
const {
collapsedGroups,
@@ -555,6 +561,8 @@ export interface NoteListProps {
onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onDiscardFile?: (relativePath: string) => Promise<void>
onAutoTriggerDiff?: () => void
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
@@ -684,6 +692,8 @@ export function useNoteListModel({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onDiscardFile,
onAutoTriggerDiff,
onOpenDeletedNote,
@@ -743,6 +753,8 @@ export function useNoteListModel({
onToggleOrganized,
onRevealFile,
onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onAutoTriggerDiff,
onDiscardFile,
onCreateNote,