diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx index 63f9e1dbc1..817b64aa1c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx @@ -3,13 +3,18 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { useObjectMetadataSelectHelpers } from '@/object-metadata/hooks/useObjectMetadataSelectHelpers'; import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useViewOrDefaultView } from '@/views/hooks/useViewOrDefaultView'; import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; +import { + buildUpdatedRecordActionFormData, + type RecordActionFormData, + type RelationManyToOneField, +} from '@/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData'; import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { t } from '@lingui/core/macro'; @@ -20,16 +25,8 @@ import { HorizontalSeparator } from 'twenty-ui/layout'; import { type SelectOption } from 'twenty-ui/input'; import { type JsonValue } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; -import { RelationType } from '~/generated-metadata/graphql'; -type RelationManyToOneField = { - id: string; -}; - -type CreateRecordFormData = { - objectName: string; - [field: string]: RelationManyToOneField | JsonValue; -}; +type CreateRecordFormData = RecordActionFormData; type WorkflowEditActionCreateRecordProps = { action: WorkflowCreateRecordAction; @@ -146,20 +143,12 @@ export const WorkflowEditActionCreateRecord = ({ return; } - const isFieldRelationManyToOne = - isFieldRelation(fieldDefinition) && - fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - - const fieldValue = isFieldRelationManyToOne - ? { - id: updatedValue, - } - : updatedValue; - - const newFormData: CreateRecordFormData = { - ...formData, - [fieldName]: fieldValue, - }; + const newFormData = buildUpdatedRecordActionFormData({ + formData, + fieldName, + fieldDefinition, + updatedValue, + }); setFormData(newFormData); @@ -231,11 +220,7 @@ export const WorkflowEditActionCreateRecord = ({ {inlineFieldDefinitions?.map((fieldDefinition) => { - const isFieldRelationManyToOne = - isFieldRelation(fieldDefinition) && - fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - - const currentValue = isFieldRelationManyToOne + const currentValue = isFieldRelationManyToOne(fieldDefinition) ? ( formData[ fieldDefinition.metadata.fieldName diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx index 56289706ab..edc9c685df 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx @@ -4,7 +4,7 @@ import { useObjectMetadataSelectHelpers } from '@/object-metadata/hooks/useObjec import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useViewOrDefaultView } from '@/views/hooks/useViewOrDefaultView'; @@ -12,6 +12,11 @@ import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpd import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; +import { + buildUpdatedRecordActionFormData, + type RecordActionFormData, + type RelationManyToOneField, +} from '@/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData'; import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { t } from '@lingui/core/macro'; @@ -22,16 +27,8 @@ import { HorizontalSeparator } from 'twenty-ui/layout'; import { type SelectOption } from 'twenty-ui/input'; import { type JsonValue } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; -import { RelationType } from '~/generated-metadata/graphql'; -type RelationManyToOneField = { - id: string; -}; - -type UpsertRecordFormData = { - objectName: string; - [field: string]: RelationManyToOneField | JsonValue; -}; +type UpsertRecordFormData = RecordActionFormData; type WorkflowEditActionUpsertRecordProps = { action: WorkflowUpsertRecordAction; @@ -163,20 +160,12 @@ export const WorkflowEditActionUpsertRecord = ({ return; } - const isFieldRelationManyToOne = - isFieldRelation(fieldDefinition) && - fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - - const fieldValue = isFieldRelationManyToOne - ? { - id: updatedValue, - } - : updatedValue; - - const newFormData: UpsertRecordFormData = { - ...formData, - [fieldName]: fieldValue, - }; + const newFormData = buildUpdatedRecordActionFormData({ + formData, + fieldName, + fieldDefinition, + updatedValue, + }); setFormData(newFormData); @@ -291,11 +280,7 @@ export const WorkflowEditActionUpsertRecord = ({ ); } - const isFieldRelationManyToOne = - isFieldRelation(fieldDefinition) && - fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - - const currentValue = isFieldRelationManyToOne + const currentValue = isFieldRelationManyToOne(fieldDefinition) ? ( formData[ fieldDefinition.metadata.fieldName diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/buildUpdatedRecordActionFormData.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/buildUpdatedRecordActionFormData.test.ts new file mode 100644 index 0000000000..ff6434183a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/buildUpdatedRecordActionFormData.test.ts @@ -0,0 +1,104 @@ +import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; +import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { + buildUpdatedRecordActionFormData, + type RecordActionFormData, +} from '@/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; + +const relationToOneFieldDefinition = { + fieldMetadataId: 'field-metadata-id', + label: 'Company', + type: FieldMetadataType.RELATION, + iconName: 'IconBuildingSkyscraper', + metadata: { + fieldName: 'company', + relationType: RelationType.MANY_TO_ONE, + objectMetadataNameSingular: 'person', + relationObjectMetadataNameSingular: 'company', + relationObjectMetadataNamePlural: 'companies', + }, +} as unknown as FieldDefinition; + +const textFieldDefinition = { + fieldMetadataId: 'field-metadata-id', + label: 'City', + type: FieldMetadataType.TEXT, + iconName: 'IconMap', + metadata: { + fieldName: 'city', + objectMetadataNameSingular: 'person', + }, +} as unknown as FieldDefinition; + +const formData: RecordActionFormData = { + objectName: 'person', + city: 'Paris', +}; + +describe('buildUpdatedRecordActionFormData', () => { + it('should wrap a selected record id in an object when the field is a to-one relation', () => { + const result = buildUpdatedRecordActionFormData({ + formData, + fieldName: 'company', + fieldDefinition: relationToOneFieldDefinition, + updatedValue: '20202020-3ec3-4fe3-8997-b76aa0bfa408', + }); + + expect(result).toEqual({ + objectName: 'person', + city: 'Paris', + company: { id: '20202020-3ec3-4fe3-8997-b76aa0bfa408' }, + }); + }); + + it('should wrap a variable in an object when the field is a to-one relation', () => { + const result = buildUpdatedRecordActionFormData({ + formData, + fieldName: 'company', + fieldDefinition: relationToOneFieldDefinition, + updatedValue: '{{trigger.object.id}}', + }); + + expect(result.company).toEqual({ id: '{{trigger.object.id}}' }); + }); + + it('should remove the field when a to-one relation is cleared', () => { + const result = buildUpdatedRecordActionFormData({ + formData: { ...formData, company: { id: 'previous-record-id' } }, + fieldName: 'company', + fieldDefinition: relationToOneFieldDefinition, + updatedValue: null, + }); + + expect(result).not.toHaveProperty('company'); + expect(result).toEqual({ objectName: 'person', city: 'Paris' }); + }); + + it('should keep null values as-is when the field is not a relation', () => { + const result = buildUpdatedRecordActionFormData({ + formData, + fieldName: 'city', + fieldDefinition: textFieldDefinition, + updatedValue: null, + }); + + expect(result.city).toBeNull(); + }); + + it('should not mutate the form data it receives', () => { + const initialFormData: RecordActionFormData = { + objectName: 'person', + company: { id: 'previous-record-id' }, + }; + + buildUpdatedRecordActionFormData({ + formData: initialFormData, + fieldName: 'company', + fieldDefinition: relationToOneFieldDefinition, + updatedValue: null, + }); + + expect(initialFormData.company).toEqual({ id: 'previous-record-id' }); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData.ts new file mode 100644 index 0000000000..648ebf80b3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/buildUpdatedRecordActionFormData.ts @@ -0,0 +1,42 @@ +import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; +import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne'; +import { isDefined } from 'twenty-shared/utils'; +import { type JsonValue } from 'type-fest'; + +export type RelationManyToOneField = { + id: string; +}; + +export type RecordActionFormData = { + objectName: string; + [field: string]: RelationManyToOneField | JsonValue; +}; + +export const buildUpdatedRecordActionFormData = ({ + formData, + fieldName, + fieldDefinition, + updatedValue, +}: { + formData: RecordActionFormData; + fieldName: keyof RecordActionFormData; + fieldDefinition: FieldDefinition; + updatedValue: JsonValue; +}): RecordActionFormData => { + const isRelationManyToOne = isFieldRelationManyToOne(fieldDefinition); + + const updatedFormData: RecordActionFormData = { ...formData }; + + if (isRelationManyToOne && !isDefined(updatedValue)) { + delete updatedFormData[fieldName]; + + return updatedFormData; + } + + updatedFormData[fieldName] = isRelationManyToOne + ? { id: updatedValue } + : updatedValue; + + return updatedFormData; +};