diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
index fbbcaf5f10..af8aa58090 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx
@@ -8,6 +8,10 @@ import { FormEmailsFieldInput } from '@/object-record/record-field/ui/form-types
import { FormFilesFieldInput } from '@/object-record/record-field/ui/form-types/components/FormFilesFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/ui/form-types/components/FormFullNameFieldInput';
import { FormLinksFieldInput } from '@/object-record/record-field/ui/form-types/components/FormLinksFieldInput';
+import {
+ FormMorphRelationToOneFieldInput,
+ type FormMorphRelationToOneValue,
+} from '@/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormPhoneFieldInput } from '@/object-record/record-field/ui/form-types/components/FormPhoneFieldInput';
@@ -43,6 +47,7 @@ import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFi
import { isFieldFiles } from '@/object-record/record-field/ui/types/guards/isFieldFiles';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
+import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
@@ -241,6 +246,16 @@ export const FormFieldInput = ({
VariablePicker={VariablePicker}
readonly={readonly}
/>
+ ) : isFieldMorphRelationManyToOne(field) ? (
+
) : isFieldArray(field) ? (
void;
+ onClear?: () => void;
+ readonly?: boolean;
+ testId?: string;
+ VariablePicker?: VariablePickerComponent;
+};
+
+export const FormMorphRelationToOneFieldInput = ({
+ label,
+ morphRelations,
+ defaultValue,
+ onChange,
+ onClear,
+ readonly,
+ testId,
+ VariablePicker,
+}: FormMorphRelationToOneFieldInputProps) => {
+ const { objectMetadataItems } = useObjectMetadataItems();
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
+
+ const componentId = useId();
+
+ const readableObjectNameSingulars = [
+ ...new Set(
+ morphRelations
+ .filter(
+ (morphRelation) =>
+ objectPermissionsByObjectMetadataId[
+ morphRelation.targetObjectMetadata.id
+ ]?.canReadObjectRecords === true,
+ )
+ .map(
+ (morphRelation) => morphRelation.targetObjectMetadata.nameSingular,
+ ),
+ ),
+ ];
+
+ const selectedMorphValue =
+ isDefined(defaultValue) && !isString(defaultValue) ? defaultValue : null;
+
+ const recordIdOrVariable: RecordId | Variable | null | undefined = isString(
+ defaultValue,
+ )
+ ? defaultValue
+ : isDefined(defaultValue)
+ ? defaultValue.id
+ : defaultValue;
+
+ const selectedTargetIsReadable =
+ !isDefined(selectedMorphValue) ||
+ objectPermissionsByObjectMetadataId[
+ selectedMorphValue.targetObjectMetadataId
+ ]?.canReadObjectRecords === true;
+
+ const hasForbiddenSelectedRecord =
+ isDefined(selectedMorphValue) && !selectedTargetIsReadable;
+
+ const selectedObjectMetadataItem = isDefined(selectedMorphValue)
+ ? objectMetadataItems.find(
+ (objectMetadataItem) =>
+ objectMetadataItem.id === selectedMorphValue.targetObjectMetadataId,
+ )
+ : undefined;
+
+ const selectedObjectNameSingular =
+ selectedObjectMetadataItem?.nameSingular ?? readableObjectNameSingulars[0];
+
+ if (hasForbiddenSelectedRecord) {
+ return (
+
+ {label ? {label} : null}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (readableObjectNameSingulars.length === 0) {
+ return (
+
+ {label ? {label} : null}
+
+
+
+ {t`No record`}
+
+
+
+
+ );
+ }
+
+ return (
+
+ onChange({
+ targetObjectMetadataId: selectedMorphItem.objectMetadataId,
+ id: selectedMorphItem.recordId,
+ })
+ }
+ disabled={readonly}
+ VariablePicker={VariablePicker}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
index ec2d5e0014..17caa7bcc5 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx
@@ -60,6 +60,10 @@ export type FormSingleRecordPickerProps = {
onClear?: () => void;
onCreate?: (searchInput?: string) => void | Promise;
objectNameSingulars: string[];
+ selectedObjectNameSingular?: string;
+ onMorphItemSelected?: (
+ selectedMorphItem: RecordPickerPickableMorphItem,
+ ) => void;
disabled?: boolean;
testId?: string;
VariablePicker?: VariablePickerComponent;
@@ -69,8 +73,10 @@ export const FormSingleRecordPicker = ({
label,
defaultValue,
objectNameSingulars,
+ selectedObjectNameSingular,
onChange,
onClear,
+ onMorphItemSelected,
onCreate,
disabled,
testId,
@@ -78,6 +84,9 @@ export const FormSingleRecordPicker = ({
}: FormSingleRecordPickerProps) => {
const { theme } = useContext(ThemeContext);
+ const resolvedObjectNameSingular =
+ selectedObjectNameSingular ?? objectNameSingulars[0];
+
const draftValue: FormSingleRecordPickerValue =
defaultValue === null
? { type: 'no-record', value: null }
@@ -97,7 +106,7 @@ export const FormSingleRecordPicker = ({
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
? defaultValue
: '',
- objectNameSingular: objectNameSingulars[0],
+ objectNameSingular: resolvedObjectNameSingular,
withSoftDeleted: true,
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
});
@@ -133,6 +142,8 @@ export const FormSingleRecordPicker = ({
if (defaultValue === selectedMorphItem.recordId) {
onClear?.();
+ } else if (isDefined(onMorphItemSelected)) {
+ onMorphItemSelected(selectedMorphItem);
} else {
onChange(selectedMorphItem.recordId);
}
@@ -185,7 +196,7 @@ export const FormSingleRecordPicker = ({
@@ -212,7 +223,7 @@ export const FormSingleRecordPicker = ({
@@ -248,6 +259,7 @@ export const FormSingleRecordPicker = ({
onVariableSelect={handleVariableTagInsert}
shouldDisplayRecordObjects={true}
shouldDisplayRecordFields={false}
+ objectNameSingularsToSelect={objectNameSingulars}
/>
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/types/VariablePickerComponent.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/types/VariablePickerComponent.ts
index 5cab4c1a63..f4807aa459 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/types/VariablePickerComponent.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/types/VariablePickerComponent.ts
@@ -5,4 +5,5 @@ export type VariablePickerComponent = React.FC<{
onVariableSelect: (variableName: string) => void;
shouldDisplayRecordObjects?: boolean;
shouldDisplayRecordFields?: boolean;
+ objectNameSingularsToSelect?: string[];
}>;
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 5fe610e4f2..c1b0eea645 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
@@ -166,6 +166,15 @@ export const WorkflowEditActionCreateRecord = ({
saveAction(newFormData);
};
+ const handleFieldClear = (fieldName: keyof CreateRecordFormData) => {
+ const newFormData: CreateRecordFormData = { ...formData };
+ delete newFormData[fieldName];
+
+ setFormData(newFormData);
+
+ saveAction(newFormData);
+ };
+
const saveAction = useDebouncedCallback(
async (formData: CreateRecordFormData) => {
if (actionOptions.readonly === true) {
@@ -242,6 +251,9 @@ export const WorkflowEditActionCreateRecord = ({
onChange={(value) => {
handleFieldChange(fieldDefinition.metadata.fieldName, value);
}}
+ onClear={() => {
+ handleFieldClear(fieldDefinition.metadata.fieldName);
+ }}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx
index a7d609b846..335b440197 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx
@@ -80,6 +80,15 @@ export const WorkflowEditActionUpdateRecord = ({
saveAction(newFormData);
};
+ const handleFieldClear = (fieldName: keyof UpdateRecordFormData) => {
+ const newFormData: UpdateRecordFormData = { ...formData };
+ delete newFormData[fieldName];
+
+ setFormData(newFormData);
+
+ saveAction(newFormData);
+ };
+
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
(item) => item.nameSingular === formData.objectNameSingular,
);
@@ -229,6 +238,9 @@ export const WorkflowEditActionUpdateRecord = ({
onChange={(value) => {
handleFieldChange(fieldName, value);
}}
+ onClear={() => {
+ handleFieldClear(fieldName);
+ }}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
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 7f5a02b38c..270c08ab50 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
@@ -183,6 +183,15 @@ export const WorkflowEditActionUpsertRecord = ({
saveAction(newFormData);
};
+ const handleFieldClear = (fieldName: keyof UpsertRecordFormData) => {
+ const newFormData: UpsertRecordFormData = { ...formData };
+ delete newFormData[fieldName];
+
+ setFormData(newFormData);
+
+ saveAction(newFormData);
+ };
+
const saveAction = useDebouncedCallback(
async (formData: UpsertRecordFormData) => {
if (actionOptions.readonly === true) {
@@ -269,6 +278,9 @@ export const WorkflowEditActionUpsertRecord = ({
onChange={(recordId) => {
handleFieldChange('id', recordId);
}}
+ onClear={() => {
+ handleFieldClear('id');
+ }}
objectNameSingulars={
isDefined(objectNameSingular) ? [objectNameSingular] : []
}
@@ -299,6 +311,9 @@ export const WorkflowEditActionUpsertRecord = ({
onChange={(value) => {
handleFieldChange(fieldDefinition.metadata.fieldName, value);
}}
+ onClear={() => {
+ handleFieldClear(fieldDefinition.metadata.fieldName);
+ }}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts
index cdf553d4b4..3b8856e67b 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts
@@ -22,6 +22,7 @@ const SUPPORTED_FORM_FIELD_TYPES = [
FieldMetadataType.UUID,
FieldMetadataType.ARRAY,
FieldMetadataType.RELATION,
+ FieldMetadataType.MORPH_RELATION,
FieldMetadataType.RICH_TEXT,
];
@@ -37,8 +38,10 @@ export const shouldDisplayFormField = ({
}
const isIdField = fieldMetadataItem.name === 'id';
+
const isNotSupportedRelation =
- fieldMetadataItem.type === FieldMetadataType.RELATION &&
+ (fieldMetadataItem.type === FieldMetadataType.RELATION ||
+ fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) &&
fieldMetadataItem.settings?.['relationType'] !== 'MANY_TO_ONE';
switch (actionType) {
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 001511e3f6..6d9abface4 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
@@ -63,6 +63,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
onVariableSelect,
shouldDisplayRecordObjects = false,
shouldDisplayRecordFields = true,
+ objectNameSingularsToSelect,
}) => {
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
const isDropdownOpen = useAtomComponentStateValue(
@@ -82,6 +83,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
disabled={disabled}
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
shouldDisplayRecordFields={shouldDisplayRecordFields}
+ objectNameSingularsToSelect={objectNameSingularsToSelect}
/>
);
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 594cdc9c6d..85dc736df7 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
@@ -39,6 +39,7 @@ export const WorkflowVariablesDropdown = ({
onVariableSelect,
shouldDisplayRecordFields,
shouldDisplayRecordObjects,
+ objectNameSingularsToSelect,
}: {
clickableComponent?: React.ReactNode;
disabled?: boolean;
@@ -47,6 +48,7 @@ export const WorkflowVariablesDropdown = ({
onVariableSelect: (variableName: string) => void;
shouldDisplayRecordFields: boolean;
shouldDisplayRecordObjects: boolean;
+ objectNameSingularsToSelect?: string[];
}) => {
const { theme } = useContext(ThemeContext);
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
@@ -119,6 +121,7 @@ export const WorkflowVariablesDropdown = ({
onSelect={handleSubItemSelect}
onBack={handleBack}
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
+ objectNameSingularsToSelect={objectNameSingularsToSelect}
/>
)
}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
index a4744fb84e..a50200b965 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems.tsx
@@ -29,6 +29,7 @@ type WorkflowVariablesDropdownStepItemsProps = {
onSelect: (value: string) => void;
onBack: () => void;
shouldDisplayRecordObjects: boolean;
+ objectNameSingularsToSelect?: string[];
};
export const WorkflowVariablesDropdownStepItems = ({
@@ -36,6 +37,7 @@ export const WorkflowVariablesDropdownStepItems = ({
onSelect,
onBack,
shouldDisplayRecordObjects,
+ objectNameSingularsToSelect,
}: WorkflowVariablesDropdownStepItemsProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
@@ -97,8 +99,18 @@ export const WorkflowVariablesDropdownStepItems = ({
: true;
const objectLabel = displayedSubStepObjectMetadata?.labelSingular;
+
+ const isSubStepObjectSelectable =
+ !isDefined(objectNameSingularsToSelect) ||
+ (isDefined(displayedSubStepObjectMetadata) &&
+ objectNameSingularsToSelect.includes(
+ displayedSubStepObjectMetadata.nameSingular,
+ ));
+
const shouldDisplaySubStepObject =
- shouldDisplayRecordObjects && isObjectFoundThroughSearch;
+ shouldDisplayRecordObjects &&
+ isObjectFoundThroughSearch &&
+ isSubStepObjectSelectable;
const displayedSubStepObjectIconProps = isDefined(
displayedSubStepObjectMetadata,
diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts
index d654f3f130..78214b6224 100644
--- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts
@@ -15,9 +15,14 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
+import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@CoreResolver()
-@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
+@UseFilters(
+ SearchApiExceptionFilter,
+ PermissionsGraphqlApiExceptionFilter,
+ PreventNestToAutoLogGraphqlErrorsFilter,
+)
@UsePipes(ResolverValidationPipe)
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
export class SearchResolver {
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts
index 4fa1c125c4..aa9b186f0e 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts
@@ -2,11 +2,44 @@ import { isObject, isString } from '@sniptt/guards';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
-import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
+import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
+import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
+import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
+type MorphRelationTargetJoinColumn = {
+ joinColumnName: string;
+ targetObjectMetadataId: string;
+};
+
+type ExtractedMorphValue = {
+ targetObjectMetadataId: string;
+ id: string;
+};
+
+const extractMorphValue = (value: unknown): ExtractedMorphValue | null => {
+ if (!isObject(value)) {
+ return null;
+ }
+
+ const record = value as Record;
+
+ if (
+ isString(record.targetObjectMetadataId) &&
+ isString(record.id) &&
+ isDefined(record.id)
+ ) {
+ return {
+ targetObjectMetadataId: record.targetObjectMetadataId,
+ id: record.id,
+ };
+ }
+
+ return null;
+};
+
const extractLegacyRelationId = (value: unknown): string | undefined => {
if (!isObject(value)) {
return undefined;
@@ -21,7 +54,113 @@ const extractLegacyRelationId = (value: unknown): string | undefined => {
return record.id;
};
-export const formatWorkflowRecordRelationFields = (
+const formatWorkflowRecordMorphRelationFields = (
+ record: Record,
+ objectMetadataInfo: ObjectMetadataInfo,
+): {
+ formattedRecord: Record;
+ joinColumnNamesByMorphFieldName: Record;
+} => {
+ const { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ objectMetadataInfo;
+
+ const objectFields = getFlatFieldsFromFlatObjectMetadata(
+ flatObjectMetadata,
+ flatFieldMetadataMaps,
+ );
+
+ const targetJoinColumnsByMorphFieldName = new Map<
+ string,
+ MorphRelationTargetJoinColumn[]
+ >();
+
+ for (const field of objectFields) {
+ if (
+ !isFlatFieldMetadataOfType(field, FieldMetadataType.MORPH_RELATION) ||
+ field.settings.relationType !== RelationType.MANY_TO_ONE
+ ) {
+ continue;
+ }
+
+ const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
+ flatEntityId: field.relationTargetObjectMetadataId,
+ flatEntityMaps: flatObjectMetadataMaps,
+ });
+
+ if (!isDefined(targetObjectMetadata)) {
+ continue;
+ }
+
+ const morphFieldName = getMorphNameFromMorphFieldMetadataName({
+ morphRelationFlatFieldMetadata: field,
+ nameSingular: targetObjectMetadata.nameSingular,
+ namePlural: targetObjectMetadata.namePlural,
+ });
+
+ const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
+ name: field.name,
+ });
+
+ const existing =
+ targetJoinColumnsByMorphFieldName.get(morphFieldName) ?? [];
+
+ targetJoinColumnsByMorphFieldName.set(morphFieldName, [
+ ...existing,
+ {
+ joinColumnName,
+ targetObjectMetadataId: field.relationTargetObjectMetadataId,
+ },
+ ]);
+ }
+
+ const formattedRecord: Record = {};
+ const joinColumnNamesByMorphFieldName: Record = {};
+
+ for (const [key, value] of Object.entries(record)) {
+ const targetJoinColumns = targetJoinColumnsByMorphFieldName.get(key);
+
+ if (!isDefined(targetJoinColumns)) {
+ formattedRecord[key] = value;
+ continue;
+ }
+
+ const registerJoinColumns = () => {
+ joinColumnNamesByMorphFieldName[key] = targetJoinColumns.map(
+ (targetJoinColumn) => targetJoinColumn.joinColumnName,
+ );
+
+ for (const { joinColumnName } of targetJoinColumns) {
+ formattedRecord[joinColumnName] = null;
+ }
+ };
+
+ if (value === null) {
+ registerJoinColumns();
+ continue;
+ }
+
+ const morphValue = extractMorphValue(value);
+
+ const matchingTargetJoinColumn = isDefined(morphValue)
+ ? targetJoinColumns.find(
+ (targetJoinColumn) =>
+ targetJoinColumn.targetObjectMetadataId ===
+ morphValue.targetObjectMetadataId,
+ )
+ : undefined;
+
+ if (!isDefined(morphValue) || !isDefined(matchingTargetJoinColumn)) {
+ continue;
+ }
+
+ registerJoinColumns();
+ formattedRecord[matchingTargetJoinColumn.joinColumnName] = morphValue.id;
+ }
+
+ return { formattedRecord, joinColumnNamesByMorphFieldName };
+};
+
+const formatWorkflowRecordSimpleRelationFields = (
record: Record,
objectMetadataInfo: ObjectMetadataInfo,
): Record => {
@@ -70,3 +209,23 @@ export const formatWorkflowRecordRelationFields = (
return formattedRecord;
};
+
+export const formatWorkflowRecordRelationFields = (
+ record: Record,
+ objectMetadataInfo: ObjectMetadataInfo,
+): {
+ formattedRecord: Record;
+ joinColumnNamesByMorphFieldName: Record;
+} => {
+ const {
+ formattedRecord: recordWithMorphRelations,
+ joinColumnNamesByMorphFieldName,
+ } = formatWorkflowRecordMorphRelationFields(record, objectMetadataInfo);
+
+ const formattedRecord = formatWorkflowRecordSimpleRelationFields(
+ recordWithMorphRelations,
+ objectMetadataInfo,
+ );
+
+ return { formattedRecord, joinColumnNamesByMorphFieldName };
+};
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts
index 6c0290be2f..9b7a8354e8 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts
@@ -59,10 +59,11 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
context,
) as WorkflowCreateRecordActionInput;
- const formattedObjectRecord = formatWorkflowRecordRelationFields(
- workflowActionInput.objectRecord,
- objectMetadataInfo,
- );
+ const { formattedRecord: formattedObjectRecord } =
+ formatWorkflowRecordRelationFields(
+ workflowActionInput.objectRecord,
+ objectMetadataInfo,
+ );
const filteredObjectRecord = filterValidFieldsInRecord(
formattedObjectRecord,
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts
index 5f9f93a9ad..3559f13b3c 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts
@@ -82,7 +82,10 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
);
}
- const formattedObjectRecord = formatWorkflowRecordRelationFields(
+ const {
+ formattedRecord: formattedObjectRecord,
+ joinColumnNamesByMorphFieldName,
+ } = formatWorkflowRecordRelationFields(
workflowActionInput.objectRecord,
objectMetadataInfo,
);
@@ -93,7 +96,11 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
objectMetadataInfo.flatFieldMetadataMaps,
);
- const filteredFieldsToUpdate = workflowActionInput.fieldsToUpdate?.filter(
+ const expandedFieldsToUpdate = workflowActionInput.fieldsToUpdate?.flatMap(
+ (fieldName) => joinColumnNamesByMorphFieldName[fieldName] ?? [fieldName],
+ );
+
+ const filteredFieldsToUpdate = expandedFieldsToUpdate?.filter(
(fieldName) => fieldName in filteredObjectRecord,
);
diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts
index 173bab49d3..dba22787fe 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts
@@ -77,10 +77,11 @@ export class UpsertRecordWorkflowAction implements WorkflowAction {
);
}
- const formattedObjectRecord = formatWorkflowRecordRelationFields(
- workflowActionInput.objectRecord,
- objectMetadataInfo,
- );
+ const { formattedRecord: formattedObjectRecord } =
+ formatWorkflowRecordRelationFields(
+ workflowActionInput.objectRecord,
+ objectMetadataInfo,
+ );
const filteredObjectRecord = filterValidFieldsInRecord(
formattedObjectRecord,