Compare commits

...

2 Commits

Author SHA1 Message Date
Test
4a48833dbc fix: use substring match for contains/not_contains on relationship fields
Plain text values now use substring matching on wikilink stems so
"Monday" matches [[Monday Ideas]], [[Monday Recap]], etc. Wikilink
syntax values ([[...]]) still use exact stem matching. any_of/none_of
remain exact match only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:18:40 +02:00
Test
d21eef8aa6 fix: resize Create View dialog and make filters scrollable
Set min-width to 600px, max-height to 80vh. The filters section now
scrolls when it grows past the available space while the header (icon +
name) and footer (Cancel / Create) stay pinned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:56:03 +02:00
3 changed files with 61 additions and 4 deletions

View File

@@ -60,11 +60,11 @@ export function CreateViewDialog({ open, onClose, onCreate, availableFields, val
return (
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onClose() }}>
<DialogContent showCloseButton={false} className="sm:max-w-[520px]">
<DialogContent showCloseButton={false} className="flex max-h-[80vh] flex-col sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Create View</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col gap-4">
<div className="flex gap-2">
<div className="w-16 space-y-1.5 relative">
<label className="text-xs font-medium text-muted-foreground">Icon</label>
@@ -90,7 +90,7 @@ export function CreateViewDialog({ open, onClose, onCreate, availableFields, val
/>
</div>
</div>
<div className="space-y-1.5">
<div className="min-h-0 flex-1 space-y-1.5 overflow-y-auto">
<label className="text-xs font-medium text-muted-foreground">Filters</label>
<FilterBuilder
group={filters}

View File

@@ -136,4 +136,58 @@ describe('evaluateView', () => {
const result = evaluateView(view, entries)
expect(result.map((e) => e.title)).toEqual(['A', 'B'])
})
it('contains on relationship uses substring match for plain text', () => {
const view: ViewDefinition = {
name: 'Monday', icon: null, color: null, sort: null,
filters: { all: [{ field: 'belongs to', op: 'contains', value: 'Monday' }] },
}
const entries = [
makeEntry({ title: 'A', relationships: { 'belongs to': ['[[Monday Ideas]]'] } }),
makeEntry({ title: 'B', relationships: { 'belongs to': ['[[Monday Recap]]'] } }),
makeEntry({ title: 'C', relationships: { 'belongs to': ['[[Tuesday Ideas]]'] } }),
]
const result = evaluateView(view, entries)
expect(result.map((e) => e.title)).toEqual(['A', 'B'])
})
it('not_contains on relationship uses substring match for plain text', () => {
const view: ViewDefinition = {
name: 'Not Monday', icon: null, color: null, sort: null,
filters: { all: [{ field: 'belongs to', op: 'not_contains', value: 'Monday' }] },
}
const entries = [
makeEntry({ title: 'A', relationships: { 'belongs to': ['[[Monday Ideas]]'] } }),
makeEntry({ title: 'B', relationships: { 'belongs to': ['[[Tuesday Ideas]]'] } }),
makeEntry({ title: 'C', relationships: { 'belongs to': [] } }),
]
const result = evaluateView(view, entries)
expect(result.map((e) => e.title)).toEqual(['B', 'C'])
})
it('contains on relationship uses exact match for wikilink syntax', () => {
const view: ViewDefinition = {
name: 'Exact', icon: null, color: null, sort: null,
filters: { all: [{ field: 'belongs to', op: 'contains', value: '[[Monday Ideas]]' }] },
}
const entries = [
makeEntry({ title: 'A', relationships: { 'belongs to': ['[[Monday Ideas]]'] } }),
makeEntry({ title: 'B', relationships: { 'belongs to': ['[[Monday Recap]]'] } }),
]
const result = evaluateView(view, entries)
expect(result.map((e) => e.title)).toEqual(['A'])
})
it('any_of / none_of on relationship always use exact stem match', () => {
const view: ViewDefinition = {
name: 'Exact list', icon: null, color: null, sort: null,
filters: { all: [{ field: 'belongs to', op: 'any_of', value: ['[[Monday]]'] }] },
}
const entries = [
makeEntry({ title: 'Exact', relationships: { 'belongs to': ['[[Monday]]'] } }),
makeEntry({ title: 'Partial', relationships: { 'belongs to': ['[[Monday Ideas]]'] } }),
]
const result = evaluateView(view, entries)
expect(result.map((e) => e.title)).toEqual(['Exact'])
})
})

View File

@@ -75,7 +75,10 @@ function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
if (resolved.array) {
const stem = wikilinkStem(condVal)
const arrayMatch = (arr: string[]) => arr.some((item) => wikilinkStem(item) === stem)
const isWikilink = condVal.trim().startsWith('[[')
const arrayMatch = (arr: string[]) => arr.some((item) =>
isWikilink ? wikilinkStem(item) === stem : wikilinkStem(item).includes(stem)
)
if (op === 'contains') return arrayMatch(resolved.array)
if (op === 'not_contains') return !arrayMatch(resolved.array)
if (op === 'any_of' && Array.isArray(value)) {