Support morph relations in workflow record nodes#21403
Conversation
|
👋 Thanks for contributing to Twenty! Your PR has been set to draft while you work on it. Once you're done, mark it as Ready for review and our automated checks will run. Looking forward to your contribution! |
| @UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter) | ||
| @UseFilters( | ||
| SearchApiExceptionFilter, | ||
| PermissionsGraphqlApiExceptionFilter, |
There was a problem hiding this comment.
so that permission errors in search do not end up at 500 anymore
There was a problem hiding this comment.
3 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx">
<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx:250">
P2: Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.</violation>
</file>
<file name="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts">
<violation number="1" location="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts:117">
P1: Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.</violation>
</file>
<file name="packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx">
<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx:150">
P2: Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| (targetJoinColumn) => targetJoinColumn.joinColumnName, | ||
| ); | ||
|
|
||
| for (const { joinColumnName } of targetJoinColumns) { |
There was a problem hiding this comment.
P1: Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts, line 117:
<comment>Malformed morph input currently wipes existing morph relation by nulling all target join columns before payload validation.</comment>
<file context>
@@ -0,0 +1,139 @@
+ (targetJoinColumn) => targetJoinColumn.joinColumnName,
+ );
+
+ for (const { joinColumnName } of targetJoinColumns) {
+ formattedRecord[joinColumnName] = null;
+ }
</file context>
| readonly={readonly} | ||
| /> | ||
| ) : isFieldMorphRelationManyToOne(field) ? ( | ||
| <FormMorphRelationToOneFieldInput |
There was a problem hiding this comment.
P2: Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx, line 250:
<comment>Morph relation input is wired without VariablePicker support, so users cannot set morph relations from workflow variables like regular relation fields.</comment>
<file context>
@@ -241,6 +246,15 @@ export const FormFieldInput = ({
readonly={readonly}
/>
+ ) : isFieldMorphRelationManyToOne(field) ? (
+ <FormMorphRelationToOneFieldInput
+ label={field.label}
+ morphRelations={field.metadata.morphRelations}
</file context>
| return; | ||
| } | ||
|
|
||
| if (recordId === selectedMorphItem.recordId) { |
There was a problem hiding this comment.
P2: Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx, line 150:
<comment>Morph selection equality check ignores target object and can clear instead of switching when IDs match across targets.</comment>
<file context>
@@ -0,0 +1,252 @@
+ return;
+ }
+
+ if (recordId === selectedMorphItem.recordId) {
+ onClear?.();
+ } else {
</file context>
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for selecting and persisting polymorphic (morph) many-to-one relations in workflow Create/Update/Upsert Record nodes. It introduces a new frontend morph relation form input and a backend formatter that maps the user-facing morph field value to the correct concrete join column(s).
Changes:
- Frontend: allow morph relation fields to appear in workflow record forms and add a polymorphic record picker input.
- Backend: add a formatter that converts
{ targetObjectMetadataId, id }into the correct per-target join column while nulling sibling columns. - Backend: update workflow record CRUD actions to run the morph formatter before existing relation formatting; add a permissions exception filter to search resolver.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts | Runs morph formatter prior to relation formatting for upsert inputs. |
| packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts | Adds morph formatting and expands fieldsToUpdate to concrete morph join columns. |
| packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts | Runs morph formatter prior to relation formatting for create inputs. |
| packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-morph-relation-fields.util.ts | New utility to map user-facing morph values to concrete join columns. |
| packages/twenty-server/src/engine/core-modules/search/search.resolver.ts | Adds permissions exception filter to Search resolver filter chain. |
| packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts | Allows MORPH_RELATION fields (many-to-one only) to be displayed in workflow record forms. |
| packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMorphRelationToOneFieldInput.tsx | New polymorphic record picker input for morph many-to-one fields. |
| packages/twenty-front/src/modules/object-record/record-field/ui/components/FormFieldInput.tsx | Wires morph relation fields to the new morph input component. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const selectedObjectNameSingular = | ||
| selectedObjectMetadataItem?.nameSingular ?? readableObjectNameSingulars[0]; | ||
|
|
||
| const { record: selectedRecord } = useFindOneRecord({ | ||
| objectRecordId: | ||
| isDefined(recordId) && isValidUuid(recordId) ? recordId : '', | ||
| objectNameSingular: selectedObjectNameSingular ?? '', | ||
| withSoftDeleted: true, | ||
| skip: | ||
| !isDefined(recordId) || | ||
| !isValidUuid(recordId) || | ||
| !isDefined(selectedObjectNameSingular) || | ||
| hasForbiddenSelectedRecord, | ||
| }); |
| joinColumnNamesByMorphFieldName[key] = targetJoinColumns.map( | ||
| (targetJoinColumn) => targetJoinColumn.joinColumnName, | ||
| ); | ||
|
|
||
| for (const { joinColumnName } of targetJoinColumns) { | ||
| formattedRecord[joinColumnName] = null; | ||
| } | ||
|
|
||
| const morphValue = extractMorphValue(value); | ||
|
|
||
| if (!isDefined(morphValue)) { | ||
| continue; | ||
| } | ||
|
|
||
| const matchingTargetJoinColumn = targetJoinColumns.find( | ||
| (targetJoinColumn) => | ||
| targetJoinColumn.targetObjectMetadataId === | ||
| morphValue.targetObjectMetadataId, | ||
| ); | ||
|
|
||
| if (isDefined(matchingTargetJoinColumn)) { | ||
| formattedRecord[matchingTargetJoinColumn.joinColumnName] = morphValue.id; | ||
| } |
| ) : isFieldMorphRelationManyToOne(field) ? ( | ||
| <FormMorphRelationToOneFieldInput | ||
| label={field.label} | ||
| morphRelations={field.metadata.morphRelations} | ||
| defaultValue={defaultValue as FormMorphRelationToOneValue} |
| defaultValue={defaultValue as FormMorphRelationToOneValue} | ||
| onClear={onClear} | ||
| onChange={onChange} | ||
| readonly={readonly} |
There was a problem hiding this comment.
missing VariablePicker, I guess we need variables for those fields?
| testId?: string; | ||
| }; | ||
|
|
||
| export const FormMorphRelationToOneFieldInput = ({ |
There was a problem hiding this comment.
I don't understand why that component is so complicated compared to FormRelationToOneFieldInput. I think we should rely more on FormSingleRecordPicker and extend it if needed. Unless I am mistaken and this is not possible?
| return null; | ||
| }; | ||
|
|
||
| export const formatWorkflowRecordMorphRelationFields = ( |
There was a problem hiding this comment.
I think we probably only need one util, formatWorkflowRecordRelationFields. We call both utils together in actions. We should probably merge and test
Support morph (polymorphic) relations in workflow record nodes
Morph relations (e.g. a polymorphic
OwneronPettargetingPersonorCompany) were not selectable in the workflow Create / Update / Upsert Record nodes. This PR adds full support for setting them.What changed
Frontend
shouldDisplayFormField: allowMORPH_RELATION(many-to-one) so morph fields appear in record forms.FormMorphRelationToOneFieldInput: a polymorphic record picker across the morph's target objects, storing a self-describing value{ targetObjectMetadataId, id }.FormFieldInput.Backend
formatWorkflowRecordMorphRelationFieldsutil: resolves the form value (stored under the base field name, e.g.owner) into the correct per-target join column (ownerCompanyId), nulling siblings to keep exactly one target referenced.fieldsToUpdateto the concrete join columns).Permissions handling
canReadObjectRecords), so it no longer breaks when a target object is inaccessible.Notes
Also handles the case where the selected record is not readable
