From e121e525ce955166bf4c94b41a3b6a92e4d27266 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:53:43 +0100 Subject: [PATCH] Fix crash when switching filter operators between single and multi-value modes (#6491) * Initial plan * Fix crash when switching filter from 'is' to 'one of' - Add logic to convert single values to arrays when switching to oneOf/notOneOf operators - Add comprehensive tests for the fix Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com> * Add type assertions to fix TypeScript errors Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com> * Keep first element when switching from 'one of' to 'is', use strict comparison - When switching from array operators (oneOf/notOneOf) to single-value operators (is/isNot), keep the first element instead of clearing - Use strict equality operators (=== instead of ==) - Add comprehensive tests for array-to-single conversion Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com> * Add release notes for PR #6491 * Fix crash due to filter value conversion issue * Address PR feedback: remove notes test, preserve single values between operators - Remove test for notes field exclusion (issue #6325 handles notes separately) - Only clear values when converting FROM arrays, not between single-value operators - Add test to verify single values are preserved when switching between single-value operators (e.g., 'is' to 'contains') Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com> * Update value handling in updateFilterReducer Handle value conversion for single-value operators and clear value for certain type switches. * Refactor value assignment for single-value operators * Refactor value assignment logic in updateFilterReducer Simplify the handling of value assignment for single-value operators by consolidating conditions. Ensure proper conversion between arrays and single values while maintaining type integrity. * Remove redundant array checks in updateFilterReducer tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Matiss Janis Aboltins --- .../filters/updateFilterReducer.test.ts | 151 ++++++++++++++++++ .../components/filters/updateFilterReducer.ts | 19 ++- upcoming-release-notes/6491.md | 6 + 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 packages/desktop-client/src/components/filters/updateFilterReducer.test.ts create mode 100644 upcoming-release-notes/6491.md diff --git a/packages/desktop-client/src/components/filters/updateFilterReducer.test.ts b/packages/desktop-client/src/components/filters/updateFilterReducer.test.ts new file mode 100644 index 0000000000..06dd052ffc --- /dev/null +++ b/packages/desktop-client/src/components/filters/updateFilterReducer.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect } from 'vitest'; + +import { updateFilterReducer } from './updateFilterReducer'; + +describe('updateFilterReducer', () => { + describe('when changing operators', () => { + it('should convert single value to array when switching from "is" to "oneOf"', () => { + const state = { + field: 'category' as const, + op: 'is' as const, + value: 'category-id-123', + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'oneOf', + }); + + expect(result.op).toBe('oneOf'); + expect(result.value).toEqual(['category-id-123']); + }); + + it('should convert single value to array when switching from "isNot" to "notOneOf"', () => { + const state = { + field: 'account' as const, + op: 'isNot' as const, + value: 'account-id-456', + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'notOneOf', + }); + + expect(result.op).toBe('notOneOf'); + expect(result.value).toEqual(['account-id-456']); + }); + + it('should keep first element when switching from "oneOf" to "is" with multiple values', () => { + const state = { + field: 'category' as const, + op: 'oneOf' as const, + value: ['category-id-123', 'category-id-456'], + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'is', + }); + + expect(result.op).toBe('is'); + expect(result.value).toBe('category-id-123'); + }); + + it('should keep first element when switching from "oneOf" to "is" with single value array', () => { + const state = { + field: 'category' as const, + op: 'oneOf' as const, + value: ['category-id-789'], + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'is', + }); + + expect(result.op).toBe('is'); + expect(result.value).toBe('category-id-789'); + }); + + it('should handle empty array when switching from "oneOf" to "is"', () => { + const state = { + field: 'category' as const, + op: 'oneOf' as const, + value: [], + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'is', + }); + + expect(result.op).toBe('is'); + expect(result.value).toBe(null); + }); + + it('should keep first element when switching from "notOneOf" to "isNot"', () => { + const state = { + field: 'account' as const, + op: 'notOneOf' as const, + value: ['account-id-111', 'account-id-222'], + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'isNot', + }); + + expect(result.op).toBe('isNot'); + expect(result.value).toBe('account-id-111'); + }); + + it('should handle null value when switching to "oneOf"', () => { + const state = { + field: 'category' as const, + op: 'is' as const, + value: null, + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'oneOf', + }); + + expect(result.op).toBe('oneOf'); + expect(result.value).toEqual([]); + }); + + it('should keep array value when already in array format for "oneOf"', () => { + const state = { + field: 'category' as const, + op: 'oneOf' as const, + value: ['category-id-123'], + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'notOneOf', + }); + + expect(result.op).toBe('notOneOf'); + expect(result.value).toEqual(['category-id-123']); + }); + + it('should preserve single value when switching between single-value operators', () => { + const state = { + field: 'category' as const, + op: 'is' as const, + value: 'category-id-123', + }; + + const result = updateFilterReducer(state, { + type: 'set-op', + op: 'contains', + }); + + expect(result.op).toBe('contains'); + expect(result.value).toBe('category-id-123'); + }); + }); +}); diff --git a/packages/desktop-client/src/components/filters/updateFilterReducer.ts b/packages/desktop-client/src/components/filters/updateFilterReducer.ts index 6074ec00bc..9b51284afe 100644 --- a/packages/desktop-client/src/components/filters/updateFilterReducer.ts +++ b/packages/desktop-client/src/components/filters/updateFilterReducer.ts @@ -23,9 +23,22 @@ export function updateFilterReducer( action.op === 'onBudget' || action.op === 'offBudget') ) { - // Clear out the value if switching between contains or - // is/oneof for the id or string type - value = null; + // When switching to single-value operators, convert array to first element + if (Array.isArray(value)) { + value = value.length > 0 ? value[0] : null; + } + } else if ( + (type === 'id' || type === 'string') && + state.field !== 'notes' && + (action.op === 'oneOf' || action.op === 'notOneOf') + ) { + // Convert single value to array when switching to oneOf/notOneOf + if (value === null || value === undefined) { + value = []; + } else if (!Array.isArray(value)) { + // @ts-expect-error - fix me + value = [value]; + } } return { ...state, op: action.op, value }; } diff --git a/upcoming-release-notes/6491.md b/upcoming-release-notes/6491.md new file mode 100644 index 0000000000..c4fe9ecc1f --- /dev/null +++ b/upcoming-release-notes/6491.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [Copilot] +--- + +Fix crash by ensuring proper filter value conversion between single and multi-value modes.