Add empty operands to UUID filter type in workflow filter action (#20821)

## Summary
- Adds `IS_EMPTY` and `IS_NOT_EMPTY` operands to the UUID entry in
`getStepFilterOperands`, aligning the workflow filter action with the
find records (search) action which already includes these operands for
ID-type fields.

## Test plan
- [ ] Open a workflow with a filter action, select an ID-type field, and
verify the operand dropdown now includes "Is empty" and "Is not empty"
- [ ] Open a workflow with a find records action, select an ID-type
field, and verify the operand dropdown is consistent with the filter
action

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette
2026-05-21 21:06:10 +02:00
committed by GitHub
parent a66fae79ee
commit 138eb5a74a
13 changed files with 1046 additions and 84 deletions
@@ -0,0 +1,238 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getRecordFilterOperands ACTOR returns ACTOR (text-like) operands when subFieldName is undefined 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands ACTOR returns IS/IS_NOT operands when subFieldName is source (actor source subField) 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands ACTOR returns IS/IS_NOT operands when subFieldName is workspaceMemberId (actor wsMember subField) 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands CURRENCY returns amountMicros operands when subFieldName is amountMicros 1`] = `
[
"GREATER_THAN_OR_EQUAL",
"LESS_THAN_OR_EQUAL",
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands CURRENCY returns amountMicros operands when subFieldName is undefined 1`] = `
[
"GREATER_THAN_OR_EQUAL",
"LESS_THAN_OR_EQUAL",
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands CURRENCY returns currencyCode operands when subFieldName is currencyCode 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=ADDRESS 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=ARRAY 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=BOOLEAN 1`] = `
[
"IS",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=DATE 1`] = `
[
"IS",
"IS_RELATIVE",
"IS_IN_PAST",
"IS_IN_FUTURE",
"IS_TODAY",
"IS_BEFORE",
"IS_AFTER",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=DATE_TIME 1`] = `
[
"IS",
"IS_RELATIVE",
"IS_IN_PAST",
"IS_IN_FUTURE",
"IS_TODAY",
"IS_BEFORE",
"IS_AFTER",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=EMAILS 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=FILES 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=FULL_NAME 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=LINKS 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=MULTI_SELECT 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=NUMBER 1`] = `
[
"IS",
"IS_NOT",
"GREATER_THAN_OR_EQUAL",
"LESS_THAN_OR_EQUAL",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=PHONES 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=RATING 1`] = `
[
"IS",
"IS_NOT",
"GREATER_THAN_OR_EQUAL",
"LESS_THAN_OR_EQUAL",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=RAW_JSON 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=RELATION 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=SELECT 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=TEXT 1`] = `
[
"CONTAINS",
"DOES_NOT_CONTAIN",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=TS_VECTOR 1`] = `
[
"VECTOR_SEARCH",
]
`;
exports[`getRecordFilterOperands returns the expected operands for filterType=UUID 1`] = `
[
"IS",
"IS_NOT",
"IS_EMPTY",
"IS_NOT_EMPTY",
]
`;
@@ -0,0 +1,96 @@
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const filterTypesWithoutSubField: FilterableAndTSVectorFieldType[] = [
'TEXT',
'EMAILS',
'FULL_NAME',
'ADDRESS',
'LINKS',
'PHONES',
'NUMBER',
'RAW_JSON',
'FILES',
'DATE_TIME',
'DATE',
'RATING',
'RELATION',
'MULTI_SELECT',
'SELECT',
'ARRAY',
'BOOLEAN',
'UUID',
'TS_VECTOR',
];
describe('getRecordFilterOperands', () => {
it.each(filterTypesWithoutSubField)(
'returns the expected operands for filterType=%s',
(filterType) => {
const operands = getRecordFilterOperands({
filterType,
subFieldName: undefined,
});
expect(operands).toMatchSnapshot();
},
);
describe('CURRENCY', () => {
it('returns currencyCode operands when subFieldName is currencyCode', () => {
const operands = getRecordFilterOperands({
filterType: 'CURRENCY',
subFieldName: 'currencyCode',
});
expect(operands).toMatchSnapshot();
});
it('returns amountMicros operands when subFieldName is amountMicros', () => {
const operands = getRecordFilterOperands({
filterType: 'CURRENCY',
subFieldName: 'amountMicros',
});
expect(operands).toMatchSnapshot();
});
it('returns amountMicros operands when subFieldName is undefined', () => {
const operands = getRecordFilterOperands({
filterType: 'CURRENCY',
subFieldName: undefined,
});
expect(operands).toMatchSnapshot();
});
});
describe('ACTOR', () => {
it('returns IS/IS_NOT operands when subFieldName is source (actor source subField)', () => {
const operands = getRecordFilterOperands({
filterType: 'ACTOR',
subFieldName: 'source',
});
expect(operands).toMatchSnapshot();
});
it('returns IS/IS_NOT operands when subFieldName is workspaceMemberId (actor wsMember subField)', () => {
const operands = getRecordFilterOperands({
filterType: 'ACTOR',
subFieldName: 'workspaceMemberId',
});
expect(operands).toMatchSnapshot();
});
it('returns ACTOR (text-like) operands when subFieldName is undefined', () => {
const operands = getRecordFilterOperands({
filterType: 'ACTOR',
subFieldName: undefined,
});
expect(operands).toMatchSnapshot();
});
});
});
@@ -112,6 +112,7 @@ export const FILTER_OPERANDS_MAP = {
],
RATING: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
RecordFilterOperand.LESS_THAN_OR_EQUAL,
...emptyOperands,
@@ -139,7 +140,7 @@ export const FILTER_OPERANDS_MAP = {
],
BOOLEAN: [RecordFilterOperand.IS],
TS_VECTOR: [RecordFilterOperand.VECTOR_SEARCH],
UUID: [RecordFilterOperand.IS],
UUID: [RecordFilterOperand.IS, RecordFilterOperand.IS_NOT, ...emptyOperands],
} as const satisfies FilterOperandMap;
export const COMPOSITE_FIELD_FILTER_OPERANDS_MAP = {
@@ -167,6 +167,7 @@ const computeValueFromFilterRating = (
)?.value;
return minusOne ?? option.value;
}
case ViewFilterOperand.IS_NOT:
case ViewFilterOperand.IS_EMPTY:
return undefined;
default:
@@ -275,6 +276,10 @@ const computeValueFromFilterUUID = (
switch (operand) {
case ViewFilterOperand.IS:
return value;
case ViewFilterOperand.IS_NOT:
case ViewFilterOperand.IS_EMPTY:
case ViewFilterOperand.IS_NOT_EMPTY:
return undefined;
default:
assertUnreachable(operand);
}