diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index fd1174150..4e4e48a65 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -47,6 +47,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; +import { FieldMetadataType } from 'twenty-shared/types'; import { JsonValue } from 'type-fest'; type FormFieldInputProps = { @@ -70,7 +71,7 @@ export const FormFieldInput = ({ error, onError, }: FormFieldInputProps) => { - return isFieldNumber(field) ? ( + return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? ( >(draftValueAsDate); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx index b035683c9..2a5796305 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody.tsx @@ -10,7 +10,9 @@ import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext'; import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/rootLevelStepFilterGroupComponentSelector'; import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/isStepFilterGroupChildAStepFilterGroup'; +import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; const StyledContainer = styled.div` @@ -27,6 +29,10 @@ const StyledChildContainer = styled.div` width: 100%; `; +const StyledDangerContainer = styled.div` + color: ${({ theme }) => theme.font.color.danger}; +`; + type WorkflowEditActionFilterBodyProps = { action: WorkflowFilterAction; actionOptions: @@ -43,6 +49,8 @@ export const WorkflowEditActionFilterBody = ({ action, actionOptions, }: WorkflowEditActionFilterBodyProps) => { + const { t } = useLingui(); + const rootStepFilterGroup = useRecoilComponentValueV2( rootLevelStepFilterGroupComponentSelector, ); @@ -69,6 +77,22 @@ export const WorkflowEditActionFilterBody = ({ }); }; + const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep( + {}, + ); + + const noAvailableVariables = availableVariablesInWorkflowStep.length === 0; + + if (noAvailableVariables) { + return ( + + + {t`No Available Step Outputs`} + + + ); + } + return ( { + return [ + FieldMetadataType.TEXT, + FieldMetadataType.NUMBER, + FieldMetadataType.BOOLEAN, + FieldMetadataType.DATE_TIME, + FieldMetadataType.DATE, + FieldMetadataType.NUMERIC, + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, + FieldMetadataType.RAW_JSON, + FieldMetadataType.RICH_TEXT_V2, + FieldMetadataType.ARRAY, + ].includes(type as FieldMetadataType); +}; + export const WorkflowStepFilterValueInput = ({ stepFilter, }: WorkflowStepFilterValueInputProps) => { @@ -17,15 +47,76 @@ export const WorkflowStepFilterValueInput = ({ const { readonly } = useContext(WorkflowStepFilterContext); const { upsertStepFilterSettings } = useUpsertStepFilterSettings(); + const { workflowVersionId } = useWorkflowStepContextOrThrow(); + + const stepId = extractRawVariableNamePart({ + rawVariableName: stepFilter.stepOutputKey, + part: 'stepId', + }); + + const stepsOutputSchema = useRecoilValue( + stepsOutputSchemaFamilySelector({ + workflowVersionId, + stepIds: [stepId], + }), + ); + const { variableType, fieldMetadataId } = searchVariableThroughOutputSchema({ + stepOutputSchema: stepsOutputSchema?.[0], + rawVariableName: stepFilter.stepOutputKey, + isFullRecord: false, + }); + + const handleValueChange = (value: JsonValue) => { + const valueToUpsert = isString(value) + ? value + : Array.isArray(value) || isObject(value) + ? JSON.stringify(value) + : String(value); - const handleValueChange = (value: string) => { upsertStepFilterSettings({ stepFilterToUpsert: { ...stepFilter, - value, + value: valueToUpsert, }, }); }; + const { getFieldMetadataItemById } = useGetFieldMetadataItemById(); + + const isDisabled = !stepFilter.operand; + + const operandHasNoInput = + (stepFilter && !configurableViewFilterOperands.has(stepFilter.operand)) ?? + true; + + if (isDisabled || operandHasNoInput) { + return null; + } + + if (isDefined(variableType) && isFilterableFieldMetadataType(variableType)) { + const selectedFieldMetadataItem = isDefined(fieldMetadataId) + ? getFieldMetadataItemById(fieldMetadataId) + : undefined; + + const field = { + type: variableType as FieldMetadataType, + label: '', + metadata: { + fieldName: selectedFieldMetadataItem?.name ?? '', + options: selectedFieldMetadataItem?.options ?? [], + } as FieldMetadata, + }; + + return ( + + ); + } return ( { - if (typeof newValue === 'string') { + if (isString(newValue)) { applyObjectFilterDropdownFilterValue(newValue); } else if (Array.isArray(newValue) || isObject(newValue)) { applyObjectFilterDropdownFilterValue(JSON.stringify(newValue)); 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 13652d1c1..0a5c435e9 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 @@ -7,6 +7,7 @@ type Leaf = { label?: string; description?: string; value: any; + fieldMetadataId?: string; }; type Node = { @@ -16,6 +17,7 @@ type Node = { label?: string; value: OutputSchema; description?: string; + fieldMetadataId?: string; }; type Link = { @@ -28,7 +30,11 @@ type Link = { export type BaseOutputSchema = Record; export type RecordOutputSchema = { - object: { nameSingular: string; fieldIdName: string } & Leaf; + object: { + nameSingular: string; + fieldIdName: string; + objectMetadataId: string; + } & 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 848b0a8f7..5b17a8572 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 @@ -12,6 +12,7 @@ describe('filterOutputSchema', () => { fieldIdName: 'id', isLeaf: true, value: 'Fake value', + objectMetadataId: '123', }, fields: {}, }; @@ -35,6 +36,7 @@ describe('filterOutputSchema', () => { fieldIdName: 'id', isLeaf: true, value: 'Fake value', + objectMetadataId: '123', }, fields, }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getCurrentSubStepFromPath.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getCurrentSubStepFromPath.test.ts index 83fe2b263..01bb5d1e7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getCurrentSubStepFromPath.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getCurrentSubStepFromPath.test.ts @@ -16,6 +16,7 @@ const mockStep = { label: 'Company', value: 'John', isLeaf: true, + objectMetadataId: '123', }, fields: { name: { label: 'Name', value: 'Twenty', isLeaf: true }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getStepHeaderLabel.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getStepHeaderLabel.test.ts index 28a4da3e6..2f51ef318 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getStepHeaderLabel.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/getStepHeaderLabel.test.ts @@ -16,6 +16,7 @@ const mockStep = { label: 'Company', value: 'John', isLeaf: true, + objectMetadataId: '123', }, fields: { name: { label: 'Name', value: 'Twenty', isLeaf: true }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts index 788572aa3..e9aa34fd9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughOutputSchema.test.ts @@ -19,6 +19,7 @@ describe('searchVariableThroughOutputSchema', () => { label: 'Company', value: 'John', isLeaf: true, + objectMetadataId: '123', }, fields: { name: { label: 'Name', value: 'Twenty', isLeaf: true }, @@ -38,6 +39,7 @@ describe('searchVariableThroughOutputSchema', () => { label: 'Person', value: 'Jane', isLeaf: true, + objectMetadataId: '123', }, fields: { firstName: { label: 'First Name', value: 'Jane', isLeaf: true }, @@ -270,6 +272,7 @@ describe('searchVariableThroughOutputSchema', () => { isLeaf: true, fieldIdName: 'properties.after.id', nameSingular: 'company', + objectMetadataId: '123', }, _outputSchemaType: 'RECORD', }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts index 2e4cc1e5a..b2d115de7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchema.ts @@ -43,6 +43,17 @@ const getVariableType = (key: string, outputSchema: OutputSchema): string => { return outputSchema[key]?.type ?? 'unknown'; }; +const getFieldMetadataId = ( + key: string, + outputSchema: OutputSchema, +): string | undefined => { + if (isRecordOutputSchema(outputSchema)) { + return outputSchema.fields[key]?.fieldMetadataId; + } + + return undefined; +}; + const searchCurrentStepOutputSchema = ({ stepOutputSchema, path, @@ -120,6 +131,10 @@ const searchCurrentStepOutputSchema = ({ isSelectedFieldInNextKey ? nextKey : selectedField, currentSubStep, ), + fieldMetadataId: getFieldMetadataId( + isSelectedFieldInNextKey ? nextKey : selectedField, + currentSubStep, + ), }; }; @@ -160,7 +175,7 @@ export const searchVariableThroughOutputSchema = ({ }; } - const { variableLabel, variablePathLabel, variableType } = + const { variableLabel, variablePathLabel, variableType, fieldMetadataId } = searchCurrentStepOutputSchema({ stepOutputSchema, path, @@ -172,5 +187,6 @@ export const searchVariableThroughOutputSchema = ({ variableLabel, variablePathLabel: `${variablePathLabel} > ${variableLabel}`, variableType, + fieldMetadataId, }; }; 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 1d21aa0e2..ba5a9a1a7 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 @@ -28,9 +28,17 @@ type Link = { export type BaseOutputSchema = Record; +export type FieldOutputSchema = + | ((Leaf | Node) & { fieldMetadataId?: string }) + | RecordOutputSchema; + export type RecordOutputSchema = { - object: { nameSingular: string; fieldIdName: string } & Leaf; - fields: BaseOutputSchema; + object: { + nameSingular: string; + fieldIdName: string; + objectMetadataId: string; + } & Leaf; + fields: Record; _outputSchemaType: 'RECORD'; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts index b209ae2ca..d3ed8dfe4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-field.spec.ts @@ -55,6 +55,7 @@ describe('generateFakeField', () => { const result = generateFakeField({ type: FieldMetadataType.TEXT, label: 'Text Field', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -63,6 +64,7 @@ describe('generateFakeField', () => { icon: undefined, label: 'Text Field', value: 'Fake Text', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(generateFakeValueSpy).toHaveBeenCalledWith( @@ -76,6 +78,7 @@ describe('generateFakeField', () => { type: FieldMetadataType.TEXT, label: 'Text Field', value: 'Test value', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -84,6 +87,7 @@ describe('generateFakeField', () => { icon: undefined, label: 'Text Field', value: 'Test value', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(generateFakeValueSpy).not.toHaveBeenCalled(); @@ -96,6 +100,7 @@ describe('generateFakeField', () => { type: FieldMetadataType.NUMBER, label: 'Number Field', icon: 'IconNumber', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -104,6 +109,7 @@ describe('generateFakeField', () => { icon: 'IconNumber', label: 'Number Field', value: 42, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); }); @@ -115,6 +121,7 @@ describe('generateFakeField', () => { const result = generateFakeField({ type: FieldMetadataType.DATE, label: 'Date Field', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -123,6 +130,7 @@ describe('generateFakeField', () => { icon: undefined, label: 'Date Field', value: fakeDate, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); }); }); @@ -140,6 +148,7 @@ describe('generateFakeField', () => { const result = generateFakeField({ type: FieldMetadataType.LINKS, label: 'Links Field', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -161,6 +170,7 @@ describe('generateFakeField', () => { value: 'https://example.com', }, }, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(generateFakeValueSpy).toHaveBeenCalledTimes(2); @@ -179,6 +189,7 @@ describe('generateFakeField', () => { type: FieldMetadataType.CURRENCY, label: 'Currency Field', icon: 'IconCurrency', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -200,6 +211,7 @@ describe('generateFakeField', () => { value: 'USD', }, }, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); }); }); @@ -213,6 +225,7 @@ describe('generateFakeField', () => { const result = generateFakeField({ type: unknownType, label: 'Unknown Field', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -221,6 +234,7 @@ describe('generateFakeField', () => { icon: undefined, label: 'Unknown Field', value: 'Unknown Value', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); }); @@ -230,6 +244,7 @@ describe('generateFakeField', () => { const result = generateFakeField({ type: FieldMetadataType.BOOLEAN, label: '', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); expect(result).toEqual({ @@ -238,6 +253,7 @@ describe('generateFakeField', () => { icon: undefined, label: '', value: 'Fake Boolean', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }); }); }); 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 19bc0b956..47462cd05 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 @@ -59,6 +59,7 @@ describe('generateFakeFormResponse', () => { expect(result).toMatchInlineSnapshot(` { "age": { + "fieldMetadataId": undefined, "icon": undefined, "isLeaf": true, "label": "Age", @@ -72,6 +73,7 @@ describe('generateFakeFormResponse', () => { "_outputSchemaType": "RECORD", "fields": { "domainName": { + "fieldMetadataId": "domainNameFieldMetadataId", "icon": "test-field-icon", "isLeaf": false, "label": "Domain Name", @@ -98,6 +100,7 @@ describe('generateFakeFormResponse', () => { }, }, "name": { + "fieldMetadataId": "nameFieldMetadataId", "icon": "test-field-icon", "isLeaf": true, "label": "Name", @@ -111,11 +114,13 @@ describe('generateFakeFormResponse', () => { "isLeaf": true, "label": "Company", "nameSingular": "company", + "objectMetadataId": "20202020-c03c-45d6-a4b0-04afe1357c5c", "value": "A company", }, }, }, "date": { + "fieldMetadataId": undefined, "icon": undefined, "isLeaf": true, "label": "Date", @@ -123,6 +128,7 @@ describe('generateFakeFormResponse', () => { "value": "mm/dd/yyyy", }, "name": { + "fieldMetadataId": undefined, "icon": undefined, "isLeaf": true, "label": "Name", diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts index de304b555..5fad88194 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts @@ -1,7 +1,7 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; jest.mock( 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields', @@ -13,8 +13,16 @@ describe('generateFakeObjectRecordEvent', () => { }); const mockFields = { - field1: { type: 'TEXT', value: 'test' }, - field2: { type: 'NUMBER', value: 123 }, + field1: { + type: 'TEXT', + value: 'test', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', + }, + field2: { + type: 'NUMBER', + value: 123, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174001', + }, }; const companyMockObjectMetadataItem = @@ -55,10 +63,19 @@ describe('generateFakeObjectRecordEvent', () => { value: 'A company', nameSingular: 'company', fieldIdName: 'properties.after.id', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', }, fields: { - 'properties.after.field1': { type: 'TEXT', value: 'test' }, - 'properties.after.field2': { type: 'NUMBER', value: 123 }, + 'properties.after.field1': { + type: 'TEXT', + value: 'test', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', + }, + 'properties.after.field2': { + type: 'NUMBER', + value: 123, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174001', + }, }, _outputSchemaType: 'RECORD', }); @@ -78,10 +95,19 @@ describe('generateFakeObjectRecordEvent', () => { value: 'A company', nameSingular: 'company', fieldIdName: 'properties.after.id', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', }, fields: { - 'properties.after.field1': { type: 'TEXT', value: 'test' }, - 'properties.after.field2': { type: 'NUMBER', value: 123 }, + 'properties.after.field1': { + type: 'TEXT', + value: 'test', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', + }, + 'properties.after.field2': { + type: 'NUMBER', + value: 123, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174001', + }, }, _outputSchemaType: 'RECORD', }); @@ -101,10 +127,19 @@ describe('generateFakeObjectRecordEvent', () => { value: 'A company', nameSingular: 'company', fieldIdName: 'properties.before.id', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', }, fields: { - 'properties.before.field1': { type: 'TEXT', value: 'test' }, - 'properties.before.field2': { type: 'NUMBER', value: 123 }, + 'properties.before.field1': { + type: 'TEXT', + value: 'test', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', + }, + 'properties.before.field2': { + type: 'NUMBER', + value: 123, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174001', + }, }, _outputSchemaType: 'RECORD', }); @@ -124,10 +159,19 @@ describe('generateFakeObjectRecordEvent', () => { value: 'A company', nameSingular: 'company', fieldIdName: 'properties.before.id', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', }, fields: { - 'properties.before.field1': { type: 'TEXT', value: 'test' }, - 'properties.before.field2': { type: 'NUMBER', value: 123 }, + 'properties.before.field1': { + type: 'TEXT', + value: 'test', + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', + }, + 'properties.before.field2': { + type: 'NUMBER', + value: 123, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174001', + }, }, _outputSchemaType: 'RECORD', }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts index fe6240a4a..4eb175dc4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts @@ -43,6 +43,7 @@ describe('generateFakeObjectRecord', () => { value: 'A company', nameSingular: 'company', fieldIdName: 'id', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', }, fields: { field1: { type: 'TEXT', value: 'test' }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts index 2f32d8c5f..480808288 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field.ts @@ -13,12 +13,14 @@ export const generateFakeField = ({ label, icon, value, + fieldMetadataId, }: { type: FieldMetadataType; label: string; + fieldMetadataId?: string; icon?: string; value?: string; -}): Leaf | Node => { +}): (Leaf | Node) & { fieldMetadataId?: string } => { const compositeType = compositeTypeDefinitions.get(type); if (compositeType) { @@ -27,6 +29,7 @@ export const generateFakeField = ({ type: type, icon: icon, label: label, + fieldMetadataId, value: compositeType.properties.reduce((acc, property) => { // @ts-expect-error legacy noImplicitAny acc[property.name] = { @@ -47,5 +50,6 @@ export const generateFakeField = ({ icon: icon, label: label, value: value || generateFakeValue(type, 'FieldMetadataType'), + fieldMetadataId, }; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index eba9365bf..1e0423175 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -60,7 +60,10 @@ export const generateFakeFormResponse = async ({ }), ); - return result.filter(isDefined).reduce((acc, curr) => { - return { ...acc, ...curr }; - }, {}); + return result.filter(isDefined).reduce( + (acc, curr) => { + return { ...acc, ...curr }; + }, + {} as Record, + ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts index fc8406dc7..55d98e06e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts @@ -1,7 +1,7 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { - BaseOutputSchema, + FieldOutputSchema, RecordOutputSchema, } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; @@ -20,7 +20,7 @@ const generateFakeObjectRecordEventWithPrefix = ({ return acc; }, - {} as BaseOutputSchema, + {} as Record, ); return { @@ -33,6 +33,7 @@ const generateFakeObjectRecordEventWithPrefix = ({ nameSingular: objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular, fieldIdName: `${prefix}.id`, + objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, }, fields: prefixedRecordFields, _outputSchemaType: 'RECORD', 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 55cbd3a9a..86c08eac3 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 @@ -19,6 +19,7 @@ export const generateFakeObjectRecord = ({ nameSingular: objectMetadataInfo.objectMetadataItemWithFieldsMaps.nameSingular, fieldIdName: 'id', + objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, }, 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 54f24aca3..e564166a8 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 @@ -2,7 +2,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; -import { BaseOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { FieldOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; @@ -15,11 +15,11 @@ export const generateObjectRecordFields = ({ }: { objectMetadataInfo: ObjectMetadataInfo; depth?: number; -}): BaseOutputSchema => { +}): Record => { const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps; return Object.values(objectMetadata.fieldsById).reduce( - (acc: BaseOutputSchema, field) => { + (acc: Record, field) => { if (!shouldGenerateFieldFakeValue(field)) { return acc; } @@ -29,6 +29,7 @@ export const generateObjectRecordFields = ({ type: field.type, label: field.label, icon: field.icon ?? undefined, + fieldMetadataId: field.id, }); return acc; @@ -51,6 +52,7 @@ export const generateObjectRecordFields = ({ isLeaf: false, icon: field.icon ?? undefined, label: field.label, + fieldMetadataId: field.id, value: generateFakeObjectRecord({ objectMetadataInfo: { objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, @@ -63,6 +65,6 @@ export const generateObjectRecordFields = ({ return acc; }, - {} as BaseOutputSchema, + {} as Record, ); }; 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 5838c6f1e..487dacceb 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 @@ -68,30 +68,11 @@ describe('evaluateFilterConditions', () => { expect(result).toBe(false); }); - it('should handle null checks', () => { - const filter1 = createFilter(ViewFilterOperand.Is, null, 'null'); - const filter2 = createFilter(ViewFilterOperand.Is, undefined, 'NULL'); - const filter3 = createFilter(ViewFilterOperand.Is, 'value', 'null'); + it('should return true when values are equal but different types', () => { + const filter = createFilter(ViewFilterOperand.Is, '123', 123); + const result = evaluateFilterConditions({ filters: [filter] }); - expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true); - expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true); - expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false); - }); - - it('should handle not null checks', () => { - const filter1 = createFilter(ViewFilterOperand.Is, 'value', 'not null'); - const filter2 = createFilter(ViewFilterOperand.Is, 'value', 'NOT NULL'); - const filter3 = createFilter(ViewFilterOperand.Is, null, 'not null'); - const filter4 = createFilter( - ViewFilterOperand.Is, - undefined, - 'not null', - ); - - expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true); - expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true); - expect(evaluateFilterConditions({ filters: [filter3] })).toBe(false); - expect(evaluateFilterConditions({ filters: [filter4] })).toBe(false); + expect(result).toBe(true); }); }); @@ -182,12 +163,12 @@ describe('evaluateFilterConditions', () => { const filter1 = createFilter( ViewFilterOperand.Contains, ['apple', 'banana', 'cherry'], - 'apple', + ['apple'], ); const filter2 = createFilter( ViewFilterOperand.Contains, ['apple', 'banana', 'cherry'], - 'grape', + ['grape'], ); expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true); @@ -198,12 +179,12 @@ describe('evaluateFilterConditions', () => { const filter1 = createFilter( ViewFilterOperand.DoesNotContain, ['apple', 'banana', 'cherry'], - 'apple', + ['apple'], ); const filter2 = createFilter( ViewFilterOperand.DoesNotContain, ['apple', 'banana', 'cherry'], - 'grape', + ['grape'], ); expect(evaluateFilterConditions({ filters: [filter1] })).toBe(false); 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 04aaa455a..771c6fd01 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 @@ -16,17 +16,19 @@ function evaluateFilter(filter: ResolvedFilter): boolean { switch (filter.operand) { case ViewFilterOperand.Is: - if (String(rightValue).toLowerCase() === 'null') { - return leftValue === null || leftValue === undefined; + switch (typeof leftValue) { + case 'string': + return ( + String(leftValue).toLowerCase() === String(rightValue).toLowerCase() + ); + case 'boolean': + return Boolean(leftValue) === Boolean(rightValue); + default: + return leftValue === rightValue; } - if (String(rightValue).toLowerCase() === 'not null') { - return leftValue !== null && leftValue !== undefined; - } - - return leftValue == rightValue; case ViewFilterOperand.IsNot: - return leftValue != rightValue; + return String(leftValue) !== String(rightValue); case ViewFilterOperand.GreaterThanOrEqual: return Number(leftValue) >= Number(rightValue); @@ -36,14 +38,38 @@ function evaluateFilter(filter: ResolvedFilter): boolean { case ViewFilterOperand.Contains: if (Array.isArray(leftValue)) { - return leftValue.includes(rightValue); + try { + const parsedRightValue = Array.isArray(rightValue) + ? rightValue + : JSON.parse(rightValue as string); + + if (Array.isArray(parsedRightValue)) { + return parsedRightValue.every((item) => leftValue.includes(item)); + } else { + return leftValue.includes(parsedRightValue); + } + } catch (error) { + return leftValue.includes(rightValue); + } } return String(leftValue).includes(String(rightValue)); case ViewFilterOperand.DoesNotContain: if (Array.isArray(leftValue)) { - return !leftValue.includes(rightValue); + try { + const parsedRightValue = Array.isArray(rightValue) + ? rightValue + : JSON.parse(rightValue as string); + + if (Array.isArray(parsedRightValue)) { + return !parsedRightValue.every((item) => leftValue.includes(item)); + } else { + return !leftValue.includes(parsedRightValue); + } + } catch (error) { + return !leftValue.includes(rightValue); + } } return !String(leftValue).includes(String(rightValue)); @@ -67,17 +93,43 @@ function evaluateFilter(filter: ResolvedFilter): boolean { case ViewFilterOperand.IsNotNull: return leftValue !== null && leftValue !== undefined; - case ViewFilterOperand.IsRelative: case ViewFilterOperand.IsInPast: + if (typeof leftValue === 'string') { + return Date.now() - new Date(leftValue).getTime() > 0; + } + + return false; + case ViewFilterOperand.IsInFuture: + if (typeof leftValue === 'string') { + return Date.now() - new Date(leftValue).getTime() < 0; + } + + return false; + case ViewFilterOperand.IsToday: + if (typeof leftValue === 'string') { + return new Date(leftValue).toDateString() === new Date().toDateString(); + } + + return false; + case ViewFilterOperand.IsBefore: + if (typeof leftValue === 'string' && typeof rightValue === 'string') { + return new Date(leftValue).getTime() < new Date(rightValue).getTime(); + } + + return false; + case ViewFilterOperand.IsAfter: - // Date/time operands - for now, return false as placeholder - // These would need proper date logic implementation + if (typeof leftValue === 'string' && typeof rightValue === 'string') { + return new Date(leftValue).getTime() > new Date(rightValue).getTime(); + } + return false; case ViewFilterOperand.VectorSearch: + case ViewFilterOperand.IsRelative: return false; default: diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/assert-form-step-is-valid.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/assert-form-step-is-valid.util.spec.ts index 5a8cbdba1..950f64233 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/assert-form-step-is-valid.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/__tests__/assert-form-step-is-valid.util.spec.ts @@ -40,6 +40,7 @@ const settings: WorkflowFormActionSettings = { label: 'Id', value: '123e4567-e89b-12d3-a456-426614174000', isLeaf: true, + fieldMetadataId: '123e4567-e89b-12d3-a456-426614174000', }, }, object: { @@ -49,6 +50,7 @@ const settings: WorkflowFormActionSettings = { isLeaf: true, fieldIdName: 'id', nameSingular: 'company', + objectMetadataId: '123e4567-e89b-12d3-a456-426614174000', }, _outputSchemaType: 'RECORD', }, diff --git a/packages/twenty-shared/src/types/StepFilters.ts b/packages/twenty-shared/src/types/StepFilters.ts index 2e5078950..6011ac68e 100644 --- a/packages/twenty-shared/src/types/StepFilters.ts +++ b/packages/twenty-shared/src/types/StepFilters.ts @@ -16,8 +16,8 @@ export type StepFilter = { id: string; type: string; label: string; - value: string; operand: ViewFilterOperand; + value: string; displayValue: string; stepFilterGroupId: string; stepOutputKey: string;