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] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
This commit is contained in:
Copilot
2025-12-31 00:53:43 +01:00
committed by GitHub
parent fc89c74445
commit e121e525ce
3 changed files with 173 additions and 3 deletions

View File

@@ -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');
});
});
});

View File

@@ -23,9 +23,22 @@ export function updateFilterReducer<T extends RuleConditionEntity>(
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 };
}

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [Copilot]
---
Fix crash by ensuring proper filter value conversion between single and multi-value modes.