fix(shared): multi-select "contains any" filter matcher should use OR semantics (#23010)
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.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23010?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
+29
-2
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user