From c8837aeaebf79cd323d0adbb865ab77b362f9253 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 13 Oct 2025 11:10:22 +0200 Subject: [PATCH] fix(filters): support project filter in parentheses (#1647) The filter regex pattern was not matching values inside parentheses correctly. The lookahead pattern only allowed `&&`, `||`, or end-of-string after filter values, but when filters are wrapped in parentheses like `( project = Filtertest )`, the closing `)` appears after the value. Fixed by adding `\)` to the lookahead pattern so it correctly handles closing parentheses. This allows the project filter (and other filters) to work properly when nested in parentheses. - Added tests for project filters in parentheses (both frontend and backend) - Backend tests confirm the backend already handled this correctly - Frontend regex pattern now matches the backend behavior Fixes #1645 --- frontend/src/helpers/filters.test.ts | 20 +++++++++++++++++ frontend/src/helpers/filters.ts | 2 +- pkg/models/task_collection_filter_test.go | 27 +++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/frontend/src/helpers/filters.test.ts b/frontend/src/helpers/filters.test.ts index 2fc095521..72d3946ab 100644 --- a/frontend/src/helpers/filters.test.ts +++ b/frontend/src/helpers/filters.test.ts @@ -187,6 +187,26 @@ describe('Filter Transformation', () => { expect(transformed).toBe('project = 1') }) + + it('should correctly resolve project in parentheses', () => { + const transformed = transformFilterStringForApi( + '( project = Filtertest )', + nullTitleToIdResolver, + (title: string) => title === 'Filtertest' ? 123 : null, + ) + + expect(transformed).toBe('( project = 123 )') + }) + + it('should correctly resolve project with OR in parentheses', () => { + const transformed = transformFilterStringForApi( + '( labels = label || project = Filtertest )', + (title: string) => title === 'label' ? 456 : null, + (title: string) => title === 'Filtertest' ? 123 : null, + ) + + expect(transformed).toBe('( labels = 456 || project = 123 )') + }) }) describe('Special Characters', () => { diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 4925488a4..b94855f38 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -81,7 +81,7 @@ export function hasFilterQuery(filter: string): boolean { } export function getFilterFieldRegexPattern(field: string): RegExp { - return new RegExp('\\b(' + field + ')\\s*' + FILTER_OPERATORS_REGEX + '\\s*(?:(["\'])((?:\\\\.|(?!\\3)[^\\\\])*?)\\3|([^&|()<]+?))(?=\\s*(?:&&|\\||$))', 'g') + return new RegExp('\\b(' + field + ')\\s*' + FILTER_OPERATORS_REGEX + '\\s*(?:(["\'])((?:\\\\.|(?!\\3)[^\\\\])*?)\\3|([^&|()<]+?))(?=\\s*(?:&&|\\||\\)|$))', 'g') } export function transformFilterStringForApi( diff --git a/pkg/models/task_collection_filter_test.go b/pkg/models/task_collection_filter_test.go index 06819de21..e8954e752 100644 --- a/pkg/models/task_collection_filter_test.go +++ b/pkg/models/task_collection_filter_test.go @@ -284,4 +284,31 @@ func TestParseFilter(t *testing.T) { assert.Equal(t, 0, date.Year()) } }) + t.Run("project with parentheses", func(t *testing.T) { + result, err := getTaskFiltersFromFilterString("( project = 1 )", "UTC") + + require.NoError(t, err) + require.Len(t, result, 1) + require.Len(t, result[0].value, 1) + + firstSet := result[0].value.([]*taskFilter) + assert.Equal(t, "project_id", firstSet[0].field) + assert.Equal(t, taskFilterComparatorEquals, firstSet[0].comparator) + assert.Equal(t, int64(1), firstSet[0].value) + }) + t.Run("project with OR in parentheses", func(t *testing.T) { + result, err := getTaskFiltersFromFilterString("( done = false || project = 1 )", "UTC") + + require.NoError(t, err) + require.Len(t, result, 1) + require.Len(t, result[0].value, 2) + + firstSet := result[0].value.([]*taskFilter) + assert.Equal(t, "done", firstSet[0].field) + assert.Equal(t, taskFilterComparatorEquals, firstSet[0].comparator) + assert.Equal(t, false, firstSet[0].value) + assert.Equal(t, "project_id", firstSet[1].field) + assert.Equal(t, taskFilterComparatorEquals, firstSet[1].comparator) + assert.Equal(t, int64(1), firstSet[1].value) + }) }