fix(workflow): handle IS/IS_NOT operand in text and array filters (#22640)

## Problem

Sentry `TWENTY-SERVER-G4F` — `Error: Operand IS not supported for this
filter type` (30k+ occurrences, 15 workspaces, ongoing).

A workflow **Filter** step throws when a step filter carries an
`IS`/`IS_NOT` operand on a text/array field type (`TEXT`,
`MULTI_SELECT`, `EMAILS`, `PHONES`, `ADDRESS`, `LINKS`, `FULL_NAME`,
`ARRAY`, `RAW_JSON`). `evaluateTextAndArrayFilter` only handled
`CONTAINS`/`DOES_NOT_CONTAIN`/`IS_EMPTY`/`IS_NOT_EMPTY` and hit
`default:` → `throw`. The throw propagates out of `FilterWorkflowAction`
and **fails the entire workflow run**.

The current frontend no longer offers `IS`/`IS_NOT` for these types, so
these are **legacy persisted step filters** in older (immutable)
workflow versions that keep executing.

## Fix

Handle `IS`/`IS_NOT` in `evaluateTextAndArrayFilter` as
`contains`/`!contains`, consistent with `evaluateSelectFilter` (chosen
over strict equality because the routed types include arrays/composites
where `==` would silently never match). No existing operand behavior
changes.

## Tests

Added coverage for legacy `IS`/`IS_NOT` on `TEXT` and `MULTI_SELECT`.
Note: the pre-existing `date operands` test failures are
timezone-dependent and unrelated to this change (they fail on `main`
too).

Fixes TWENTY-SERVER-G4F

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22640?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:
Thomas Trompette
2026-07-08 13:12:08 +02:00
committed by GitHub
parent be68c0cbab
commit bfeaaa56a3
2 changed files with 99 additions and 0 deletions
@@ -804,6 +804,100 @@ describe('evaluateFilterConditions', () => {
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true);
});
it('should match legacy Is operand on text by equality', () => {
const matching = createFilter(
ViewFilterOperand.IS,
'World',
'World',
'TEXT',
);
const substring = createFilter(
ViewFilterOperand.IS,
'Hello World',
'World',
'TEXT',
);
const notMatching = createFilter(
ViewFilterOperand.IS,
'Hello',
'World',
'TEXT',
);
expect(evaluateFilterConditions({ filters: [matching] })).toBe(true);
expect(evaluateFilterConditions({ filters: [substring] })).toBe(false);
expect(evaluateFilterConditions({ filters: [notMatching] })).toBe(
false,
);
});
it('should match legacy IsNot operand on text by inequality', () => {
const matching = createFilter(
ViewFilterOperand.IS_NOT,
'Hello',
'World',
'TEXT',
);
const notMatching = createFilter(
ViewFilterOperand.IS_NOT,
'World',
'World',
'TEXT',
);
expect(evaluateFilterConditions({ filters: [matching] })).toBe(true);
expect(evaluateFilterConditions({ filters: [notMatching] })).toBe(
false,
);
});
it('should match legacy Is operand on arrays by equality', () => {
const matching = createFilter(
ViewFilterOperand.IS,
['apple', 'banana'],
['apple', 'banana'],
'MULTI_SELECT',
);
const subset = createFilter(
ViewFilterOperand.IS,
['apple', 'banana'],
['apple'],
'MULTI_SELECT',
);
const notMatching = createFilter(
ViewFilterOperand.IS,
['apple', 'banana'],
['grape'],
'MULTI_SELECT',
);
expect(evaluateFilterConditions({ filters: [matching] })).toBe(true);
expect(evaluateFilterConditions({ filters: [subset] })).toBe(false);
expect(evaluateFilterConditions({ filters: [notMatching] })).toBe(
false,
);
});
it('should match legacy IsNot operand on arrays by set inequality', () => {
const matching = createFilter(
ViewFilterOperand.IS_NOT,
['apple', 'banana'],
['grape'],
'MULTI_SELECT',
);
const notMatching = createFilter(
ViewFilterOperand.IS_NOT,
['apple', 'banana'],
['apple', 'banana'],
'MULTI_SELECT',
);
expect(evaluateFilterConditions({ filters: [matching] })).toBe(true);
expect(evaluateFilterConditions({ filters: [notMatching] })).toBe(
false,
);
});
});
describe('empty operands', () => {
@@ -4,6 +4,7 @@ import {
isObject,
isString,
} from '@sniptt/guards';
import isEqual from 'lodash.isequal';
import { Temporal } from 'temporal-polyfill';
import {
type StepFilter,
@@ -186,6 +187,10 @@ function evaluateTextAndArrayFilter(
(isDefined(nullEquivalentRightValue) &&
isNotEmptyTextOrArray(filter.leftOperand))
);
case ViewFilterOperand.IS:
return isEqual(filter.leftOperand, filter.rightOperand);
case ViewFilterOperand.IS_NOT:
return !isEqual(filter.leftOperand, filter.rightOperand);
case ViewFilterOperand.IS_EMPTY:
return !isNotEmptyTextOrArray(filter.leftOperand);