Remove all vestiges of the abandoned Trash system: trashed/trashedAt fields from types, frontmatter parsing, sidebar filtering, editor banners, inspector components, mock data, and all related tests. Delete is already permanent via useDeleteActions with a confirmation dialog. Notes with trashed:true in existing vault frontmatter are now treated as normal notes (the flag is ignored by the parser). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
import type { VaultEntry } from '../types'
|
|
import { fuzzyMatch, bestSearchRank } from '../utils/fuzzyMatch'
|
|
import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors'
|
|
import { getTypeIcon } from '../components/NoteItem'
|
|
import type { NoteSearchResultItem } from '../components/NoteSearchList'
|
|
import { isEmoji } from '../utils/emoji'
|
|
|
|
const DEFAULT_MAX_RESULTS = 20
|
|
|
|
export interface NoteSearchResult extends NoteSearchResultItem {
|
|
entry: VaultEntry
|
|
}
|
|
|
|
function toResult(e: VaultEntry, typeEntryMap: Record<string, VaultEntry>): NoteSearchResult {
|
|
const noteType = e.isA || undefined
|
|
const te = typeEntryMap[e.isA ?? '']
|
|
const emojiPrefix = e.icon && isEmoji(e.icon) ? `${e.icon} ` : ''
|
|
return {
|
|
entry: e,
|
|
title: `${emojiPrefix}${e.title}`,
|
|
noteType,
|
|
typeColor: noteType ? getTypeColor(e.isA, te?.color) : undefined,
|
|
typeLightColor: noteType ? getTypeLightColor(e.isA, te?.color) : undefined,
|
|
TypeIcon: noteType ? getTypeIcon(e.isA, te?.icon) : undefined,
|
|
}
|
|
}
|
|
|
|
/** Types excluded from note search results (internal infrastructure). */
|
|
const SEARCH_EXCLUDED_TYPES = new Set(['Config'])
|
|
|
|
export function useNoteSearch(entries: VaultEntry[], query: string, maxResults = DEFAULT_MAX_RESULTS) {
|
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries])
|
|
|
|
const searchableEntries = useMemo(
|
|
() => entries.filter((e) => !SEARCH_EXCLUDED_TYPES.has(e.isA ?? '')),
|
|
[entries],
|
|
)
|
|
|
|
const results: NoteSearchResult[] = useMemo(() => {
|
|
const mapResult = (e: VaultEntry) => toResult(e, typeEntryMap)
|
|
if (!query.trim()) {
|
|
return [...searchableEntries]
|
|
.sort((a, b) => (b.modifiedAt ?? 0) - (a.modifiedAt ?? 0))
|
|
.slice(0, maxResults)
|
|
.map(mapResult)
|
|
}
|
|
return searchableEntries
|
|
.map((e) => ({
|
|
entry: e,
|
|
...fuzzyMatch(query, e.title),
|
|
rank: bestSearchRank(query, e.title, e.aliases),
|
|
}))
|
|
.filter((r) => r.match)
|
|
.sort((a, b) => a.rank - b.rank || b.score - a.score)
|
|
.slice(0, maxResults)
|
|
.map((r) => mapResult(r.entry))
|
|
}, [searchableEntries, query, maxResults, typeEntryMap])
|
|
|
|
useEffect(() => {
|
|
setSelectedIndex(0) // eslint-disable-line react-hooks/set-state-in-effect -- reset on query change
|
|
}, [query])
|
|
|
|
const selectedEntry = results[selectedIndex]?.entry ?? null
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent | KeyboardEvent) => {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault()
|
|
setSelectedIndex((i) => Math.min(i + 1, results.length - 1))
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault()
|
|
setSelectedIndex((i) => Math.max(i - 1, 0))
|
|
}
|
|
},
|
|
[results.length],
|
|
)
|
|
|
|
return { results, selectedIndex, setSelectedIndex, selectedEntry, handleKeyDown }
|
|
}
|