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
@@ -484,6 +484,18 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
expect(result).toHaveProperty('rating.in');
});
it('should handle IS_NOT operand as wrapped eq', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-rating', RecordFilterOperand.IS_NOT, '3'),
fieldMetadataItemById,
});
expect(result).toEqual({
not: { rating: { eq: 'RATING_3' } },
});
});
});
describe('BOOLEAN filter', () => {
@@ -920,6 +932,24 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
expect(result).toHaveProperty('recordId.in');
});
it('should handle IS_NOT operand as wrapped in', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-uuid',
RecordFilterOperand.IS_NOT,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
fieldMetadataItemById,
});
expect(result).toEqual({
not: {
recordId: { in: ['550e8400-e29b-41d4-a716-446655440000'] },
},
});
});
});
describe('relation traversal', () => {
@@ -507,6 +507,14 @@ const buildDirectFieldGqlOperationFilter = ({
eq: convertRatingToRatingValue(parseFloat(recordFilter.value)),
} as RatingFilter,
};
case RecordFilterOperand.IS_NOT:
return {
not: {
[fieldMetadataItem.name]: {
eq: convertRatingToRatingValue(parseFloat(recordFilter.value)),
} as RatingFilter,
},
};
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
return {
[fieldMetadataItem.name]: {
@@ -1525,9 +1533,16 @@ const buildDirectFieldGqlOperationFilter = ({
};
}
case 'UUID': {
const recordIds = arrayOfUuidOrVariableSchema.parse(recordFilter.value);
const parsedRecordIds = arrayOfUuidOrVariableSchema.parse(
recordFilter.value,
);
if (!isDefined(recordIds) || recordIds.length === 0) return;
// Fall back to a sentinel v4 UUID when the input isn't a valid UUID so the
// filter compiles to a guaranteed no-match instead of being silently dropped.
const recordIds =
isDefined(parsedRecordIds) && parsedRecordIds.length > 0
? parsedRecordIds
: ['00000000-0000-4000-8000-000000000000'];
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
@@ -1536,6 +1551,14 @@ const buildDirectFieldGqlOperationFilter = ({
in: recordIds,
} as UUIDFilter,
};
case RecordFilterOperand.IS_NOT:
return {
not: {
[fieldMetadataItem.name]: {
in: recordIds,
} as UUIDFilter,
},
};
default:
throw new Error(
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
@@ -97,6 +97,16 @@ describe('getEmptyRecordGqlOperationFilter', () => {
});
});
it('should handle UUID type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.UUID),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle CURRENCY type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.CURRENCY),
@@ -15,6 +15,7 @@ import {
type RelationFilter,
type SelectFilter,
type StringFilter,
type UUIDFilter,
} from '@/types';
import { CustomError } from '@/utils/errors';
import { computeEmptyGqlOperationFilterForEmails } from '@/utils/filter/computeEmptyGqlOperationFilterForEmails';
@@ -308,6 +309,11 @@ export const getEmptyRecordGqlOperationFilter = ({
[correspondingField.name]: { is: 'NULL' } as SelectFilter,
};
break;
case 'UUID':
emptyRecordFilter = {
[correspondingField.name]: { is: 'NULL' } as UUIDFilter,
};
break;
case 'MULTI_SELECT':
emptyRecordFilter = {
or: [