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>
This commit is contained in:
lucaronin
2026-04-04 02:18:40 +02:00
parent e0cd5378a3
commit fbf8909b9d
2 changed files with 58 additions and 1 deletions

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)) {