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`,