From da0e5ba3428e0c23f84fa56daaba32fecf23c574 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 28 Oct 2025 15:13:10 +0100 Subject: [PATCH] Support primitive types in filters (#15402) When using primitive types such as array, number and boolean, we display a text field in filters because fieldmetadataId is empty. We should instead support these as we would do for our own fields. Adding also a fix for https://github.com/twentyhq/twenty/issues/15282 --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../WorkflowStepFilterValueInput.tsx | 48 +++++++++++++++---- .../WorkflowStepFilterValueInput.stories.tsx | 2 +- .../utils/getStepFilterOperands.ts | 5 +- .../evaluate-filter-conditions.util.spec.ts | 7 ++- .../utils/evaluate-filter-conditions.util.ts | 13 ++++- packages/twenty-shared/src/workflow/index.ts | 1 + .../utils/parseBooleanFromStringValue.ts | 11 +++++ 7 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-shared/src/workflow/utils/parseBooleanFromStringValue.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx index 3889d239b0c..3227a6ecaa8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput.tsx @@ -2,7 +2,9 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands'; import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput'; +import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput'; +import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; import { FormRelativeDatePicker } from '@/object-record/record-field/ui/form-types/components/FormRelativeDatePicker'; import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; @@ -21,6 +23,7 @@ import { type StepFilter, } from 'twenty-shared/types'; import { isDefined, parseJson } from 'twenty-shared/utils'; +import { parseBooleanFromStringValue } from 'twenty-shared/workflow'; import { type JsonValue } from 'type-fest'; type WorkflowStepFilterValueInputProps = { @@ -35,9 +38,9 @@ const COMPOSITE_FIELD_METADATA_TYPES = [ FieldMetadataType.CURRENCY, ]; -const isFilterableFieldMetadataType = ( +const isFilterableFieldType = ( type: string, -): type is FieldMetadataType => { +): type is FieldMetadataType | 'array' | 'boolean' | 'number' => { return [ FieldMetadataType.TEXT, FieldMetadataType.NUMBER, @@ -53,6 +56,9 @@ const isFilterableFieldMetadataType = ( FieldMetadataType.UUID, FieldMetadataType.RELATION, ...COMPOSITE_FIELD_METADATA_TYPES, + 'array', + 'boolean', + 'number', ].includes(type as FieldMetadataType); }; @@ -123,11 +129,7 @@ export const WorkflowStepFilterValueInput = ({ ); } - if ( - !isDefined(variableType) || - !isFilterableFieldMetadataType(variableType) || - !isDefined(selectedFieldMetadataItem) - ) { + if (!isDefined(variableType) || !isFilterableFieldType(variableType)) { return ( (stepFilter.value) ?? []; return ( @@ -186,6 +188,36 @@ export const WorkflowStepFilterValueInput = ({ ); } + if ( + variableType === FieldMetadataType.BOOLEAN || + variableType === 'boolean' + ) { + const parsedValue = parseBooleanFromStringValue(stepFilter.value) as + | boolean + | undefined + | string; + + return ( + + ); + } + + if (variableType === 'number') { + return ( + + ); + } + const field = { type: variableType as FieldMetadataType, label: '', diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterValueInput.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterValueInput.stories.tsx index 4731ecc2af7..6d9101cf8cc 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterValueInput.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterValueInput.stories.tsx @@ -67,6 +67,6 @@ export const NumberInput: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await expect(await canvas.findByText('100')).toBeVisible(); + await expect(await canvas.findByDisplayValue(100)).toBeVisible(); }, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands.ts index 79269347859..57abe97a7ec 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands.ts @@ -60,7 +60,7 @@ export const FILTER_OPERANDS_MAP = { ...emptyOperands, ], BOOLEAN: [ViewFilterOperand.IS], - UUID: [ViewFilterOperand.IS], + UUID: [ViewFilterOperand.IS, ViewFilterOperand.IS_NOT], NUMERIC: [ ViewFilterOperand.GREATER_THAN_OR_EQUAL, ViewFilterOperand.LESS_THAN_OR_EQUAL, @@ -108,6 +108,7 @@ export const getStepFilterOperands = ({ } } case 'NUMBER': + case 'number': return FILTER_OPERANDS_MAP.NUMBER; case 'RAW_JSON': return FILTER_OPERANDS_MAP.RAW_JSON; @@ -123,8 +124,10 @@ export const getStepFilterOperands = ({ case 'SELECT': return FILTER_OPERANDS_MAP.SELECT; case 'ARRAY': + case 'array': return FILTER_OPERANDS_MAP.ARRAY; case 'BOOLEAN': + case 'boolean': return FILTER_OPERANDS_MAP.BOOLEAN; case 'UUID': return FILTER_OPERANDS_MAP.UUID; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts index fdee4180381..cd76cd98f68 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts @@ -412,7 +412,12 @@ describe('evaluateFilterConditions', () => { }); it('should handle truthy/falsy conversion', () => { - const filter = createFilter(ViewFilterOperand.IS, 1, true, 'BOOLEAN'); + const filter = createFilter( + ViewFilterOperand.IS, + 'true', + true, + 'BOOLEAN', + ); const result = evaluateFilterConditions({ filters: [filter] }); expect(result).toBe(true); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts index ff7924fd781..7e5155d502d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts @@ -11,6 +11,7 @@ import { type ViewFilterOperandDeprecated, } from 'twenty-shared/types'; import { convertViewFilterOperandToCoreOperand as convertViewFilterOperandDeprecated } from 'twenty-shared/utils'; +import { parseBooleanFromStringValue } from 'twenty-shared/workflow'; import { parseAndEvaluateRelativeDateFilter } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/parse-and-evaluate-relative-date-filter.util'; @@ -43,6 +44,7 @@ function evaluateFilter( switch (filter.type) { case 'NUMBER': case 'NUMERIC': + case 'number': return evaluateNumberFilter(filterWithConvertedOperand); case 'DATE': case 'DATE_TIME': @@ -55,11 +57,13 @@ function evaluateFilter( case 'ADDRESS': case 'LINKS': case 'ARRAY': + case 'array': case 'RAW_JSON': return evaluateTextAndArrayFilter(filterWithConvertedOperand); case 'SELECT': return evaluateSelectFilter(filterWithConvertedOperand); case 'BOOLEAN': + case 'boolean': return evaluateBooleanFilter(filterWithConvertedOperand); case 'UUID': return evaluateUuidFilter(filterWithConvertedOperand); @@ -168,7 +172,10 @@ function isNotEmptyTextOrArray(value: unknown): boolean { function evaluateBooleanFilter(filter: ResolvedFilter): boolean { switch (filter.operand) { case ViewFilterOperand.IS: - return Boolean(filter.leftOperand) === Boolean(filter.rightOperand); + return ( + parseBooleanFromStringValue(filter.leftOperand) === + parseBooleanFromStringValue(filter.rightOperand) + ); default: throw new Error( `Operand ${filter.operand} not supported for boolean filter`, @@ -263,6 +270,10 @@ function evaluateRelationFilter(filter: ResolvedFilter): boolean { return leftValue === rightValue; case ViewFilterOperand.IS_NOT: return leftValue !== rightValue; + case ViewFilterOperand.IS_EMPTY: + return !isNonEmptyString(leftValue); + case ViewFilterOperand.IS_NOT_EMPTY: + return isNonEmptyString(leftValue); default: throw new Error( `Operand ${filter.operand} not supported for relation filter`, diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts index 543e2dd41da..806d3c6ba7a 100644 --- a/packages/twenty-shared/src/workflow/index.ts +++ b/packages/twenty-shared/src/workflow/index.ts @@ -64,6 +64,7 @@ export { StepStatus } from './types/WorkflowRunStateStepInfos'; export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow'; export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts'; export { getWorkflowRunContext } from './utils/getWorkflowRunContext'; +export { parseBooleanFromStringValue } from './utils/parseBooleanFromStringValue'; export { parseDataFromContentType } from './utils/parseDataFromContentType'; export type { LeafType, diff --git a/packages/twenty-shared/src/workflow/utils/parseBooleanFromStringValue.ts b/packages/twenty-shared/src/workflow/utils/parseBooleanFromStringValue.ts new file mode 100644 index 00000000000..04ca1a41894 --- /dev/null +++ b/packages/twenty-shared/src/workflow/utils/parseBooleanFromStringValue.ts @@ -0,0 +1,11 @@ +export const parseBooleanFromStringValue = (value: unknown): boolean | unknown => { + if (value === 'true') { + return true; + } + + if (value === 'false') { + return false; + } + + return value; +};