refactor: extract SearchResultItem and SearchResultsList from SearchContent (#124)
* design: search subtitle metadata layout with 2 frames Frame 1: Search result items with metadata subtitle line showing modified date, word count, and link count below the snippet. Frame 2: NoteList items with enhanced subtitle including link count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add metadata subtitle to search results and note list items - formatSubtitle now shows link count (e.g. "2h ago · 342 words · 5 links") - New formatSearchSubtitle shows full metadata in search results: modified date, created date, word count, and link count - SearchPanel renders metadata line under each search result snippet - Mock entries updated with realistic outgoingLinks data - 11 new tests covering formatSearchSubtitle and search result metadata Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract SearchResultItem and SearchResultsList from SearchContent Reduces cyclomatic complexity from 24 to manageable levels by splitting the monolithic SearchContent into three focused components. Code Health: 8.97 -> 9.23. 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>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import { formatSubtitle, relativeDate } from './noteListHelpers'
|
||||
import { formatSubtitle, formatSearchSubtitle, relativeDate } from './noteListHelpers'
|
||||
import type { VaultEntry } from '../types'
|
||||
|
||||
function makeEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {
|
||||
@@ -48,6 +48,80 @@ describe('formatSubtitle', () => {
|
||||
expect(result).toContain('50 words')
|
||||
expect(result).toContain('\u00b7')
|
||||
})
|
||||
|
||||
it('includes link count when outgoingLinks is non-empty', () => {
|
||||
const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 200, outgoingLinks: ['a', 'b', 'c'] })
|
||||
const result = formatSubtitle(entry)
|
||||
expect(result).toContain('3 links')
|
||||
})
|
||||
|
||||
it('uses singular "link" when exactly one', () => {
|
||||
const entry = makeEntry({ wordCount: 100, outgoingLinks: ['one'] })
|
||||
expect(formatSubtitle(entry)).toContain('1 link')
|
||||
expect(formatSubtitle(entry)).not.toContain('1 links')
|
||||
})
|
||||
|
||||
it('omits link count when outgoingLinks is empty', () => {
|
||||
const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 50, outgoingLinks: [] })
|
||||
expect(formatSubtitle(entry)).not.toContain('link')
|
||||
})
|
||||
|
||||
it('formats word count with locale separators for large numbers', () => {
|
||||
const entry = makeEntry({ wordCount: 1240 })
|
||||
const result = formatSubtitle(entry)
|
||||
expect(result).toMatch(/1,?240 words/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatSearchSubtitle', () => {
|
||||
afterEach(() => { vi.restoreAllMocks() })
|
||||
|
||||
it('shows modified date, created date, word count, and links', () => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const entry = makeEntry({
|
||||
modifiedAt: now - 3600,
|
||||
createdAt: now - 86400 * 30,
|
||||
wordCount: 520,
|
||||
outgoingLinks: ['a', 'b', 'c', 'd', 'e'],
|
||||
})
|
||||
const result = formatSearchSubtitle(entry)
|
||||
expect(result).toContain('1h ago')
|
||||
expect(result).toContain('Created')
|
||||
expect(result).toContain('520 words')
|
||||
expect(result).toContain('5 links')
|
||||
})
|
||||
|
||||
it('omits created date when same as modified', () => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const entry = makeEntry({ modifiedAt: now, createdAt: now, wordCount: 100 })
|
||||
const result = formatSearchSubtitle(entry)
|
||||
expect(result).not.toContain('Created')
|
||||
})
|
||||
|
||||
it('omits created date when createdAt is null', () => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const entry = makeEntry({ modifiedAt: now, createdAt: null, wordCount: 100 })
|
||||
const result = formatSearchSubtitle(entry)
|
||||
expect(result).not.toContain('Created')
|
||||
})
|
||||
|
||||
it('shows "Empty" for zero word count', () => {
|
||||
const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 0 })
|
||||
expect(formatSearchSubtitle(entry)).toContain('Empty')
|
||||
})
|
||||
|
||||
it('omits link count when no outgoing links', () => {
|
||||
const entry = makeEntry({ modifiedAt: 1700000000, wordCount: 50, outgoingLinks: [] })
|
||||
expect(formatSearchSubtitle(entry)).not.toContain('link')
|
||||
})
|
||||
|
||||
it('falls back to createdAt when modifiedAt is null', () => {
|
||||
const entry = makeEntry({ createdAt: 1700000000, wordCount: 200, outgoingLinks: ['a'] })
|
||||
const result = formatSearchSubtitle(entry)
|
||||
expect(result).toContain('200 words')
|
||||
expect(result).toContain('1 link')
|
||||
expect(result).not.toContain('Created')
|
||||
})
|
||||
})
|
||||
|
||||
describe('relativeDate', () => {
|
||||
|
||||
Reference in New Issue
Block a user