feat: copy note git URLs
This commit is contained in:
@@ -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} />)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user