feat: expand note list context menu

Co-authored-by: Brian Lee <engbrianlee@gmail.com>
This commit is contained in:
lucaronin
2026-05-21 09:49:38 +02:00
parent d5b2773436
commit ac196bec58
5 changed files with 344 additions and 66 deletions

View File

@@ -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} />

View File

@@ -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()
})
})

View File

@@ -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}
/>
)

View File

@@ -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>
)
}

View File

@@ -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,