From 8b0e7a93a4b9078594f094a85363675fe284b434 Mon Sep 17 00:00:00 2001 From: Madan kumar Date: Tue, 21 Jul 2026 19:49:06 +0530 Subject: [PATCH] fix(shared): multi-select "contains any" filter matcher should use OR semantics (#23010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-memory `isMatchingMultiSelectFilter` evaluated the `containsAny` operand with `Array.every`, which requires a record to hold **all** selected options. But `containsAny` means "any overlap": the server evaluates it as a Postgres array-overlap (`field::text[] && ARRAY[...]`), and the "Contains" UI operand for a MULTI_SELECT field builds exactly this operand — both match on **at least one** shared option. So the matcher disagreed with the server. In a "Tags contains any of [A, B]" view, an optimistic create/update of a record whose tags are just `[A]` was treated as not matching, so it failed to appear (or was wrongly dropped) until a refetch; `DOES_NOT_CONTAIN` (built as `not { containsAny }`) inverted the same way. The same helper backs the row-level-permission predicate matcher. Switched to `Array.some` to match the OR semantics, and updated the tests (partial-overlap, single-overlap, no-overlap, empty-array). The sibling `isMatching*Filter` helpers were checked — this is the only affected one. Review in cubic --- .../isMatchingMultiSelectFilter.test.ts | 31 +++++++++++++++++-- .../utils/isMatchingMultiSelectFilter.ts | 2 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingMultiSelectFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingMultiSelectFilter.test.ts index 4c72a4e169..b768f60589 100644 --- a/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingMultiSelectFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingMultiSelectFilter.test.ts @@ -2,7 +2,7 @@ import { isMatchingMultiSelectFilter } from '@/utils/filter/utils/isMatchingMult describe('isMatchingMultiSelectFilter', () => { describe('containsAny', () => { - it('should return true when value contains all filter items', () => { + it('should return true when value contains every filter item', () => { expect( isMatchingMultiSelectFilter({ multiSelectFilter: { containsAny: ['A', 'B'] }, @@ -11,12 +11,39 @@ describe('isMatchingMultiSelectFilter', () => { ).toBe(true); }); - it('should return false when value does not contain all filter items', () => { + it('should return true when value contains only some of the filter items', () => { expect( isMatchingMultiSelectFilter({ multiSelectFilter: { containsAny: ['A', 'D'] }, value: ['A', 'B', 'C'], }), + ).toBe(true); + }); + + it('should return true when value shares a single option with the filter', () => { + expect( + isMatchingMultiSelectFilter({ + multiSelectFilter: { containsAny: ['B'] }, + value: ['A', 'B', 'C'], + }), + ).toBe(true); + }); + + it('should return false when value shares no option with the filter', () => { + expect( + isMatchingMultiSelectFilter({ + multiSelectFilter: { containsAny: ['X', 'Y'] }, + value: ['A', 'B', 'C'], + }), + ).toBe(false); + }); + + it('should return false for an empty value array', () => { + expect( + isMatchingMultiSelectFilter({ + multiSelectFilter: { containsAny: ['A'] }, + value: [], + }), ).toBe(false); }); diff --git a/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts index 4595f7b36e..7ff7628e96 100644 --- a/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts @@ -11,7 +11,7 @@ export const isMatchingMultiSelectFilter = ({ case multiSelectFilter.containsAny !== undefined: { return ( Array.isArray(value) && - multiSelectFilter.containsAny.every((item) => value.includes(item)) + multiSelectFilter.containsAny.some((item) => value.includes(item)) ); } case multiSelectFilter.isEmptyArray !== undefined: {