From 47f5da179878cde65ff8c272c6648e31e2fb2342 Mon Sep 17 00:00:00 2001 From: Andrius Miasnikovas Date: Thu, 14 May 2026 11:08:48 +0300 Subject: [PATCH] Fix equals and not equals filters not working in views --- src/utils/viewFilters.test.ts | 47 +++++++++++++++++++++++++++++++++++ src/utils/viewFilters.ts | 6 +++++ 2 files changed, 53 insertions(+) diff --git a/src/utils/viewFilters.test.ts b/src/utils/viewFilters.test.ts index 051a3a17..b4f93913 100644 --- a/src/utils/viewFilters.test.ts +++ b/src/utils/viewFilters.test.ts @@ -181,6 +181,53 @@ describe('evaluateView', () => { expect(result.map((e) => e.title)).toEqual(['A']) }) + it('equals on relationship matches a single-item array by stem', () => { + const view: ViewDefinition = { + name: 'session-trail', icon: null, color: null, sort: null, + filters: { + any: [ + { field: 'belongs_to', op: 'equals', value: 'svc-session-trail' }, + { field: 'related_to', op: 'equals', value: 'svc-session-trail' }, + ], + }, + } + const entries = [ + makeEntry({ title: 'Matches', relationships: { related_to: ['[[svc-session-trail]]'] } }), + makeEntry({ title: 'Bracketed value also matches', relationships: { related_to: ['[[svc-session-trail|Trail]]'] } }), + makeEntry({ title: 'Other relation', relationships: { related_to: ['[[unrelated]]'] } }), + makeEntry({ title: 'No rels', relationships: {} }), + ] + const result = evaluateView(view, entries) + expect(result.map((e) => e.title)).toEqual(['Matches', 'Bracketed value also matches']) + }) + + it('equals on relationship requires a single-item array (mirrors Rust semantics)', () => { + const view: ViewDefinition = { + name: 'single', icon: null, color: null, sort: null, + filters: { all: [{ field: 'related_to', op: 'equals', value: 'svc-session-trail' }] }, + } + const entries = [ + makeEntry({ title: 'Single', relationships: { related_to: ['[[svc-session-trail]]'] } }), + makeEntry({ title: 'Multiple', relationships: { related_to: ['[[svc-session-trail]]', '[[other]]'] } }), + ] + const result = evaluateView(view, entries) + expect(result.map((e) => e.title)).toEqual(['Single']) + }) + + it('not_equals on relationship is the inverse of equals', () => { + const view: ViewDefinition = { + name: 'not-equals', icon: null, color: null, sort: null, + filters: { all: [{ field: 'related_to', op: 'not_equals', value: 'svc-session-trail' }] }, + } + const entries = [ + makeEntry({ title: 'Single match', relationships: { related_to: ['[[svc-session-trail]]'] } }), + makeEntry({ title: 'Multiple', relationships: { related_to: ['[[svc-session-trail]]', '[[other]]'] } }), + makeEntry({ title: 'Other', relationships: { related_to: ['[[unrelated]]'] } }), + ] + const result = evaluateView(view, entries) + expect(result.map((e) => e.title)).toEqual(['Multiple', 'Other']) + }) + it('any_of / none_of on relationship always use exact stem match', () => { const view: ViewDefinition = { name: 'Exact list', icon: null, color: null, sort: null, diff --git a/src/utils/viewFilters.ts b/src/utils/viewFilters.ts index 50f805e1..c7ff338a 100644 --- a/src/utils/viewFilters.ts +++ b/src/utils/viewFilters.ts @@ -143,12 +143,18 @@ function hasArrayMatch(values: string[], condVal: string): boolean { )) } +function isSingleRelationshipMatch(values: string[], condVal: string): boolean { + return values.length === 1 && wikilinkEquals(values[0], condVal) +} + function evaluateArrayCondition(cond: FilterCondition, values: string[], condVal: string, regex: RegExp | null): boolean { const { op, value } = cond if (regex) return evaluateRegexArrayCondition(op, values, regex) if (op === 'contains') return hasArrayMatch(values, condVal) if (op === 'not_contains') return !hasArrayMatch(values, condVal) + if (op === 'equals') return isSingleRelationshipMatch(values, condVal) + if (op === 'not_equals') return !isSingleRelationshipMatch(values, condVal) if (op === 'any_of' && Array.isArray(value)) { return values.some((item) => (value as string[]).some((v) => wikilinkEquals(item, v))) }