feat: add body content filter for Views (contains/does not contain)

Adds 'body' as a built-in filter field that searches note snippet text
(case-insensitive). Visually separated from property fields in the
FilterBuilder dropdown with a separator. Combines with other filters via AND.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test
2026-04-04 16:13:21 +02:00
parent 9f905169cb
commit f8d080e678
5 changed files with 85 additions and 3 deletions

View File

@@ -198,4 +198,16 @@ describe('FilterBuilder wikilink autocomplete', () => {
expect(input).toBeInTheDocument()
expect(input).not.toHaveAttribute('data-testid', 'filter-value-input')
})
it('shows body field in field dropdown separated from property fields', () => {
render(
<FilterBuilder
group={{ all: [{ field: 'body', op: 'contains', value: 'test' }] }}
onChange={vi.fn()}
availableFields={['type', 'status', 'body']}
/>,
)
// Body field should be selected as the current value
expect(screen.getByText('body')).toBeInTheDocument()
})
})

View File

@@ -2,7 +2,7 @@ import { useState, useRef, useMemo, useEffect, useCallback } from 'react'
import { Plus, X } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'
import type { FilterCondition, FilterOp, FilterGroup, FilterNode, VaultEntry } from '../types'
import { buildTypeEntryMap, getTypeColor, getTypeLightColor } from '../utils/typeColors'
import { getTypeIcon } from './NoteItem'
@@ -38,12 +38,16 @@ function setGroupChildren(mode: 'all' | 'any', children: FilterNode[]): FilterGr
return mode === 'all' ? { all: children } : { any: children }
}
const CONTENT_FIELDS = new Set(['body'])
function FieldSelect({ value, fields, onChange }: {
value: string
fields: string[]
onChange: (v: string) => void
}) {
const isCustom = value !== '' && !fields.includes(value)
const propertyFields = fields.filter(f => !CONTENT_FIELDS.has(f))
const contentFields = fields.filter(f => CONTENT_FIELDS.has(f))
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger
@@ -54,9 +58,17 @@ function FieldSelect({ value, fields, onChange }: {
</SelectTrigger>
<SelectContent position="popper">
{isCustom && <SelectItem value={value}>{value}</SelectItem>}
{fields.map((f) => (
{propertyFields.map((f) => (
<SelectItem key={f} value={f}>{f}</SelectItem>
))}
{contentFields.length > 0 && (
<>
<SelectSeparator />
{contentFields.map((f) => (
<SelectItem key={f} value={f}>{f}</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
)