From fbf8909b9d5ff2d2b4a3295a6a2dbc37924c03e4 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 4 Apr 2026 02:18:40 +0200 Subject: [PATCH] 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) --- src/utils/viewFilters.test.ts | 54 +++++++++++++++++++++++++++++++++++ src/utils/viewFilters.ts | 5 +++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/utils/viewFilters.test.ts b/src/utils/viewFilters.test.ts index 0f04f21f..4454c12a 100644 --- a/src/utils/viewFilters.test.ts +++ b/src/utils/viewFilters.test.ts @@ -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']) + }) }) diff --git a/src/utils/viewFilters.ts b/src/utils/viewFilters.ts index c0b6dfc8..7423e79d 100644 --- a/src/utils/viewFilters.ts +++ b/src/utils/viewFilters.ts @@ -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)) {