Support morph relations in workflow record nodes (#21403)

## Support morph (polymorphic) relations in workflow record nodes

Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.

### What changed

**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.

**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: 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.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).

### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.

### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.

<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>

Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
This commit is contained in:
Marie
2026-06-15 14:56:17 +02:00
committed by GitHub
parent 5207493cda
commit 4be76e3fd1
16 changed files with 450 additions and 18 deletions
@@ -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}
/>
</StyledSearchVariablesDropdownContainer>
);
@@ -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}
/>
)
}
@@ -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,