From d9dbfc4f7e295d2ddcd964b500ccd38be9688e0c Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 6 Aug 2025 14:57:10 +0200 Subject: [PATCH] Handle relations for filters (#13654) - Refacto variable dropdown so it can display both objects and fields - we do not filter on object name anymore to simplify the code - add relation handler in filters https://github.com/user-attachments/assets/e4f03f11-45cb-4d3f-b628-e996129dd996 --- .../components/FormSingleRecordPicker.tsx | 3 +- .../types/VariablePickerComponent.ts | 3 +- .../WorkflowStepFilterFieldSelect.tsx | 12 +- .../WorkflowStepFilterValueInput.tsx | 4 + .../components/WorkflowVariablePicker.tsx | 6 +- .../components/WorkflowVariablesDropdown.tsx | 18 +- ... => WorkflowVariablesDropdownAllItems.tsx} | 54 ++- .../WorkflowVariablesDropdownFieldItems.tsx | 1 - .../useAvailableVariablesInWorkflowStep.ts | 15 +- .../hooks/useVariableDropdown.ts | 8 +- .../types/StepOutputSchema.ts | 1 + .../__tests__/filterOutputSchema.test.ts | 343 ++++++++++-------- .../getVariableTemplateFromPath.test.ts | 21 ++ .../utils/filterOutputSchema.ts | 104 ++++-- .../utils/getVariableTemplateFromPath.ts | 13 + .../types/output-schema.type.ts | 1 + .../generate-fake-form-response.spec.ts | 154 ++++---- .../utils/generate-fake-object-record.ts | 3 + .../utils/generate-object-record-fields.ts | 2 + .../evaluate-filter-conditions.util.spec.ts | 217 +++++++++++ .../utils/evaluate-filter-conditions.util.ts | 29 +- 21 files changed, 705 insertions(+), 307 deletions(-) rename packages/twenty-front/src/modules/workflow/workflow-variables/components/{WorkflowVariablesDropdownObjectItems.tsx => WorkflowVariablesDropdownAllItems.tsx} (76%) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getVariableTemplateFromPath.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/getVariableTemplateFromPath.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx index eb68fb48029..f5e6be2b294 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx @@ -222,7 +222,8 @@ export const FormSingleRecordPicker = ({ instanceId={variablesDropdownId} disabled={disabled} onVariableSelect={handleVariableTagInsert} - objectNameSingularToSelect={objectNameSingular} + shouldDisplayRecordObjects={true} + shouldDisplayRecordFields={false} /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/types/VariablePickerComponent.ts b/packages/twenty-front/src/modules/object-record/record-field/form-types/types/VariablePickerComponent.ts index d4128d9b13e..5cab4c1a638 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/types/VariablePickerComponent.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/types/VariablePickerComponent.ts @@ -3,5 +3,6 @@ export type VariablePickerComponent = React.FC<{ disabled?: boolean; multiline?: boolean; onVariableSelect: (variableName: string) => void; - objectNameSingularToSelect?: string; + shouldDisplayRecordObjects?: boolean; + shouldDisplayRecordFields?: boolean; }>; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx index 461d5411c0e..d860bcae1a4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect.tsx @@ -24,6 +24,8 @@ export const WorkflowStepFilterFieldSelect = ({ stepFilter, }: WorkflowStepFilterFieldSelectProps) => { const { readonly } = useContext(WorkflowStepFilterContext); + const shouldDisplayRecordFields = true; + const shouldDisplayRecordObjects = true; const { upsertStepFilterSettings } = useUpsertStepFilterSettings(); @@ -44,9 +46,10 @@ export const WorkflowStepFilterFieldSelect = ({ const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); - const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep( - {}, - ); + const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + }); const noAvailableVariables = availableVariablesInWorkflowStep.length === 0; @@ -157,6 +160,9 @@ export const WorkflowStepFilterFieldSelect = ({ textAccent={isSelectedFieldNotFound ? 'placeholder' : 'default'} /> } + shouldDisplayRecordFields={shouldDisplayRecordFields} + shouldDisplayRecordObjects={shouldDisplayRecordObjects} + shouldEnableSelectRelationObject={true} /> ); }; 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 29ea2c8a9ee..0a59904d380 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 @@ -43,6 +43,7 @@ const isFilterableFieldMetadataType = ( FieldMetadataType.RICH_TEXT_V2, FieldMetadataType.ARRAY, FieldMetadataType.UUID, + FieldMetadataType.RELATION, ...COMPOSITE_FIELD_METADATA_TYPES, ].includes(type as FieldMetadataType); }; @@ -142,6 +143,9 @@ export const WorkflowStepFilterValueInput = ({ metadata: { fieldName: selectedFieldMetadataItem?.name ?? '', options: selectedFieldMetadataItem?.options ?? [], + relationObjectMetadataNameSingular: + selectedFieldMetadataItem?.relation?.targetObjectMetadata?.nameSingular, + relationType: selectedFieldMetadataItem?.relation?.type, } as FieldMetadata, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablePicker.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablePicker.tsx index 3fbca9f51d1..d739896cd7a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablePicker.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablePicker.tsx @@ -41,7 +41,8 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({ disabled, multiline, onVariableSelect, - objectNameSingularToSelect, + shouldDisplayRecordObjects = false, + shouldDisplayRecordFields = true, }) => { return ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdown.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdown.tsx index c657b3d1d66..f43cfde2822 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdown.tsx @@ -3,8 +3,8 @@ import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/S import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { WorkflowVariablesDropdownAllItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownAllItems'; import { WorkflowVariablesDropdownFieldItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems'; -import { WorkflowVariablesDropdownObjectItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownObjectItems'; import { WorkflowVariablesDropdownWorkflowStepItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownWorkflowStepItems'; import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/workflow-variables/constants/SearchVariablesDropdownId'; @@ -35,14 +35,18 @@ export const WorkflowVariablesDropdown = ({ instanceId, onVariableSelect, disabled, - objectNameSingularToSelect, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + shouldEnableSelectRelationObject, multiline, clickableComponent, }: { instanceId: string; onVariableSelect: (variableName: string) => void; + shouldDisplayRecordFields: boolean; + shouldDisplayRecordObjects: boolean; + shouldEnableSelectRelationObject?: boolean; disabled?: boolean; - objectNameSingularToSelect?: string; multiline?: boolean; clickableComponent?: React.ReactNode; }) => { @@ -55,7 +59,8 @@ export const WorkflowVariablesDropdown = ({ ); const { closeDropdown } = useCloseDropdown(); const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({ - objectNameSingularToSelect, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, }); const noAvailableVariables = availableVariablesInWorkflowStep.length === 0; @@ -120,11 +125,12 @@ export const WorkflowVariablesDropdown = ({ steps={availableVariablesInWorkflowStep} onSelect={handleStepSelect} /> - ) : isDefined(objectNameSingularToSelect) ? ( - ) : ( void; onBack: () => void; + shouldEnableSelectRelationObject?: boolean; }; -export const WorkflowVariablesDropdownObjectItems = ({ +export const WorkflowVariablesDropdownAllItems = ({ step, onSelect, onBack, -}: WorkflowVariablesDropdownObjectItemsProps) => { + shouldEnableSelectRelationObject, +}: WorkflowVariablesDropdownAllItemsProps) => { + const { t } = useLingui(); const { getIcon } = useIcons(); const { - currentPath, - filteredOptions, searchInputValue, setSearchInputValue, handleSelectField, goBack, + filteredOptions, + currentPath, } = useVariableDropdown({ step, onSelect, @@ -61,9 +65,25 @@ export const WorkflowVariablesDropdownObjectItems = ({ return; } - onSelect( - `{{${step.id}.${[...currentPath, currentSubStep.object.fieldIdName].join('.')}}}`, - ); + const isRelationField = currentSubStep.object.isRelationField ?? false; + const isRelationObjectSelectable = + shouldEnableSelectRelationObject ?? false; + + if (isRelationField && isRelationObjectSelectable) { + onSelect( + getVariableTemplateFromPath({ + stepId: step.id, + path: currentPath, + }), + ); + } else { + onSelect( + getVariableTemplateFromPath({ + stepId: step.id, + path: [...currentPath, currentSubStep.object.fieldIdName], + }), + ); + } }; const displayedSubStepObject = getDisplayedSubStepObject(); @@ -118,17 +138,17 @@ export const WorkflowVariablesDropdownObjectItems = ({ {filteredOptions.length > 0 && shouldDisplayObject && ( )} - {filteredOptions.map(([key, option]) => ( + {filteredOptions.map(([key, subStep]) => ( handleSelectField(key)} - text={option.label || key} - hasSubMenu={!option.isLeaf} - LeftIcon={option.icon ? getIcon(option.icon) : undefined} + text={subStep.label || key} + hasSubMenu={!subStep.isLeaf} + LeftIcon={subStep.icon ? getIcon(subStep.icon) : undefined} contextualText={ - option.isLeaf ? option?.value?.toString() : undefined + subStep.isLeaf ? subStep?.value?.toString() : undefined } /> ))} diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx index 9c8f8f9d35c..760aca0eaf0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx @@ -50,7 +50,6 @@ export const WorkflowVariablesDropdownFieldItems = ({ Icon={IconChevronLeft} /> } - style={{ position: 'fixed' }} > { const workflowSelectedNode = useWorkflowSelectedNodeOrThrow(); const flow = useFlowOrThrow(); @@ -35,10 +37,11 @@ export const useAvailableVariablesInWorkflowStep = ({ const availableVariablesInWorkflowStep = availableStepsOutputSchema .map((stepOutputSchema) => { - const outputSchema = filterOutputSchema( - stepOutputSchema.outputSchema, - objectNameSingularToSelect, - ) as OutputSchema; + const outputSchema = filterOutputSchema({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + outputSchema: stepOutputSchema.outputSchema, + }) as OutputSchema; if (!isDefined(outputSchema) || isEmptyObject(outputSchema)) { return undefined; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts index 28810df4a38..57ea4ab78a6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts @@ -2,6 +2,7 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { workflowDiagramTriggerNodeSelectionComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramTriggerNodeSelectionComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; +import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { @@ -70,7 +71,12 @@ export const useVariableDropdown = ({ setCurrentPath([...currentPath, key]); setSearchInputValue(''); } else { - onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`); + onSelect( + getVariableTemplateFromPath({ + stepId: step.id, + path: [...currentPath, key], + }), + ); } }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchema.ts index 265b71fbb46..035498f5fb8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchema.ts @@ -36,6 +36,7 @@ export type RecordOutputSchema = { nameSingular: string; fieldIdName: string; objectMetadataId: string; + isRelationField?: boolean; } & Leaf; fields: BaseOutputSchema; _outputSchemaType: 'RECORD'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts index 5b17a85722e..dfeee661acb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts @@ -3,189 +3,226 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { filterOutputSchema } from '../filterOutputSchema'; describe('filterOutputSchema', () => { - describe('edge cases', () => { - it('should return the input schema when objectNameSingularToSelect is undefined', () => { - const inputSchema: OutputSchema = { - _outputSchemaType: 'RECORD', - object: { - nameSingular: 'person', - fieldIdName: 'id', - isLeaf: true, - value: 'Fake value', - objectMetadataId: '123', - }, - fields: {}, - }; + const createRecordSchema = ( + nameSingular: string, + fields = {}, + ): OutputSchema => ({ + _outputSchemaType: 'RECORD', + object: { + nameSingular, + fieldIdName: 'id', + isLeaf: true, + value: 'Fake value', + objectMetadataId: '123', + }, + fields, + }); - expect(filterOutputSchema(inputSchema, undefined)).toBe(inputSchema); + const createBaseSchema = (fields = {}): OutputSchema => ({ + ...fields, + }); + + describe('shouldDisplayRecordFields only (true, false)', () => { + describe('record schema', () => { + it('should return the input schema unchanged', () => { + const inputSchema = createRecordSchema('person', { + name: { isLeaf: true, value: 'string' }, + id: { isLeaf: true, type: FieldMetadataType.UUID }, + }); + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: false, + outputSchema: inputSchema, + }), + ).toBe(inputSchema); + }); + + it('should return undefined when input schema is undefined', () => { + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: false, + outputSchema: undefined, + }), + ).toBeUndefined(); + }); }); - it('should return undefined when input schema is undefined', () => { - expect(filterOutputSchema(undefined, 'person')).toBeUndefined(); + describe('base schema', () => { + it('should return the input schema unchanged', () => { + const inputSchema = createBaseSchema({ + field1: { isLeaf: true, value: 'string' }, + field2: { isLeaf: true, type: FieldMetadataType.NUMBER }, + }); + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: false, + outputSchema: inputSchema, + }), + ).toBe(inputSchema); + }); }); }); - describe('record output schema', () => { - const createRecordSchema = ( - nameSingular: string, - fields = {}, - ): OutputSchema => ({ - _outputSchemaType: 'RECORD', - object: { - nameSingular, - fieldIdName: 'id', - isLeaf: true, - value: 'Fake value', - objectMetadataId: '123', - }, - fields, - }); - - it('should keep a matching record schema', () => { - const inputSchema = createRecordSchema('person'); - - expect(filterOutputSchema(inputSchema, 'person')).toEqual(inputSchema); - }); - - it('should filter out a non-matching record schema with no valid fields', () => { - const inputSchema = createRecordSchema('company'); - - expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined(); - }); - - it('should keep valid nested records while filtering out invalid ones', () => { - const inputSchema = createRecordSchema('company', { - employee: { - isLeaf: false, - value: createRecordSchema('person', { - manager: { - isLeaf: false, - value: createRecordSchema('person'), - }, - }), - }, - department: { - isLeaf: false, - value: createRecordSchema('department'), - }, - }); - - const expectedSchema = { - _outputSchemaType: 'RECORD', - fields: { + describe('shouldDisplayRecordObjects only (false, true)', () => { + describe('record schema', () => { + it('should keep record schema with object and filter compatible fields', () => { + const inputSchema = createRecordSchema('person', { + name: { isLeaf: true, value: 'string' }, + id: { isLeaf: true, type: FieldMetadataType.UUID }, employee: { isLeaf: false, - value: createRecordSchema('person', { - manager: { - isLeaf: false, - value: createRecordSchema('person'), - }, - }), + value: createRecordSchema('employee'), }, - }, - }; + }); - expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema); - }); - - it('should ignore leaf fields that are field metadata types', () => { - const inputSchema = createRecordSchema('company', { - name: { isLeaf: true, value: 'string' }, - id: { isLeaf: true, type: FieldMetadataType.UUID }, - employee: { - isLeaf: false, - value: createRecordSchema('person'), - }, - }); - - const expectedSchema = { - _outputSchemaType: 'RECORD', - fields: { + const expectedSchema = createRecordSchema('person', { name: { isLeaf: true, value: 'string' }, employee: { + isLeaf: false, + value: createRecordSchema('employee'), + }, + }); + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toEqual(expectedSchema); + }); + + it('should return undefined for record schema without object and no valid fields', () => { + const inputSchema = { + _outputSchemaType: 'RECORD', + fields: { + invalidField: { isLeaf: true, type: FieldMetadataType.NUMBER }, + }, + } as any; + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toBeUndefined(); + }); + }); + + describe('base schema', () => { + it('should keep base schema with valid nested records', () => { + const inputSchema = createBaseSchema({ + field1: { isLeaf: false, value: createRecordSchema('person'), }, - }, - }; + field2: { isLeaf: true, type: FieldMetadataType.NUMBER }, + }); - expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema); + const expectedSchema = { + field1: { + isLeaf: false, + value: createRecordSchema('person'), + }, + }; + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toEqual(expectedSchema); + }); + + it('should return undefined for base schema with no valid records', () => { + const inputSchema = createBaseSchema({ + field1: { isLeaf: true, type: FieldMetadataType.NUMBER }, + field2: { isLeaf: true, type: FieldMetadataType.BOOLEAN }, + }); + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toBeUndefined(); + }); }); }); - describe('base output schema', () => { - const createBaseSchema = (fields = {}): OutputSchema => ({ - ...fields, + describe('both shouldDisplayRecordFields and shouldDisplayRecordObjects (true, true)', () => { + it('should return the input schema unchanged for record schema', () => { + const inputSchema = createRecordSchema('person', { + name: { isLeaf: true, value: 'string' }, + id: { isLeaf: true, type: FieldMetadataType.UUID }, + }); + + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toBe(inputSchema); }); - it('should filter out base schema with no valid records', () => { + it('should return the input schema unchanged for base schema', () => { const inputSchema = createBaseSchema({ - field1: { - isLeaf: true, - type: FieldMetadataType.TEXT, - value: 'string', - }, + field1: { isLeaf: true, value: 'string' }, + field2: { isLeaf: true, type: FieldMetadataType.NUMBER }, }); - expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined(); + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: true, + outputSchema: inputSchema, + }), + ).toBe(inputSchema); }); - it('should keep base schema with valid nested records', () => { - const inputSchema = createBaseSchema({ - field1: { - isLeaf: false, - value: { - _outputSchemaType: 'RECORD', - object: { nameSingular: 'person' }, - fields: {}, - }, - }, + it('should return undefined when input schema is undefined', () => { + expect( + filterOutputSchema({ + shouldDisplayRecordFields: true, + shouldDisplayRecordObjects: true, + outputSchema: undefined, + }), + ).toBeUndefined(); + }); + }); + + describe('both shouldDisplayRecordFields and shouldDisplayRecordObjects false (false, false)', () => { + it('should return the input schema unchanged', () => { + const inputSchema = createRecordSchema('person', { + name: { isLeaf: true, value: 'string' }, }); - expect(filterOutputSchema(inputSchema, 'person')).toEqual({ - field1: { - isLeaf: false, - value: { - _outputSchemaType: 'RECORD', - object: { nameSingular: 'person' }, - fields: {}, - }, - }, - }); + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: false, + outputSchema: inputSchema, + }), + ).toBe(inputSchema); }); - it('should handle deeply nested valid records', () => { - const inputSchema = createBaseSchema({ - level1: { - isLeaf: false, - value: createBaseSchema({ - level2: { - isLeaf: false, - value: { - _outputSchemaType: 'RECORD', - object: { nameSingular: 'person' }, - fields: {}, - }, - }, - }), - }, - }); - - expect(filterOutputSchema(inputSchema, 'person')).toEqual({ - level1: { - isLeaf: false, - value: { - level2: { - isLeaf: false, - value: { - _outputSchemaType: 'RECORD', - object: { nameSingular: 'person' }, - fields: {}, - }, - }, - }, - }, - }); + it('should return undefined when input schema is undefined', () => { + expect( + filterOutputSchema({ + shouldDisplayRecordFields: false, + shouldDisplayRecordObjects: false, + outputSchema: undefined, + }), + ).toBeUndefined(); }); }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getVariableTemplateFromPath.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getVariableTemplateFromPath.test.ts new file mode 100644 index 00000000000..1a71d386170 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getVariableTemplateFromPath.test.ts @@ -0,0 +1,21 @@ +import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils/getVariableTemplateFromPath'; + +describe('getVariableTemplateFromPath', () => { + it('should return stepId template when path is empty', () => { + const result = getVariableTemplateFromPath({ + stepId: 'step-1', + path: [], + }); + + expect(result).toBe('{{step-1}}'); + }); + + it('should return stepId with path', () => { + const result = getVariableTemplateFromPath({ + stepId: 'step-2', + path: ['company', 'name'], + }); + + expect(result).toBe('{{step-2.company.name}}'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts index c4d128cc6d2..db96e538385 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts @@ -9,24 +9,31 @@ import { isLinkOutputSchema } from '@/workflow/workflow-variables/utils/isLinkOu import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema'; import { isDefined } from 'twenty-shared/utils'; -const isValidRecordOutputSchema = ( - outputSchema: RecordOutputSchema, - objectNameSingularToSelect?: string, -): boolean => { - if (isDefined(objectNameSingularToSelect)) { - return ( - isDefined(outputSchema.object) && - outputSchema.object.nameSingular === objectNameSingularToSelect - ); +const isValidRecordOutputSchema = ({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + outputSchema, +}: { + shouldDisplayRecordFields: boolean; + shouldDisplayRecordObjects: boolean; + outputSchema: RecordOutputSchema; +}): boolean => { + if (shouldDisplayRecordObjects && !shouldDisplayRecordFields) { + return isDefined(outputSchema.object); } return true; }; -const filterRecordOutputSchema = ( - outputSchema: RecordOutputSchema, - objectNameSingularToSelect: string, -): RecordOutputSchema | undefined => { +const filterRecordOutputSchema = ({ + outputSchema, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, +}: { + outputSchema: RecordOutputSchema; + shouldDisplayRecordFields: boolean; + shouldDisplayRecordObjects: boolean; +}): RecordOutputSchema | undefined => { const filteredFields: BaseOutputSchema = {}; let hasValidFields = false; @@ -41,10 +48,12 @@ const filterRecordOutputSchema = ( continue; } - const validSubSchema = filterOutputSchema( - field.value, - objectNameSingularToSelect, - ); + const validSubSchema = filterOutputSchema({ + outputSchema: field.value, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + }); + if (isDefined(validSubSchema)) { filteredFields[key] = { ...field, @@ -54,7 +63,13 @@ const filterRecordOutputSchema = ( } } - if (isValidRecordOutputSchema(outputSchema, objectNameSingularToSelect)) { + if ( + isValidRecordOutputSchema({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + outputSchema, + }) + ) { return { ...outputSchema, fields: filteredFields, @@ -69,10 +84,15 @@ const filterRecordOutputSchema = ( return undefined; }; -const filterBaseOutputSchema = ( - outputSchema: BaseOutputSchema, - objectNameSingularToSelect: string, -): BaseOutputSchema | undefined => { +const filterBaseOutputSchema = ({ + outputSchema, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, +}: { + outputSchema: BaseOutputSchema; + shouldDisplayRecordFields: boolean; + shouldDisplayRecordObjects: boolean; +}): BaseOutputSchema | undefined => { const filteredSchema: BaseOutputSchema = {}; let hasValidFields = false; @@ -87,10 +107,11 @@ const filterBaseOutputSchema = ( continue; } - const validSubSchema = filterOutputSchema( - field.value, - objectNameSingularToSelect, - ); + const validSubSchema = filterOutputSchema({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + outputSchema: field.value, + }); if (isDefined(validSubSchema)) { filteredSchema[key] = { ...field, @@ -107,20 +128,37 @@ const filterBaseOutputSchema = ( return undefined; }; -export const filterOutputSchema = ( - outputSchema?: OutputSchema, - objectNameSingularToSelect?: string, -): OutputSchema | undefined => { - if (!objectNameSingularToSelect || !outputSchema) { +export const filterOutputSchema = ({ + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + outputSchema, +}: { + shouldDisplayRecordFields: boolean; + shouldDisplayRecordObjects: boolean; + outputSchema?: OutputSchema; +}): OutputSchema | undefined => { + if ( + !shouldDisplayRecordObjects || + shouldDisplayRecordFields || + !outputSchema + ) { return outputSchema; } if (isLinkOutputSchema(outputSchema)) { return outputSchema; } else if (isRecordOutputSchema(outputSchema)) { - return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect); + return filterRecordOutputSchema({ + outputSchema, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + }); } else if (isBaseOutputSchema(outputSchema)) { - return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect); + return filterBaseOutputSchema({ + outputSchema, + shouldDisplayRecordFields, + shouldDisplayRecordObjects, + }); } return undefined; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getVariableTemplateFromPath.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getVariableTemplateFromPath.ts new file mode 100644 index 00000000000..321bc528ecc --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getVariableTemplateFromPath.ts @@ -0,0 +1,13 @@ +export const getVariableTemplateFromPath = ({ + stepId, + path, +}: { + stepId: string; + path: string[]; +}) => { + if (path.length === 0) { + return `{{${stepId}}}`; + } + + return `{{${stepId}.${path.join('.')}}}`; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type.ts index a52d9db7efe..6f9f9624288 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type.ts @@ -40,6 +40,7 @@ export type RecordOutputSchema = { nameSingular: string; fieldIdName: string; objectMetadataId: string; + isRelationField?: boolean; } & Leaf; fields: Record; _outputSchemaType: 'RECORD'; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index f49fed0b9e0..1571fe428fc 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -56,92 +56,84 @@ describe('generateFakeFormResponse', () => { objectMetadataMaps: mockObjectMetadataMaps, }); - expect(result).toMatchInlineSnapshot(` -{ - "age": { - "fieldMetadataId": undefined, - "icon": undefined, - "isLeaf": true, - "label": "Age", - "type": "NUMBER", - "value": 20, - }, - "company": { - "isLeaf": false, - "label": "Company", - "value": { - "_outputSchemaType": "RECORD", - "fields": { - "domainName": { - "fieldMetadataId": "domainNameFieldMetadataId", - "icon": "test-field-icon", - "isLeaf": false, - "label": "Domain Name", - "type": "LINKS", - "value": { - "primaryLinkLabel": { - "fieldMetadataId": "domainNameFieldMetadataId", - "isCompositeSubField": true, - "isLeaf": true, - "label": "Primary Link Label", - "type": "TEXT", - "value": "My text", + expect(result).toEqual({ + age: { + isLeaf: true, + label: 'Age', + type: 'NUMBER', + value: 20, + }, + company: { + isLeaf: false, + label: 'Company', + value: { + _outputSchemaType: 'RECORD', + fields: { + domainName: { + fieldMetadataId: 'domainNameFieldMetadataId', + icon: 'test-field-icon', + isLeaf: false, + label: 'Domain Name', + type: 'LINKS', + value: { + primaryLinkLabel: { + fieldMetadataId: 'domainNameFieldMetadataId', + isCompositeSubField: true, + isLeaf: true, + label: 'Primary Link Label', + type: 'TEXT', + value: 'My text', + }, + primaryLinkUrl: { + fieldMetadataId: 'domainNameFieldMetadataId', + isCompositeSubField: true, + isLeaf: true, + label: 'Primary Link Url', + type: 'TEXT', + value: 'My text', + }, + secondaryLinks: { + fieldMetadataId: 'domainNameFieldMetadataId', + isCompositeSubField: true, + isLeaf: true, + label: 'Secondary Links', + type: 'RAW_JSON', + value: null, + }, + }, }, - "primaryLinkUrl": { - "fieldMetadataId": "domainNameFieldMetadataId", - "isCompositeSubField": true, - "isLeaf": true, - "label": "Primary Link Url", - "type": "TEXT", - "value": "My text", - }, - "secondaryLinks": { - "fieldMetadataId": "domainNameFieldMetadataId", - "isCompositeSubField": true, - "isLeaf": true, - "label": "Secondary Links", - "type": "RAW_JSON", - "value": null, + name: { + fieldMetadataId: 'nameFieldMetadataId', + icon: 'test-field-icon', + isLeaf: true, + label: 'Name', + type: 'TEXT', + value: 'My text', }, }, - }, - "name": { - "fieldMetadataId": "nameFieldMetadataId", - "icon": "test-field-icon", - "isLeaf": true, - "label": "Name", - "type": "TEXT", - "value": "My text", + object: { + fieldIdName: 'id', + icon: 'test-company-icon', + isLeaf: true, + label: 'Company', + nameSingular: 'company', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + value: 'A company', + }, }, }, - "object": { - "fieldIdName": "id", - "icon": "test-company-icon", - "isLeaf": true, - "label": "Company", - "nameSingular": "company", - "objectMetadataId": "20202020-c03c-45d6-a4b0-04afe1357c5c", - "value": "A company", + date: { + isLeaf: true, + label: 'Date', + type: 'DATE', + value: 'mm/dd/yyyy', }, - }, - }, - "date": { - "fieldMetadataId": undefined, - "icon": undefined, - "isLeaf": true, - "label": "Date", - "type": "DATE", - "value": "mm/dd/yyyy", - }, - "name": { - "fieldMetadataId": undefined, - "icon": undefined, - "isLeaf": true, - "label": "Name", - "type": "TEXT", - "value": "My text", - }, -} -`); + name: { + isLeaf: true, + label: 'Name', + type: 'TEXT', + value: 'My text', + }, + }); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index 86c08eac353..bcc671a2828 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -5,9 +5,11 @@ import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builde export const generateFakeObjectRecord = ({ objectMetadataInfo, depth = 0, + isRelationField, }: { objectMetadataInfo: ObjectMetadataInfo; depth?: number; + isRelationField?: boolean; }): RecordOutputSchema => { return { object: { @@ -20,6 +22,7 @@ export const generateFakeObjectRecord = ({ objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular, fieldIdName: 'id', objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, + isRelationField, }, fields: generateObjectRecordFields({ objectMetadataInfo, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index e564166a8df..2b5d0cd789e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -52,6 +52,7 @@ export const generateObjectRecordFields = ({ isLeaf: false, icon: field.icon ?? undefined, label: field.label, + type: field.type, fieldMetadataId: field.id, value: generateFakeObjectRecord({ objectMetadataInfo: { @@ -59,6 +60,7 @@ export const generateObjectRecordFields = ({ objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, }, depth: depth + 1, + isRelationField: true, }), }; } 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 120dafc03fd..978f22b3748 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 @@ -95,6 +95,223 @@ describe('evaluateFilterConditions', () => { expect(result).toBe(true); }); + + // Enhanced relation filter tests with object id extraction + it('should extract id from left operand object for relation comparison', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const leftObject = { id: uuid1, name: 'John Doe' }; + const rightValue = uuid1; + + const filter = createFilter( + ViewFilterOperand.Is, + leftObject, + rightValue, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should extract id from right operand object for relation comparison', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const leftValue = uuid1; + const rightObject = { id: uuid1, name: 'John Doe' }; + + const filter = createFilter( + ViewFilterOperand.Is, + leftValue, + rightObject, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should extract id from both operands when they are objects for relation comparison', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const leftObject = { id: uuid1, name: 'John Doe' }; + const rightObject = { id: uuid1, title: 'Admin' }; + + const filter = createFilter( + ViewFilterOperand.Is, + leftObject, + rightObject, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should return false when extracted ids do not match for relation comparison', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const leftObject = { id: uuid1, name: 'John Doe' }; + const rightObject = { id: uuid2, name: 'Jane Smith' }; + + const filter = createFilter( + ViewFilterOperand.Is, + leftObject, + rightObject, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); + }); + + it('should handle IsNot with object id extraction for relation comparison', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const leftObject = { id: uuid1, name: 'John Doe' }; + const rightObject = { id: uuid2, name: 'Jane Smith' }; + + const filter = createFilter( + ViewFilterOperand.IsNot, + leftObject, + rightObject, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should handle objects without id property for relation comparison', () => { + const leftObject = { name: 'John Doe' }; + const rightObject = { name: 'John Doe' }; + + const filter = createFilter( + ViewFilterOperand.Is, + leftObject, + rightObject, + 'RELATION', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); // Objects are different references + }); + + it('should throw error for unsupported relation filter operand', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const filter = createFilter( + ViewFilterOperand.Contains, + uuid1, + uuid2, + 'RELATION', + ); + + expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow( + 'Operand contains not supported for relation filter', + ); + }); + }); + + describe('UUID filter operands', () => { + const uuid1 = '550e8400-e29b-41d4-a716-446655440000'; + const uuid2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + + it('should return true when UUIDs are equal (Is)', () => { + const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid1, 'UUID'); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should return false when UUIDs are not equal (Is)', () => { + const filter = createFilter(ViewFilterOperand.Is, uuid1, uuid2, 'UUID'); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); + }); + + it('should return false when UUIDs are equal (IsNot)', () => { + const filter = createFilter( + ViewFilterOperand.IsNot, + uuid1, + uuid1, + 'UUID', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(false); + }); + + it('should return true when UUIDs are not equal (IsNot)', () => { + const filter = createFilter( + ViewFilterOperand.IsNot, + uuid1, + uuid2, + 'UUID', + ); + const result = evaluateFilterConditions({ filters: [filter] }); + + expect(result).toBe(true); + }); + + it('should handle null/undefined UUIDs with Is operand', () => { + const filter1 = createFilter(ViewFilterOperand.Is, null, null, 'UUID'); + const filter2 = createFilter( + ViewFilterOperand.Is, + undefined, + undefined, + 'UUID', + ); + const filter3 = createFilter(ViewFilterOperand.Is, uuid1, null, 'UUID'); + + expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true); + expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true); + expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false); + }); + + it('should handle null/undefined UUIDs with IsNot operand', () => { + const filter1 = createFilter( + ViewFilterOperand.IsNot, + null, + null, + 'UUID', + ); + const filter2 = createFilter( + ViewFilterOperand.IsNot, + undefined, + undefined, + 'UUID', + ); + const filter3 = createFilter( + ViewFilterOperand.IsNot, + uuid1, + null, + 'UUID', + ); + + expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false); + expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false); + expect(evaluateFilterConditions({ filters: [filter3] })).toBe(true); + }); + + it('should handle empty string UUIDs', () => { + const filter1 = createFilter(ViewFilterOperand.Is, '', '', 'UUID'); + const filter2 = createFilter(ViewFilterOperand.Is, uuid1, '', 'UUID'); + + expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true); + expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false); + }); + + it('should throw error for unsupported UUID filter operand', () => { + const filter = createFilter( + ViewFilterOperand.Contains, + uuid1, + uuid2, + 'UUID', + ); + + expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow( + 'Operand contains not supported for uuid filter', + ); + }); }); describe('Boolean filter operands', () => { 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 d4e7d7a4ab0..1919a964222 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 @@ -1,4 +1,4 @@ -import { isString } from '@sniptt/guards'; +import { isObject, isString } from '@sniptt/guards'; import { StepFilter, StepFilterGroup, @@ -32,6 +32,7 @@ function evaluateFilter(filter: ResolvedFilter): boolean { case 'BOOLEAN': return evaluateBooleanFilter(filter); case 'UUID': + return evaluateUuidFilter(filter); case 'RELATION': return evaluateRelationFilter(filter); case 'CURRENCY': @@ -193,12 +194,36 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean { } } -function evaluateRelationFilter(filter: ResolvedFilter): boolean { +function evaluateUuidFilter(filter: ResolvedFilter): boolean { switch (filter.operand) { case ViewFilterOperand.Is: return filter.leftOperand === filter.rightOperand; case ViewFilterOperand.IsNot: return filter.leftOperand !== filter.rightOperand; + default: + throw new Error( + `Operand ${filter.operand} not supported for uuid filter`, + ); + } +} + +function evaluateRelationFilter(filter: ResolvedFilter): boolean { + // compare only the ids. If the left operand is the relation object, get the id + const leftValue = + isObject(filter.leftOperand) && 'id' in filter.leftOperand + ? filter.leftOperand.id + : filter.leftOperand; + + const rightValue = + isObject(filter.rightOperand) && 'id' in filter.rightOperand + ? filter.rightOperand.id + : filter.rightOperand; + + switch (filter.operand) { + case ViewFilterOperand.Is: + return leftValue === rightValue; + case ViewFilterOperand.IsNot: + return leftValue !== rightValue; default: throw new Error( `Operand ${filter.operand} not supported for relation filter`,