diff --git a/src/components/SearchPanel.test.tsx b/src/components/SearchPanel.test.tsx index 94484e0c..51c4fbce 100644 --- a/src/components/SearchPanel.test.tsx +++ b/src/components/SearchPanel.test.tsx @@ -12,6 +12,8 @@ vi.mock('../mock-tauri', () => ({ import { mockInvoke } from '../mock-tauri' const mockInvokeFn = vi.mocked(mockInvoke) +const NOW = Math.floor(Date.now() / 1000) + const MOCK_ENTRIES: VaultEntry[] = [ { path: '/vault/essay/ai-apis.md', @@ -27,16 +29,16 @@ const MOCK_ENTRIES: VaultEntry[] = [ archived: false, trashed: false, trashedAt: null, - modifiedAt: Date.now() / 1000, - createdAt: Date.now() / 1000, + modifiedAt: NOW - 7200, + createdAt: NOW - 86400 * 30, fileSize: 500, snippet: 'A guide to designing APIs for AI', - wordCount: 0, + wordCount: 1247, relationships: {}, icon: null, color: null, order: null, - outgoingLinks: [], + outgoingLinks: ['topic/ai', 'topic/api-design', 'person/luca'], }, { path: '/vault/event/retreat.md', @@ -52,16 +54,16 @@ const MOCK_ENTRIES: VaultEntry[] = [ archived: false, trashed: false, trashedAt: null, - modifiedAt: Date.now() / 1000, - createdAt: Date.now() / 1000, + modifiedAt: NOW - 86400 * 5, + createdAt: NOW - 86400 * 5, fileSize: 300, snippet: 'Team retreat event', - wordCount: 0, + wordCount: 856, relationships: {}, icon: null, color: null, order: null, - outgoingLinks: [], + outgoingLinks: ['person/bob'], }, ] @@ -270,6 +272,49 @@ describe('SearchPanel', () => { }) }) + it('shows metadata subtitle with word count and links', async () => { + mockInvokeFn.mockResolvedValue({ + results: [ + { title: 'How to Design AI-first APIs', path: '/vault/essay/ai-apis.md', snippet: 'Content', score: 0.9, note_type: null }, + ], + elapsed_ms: 20, + }) + + render( + , + ) + + fireEvent.change(screen.getByPlaceholderText('Search in all notes...'), { target: { value: 'api' } }) + + await waitFor(() => { + expect(screen.getByText(/1,247 words/)).toBeInTheDocument() + expect(screen.getByText(/3 links/)).toBeInTheDocument() + }) + }) + + it('omits links from subtitle when entry has zero outgoing links', async () => { + const noLinksEntries = MOCK_ENTRIES.map(e => + e.path === '/vault/essay/ai-apis.md' ? { ...e, outgoingLinks: [] } : e, + ) + mockInvokeFn.mockResolvedValue({ + results: [ + { title: 'How to Design AI-first APIs', path: '/vault/essay/ai-apis.md', snippet: '', score: 0.9, note_type: null }, + ], + elapsed_ms: 20, + }) + + render( + , + ) + + fireEvent.change(screen.getByPlaceholderText('Search in all notes...'), { target: { value: 'api' } }) + + await waitFor(() => { + expect(screen.getByText(/1,247 words/)).toBeInTheDocument() + expect(screen.queryByText(/links/)).not.toBeInTheDocument() + }) + }) + it('shows loading spinner while searching', async () => { const resolvers: ((v: unknown) => void)[] = [] mockInvokeFn.mockImplementation( diff --git a/src/components/SearchPanel.tsx b/src/components/SearchPanel.tsx index 28e05b6d..10c9e69e 100644 --- a/src/components/SearchPanel.tsx +++ b/src/components/SearchPanel.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge' import type { SearchResult, VaultEntry } from '../types' import { useUnifiedSearch } from '../hooks/useUnifiedSearch' import { getTypeColor, getTypeLightColor, buildTypeEntryMap } from '../utils/typeColors' +import { formatSearchSubtitle } from '../utils/noteListHelpers' interface SearchPanelProps { open: boolean @@ -63,8 +64,8 @@ export function SearchPanel({ open, vaultPath, entries, onSelectNote, onClose }: const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const entryLookup = useMemo(() => { - const map = new Map() - for (const e of entries) map.set(e.path, { isA: e.isA }) + const map = new Map() + for (const e of entries) map.set(e.path, e) return map }, [entries]) @@ -148,7 +149,7 @@ interface SearchContentProps { selectedIndex: number loading: boolean elapsedMs: number | null - entryLookup: Map + entryLookup: Map typeEntryMap: Record listRef: React.RefObject onSelect: (result: SearchResult) => void @@ -190,10 +191,12 @@ function SearchContent({
{results.map((result, i) => { - const isA = entryLookup.get(result.path)?.isA ?? result.noteType + const entry = entryLookup.get(result.path) + const isA = entry?.isA ?? result.noteType const noteType = isA && isA !== 'Note' ? isA : null const typeColor = noteType ? getTypeColor(isA, typeEntryMap[isA ?? '']?.color) : undefined const typeLightColor = noteType ? getTypeLightColor(isA, typeEntryMap[isA ?? '']?.color) : undefined + const subtitle = entry ? formatSearchSubtitle(entry) : null return (
)}
- {result.snippet && ( -

- {result.snippet} + {subtitle && ( +

+ {subtitle}

)}