2026-03-03 11:22:04 +01:00
|
|
|
import { useState, useMemo, useCallback, useEffect, useRef, memo } from 'react'
|
2026-02-25 21:43:16 +01:00
|
|
|
import { useDragRegion } from '../hooks/useDragRegion'
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
2026-02-24 16:35:47 +01:00
|
|
|
import type { VaultEntry, SidebarSelection, ModifiedFile, NoteStatus } from '../types'
|
2026-02-16 16:56:44 +01:00
|
|
|
import { Input } from '@/components/ui/input'
|
2026-02-17 18:45:34 +01:00
|
|
|
import {
|
2026-02-22 10:18:05 +01:00
|
|
|
MagnifyingGlass, Plus, CaretDown, CaretRight, Warning,
|
2026-02-17 18:45:34 +01:00
|
|
|
} from '@phosphor-icons/react'
|
2026-02-26 04:05:22 +01:00
|
|
|
import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors'
|
2026-02-22 10:18:05 +01:00
|
|
|
import { NoteItem, getTypeIcon } from './NoteItem'
|
2026-03-09 13:05:18 +01:00
|
|
|
import { prefetchNoteContent } from '../hooks/useTabManagement'
|
2026-02-22 10:18:05 +01:00
|
|
|
import { SortDropdown } from './SortDropdown'
|
2026-02-27 15:14:21 +01:00
|
|
|
import { BulkActionBar } from './BulkActionBar'
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
import { useMultiSelect, type MultiSelectState } from '../hooks/useMultiSelect'
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
import { useNoteListKeyboard } from '../hooks/useNoteListKeyboard'
|
2026-02-22 10:18:05 +01:00
|
|
|
import {
|
2026-02-22 12:08:53 +01:00
|
|
|
type SortOption, type SortDirection, type SortConfig, type RelationshipGroup,
|
2026-03-03 02:31:18 +01:00
|
|
|
getSortComparator, extractSortableProperties,
|
2026-02-22 10:18:05 +01:00
|
|
|
buildRelationshipGroups, filterEntries,
|
2026-02-27 15:27:38 +01:00
|
|
|
relativeDate, getDisplayDate,
|
2026-02-22 10:18:05 +01:00
|
|
|
loadSortPreferences, saveSortPreferences,
|
2026-03-03 11:22:04 +01:00
|
|
|
parseSortConfig, serializeSortConfig, clearListSortFromLocalStorage,
|
2026-02-22 10:18:05 +01:00
|
|
|
} from '../utils/noteListHelpers'
|
2026-02-17 18:45:34 +01:00
|
|
|
|
2026-02-14 18:22:42 +01:00
|
|
|
interface NoteListProps {
|
|
|
|
|
entries: VaultEntry[]
|
2026-02-14 19:44:39 +01:00
|
|
|
selection: SidebarSelection
|
2026-02-14 20:07:23 +01:00
|
|
|
selectedNote: VaultEntry | null
|
2026-02-24 16:35:47 +01:00
|
|
|
modifiedFiles?: ModifiedFile[]
|
2026-03-02 23:25:54 +01:00
|
|
|
modifiedFilesError?: string | null
|
2026-02-24 15:54:24 +01:00
|
|
|
getNoteStatus?: (path: string) => NoteStatus
|
2026-02-27 22:43:19 +01:00
|
|
|
sidebarCollapsed?: boolean
|
2026-02-14 20:07:23 +01:00
|
|
|
onSelectNote: (entry: VaultEntry) => void
|
2026-02-23 20:12:54 +01:00
|
|
|
onReplaceActiveTab: (entry: VaultEntry) => void
|
2026-02-14 21:14:16 +01:00
|
|
|
onCreateNote: () => void
|
2026-02-27 15:14:21 +01:00
|
|
|
onBulkArchive?: (paths: string[]) => void
|
|
|
|
|
onBulkTrash?: (paths: string[]) => void
|
2026-03-03 11:22:04 +01:00
|
|
|
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
|
|
|
|
updateEntry?: (path: string, patch: Partial<VaultEntry>) => void
|
2026-02-14 19:44:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-27 15:27:38 +01:00
|
|
|
function PinnedCard({ entry, typeEntryMap, onClickNote, showDate }: {
|
2026-02-22 10:18:05 +01:00
|
|
|
entry: VaultEntry
|
|
|
|
|
typeEntryMap: Record<string, VaultEntry>
|
2026-02-23 20:12:54 +01:00
|
|
|
onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void
|
2026-02-27 15:27:38 +01:00
|
|
|
showDate?: boolean
|
2026-02-22 10:18:05 +01:00
|
|
|
}) {
|
|
|
|
|
const te = typeEntryMap[entry.isA ?? '']
|
|
|
|
|
const color = getTypeColor(entry.isA ?? '', te?.color)
|
|
|
|
|
const bgColor = getTypeLightColor(entry.isA ?? '', te?.color)
|
2026-02-22 12:08:53 +01:00
|
|
|
const Icon = getTypeIcon(entry.isA, te?.icon)
|
2026-02-22 10:18:05 +01:00
|
|
|
return (
|
2026-02-23 20:12:54 +01:00
|
|
|
<div className="relative cursor-pointer border-b border-[var(--border)]" style={{ backgroundColor: bgColor, padding: '14px 16px' }} onClick={(e: React.MouseEvent) => onClickNote(entry, e)}>
|
2026-02-23 08:53:43 +01:00
|
|
|
{/* eslint-disable-next-line react-hooks/static-components */}
|
2026-02-22 10:18:05 +01:00
|
|
|
<Icon width={16} height={16} className="absolute right-3 top-3.5" style={{ color }} data-testid="type-icon" />
|
|
|
|
|
<div className="pr-6 text-[14px] font-bold" style={{ color }}>{entry.title}</div>
|
2026-02-27 15:27:38 +01:00
|
|
|
<div className="mt-1 text-[12px] leading-[1.5] opacity-80" style={{ color, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{entry.snippet}</div>
|
|
|
|
|
{showDate && <div className="mt-1 text-[11px] opacity-60" style={{ color }}>{relativeDate(getDisplayDate(entry))}</div>}
|
2026-02-22 10:18:05 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
2026-02-21 17:09:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
function RelationshipGroupSection({ group, isCollapsed, sortPrefs, onToggle, handleSortChange, renderItem }: {
|
|
|
|
|
group: RelationshipGroup
|
|
|
|
|
isCollapsed: boolean
|
2026-02-22 12:08:53 +01:00
|
|
|
sortPrefs: Record<string, SortConfig>
|
2026-02-22 10:18:05 +01:00
|
|
|
onToggle: () => void
|
2026-02-22 12:08:53 +01:00
|
|
|
handleSortChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void
|
2026-02-22 10:18:05 +01:00
|
|
|
renderItem: (entry: VaultEntry) => React.ReactNode
|
|
|
|
|
}) {
|
2026-02-22 12:08:53 +01:00
|
|
|
const groupConfig = sortPrefs[group.label] ?? { option: 'modified' as SortOption, direction: 'desc' as SortDirection }
|
|
|
|
|
const sortedEntries = [...group.entries].sort(getSortComparator(groupConfig.option, groupConfig.direction))
|
2026-03-03 02:31:18 +01:00
|
|
|
const customProperties = useMemo(() => extractSortableProperties(group.entries), [group.entries])
|
2026-02-22 10:18:05 +01:00
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex w-full items-center justify-between bg-muted" style={{ height: 32, padding: '0 16px' }}>
|
|
|
|
|
<button className="flex flex-1 items-center gap-1.5 border-none bg-transparent cursor-pointer p-0" onClick={onToggle}>
|
|
|
|
|
<span className="font-mono-label text-muted-foreground">{group.label}</span>
|
|
|
|
|
<span className="font-mono-label text-muted-foreground" style={{ fontWeight: 400 }}>{group.entries.length}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<span className="flex items-center gap-1.5">
|
2026-03-03 02:31:18 +01:00
|
|
|
<SortDropdown groupLabel={group.label} current={groupConfig.option} direction={groupConfig.direction} customProperties={customProperties} onChange={handleSortChange} />
|
2026-02-22 10:18:05 +01:00
|
|
|
<button className="flex items-center border-none bg-transparent cursor-pointer p-0 text-muted-foreground" onClick={onToggle}>
|
|
|
|
|
{isCollapsed ? <CaretRight size={12} /> : <CaretDown size={12} />}
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{!isCollapsed && sortedEntries.map((entry) => renderItem(entry))}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
feat: implement context view in NoteList
When a note is selected from the sidebar (entity selection), NoteList
now shows a context view instead of a flat list:
- Prominent top card with type-colored background, bold title, snippet,
timestamp, and type icon
- Grouped relationship sections (Children, Events, Referenced By,
Belongs To, Related To, Backlinks) with collapse/expand chevrons
- Backlinks detected by scanning allContent for wikilink references
- Groups show ALL-CAPS headers with count and CaretDown/CaretRight toggle
- Normal flat list shown when sidebar selection is "All Notes"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:47:38 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
function TrashWarningBanner({ expiredCount }: { expiredCount: number }) {
|
|
|
|
|
if (expiredCount === 0) return null
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-start gap-2 border-b border-[var(--border)]" style={{ padding: '10px 12px', background: 'color-mix(in srgb, var(--destructive) 6%, transparent)' }}>
|
|
|
|
|
<Warning size={16} className="shrink-0" style={{ color: 'var(--destructive)', marginTop: 1 }} />
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--destructive)' }}>Notes in trash for 30+ days will be permanently deleted</div>
|
|
|
|
|
<div className="text-muted-foreground" style={{ fontSize: 11 }}>{expiredCount} {expiredCount === 1 ? 'note is' : 'notes are'} past the 30-day retention period</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2026-02-17 19:11:01 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
function EmptyMessage({ text }: { text: string }) {
|
|
|
|
|
return <div className="px-4 py-8 text-center text-[13px] text-muted-foreground">{text}</div>
|
2026-02-15 10:30:13 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
function resolveHeaderTitle(selection: SidebarSelection, typeDocument: VaultEntry | null): string {
|
|
|
|
|
if (selection.kind === 'entity') return selection.entry.title
|
|
|
|
|
if (typeDocument) return typeDocument.title
|
|
|
|
|
if (selection.kind === 'filter' && selection.filter === 'archived') return 'Archive'
|
|
|
|
|
if (selection.kind === 'filter' && selection.filter === 'trash') return 'Trash'
|
2026-02-24 14:42:06 +01:00
|
|
|
if (selection.kind === 'filter' && selection.filter === 'changes') return 'Changes'
|
2026-02-22 10:18:05 +01:00
|
|
|
return 'Notes'
|
2026-02-14 19:44:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
function useTypeEntryMap(entries: VaultEntry[]) {
|
2026-02-26 04:05:22 +01:00
|
|
|
return useMemo(() => buildTypeEntryMap(entries), [entries])
|
2026-02-21 17:09:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
// --- View sub-components ---
|
2026-02-14 19:46:44 +01:00
|
|
|
|
2026-02-23 20:12:54 +01:00
|
|
|
function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, onToggleGroup, onSortChange, renderItem, typeEntryMap, onClickNote }: {
|
2026-02-22 10:48:13 +01:00
|
|
|
entity: VaultEntry; groups: RelationshipGroup[]; query: string
|
2026-02-22 12:08:53 +01:00
|
|
|
collapsedGroups: Set<string>; sortPrefs: Record<string, SortConfig>
|
|
|
|
|
onToggleGroup: (label: string) => void; onSortChange: (label: string, opt: SortOption, dir: SortDirection) => void
|
2026-02-22 10:48:13 +01:00
|
|
|
renderItem: (entry: VaultEntry) => React.ReactNode
|
2026-02-23 20:12:54 +01:00
|
|
|
typeEntryMap: Record<string, VaultEntry>; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void
|
2026-02-22 10:48:13 +01:00
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full overflow-y-auto">
|
2026-02-27 15:27:38 +01:00
|
|
|
<PinnedCard entry={entity} typeEntryMap={typeEntryMap} onClickNote={onClickNote} showDate />
|
2026-02-22 10:48:13 +01:00
|
|
|
{groups.length === 0
|
|
|
|
|
? <EmptyMessage text={query ? 'No matching items' : 'No related items'} />
|
|
|
|
|
: groups.map((group) => (
|
|
|
|
|
<RelationshipGroupSection key={group.label} group={group} isCollapsed={collapsedGroups.has(group.label)} sortPrefs={sortPrefs} onToggle={() => onToggleGroup(group.label)} handleSortChange={onSortChange} renderItem={renderItem} />
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-15 12:56:28 +01:00
|
|
|
|
2026-02-28 19:43:30 +01:00
|
|
|
function ListViewHeader({ isTrashView, expiredTrashCount }: {
|
|
|
|
|
isTrashView: boolean; expiredTrashCount: number
|
2026-02-23 21:11:41 +01:00
|
|
|
}) {
|
2026-02-28 19:43:30 +01:00
|
|
|
return <TrashWarningBanner expiredCount={isTrashView ? expiredTrashCount : 0} />
|
2026-02-23 21:11:41 +01:00
|
|
|
}
|
|
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function resolveEmptyText(isChangesView: boolean, changesError: string | null | undefined, isTrashView: boolean, query: string): string {
|
|
|
|
|
if (isChangesView && changesError) return `Failed to load changes: ${changesError}`
|
|
|
|
|
if (isChangesView) return 'No pending changes'
|
|
|
|
|
if (isTrashView) return 'Trash is empty'
|
|
|
|
|
return query ? 'No matching notes' : 'No notes found'
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 23:25:54 +01:00
|
|
|
function ListView({ isTrashView, isChangesView, changesError, expiredTrashCount, searched, query, renderItem, virtuosoRef }: {
|
|
|
|
|
isTrashView: boolean; isChangesView?: boolean; changesError?: string | null; expiredTrashCount: number
|
2026-02-22 10:48:13 +01:00
|
|
|
searched: VaultEntry[]; query: string
|
|
|
|
|
renderItem: (entry: VaultEntry) => React.ReactNode
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
virtuosoRef?: React.RefObject<VirtuosoHandle | null>
|
2026-02-22 10:48:13 +01:00
|
|
|
}) {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, isTrashView, query)
|
2026-02-28 19:43:30 +01:00
|
|
|
const hasHeader = isTrashView && expiredTrashCount > 0
|
2026-02-23 21:11:41 +01:00
|
|
|
|
|
|
|
|
if (searched.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full overflow-y-auto">
|
2026-02-28 19:43:30 +01:00
|
|
|
{hasHeader && <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} />}
|
2026-02-23 21:11:41 +01:00
|
|
|
<EmptyMessage text={emptyText} />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
return (
|
2026-02-23 21:11:41 +01:00
|
|
|
<Virtuoso
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
ref={virtuosoRef}
|
2026-02-23 21:11:41 +01:00
|
|
|
style={{ height: '100%' }}
|
|
|
|
|
data={searched}
|
|
|
|
|
overscan={200}
|
|
|
|
|
components={{
|
2026-02-28 19:43:30 +01:00
|
|
|
Header: hasHeader ? () => <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} /> : undefined,
|
2026-02-23 21:11:41 +01:00
|
|
|
}}
|
|
|
|
|
itemContent={(_index, entry) => renderItem(entry)}
|
|
|
|
|
/>
|
2026-02-22 10:48:13 +01:00
|
|
|
)
|
|
|
|
|
}
|
2026-02-21 17:09:29 +01:00
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
// --- Pure helpers ---
|
feat: implement context view in NoteList
When a note is selected from the sidebar (entity selection), NoteList
now shows a context view instead of a flat list:
- Prominent top card with type-colored background, bold title, snippet,
timestamp, and type icon
- Grouped relationship sections (Children, Events, Referenced By,
Belongs To, Related To, Backlinks) with collapse/expand chevrons
- Backlinks detected by scanning allContent for wikilink references
- Groups show ALL-CAPS headers with count and CaretDown/CaretRight toggle
- Normal flat list shown when sidebar selection is "All Notes"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:47:38 +01:00
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
function filterByQuery<T extends { title: string }>(items: T[], query: string): T[] {
|
|
|
|
|
return query ? items.filter((e) => e.title.toLowerCase().includes(query)) : items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterGroupsByQuery(groups: RelationshipGroup[], query: string): RelationshipGroup[] {
|
|
|
|
|
if (!query) return groups
|
|
|
|
|
return groups.map((g) => ({ ...g, entries: filterByQuery(g.entries, query) })).filter((g) => g.entries.length > 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countExpiredTrash(entries: VaultEntry[]): number {
|
|
|
|
|
const now = Date.now() / 1000
|
|
|
|
|
return entries.filter((e) => e.trashedAt && (now - e.trashedAt) >= 86400 * 30).length
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 20:12:54 +01:00
|
|
|
// --- Click routing ---
|
|
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
interface ClickActions {
|
|
|
|
|
onReplace: (entry: VaultEntry) => void
|
|
|
|
|
onSelect: (entry: VaultEntry) => void
|
|
|
|
|
multiSelect: { selectRange: (path: string) => void; clear: () => void; setAnchor: (path: string) => void }
|
|
|
|
|
}
|
2026-02-27 15:14:21 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function routeNoteClick(entry: VaultEntry, e: React.MouseEvent, actions: ClickActions) {
|
|
|
|
|
if (e.shiftKey) { actions.multiSelect.selectRange(entry.path) }
|
|
|
|
|
else if (e.metaKey || e.ctrlKey) { actions.multiSelect.clear(); actions.onSelect(entry) }
|
|
|
|
|
else { actions.multiSelect.clear(); actions.multiSelect.setAnchor(entry.path); actions.onReplace(entry) }
|
2026-02-23 20:12:54 +01:00
|
|
|
}
|
|
|
|
|
|
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
|
|
|
// --- Pure helpers extracted from NoteListInner to reduce cyclomatic complexity ---
|
|
|
|
|
|
|
|
|
|
function createNoteStatusResolver(
|
|
|
|
|
getNoteStatus: ((path: string) => NoteStatus) | undefined,
|
|
|
|
|
modifiedFiles: ModifiedFile[] | undefined,
|
|
|
|
|
modifiedPathSet: Set<string>,
|
|
|
|
|
): (path: string) => NoteStatus {
|
|
|
|
|
if (getNoteStatus) return getNoteStatus
|
|
|
|
|
if (modifiedFiles && modifiedFiles.length > 0) {
|
|
|
|
|
return (path: string) => modifiedPathSet.has(path) ? 'modified' : 'clean'
|
|
|
|
|
}
|
|
|
|
|
return defaultGetNoteStatus
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSetMember<T>(set: Set<T>, member: T): Set<T> {
|
|
|
|
|
const next = new Set(set)
|
|
|
|
|
if (next.has(member)) next.delete(member)
|
|
|
|
|
else next.add(member)
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
// --- Data hooks ---
|
|
|
|
|
|
|
|
|
|
interface NoteListDataParams {
|
2026-03-08 22:15:08 +01:00
|
|
|
entries: VaultEntry[]; selection: SidebarSelection
|
2026-02-23 21:55:57 +01:00
|
|
|
query: string; listSort: SortOption; listDirection: SortDirection
|
2026-03-02 23:25:54 +01:00
|
|
|
modifiedPathSet: Set<string>; modifiedSuffixes: string[]
|
2026-02-22 10:48:13 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-02 23:25:54 +01:00
|
|
|
function isModifiedEntry(path: string, pathSet: Set<string>, suffixes: string[]): boolean {
|
|
|
|
|
if (pathSet.has(path)) return true
|
|
|
|
|
return suffixes.some((suffix) => path.endsWith(suffix))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 02:31:18 +01:00
|
|
|
function useFilteredEntries(entries: VaultEntry[], selection: SidebarSelection, modifiedPathSet: Set<string>, modifiedSuffixes: string[]) {
|
|
|
|
|
const isEntityView = selection.kind === 'entity'
|
|
|
|
|
const isChangesView = selection.kind === 'filter' && selection.filter === 'changes'
|
|
|
|
|
return useMemo(() => {
|
|
|
|
|
if (isEntityView) return []
|
|
|
|
|
if (isChangesView) return entries.filter((e) => isModifiedEntry(e.path, modifiedPathSet, modifiedSuffixes))
|
|
|
|
|
return filterEntries(entries, selection)
|
|
|
|
|
}, [entries, selection, isEntityView, isChangesView, modifiedPathSet, modifiedSuffixes])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 22:15:08 +01:00
|
|
|
function useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes }: NoteListDataParams) {
|
2026-02-22 10:48:13 +01:00
|
|
|
const isEntityView = selection.kind === 'entity'
|
|
|
|
|
const isTrashView = selection.kind === 'filter' && selection.filter === 'trash'
|
2026-02-21 13:27:33 +01:00
|
|
|
|
2026-03-03 02:31:18 +01:00
|
|
|
const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes)
|
|
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
const searched = useMemo(() => {
|
2026-03-03 02:31:18 +01:00
|
|
|
const sorted = [...filteredEntries].sort(getSortComparator(listSort, listDirection))
|
2026-02-22 10:48:13 +01:00
|
|
|
return filterByQuery(sorted, query)
|
2026-03-03 02:31:18 +01:00
|
|
|
}, [filteredEntries, listSort, listDirection, query])
|
2026-02-17 17:14:36 +01:00
|
|
|
|
2026-02-22 10:18:05 +01:00
|
|
|
const searchedGroups = useMemo(() => {
|
|
|
|
|
if (!isEntityView) return []
|
2026-03-08 22:15:08 +01:00
|
|
|
const groups = buildRelationshipGroups(selection.entry, entries)
|
2026-02-22 10:48:13 +01:00
|
|
|
return filterGroupsByQuery(groups, query)
|
2026-03-08 22:15:08 +01:00
|
|
|
}, [isEntityView, selection, entries, query])
|
2026-02-14 19:48:59 +01:00
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
const expiredTrashCount = useMemo(
|
|
|
|
|
() => isTrashView ? countExpiredTrash(searched) : 0,
|
|
|
|
|
[isTrashView, searched],
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-03 11:22:04 +01:00
|
|
|
return { isEntityView, isTrashView, searched, searchedGroups, expiredTrashCount }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Pure helpers ---
|
|
|
|
|
|
|
|
|
|
const DEFAULT_LIST_CONFIG: SortConfig = { option: 'modified', direction: 'desc' }
|
|
|
|
|
|
|
|
|
|
function resolveListSortConfig(typeDocument: VaultEntry | null, sortPrefs: Record<string, SortConfig>): SortConfig {
|
|
|
|
|
if (typeDocument?.sort) {
|
|
|
|
|
const parsed = parseSortConfig(typeDocument.sort)
|
|
|
|
|
if (parsed) return parsed
|
|
|
|
|
}
|
|
|
|
|
return sortPrefs['__list__'] ?? DEFAULT_LIST_CONFIG
|
2026-02-22 10:48:13 +01:00
|
|
|
}
|
|
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
// --- Extracted hooks ---
|
2026-02-22 10:48:13 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
interface SortPersistence {
|
|
|
|
|
onUpdateTypeSort: (path: string, key: string, value: string) => void
|
|
|
|
|
updateEntry: (path: string, patch: Partial<VaultEntry>) => void
|
|
|
|
|
}
|
2026-02-24 15:54:24 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function persistSortToType(path: string, config: SortConfig, persistence: SortPersistence) {
|
|
|
|
|
const serialized = serializeSortConfig(config)
|
|
|
|
|
persistence.onUpdateTypeSort(path, 'sort', serialized)
|
|
|
|
|
persistence.updateEntry(path, { sort: serialized })
|
|
|
|
|
clearListSortFromLocalStorage()
|
|
|
|
|
}
|
2026-02-22 10:48:13 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function migrateListSortToType(typeDoc: VaultEntry, sortPrefs: Record<string, SortConfig>, migrationDone: Set<string>, persistence: SortPersistence) {
|
|
|
|
|
if (typeDoc.sort || migrationDone.has(typeDoc.path)) return
|
|
|
|
|
const lsConfig = sortPrefs['__list__']
|
|
|
|
|
if (!lsConfig) return
|
|
|
|
|
migrationDone.add(typeDoc.path)
|
|
|
|
|
persistSortToType(typeDoc.path, lsConfig, persistence)
|
|
|
|
|
}
|
2026-02-24 16:35:47 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function saveGroupSort(groupLabel: string, option: SortOption, direction: SortDirection, setSortPrefs: React.Dispatch<React.SetStateAction<Record<string, SortConfig>>>) {
|
|
|
|
|
setSortPrefs((prev) => { const next = { ...prev, [groupLabel]: { option, direction } }; saveSortPreferences(next); return next })
|
|
|
|
|
}
|
2026-03-02 23:25:54 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function deriveEffectiveSort(configOption: SortOption, customProperties: string[]): SortOption {
|
|
|
|
|
if (!configOption.startsWith('property:')) return configOption
|
|
|
|
|
return customProperties.includes(configOption.slice('property:'.length)) ? configOption : 'modified'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface UseNoteListSortParams {
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
selection: SidebarSelection
|
|
|
|
|
modifiedPathSet: Set<string>
|
|
|
|
|
modifiedSuffixes: string[]
|
|
|
|
|
onUpdateTypeSort?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
|
|
|
|
updateEntry?: (path: string, patch: Partial<VaultEntry>) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useNoteListSort({ entries, selection, modifiedPathSet, modifiedSuffixes, onUpdateTypeSort, updateEntry }: UseNoteListSortParams) {
|
|
|
|
|
const [sortPrefs, setSortPrefs] = useState<Record<string, SortConfig>>(loadSortPreferences)
|
2026-02-24 16:35:47 +01:00
|
|
|
|
2026-03-03 11:22:04 +01:00
|
|
|
const typeDocument = useMemo(() => {
|
|
|
|
|
if (selection.kind !== 'sectionGroup') return null
|
|
|
|
|
return entries.find((e) => e.isA === 'Type' && e.title === selection.type) ?? null
|
|
|
|
|
}, [selection, entries])
|
|
|
|
|
|
|
|
|
|
const listConfig = resolveListSortConfig(typeDocument, sortPrefs)
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const persistence = useMemo<SortPersistence | null>(
|
|
|
|
|
() => (onUpdateTypeSort && updateEntry) ? { onUpdateTypeSort, updateEntry } : null,
|
|
|
|
|
[onUpdateTypeSort, updateEntry],
|
|
|
|
|
)
|
2026-03-03 11:22:04 +01:00
|
|
|
|
|
|
|
|
const migrationDoneRef = useRef<Set<string>>(new Set())
|
|
|
|
|
useEffect(() => {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
if (!typeDocument || !persistence) return
|
|
|
|
|
migrateListSortToType(typeDocument, sortPrefs, migrationDoneRef.current, persistence)
|
|
|
|
|
}, [typeDocument, sortPrefs, persistence])
|
2026-03-03 11:22:04 +01:00
|
|
|
|
2026-02-22 12:08:53 +01:00
|
|
|
const handleSortChange = useCallback((groupLabel: string, option: SortOption, direction: SortDirection) => {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
if (groupLabel === '__list__' && typeDocument && persistence) {
|
|
|
|
|
persistSortToType(typeDocument.path, { option, direction }, persistence)
|
2026-03-03 11:22:04 +01:00
|
|
|
} else {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
saveGroupSort(groupLabel, option, direction, setSortPrefs)
|
2026-03-03 11:22:04 +01:00
|
|
|
}
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
}, [typeDocument, persistence])
|
2026-02-22 10:48:13 +01:00
|
|
|
|
2026-03-03 02:31:18 +01:00
|
|
|
const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes)
|
|
|
|
|
const customProperties = useMemo(() => extractSortableProperties(filteredEntries), [filteredEntries])
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const listSort = useMemo<SortOption>(() => deriveEffectiveSort(listConfig.option, customProperties), [listConfig.option, customProperties])
|
2026-03-03 02:31:18 +01:00
|
|
|
const listDirection = listSort === listConfig.option ? listConfig.direction : 'desc'
|
2026-02-21 16:54:59 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
return { listSort, listDirection, customProperties, handleSortChange, sortPrefs, typeDocument }
|
|
|
|
|
}
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function useNoteListSearch() {
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
const [searchVisible, setSearchVisible] = useState(false)
|
|
|
|
|
const query = search.trim().toLowerCase()
|
2026-02-27 15:14:21 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const toggleSearch = useCallback(() => {
|
|
|
|
|
setSearchVisible((v) => { if (v) setSearch(''); return !v })
|
|
|
|
|
}, [])
|
2026-02-27 15:14:21 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
return { search, setSearch, query, searchVisible, toggleSearch }
|
|
|
|
|
}
|
2026-02-27 15:14:21 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function isInputFocused(): boolean {
|
|
|
|
|
const el = document.activeElement
|
|
|
|
|
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || !!(el as HTMLElement)?.isContentEditable
|
|
|
|
|
}
|
2026-02-27 22:46:26 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function handleEscapeKey(e: KeyboardEvent, multiSelect: MultiSelectState) {
|
|
|
|
|
if (e.key !== 'Escape' || !multiSelect.isMultiSelecting) return
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
multiSelect.clear()
|
|
|
|
|
}
|
2026-02-27 22:46:26 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function handleSelectAllKey(e: KeyboardEvent, multiSelect: MultiSelectState, isEntityView: boolean) {
|
|
|
|
|
if (e.key !== 'a' || !(e.metaKey || e.ctrlKey) || isEntityView || isInputFocused()) return
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
multiSelect.selectAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleBulkActionKey(e: KeyboardEvent, multiSelect: MultiSelectState, onArchive: () => void, onTrash: () => void) {
|
|
|
|
|
if (!multiSelect.isMultiSelecting || !(e.metaKey || e.ctrlKey)) return
|
|
|
|
|
if (e.key === 'e') { e.preventDefault(); e.stopPropagation(); onArchive() }
|
|
|
|
|
if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); e.stopPropagation(); onTrash() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useMultiSelectKeyboard(multiSelect: MultiSelectState, isEntityView: boolean, onBulkArchive: () => void, onBulkTrash: () => void) {
|
2026-02-27 15:14:21 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
function handleKeyDown(e: KeyboardEvent) {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
handleEscapeKey(e, multiSelect)
|
|
|
|
|
handleSelectAllKey(e, multiSelect, isEntityView)
|
|
|
|
|
handleBulkActionKey(e, multiSelect, onBulkArchive, onBulkTrash)
|
2026-02-27 15:14:21 +01:00
|
|
|
}
|
2026-02-27 22:46:26 +01:00
|
|
|
window.addEventListener('keydown', handleKeyDown, true)
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
}, [multiSelect, isEntityView, onBulkArchive, onBulkTrash])
|
|
|
|
|
}
|
2026-02-23 20:12:54 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
// --- Header component ---
|
2026-02-20 21:47:42 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
function NoteListHeader({ title, typeDocument, isEntityView, listSort, listDirection, customProperties, sidebarCollapsed, searchVisible, search, onSortChange, onCreateNote, onOpenType, onToggleSearch, onSearchChange }: {
|
|
|
|
|
title: string
|
|
|
|
|
typeDocument: VaultEntry | null
|
|
|
|
|
isEntityView: boolean
|
|
|
|
|
listSort: SortOption
|
|
|
|
|
listDirection: SortDirection
|
|
|
|
|
customProperties: string[]
|
|
|
|
|
sidebarCollapsed?: boolean
|
|
|
|
|
searchVisible: boolean
|
|
|
|
|
search: string
|
|
|
|
|
onSortChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void
|
|
|
|
|
onCreateNote: () => void
|
|
|
|
|
onOpenType: (entry: VaultEntry) => void
|
|
|
|
|
onToggleSearch: () => void
|
|
|
|
|
onSearchChange: (value: string) => void
|
|
|
|
|
}) {
|
|
|
|
|
const { onMouseDown: onDragMouseDown } = useDragRegion()
|
2026-02-14 18:22:42 +01:00
|
|
|
return (
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
<>
|
2026-02-27 22:43:19 +01:00
|
|
|
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-4" onMouseDown={onDragMouseDown} style={{ cursor: 'default', paddingLeft: sidebarCollapsed ? 80 : undefined }}>
|
2026-02-28 19:43:30 +01:00
|
|
|
<h3
|
|
|
|
|
className="m-0 min-w-0 flex-1 truncate text-[14px] font-semibold"
|
|
|
|
|
style={typeDocument ? { cursor: 'pointer' } : undefined}
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
onClick={typeDocument ? () => onOpenType(typeDocument) : undefined}
|
2026-02-28 19:43:30 +01:00
|
|
|
data-testid={typeDocument ? 'type-header-link' : undefined}
|
|
|
|
|
>
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
{title}
|
2026-02-28 19:43:30 +01:00
|
|
|
</h3>
|
2026-02-17 11:03:23 +01:00
|
|
|
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
{!isEntityView && <SortDropdown groupLabel="__list__" current={listSort} direction={listDirection} customProperties={customProperties} onChange={onSortChange} />}
|
|
|
|
|
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={onToggleSearch} title="Search notes">
|
2026-02-17 11:03:23 +01:00
|
|
|
<MagnifyingGlass size={16} />
|
|
|
|
|
</button>
|
2026-02-22 11:38:03 +01:00
|
|
|
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={() => onCreateNote()} title="Create new note">
|
2026-02-17 11:03:23 +01:00
|
|
|
<Plus size={16} />
|
|
|
|
|
</button>
|
2026-02-14 21:14:16 +01:00
|
|
|
</div>
|
2026-02-14 19:46:44 +01:00
|
|
|
</div>
|
2026-02-17 11:03:23 +01:00
|
|
|
{searchVisible && (
|
|
|
|
|
<div className="border-b border-border px-3 py-2">
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
<Input placeholder="Search notes..." value={search} onChange={(e) => onSearchChange(e.target.value)} className="h-8 text-[13px]" autoFocus />
|
2026-02-17 11:03:23 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Main component ---
|
|
|
|
|
|
|
|
|
|
const defaultGetNoteStatus = (): NoteStatus => 'clean'
|
|
|
|
|
|
|
|
|
|
function useModifiedFilesState(modifiedFiles: ModifiedFile[] | undefined, getNoteStatus: ((path: string) => NoteStatus) | undefined) {
|
|
|
|
|
const modifiedPathSet = useMemo(() => new Set((modifiedFiles ?? []).map((f) => f.path)), [modifiedFiles])
|
|
|
|
|
const modifiedSuffixes = useMemo(() => (modifiedFiles ?? []).map((f) => '/' + f.relativePath), [modifiedFiles])
|
|
|
|
|
const resolvedGetNoteStatus = useMemo<(path: string) => NoteStatus>(
|
|
|
|
|
() => createNoteStatusResolver(getNoteStatus, modifiedFiles, modifiedPathSet),
|
|
|
|
|
[getNoteStatus, modifiedFiles, modifiedPathSet],
|
|
|
|
|
)
|
|
|
|
|
return { modifiedPathSet, modifiedSuffixes, resolvedGetNoteStatus }
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 22:15:08 +01:00
|
|
|
function NoteListInner({ entries, selection, selectedNote, modifiedFiles, modifiedFilesError, getNoteStatus, sidebarCollapsed, onSelectNote, onReplaceActiveTab, onCreateNote, onBulkArchive, onBulkTrash, onUpdateTypeSort, updateEntry }: NoteListProps) {
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const { modifiedPathSet, modifiedSuffixes, resolvedGetNoteStatus } = useModifiedFilesState(modifiedFiles, getNoteStatus)
|
|
|
|
|
const { listSort, listDirection, customProperties, handleSortChange, sortPrefs, typeDocument } = useNoteListSort({ entries, selection, modifiedPathSet, modifiedSuffixes, onUpdateTypeSort, updateEntry })
|
|
|
|
|
const { search, setSearch, query, searchVisible, toggleSearch } = useNoteListSearch()
|
|
|
|
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set())
|
|
|
|
|
|
|
|
|
|
const typeEntryMap = useTypeEntryMap(entries)
|
2026-03-08 22:15:08 +01:00
|
|
|
const { isEntityView, isTrashView, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes })
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const isChangesView = selection.kind === 'filter' && selection.filter === 'changes'
|
|
|
|
|
const entitySelection = isEntityView && selection.kind === 'entity' ? selection : null
|
|
|
|
|
|
|
|
|
|
const noteListKeyboard = useNoteListKeyboard({ items: searched, selectedNotePath: selectedNote?.path ?? null, onOpen: onReplaceActiveTab, enabled: !isEntityView })
|
|
|
|
|
const multiSelect = useMultiSelect(searched, selectedNote?.path ?? null)
|
|
|
|
|
useEffect(() => { multiSelect.clear() }, [selection]) // eslint-disable-line react-hooks/exhaustive-deps -- clear on selection change only
|
2026-02-16 16:56:44 +01:00
|
|
|
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
const handleClickNote = useCallback((entry: VaultEntry, e: React.MouseEvent) => {
|
|
|
|
|
routeNoteClick(entry, e, { onReplace: onReplaceActiveTab, onSelect: onSelectNote, multiSelect })
|
|
|
|
|
}, [onReplaceActiveTab, onSelectNote, multiSelect])
|
|
|
|
|
|
|
|
|
|
const handleBulkArchive = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkArchive?.(paths) }, [multiSelect, onBulkArchive])
|
|
|
|
|
const handleBulkTrash = useCallback(() => { const paths = [...multiSelect.selectedPaths]; multiSelect.clear(); onBulkTrash?.(paths) }, [multiSelect, onBulkTrash])
|
|
|
|
|
useMultiSelectKeyboard(multiSelect, isEntityView, handleBulkArchive, handleBulkTrash)
|
|
|
|
|
|
|
|
|
|
const renderItem = useCallback((entry: VaultEntry) => (
|
2026-03-09 13:05:18 +01:00
|
|
|
<NoteItem key={entry.path} entry={entry} isSelected={selectedNote?.path === entry.path} isMultiSelected={multiSelect.selectedPaths.has(entry.path)} isHighlighted={entry.path === noteListKeyboard.highlightedPath} noteStatus={resolvedGetNoteStatus(entry.path)} typeEntryMap={typeEntryMap} onClickNote={handleClickNote} onPrefetch={prefetchNoteContent} />
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
), [selectedNote?.path, handleClickNote, typeEntryMap, resolvedGetNoteStatus, multiSelect.selectedPaths, noteListKeyboard.highlightedPath])
|
|
|
|
|
|
|
|
|
|
const toggleGroup = useCallback((label: string) => { setCollapsedGroups((prev) => toggleSetMember(prev, label)) }, [])
|
|
|
|
|
const title = resolveHeaderTitle(selection, typeDocument)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
|
|
|
|
|
<NoteListHeader title={title} typeDocument={typeDocument} isEntityView={isEntityView} listSort={listSort} listDirection={listDirection} customProperties={customProperties} sidebarCollapsed={sidebarCollapsed} searchVisible={searchVisible} search={search} onSortChange={handleSortChange} onCreateNote={onCreateNote} onOpenType={onReplaceActiveTab} onToggleSearch={toggleSearch} onSearchChange={setSearch} />
|
feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter (#158)
* feat: keyboard-first navigation — menu bar shortcuts, note list nav, inspector Tab/Enter
- Add missing menu bar items: Archive Note (⌘E), Trash Note (⌘⌫),
Find in Vault (⌘⇧F), Go Back (⌘[), Go Forward (⌘])
- Wire new menu events through useMenuEvents → useAppCommands
- Note list: ↑↓ moves highlight, Enter opens selected note (useNoteListKeyboard hook)
- Inspector properties: Tab between rows, Enter to start editing
- Settings panel: auto-focus first input on open
- Extract StatusDot, StateBadge, noteItemStyle from NoteItem (reduces complexity)
- Extract dispatchActiveTabEvent/dispatchOptionalEvent from useMenuEvents
- 21 new tests (useNoteListKeyboard + useMenuEvents for new events)
- All 1275 frontend tests pass, 319 Rust tests pass
- CodeScene quality gates: passed (NoteItem improved from 22→18 complexity)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: keyboard-first-nav wireframes (note list highlight, menu shortcuts, inspector tab nav)
* test: add search.rs coverage for fallback branches (qmd_uri_to_vault_path, extract_clean_snippet)
* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup code)
---------
Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:32:15 +01:00
|
|
|
<div className="flex-1 overflow-hidden outline-none" style={{ minHeight: 0 }} tabIndex={0} onKeyDown={noteListKeyboard.handleKeyDown} onFocus={noteListKeyboard.handleFocus} data-testid="note-list-container">
|
refactor: extract hooks and components from NoteListInner for code health
Split NoteListInner (190 lines, 14 props) into focused units:
- useNoteListSort: sort state, migration, type frontmatter persistence
- useNoteListSearch: search/query state and toggle
- useMultiSelectKeyboard: keyboard shortcuts for bulk actions
- useModifiedFilesState: modified file tracking and status resolution
- NoteListHeader: 52px header bar with sort, search, and create
Also extracted pure helpers (handleEscapeKey, handleSelectAllKey,
handleBulkActionKey, resolveEmptyText, deriveEffectiveSort, etc.)
to reduce cyclomatic complexity in remaining functions.
NoteListInner body: 190 → 48 lines. CodeScene health: 8.54 → 9.36.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:53:15 +01:00
|
|
|
{entitySelection ? (
|
|
|
|
|
<EntityView entity={entitySelection.entry} groups={searchedGroups} query={query} collapsedGroups={collapsedGroups} sortPrefs={sortPrefs} onToggleGroup={toggleGroup} onSortChange={handleSortChange} renderItem={renderItem} typeEntryMap={typeEntryMap} onClickNote={handleClickNote} />
|
2026-02-22 10:18:05 +01:00
|
|
|
) : (
|
2026-03-02 23:25:54 +01:00
|
|
|
<ListView isTrashView={isTrashView} isChangesView={isChangesView} changesError={modifiedFilesError} expiredTrashCount={expiredTrashCount} searched={searched} query={query} renderItem={renderItem} virtuosoRef={noteListKeyboard.virtuosoRef} />
|
2026-02-14 18:22:42 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-27 15:14:21 +01:00
|
|
|
{multiSelect.isMultiSelecting && (
|
|
|
|
|
<BulkActionBar count={multiSelect.selectedPaths.size} onArchive={handleBulkArchive} onTrash={handleBulkTrash} onClear={multiSelect.clear} />
|
|
|
|
|
)}
|
2026-02-14 18:22:42 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-17 17:14:36 +01:00
|
|
|
|
|
|
|
|
export const NoteList = memo(NoteListInner)
|