mirror of
https://github.com/actualbudget/actual.git
synced 2026-01-06 05:29:55 -06:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/6491.md
Normal file
6
upcoming-release-notes/6491.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [Copilot]
|
||||
---
|
||||
|
||||
Fix crash by ensuring proper filter value conversion between single and multi-value modes.
|
||||
Reference in New Issue
Block a user