feat: MVP-2 completion - search, quick add, backlinks, guided forms

## Search & Retrieval
- Improved search ranking with scoring (title match, favorites, recency)
- Highlight matches with excerpt extraction
- Fuzzy search with string-similarity
- Unified noteQuery function

## Quick Capture
- Quick Add API (POST /api/notes/quick) with type prefixes
- Quick add parser with tag extraction
- Global Quick Add UI (Ctrl+N shortcut)
- Tag autocomplete in forms

## Note Relations
- Automatic backlinks with sync on create/update/delete
- Backlinks API (GET /api/notes/[id]/backlinks)
- Related notes with scoring and reasons

## Guided Forms
- Type-specific form fields (command, snippet, decision, recipe, procedure, inventory)
- Serialization to/from markdown
- Tag suggestions based on content (GET /api/tags/suggest)

## UX by Type
- Command: Copy button for code blocks
- Snippet: Syntax highlighting with react-syntax-highlighter
- Procedure: Interactive checkboxes

## Quality
- Standardized error handling across all APIs
- Integration tests (28 tests passing)
- Unit tests for search, tags, quick-add

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 13:51:39 -03:00
parent 6694bce736
commit 8b77c7b5df
30 changed files with 6548 additions and 282 deletions

63
__tests__/search.test.ts Normal file
View File

@@ -0,0 +1,63 @@
import { highlightMatches } from '@/lib/search'
// Mock prisma before importing search module
jest.mock('@/lib/prisma', () => ({
prisma: {
note: {
findMany: jest.fn().mockResolvedValue([]),
},
},
}))
describe('search.ts', () => {
describe('highlightMatches', () => {
it('returns first 150 characters when query is empty', () => {
const text = 'This is a long text that should be truncated to 150 characters. ' +
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.'
const result = highlightMatches(text, '')
expect(result.length).toBeLessThanOrEqual(150)
})
it('finds and highlights matching words', () => {
const text = 'This is a test document about JavaScript programming.'
const result = highlightMatches(text, 'JavaScript')
expect(result).toContain('<mark>JavaScript</mark>')
})
it('handles multiple word queries', () => {
const text = 'React is a JavaScript library for building user interfaces.'
const result = highlightMatches(text, 'JavaScript React')
expect(result).toContain('<mark>JavaScript</mark>')
expect(result).toContain('<mark>React</mark>')
})
it('escapes regex special characters in query', () => {
const text = 'What is $100 + $200?'
const result = highlightMatches(text, '$100')
expect(result).toContain('<mark>$100</mark>')
})
it('adds ellipsis when match is not at start', () => {
const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. JavaScript is great.'
const result = highlightMatches(text, 'JavaScript')
// JavaScript is at position 61, start is max(0, 61-75) = 0, so no ellipsis needed
expect(result).toContain('<mark>JavaScript</mark>')
})
it('returns plain text when no match found', () => {
const text = 'This is a simple text without the word we are looking for.'
const result = highlightMatches(text, 'xyz123')
expect(result).not.toContain('<mark>')
})
it('filters out single character words and returns excerpt', () => {
const text = 'A B C D E F G H I J K L M N O P Q R S T U V W X Y Z'
const result = highlightMatches(text, 'A B C')
// Single char words (A, B, C) are filtered, returns 150 chars
expect(result.length).toBeLessThanOrEqual(150)
})
})
// Integration tests would go here but require database setup
// These would test noteQuery and searchNotes with actual data
})