feat: expand note list context menu
Co-authored-by: Brian Lee <engbrianlee@gmail.com>
This commit is contained in:
@@ -1704,7 +1704,7 @@ function App() {
|
||||
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
|
||||
<PulseView vaultPath={gitSurfaces.historyRepositoryPath} onOpenNote={handlePulseOpenNote} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => handleSetViewMode('all')} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.historyRepositoryPath} onRepositoryChange={gitSurfaces.setHistoryRepositoryPath} locale={appLocale} />
|
||||
) : (
|
||||
<NoteList entries={visibleEntries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} loading={isVaultContentLoading} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={noteListModifiedFiles} modifiedFilesError={noteListModifiedFilesError} gitRepositories={gitRepositories} selectedGitRepositoryPath={gitSurfaces.changesRepositoryPath} onGitRepositoryChange={gitSurfaces.setChangesRepositoryPath} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} allNotesFileVisibility={allNotesFileVisibility} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} />
|
||||
<NoteList entries={visibleEntries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} loading={isVaultContentLoading} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={noteListModifiedFiles} modifiedFilesError={noteListModifiedFilesError} gitRepositories={gitRepositories} selectedGitRepositoryPath={gitSurfaces.changesRepositoryPath} onGitRepositoryChange={gitSurfaces.setChangesRepositoryPath} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onToggleFavorite={entryActions.handleToggleFavorite} onToggleOrganized={explicitOrganizationEnabled ? entryActions.handleToggleOrganized : undefined} onRevealFile={fileActions.revealFile} onCopyFilePath={fileActions.copyFilePath} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} allNotesFileVisibility={allNotesFileVisibility} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} />
|
||||
)}
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleNoteListResize} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mockEntries, renderNoteList } from '../test-utils/noteListTestUtils'
|
||||
import { APP_COMMAND_IDS, getAppCommandShortcutDisplay } from '../hooks/appCommandCatalog'
|
||||
import { makeEntry, mockEntries, renderNoteList } from '../test-utils/noteListTestUtils'
|
||||
|
||||
describe('NoteList context menu', () => {
|
||||
it('opens note actions from a right-clicked note item', () => {
|
||||
@@ -8,24 +9,53 @@ describe('NoteList context menu', () => {
|
||||
const onEnterNeighborhood = vi.fn()
|
||||
const onBulkArchive = vi.fn()
|
||||
const onBulkDeletePermanently = vi.fn()
|
||||
const onToggleFavorite = vi.fn()
|
||||
const onToggleOrganized = vi.fn()
|
||||
const onRevealFile = vi.fn()
|
||||
const onCopyFilePath = vi.fn()
|
||||
|
||||
renderNoteList({
|
||||
onOpenInNewWindow,
|
||||
onEnterNeighborhood,
|
||||
onBulkArchive,
|
||||
onBulkDeletePermanently,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
})
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
|
||||
expect(screen.getByTestId('note-list-context-menu')).toBeInTheDocument()
|
||||
expect(screen.getByText(getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteOpenInNewWindow)!)).toBeInTheDocument()
|
||||
expect(screen.getByText(getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteToggleFavorite)!)).toBeInTheDocument()
|
||||
expect(screen.getByText(getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteToggleOrganized)!)).toBeInTheDocument()
|
||||
expect(screen.getByText(getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteDelete)!)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Open in New Window'))
|
||||
expect(onOpenInNewWindow).toHaveBeenCalledWith(mockEntries[0])
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText('Add to Favorites'))
|
||||
expect(onToggleFavorite).toHaveBeenCalledWith(mockEntries[0].path)
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText('Mark as Organized'))
|
||||
expect(onToggleOrganized).toHaveBeenCalledWith(mockEntries[0].path)
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText("Open note's neighborhood"))
|
||||
expect(onEnterNeighborhood).toHaveBeenCalledWith(mockEntries[0])
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText('Reveal in Finder'))
|
||||
expect(onRevealFile).toHaveBeenCalledWith(mockEntries[0].path)
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText('Copy file path'))
|
||||
expect(onCopyFilePath).toHaveBeenCalledWith(mockEntries[0].path)
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
|
||||
fireEvent.click(screen.getByText('Archive this note'))
|
||||
expect(onBulkArchive).toHaveBeenCalledWith([mockEntries[0].path])
|
||||
@@ -34,4 +64,24 @@ describe('NoteList context menu', () => {
|
||||
fireEvent.click(screen.getByText('Delete this note'))
|
||||
expect(onBulkDeletePermanently).toHaveBeenCalledWith([mockEntries[0].path])
|
||||
})
|
||||
|
||||
it('shows stateful favorite and organized labels for pinned notes', () => {
|
||||
renderNoteList({
|
||||
entries: [
|
||||
makeEntry({
|
||||
favorite: true,
|
||||
organized: true,
|
||||
path: '/vault/stateful.md',
|
||||
title: 'Stateful Note',
|
||||
}),
|
||||
],
|
||||
onToggleFavorite: vi.fn(),
|
||||
onToggleOrganized: vi.fn(),
|
||||
})
|
||||
|
||||
fireEvent.contextMenu(screen.getByText('Stateful Note'))
|
||||
|
||||
expect(screen.getByText('Remove from Favorites')).toBeInTheDocument()
|
||||
expect(screen.getByText('Mark as Unorganized')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,6 +22,10 @@ interface NoteListContextMenuParams {
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onArchivePaths?: (paths: string[]) => void
|
||||
onDeletePaths?: (paths: string[]) => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onRevealFile?: (path: string) => void
|
||||
onCopyFilePath?: (path: string) => void
|
||||
}
|
||||
|
||||
function hasNoteListContextActions({
|
||||
@@ -30,13 +34,21 @@ function hasNoteListContextActions({
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
}: NoteListContextMenuParams & { entry: VaultEntry }) {
|
||||
return Boolean(
|
||||
onOpenInNewWindow
|
||||
|| (onEnterNeighborhood && entry.fileKind !== 'binary')
|
||||
|| (onArchivePaths && !entry.archived)
|
||||
|| onDeletePaths,
|
||||
)
|
||||
return [
|
||||
onOpenInNewWindow,
|
||||
onEnterNeighborhood && entry.fileKind !== 'binary',
|
||||
onArchivePaths && !entry.archived,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
].some(Boolean)
|
||||
}
|
||||
|
||||
export function useNoteListContextMenu({
|
||||
@@ -45,6 +57,10 @@ export function useNoteListContextMenu({
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
}: NoteListContextMenuParams) {
|
||||
const [ctxMenu, setCtxMenu] = useState<NoteListContextMenuState | null>(null)
|
||||
const ctxMenuRef = useRef<HTMLDivElement>(null)
|
||||
@@ -69,12 +85,31 @@ export function useNoteListContextMenu({
|
||||
}, [ctxMenu, closeContextMenu])
|
||||
|
||||
const handleNoteContextMenu = useCallback((entry: VaultEntry, event: ReactMouseEvent) => {
|
||||
if (!hasNoteListContextActions({ entry, onEnterNeighborhood, onOpenInNewWindow, onArchivePaths, onDeletePaths })) return
|
||||
if (!hasNoteListContextActions({
|
||||
entry,
|
||||
onEnterNeighborhood,
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
})) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
trackEvent('note_item_context_menu_opened')
|
||||
setCtxMenu({ x: event.clientX, y: event.clientY, entry })
|
||||
}, [onArchivePaths, onDeletePaths, onEnterNeighborhood, onOpenInNewWindow])
|
||||
}, [
|
||||
onArchivePaths,
|
||||
onCopyFilePath,
|
||||
onDeletePaths,
|
||||
onEnterNeighborhood,
|
||||
onOpenInNewWindow,
|
||||
onRevealFile,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
])
|
||||
|
||||
const contextMenuNode = (
|
||||
<NoteListContextMenuNode
|
||||
@@ -85,6 +120,10 @@ export function useNoteListContextMenu({
|
||||
onOpenInNewWindow={onOpenInNewWindow}
|
||||
onArchivePaths={onArchivePaths}
|
||||
onDeletePaths={onDeletePaths}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleOrganized={onToggleOrganized}
|
||||
onRevealFile={onRevealFile}
|
||||
onCopyFilePath={onCopyFilePath}
|
||||
onClose={closeContextMenu}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,213 @@
|
||||
import type { RefObject } from 'react'
|
||||
import { Archive, ArrowSquareOut, MapTrifold, Trash } from '@phosphor-icons/react'
|
||||
import {
|
||||
Archive,
|
||||
ArrowSquareOut,
|
||||
CheckCircle,
|
||||
ClipboardText,
|
||||
FolderOpen,
|
||||
MapTrifold,
|
||||
Star,
|
||||
Trash,
|
||||
type Icon,
|
||||
} from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { APP_COMMAND_IDS, getAppCommandShortcutDisplay } from '../../hooks/appCommandCatalog'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import { trackEvent } from '../../lib/telemetry'
|
||||
import type { VaultEntry } from '../../types'
|
||||
import type { NoteListContextMenuState } from './NoteListContextMenu'
|
||||
|
||||
interface NoteListContextMenuItem {
|
||||
destructive?: boolean
|
||||
icon: Icon
|
||||
iconWeight?: 'bold' | 'fill' | 'regular'
|
||||
label: string
|
||||
onSelect: () => void
|
||||
shortcut?: string
|
||||
}
|
||||
|
||||
type SelectContextAction = (action: string, run: () => void) => void
|
||||
|
||||
interface NoteListContextMenuNodeProps {
|
||||
ctxMenu: NoteListContextMenuState | null
|
||||
ctxMenuRef: RefObject<HTMLDivElement | null>
|
||||
locale: AppLocale
|
||||
onEnterNeighborhood?: (entry: VaultEntry) => void
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onArchivePaths?: (paths: string[]) => void
|
||||
onDeletePaths?: (paths: string[]) => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onRevealFile?: (path: string) => void
|
||||
onCopyFilePath?: (path: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type BuildContextMenuItemsParams = Pick<
|
||||
NoteListContextMenuNodeProps,
|
||||
| 'locale'
|
||||
| 'onEnterNeighborhood'
|
||||
| 'onOpenInNewWindow'
|
||||
| 'onArchivePaths'
|
||||
| 'onDeletePaths'
|
||||
| 'onToggleFavorite'
|
||||
| 'onToggleOrganized'
|
||||
| 'onRevealFile'
|
||||
| 'onCopyFilePath'
|
||||
>
|
||||
|
||||
function openWindowItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onOpenInNewWindow: ((entry: VaultEntry) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onOpenInNewWindow) return []
|
||||
return [{
|
||||
icon: ArrowSquareOut,
|
||||
label: translate(locale, 'command.note.openNewWindow'),
|
||||
onSelect: () => selectAction('open_new_window', () => onOpenInNewWindow(entry)),
|
||||
shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteOpenInNewWindow),
|
||||
}]
|
||||
}
|
||||
|
||||
function favoriteItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onToggleFavorite: ((path: string) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onToggleFavorite) return []
|
||||
return [{
|
||||
icon: Star,
|
||||
iconWeight: entry.favorite ? 'fill' as const : 'regular' as const,
|
||||
label: translate(locale, entry.favorite ? 'command.note.removeFavorite' : 'command.note.addFavorite'),
|
||||
onSelect: () => selectAction('toggle_favorite', () => onToggleFavorite(entry.path)),
|
||||
shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteToggleFavorite),
|
||||
}]
|
||||
}
|
||||
|
||||
function organizedItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onToggleOrganized: ((path: string) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onToggleOrganized) return []
|
||||
return [{
|
||||
icon: CheckCircle,
|
||||
iconWeight: entry.organized ? 'fill' as const : 'regular' as const,
|
||||
label: translate(locale, entry.organized ? 'command.note.markUnorganized' : 'command.note.markOrganized'),
|
||||
onSelect: () => selectAction('toggle_organized', () => onToggleOrganized(entry.path)),
|
||||
shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteToggleOrganized),
|
||||
}]
|
||||
}
|
||||
|
||||
function neighborhoodItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onEnterNeighborhood: ((entry: VaultEntry) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onEnterNeighborhood || entry.fileKind === 'binary') return []
|
||||
return [{
|
||||
icon: MapTrifold,
|
||||
label: translate(locale, 'editor.toolbar.openNeighborhood'),
|
||||
onSelect: () => selectAction('open_neighborhood', () => onEnterNeighborhood(entry)),
|
||||
}]
|
||||
}
|
||||
|
||||
function revealFileItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onRevealFile: ((path: string) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onRevealFile) return []
|
||||
return [{
|
||||
icon: FolderOpen,
|
||||
label: translate(locale, 'editor.toolbar.revealFile'),
|
||||
onSelect: () => selectAction('reveal_file', () => onRevealFile(entry.path)),
|
||||
}]
|
||||
}
|
||||
|
||||
function copyFilePathItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onCopyFilePath: ((path: string) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onCopyFilePath) return []
|
||||
return [{
|
||||
icon: ClipboardText,
|
||||
label: translate(locale, 'editor.toolbar.copyFilePath'),
|
||||
onSelect: () => selectAction('copy_file_path', () => onCopyFilePath(entry.path)),
|
||||
}]
|
||||
}
|
||||
|
||||
function archiveItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onArchivePaths: ((paths: string[]) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onArchivePaths || entry.archived) return []
|
||||
return [{
|
||||
icon: Archive,
|
||||
label: translate(locale, 'editor.toolbar.archive'),
|
||||
onSelect: () => selectAction('archive', () => onArchivePaths([entry.path])),
|
||||
}]
|
||||
}
|
||||
|
||||
function deleteItem(
|
||||
entry: VaultEntry,
|
||||
locale: AppLocale,
|
||||
onDeletePaths: ((paths: string[]) => void) | undefined,
|
||||
selectAction: SelectContextAction,
|
||||
) {
|
||||
if (!onDeletePaths) return []
|
||||
return [{
|
||||
destructive: true,
|
||||
icon: Trash,
|
||||
label: translate(locale, 'editor.toolbar.delete'),
|
||||
onSelect: () => selectAction('delete', () => onDeletePaths([entry.path])),
|
||||
shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.noteDelete),
|
||||
}]
|
||||
}
|
||||
|
||||
function buildContextMenuItems(
|
||||
props: BuildContextMenuItemsParams,
|
||||
entry: VaultEntry,
|
||||
selectAction: SelectContextAction,
|
||||
): NoteListContextMenuItem[] {
|
||||
return [
|
||||
...openWindowItem(entry, props.locale, props.onOpenInNewWindow, selectAction),
|
||||
...favoriteItem(entry, props.locale, props.onToggleFavorite, selectAction),
|
||||
...organizedItem(entry, props.locale, props.onToggleOrganized, selectAction),
|
||||
...neighborhoodItem(entry, props.locale, props.onEnterNeighborhood, selectAction),
|
||||
...revealFileItem(entry, props.locale, props.onRevealFile, selectAction),
|
||||
...copyFilePathItem(entry, props.locale, props.onCopyFilePath, selectAction),
|
||||
...archiveItem(entry, props.locale, props.onArchivePaths, selectAction),
|
||||
...deleteItem(entry, props.locale, props.onDeletePaths, selectAction),
|
||||
]
|
||||
}
|
||||
|
||||
function NoteListContextMenuButton({ item }: { item: NoteListContextMenuItem }) {
|
||||
const IconComponent = item.icon
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={`flex h-auto w-full cursor-default items-center justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground ${item.destructive ? 'text-destructive hover:text-destructive' : ''}`}
|
||||
onClick={item.onSelect}
|
||||
>
|
||||
<IconComponent size={16} weight={item.iconWeight} className="shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate text-left">{item.label}</span>
|
||||
{item.shortcut && <span className="ml-4 shrink-0 text-xs text-muted-foreground">{item.shortcut}</span>}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoteListContextMenuNode({
|
||||
ctxMenu,
|
||||
ctxMenuRef,
|
||||
@@ -14,17 +216,12 @@ export function NoteListContextMenuNode({
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
onClose,
|
||||
}: {
|
||||
ctxMenu: NoteListContextMenuState | null
|
||||
ctxMenuRef: RefObject<HTMLDivElement | null>
|
||||
locale: AppLocale
|
||||
onEnterNeighborhood?: (entry: VaultEntry) => void
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onArchivePaths?: (paths: string[]) => void
|
||||
onDeletePaths?: (paths: string[]) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
}: NoteListContextMenuNodeProps) {
|
||||
if (!ctxMenu) return null
|
||||
|
||||
const { entry } = ctxMenu
|
||||
@@ -33,58 +230,26 @@ export function NoteListContextMenuNode({
|
||||
onClose()
|
||||
run()
|
||||
}
|
||||
const items = buildContextMenuItems({
|
||||
locale,
|
||||
onEnterNeighborhood,
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths,
|
||||
onDeletePaths,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
}, entry, selectAction)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ctxMenuRef}
|
||||
className="fixed z-50 rounded-md border bg-popover p-1 shadow-md"
|
||||
style={{ left: ctxMenu.x, top: ctxMenu.y, minWidth: 210 }}
|
||||
style={{ left: ctxMenu.x, top: ctxMenu.y, minWidth: 240 }}
|
||||
data-testid="note-list-context-menu"
|
||||
>
|
||||
{onOpenInNewWindow && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-auto w-full cursor-default items-center justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => selectAction('open_new_window', () => onOpenInNewWindow(entry))}
|
||||
>
|
||||
<ArrowSquareOut size={16} />
|
||||
{translate(locale, 'command.note.openNewWindow')}
|
||||
</Button>
|
||||
)}
|
||||
{onEnterNeighborhood && entry.fileKind !== 'binary' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-auto w-full cursor-default items-center justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => selectAction('open_neighborhood', () => onEnterNeighborhood(entry))}
|
||||
>
|
||||
<MapTrifold size={16} />
|
||||
{translate(locale, 'editor.toolbar.openNeighborhood')}
|
||||
</Button>
|
||||
)}
|
||||
{onArchivePaths && !entry.archived && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-auto w-full cursor-default items-center justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => selectAction('archive', () => onArchivePaths([entry.path]))}
|
||||
>
|
||||
<Archive size={16} />
|
||||
{translate(locale, 'editor.toolbar.archive')}
|
||||
</Button>
|
||||
)}
|
||||
{onDeletePaths && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-auto w-full cursor-default items-center justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm text-destructive transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => selectAction('delete', () => onDeletePaths([entry.path]))}
|
||||
>
|
||||
<Trash size={16} />
|
||||
{translate(locale, 'editor.toolbar.delete')}
|
||||
</Button>
|
||||
)}
|
||||
{items.map((item) => <NoteListContextMenuButton key={item.label} item={item} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -306,6 +306,10 @@ interface UseNoteListInteractionStateParams {
|
||||
onEnterNeighborhood?: (entry: VaultEntry) => void
|
||||
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onRevealFile?: (path: string) => void
|
||||
onCopyFilePath?: (path: string) => void
|
||||
onAutoTriggerDiff?: () => void
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
onCreateNote: (type?: string) => void
|
||||
@@ -330,6 +334,10 @@ function useNoteListInteractionState({
|
||||
onEnterNeighborhood,
|
||||
onOpenDeletedNote,
|
||||
onOpenInNewWindow,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
onAutoTriggerDiff,
|
||||
onDiscardFile,
|
||||
onCreateNote,
|
||||
@@ -344,6 +352,10 @@ function useNoteListInteractionState({
|
||||
onOpenInNewWindow,
|
||||
onArchivePaths: onBulkArchive,
|
||||
onDeletePaths: onBulkDeletePermanently,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
})
|
||||
const {
|
||||
collapsedGroups,
|
||||
@@ -501,6 +513,10 @@ export interface NoteListProps {
|
||||
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
||||
updateEntry?: (path: string, patch: Partial<VaultEntry>) => void
|
||||
onOpenInNewWindow?: (entry: VaultEntry) => void
|
||||
onToggleFavorite?: (path: string) => void
|
||||
onToggleOrganized?: (path: string) => void
|
||||
onRevealFile?: (path: string) => void
|
||||
onCopyFilePath?: (path: string) => void
|
||||
onDiscardFile?: (relativePath: string) => Promise<void>
|
||||
onAutoTriggerDiff?: () => void
|
||||
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
|
||||
@@ -625,6 +641,10 @@ export function useNoteListModel({
|
||||
onUpdateTypeSort,
|
||||
updateEntry,
|
||||
onOpenInNewWindow,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
onDiscardFile,
|
||||
onAutoTriggerDiff,
|
||||
onOpenDeletedNote,
|
||||
@@ -679,6 +699,10 @@ export function useNoteListModel({
|
||||
onEnterNeighborhood,
|
||||
onOpenDeletedNote,
|
||||
onOpenInNewWindow,
|
||||
onToggleFavorite,
|
||||
onToggleOrganized,
|
||||
onRevealFile,
|
||||
onCopyFilePath,
|
||||
onAutoTriggerDiff,
|
||||
onDiscardFile,
|
||||
onCreateNote,
|
||||
|
||||
Reference in New Issue
Block a user