feat: add-create-update-record in workflow (#14654)

## Description

- this PR focuses on issue
https://github.com/twentyhq/core-team-issues/issues/1476
- Added upsert action


## Visual Appearance

<img width="1792" height="1041" alt="Screenshot 2025-10-03 at 12 57
58 PM"
src="https://github.com/user-attachments/assets/57afb96c-d4b3-4a87-95f0-11ac4bd61dd8"
/>




<img width="1792" height="1031" alt="Screenshot 2025-10-03 at 12 57
48 PM"
src="https://github.com/user-attachments/assets/9032d4c2-f0d2-46f1-8682-a7e5c280a303"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
Harshit Singh
2025-10-21 19:18:25 +05:30
committed by GitHub
parent f5f23a9d31
commit 27f50c4f4e
41 changed files with 879 additions and 268 deletions
@@ -11,6 +11,7 @@ import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord';
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
@@ -172,6 +173,18 @@ export const WorkflowRunStepNodeDetail = ({
);
}
case 'UPSERT_RECORD': {
return (
<WorkflowEditActionUpsertRecord
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'FORM': {
return (
<WorkflowEditActionFormFiller
@@ -10,6 +10,7 @@ import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord';
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
@@ -162,6 +163,16 @@ export const WorkflowStepDetail = ({
);
}
case 'UPSERT_RECORD': {
return (
<WorkflowEditActionUpsertRecord
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
case 'FORM': {
return (
<WorkflowEditActionFormBuilder
@@ -80,6 +80,7 @@ describe('useCreateStep', () => {
stepType: 'CODE',
parentStepId: 'parent-step-id',
nextStepId: undefined,
parentStepConnectionOptions: undefined,
position: undefined,
});
});
@@ -1,7 +1,7 @@
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState';
import { type WorkflowStepType } from '@/workflow/types/Workflow';
import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { type WorkflowStepConnectionOptions } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowStepConnectionOptions';
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
@@ -35,7 +35,7 @@ export const useCreateStep = () => {
position,
connectionOptions,
}: {
newStepType: WorkflowStepType;
newStepType: WorkflowActionType;
parentStepId: string | undefined;
nextStepId: string | undefined;
position?: { x: number; y: number };
@@ -6,14 +6,13 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { UPDATE_WORKFLOW_RUN_STEP } from '@/workflow/graphql/mutations/updateWorkflowRunStep';
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { type WorkflowStep, type WorkflowRun } from '@/workflow/types/Workflow';
import { useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import {
type UpdateWorkflowRunStepInput,
type UpdateWorkflowRunStepMutation,
type UpdateWorkflowRunStepMutationVariables,
type WorkflowAction,
} from '~/generated-metadata/graphql';
export const useUpdateWorkflowRunStep = () => {
@@ -60,7 +59,7 @@ export const useUpdateWorkflowRunStep = () => {
...cachedRecord.state,
flow: {
...cachedRecord.state.flow,
steps: cachedRecord.state.flow.steps.map((step: WorkflowAction) => {
steps: cachedRecord.state.flow.steps.map((step: WorkflowStep) => {
if (step.id === updatedStep.id) {
return updatedStep;
}
@@ -6,14 +6,16 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { UPDATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/updateWorkflowVersionStep';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import {
type WorkflowVersion,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import {
type UpdateWorkflowVersionStepInput,
type UpdateWorkflowVersionStepMutation,
type UpdateWorkflowVersionStepMutationVariables,
type WorkflowAction,
} from '~/generated-metadata/graphql';
export const useUpdateWorkflowVersionStep = () => {
@@ -52,7 +54,7 @@ export const useUpdateWorkflowVersionStep = () => {
const newCachedRecord = {
...cachedRecord,
steps: (cachedRecord.steps || []).map((step: WorkflowAction) => {
steps: (cachedRecord.steps || []).map((step: WorkflowStep) => {
if (step.id === updatedStep.id) {
return updatedStep;
}
@@ -1,28 +1,11 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select } from '@/ui/input/components/Select';
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { useEffect, useState } from 'react';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowUpdateRecordBody } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowUpdateRecordBody';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { RelationType } from '~/generated-metadata/graphql';
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionUpdateRecordProps = {
action: WorkflowUpdateRecordAction;
@@ -36,127 +19,39 @@ type WorkflowEditActionUpdateRecordProps = {
};
};
type UpdateRecordFormData = {
objectNameSingular: string;
objectRecordId: string;
fieldsToUpdate: string[];
[field: string]: unknown;
};
export const WorkflowEditActionUpdateRecord = ({
action,
actionOptions,
}: WorkflowEditActionUpdateRecordProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
Icon: getIcon(item.icon),
label: item.labelPlural,
value: item.nameSingular,
}));
const [formData, setFormData] = useState<UpdateRecordFormData>({
objectNameSingular: action.settings.input.objectName,
objectRecordId: action.settings.input.objectRecordId,
fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [],
...action.settings.input.objectRecord,
});
const isFormDisabled = actionOptions.readonly;
const handleFieldChange = (
fieldName: keyof UpdateRecordFormData,
updatedValue: JsonValue,
) => {
const newFormData: UpdateRecordFormData = {
...formData,
[fieldName]: updatedValue,
};
setFormData(newFormData);
saveAction(newFormData);
};
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
(item) => item.nameSingular === formData.objectNameSingular,
);
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
.filter((fieldMetadataItem) =>
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
);
const inlineFieldDefinitions = isDefined(selectedObjectMetadataItem)
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem: selectedObjectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
)
: [];
const saveAction = useDebouncedCallback(
async (formData: UpdateRecordFormData) => {
if (actionOptions.readonly === true) {
return;
}
const {
objectNameSingular: updatedObjectName,
objectRecordId: updatedObjectRecordId,
fieldsToUpdate: updatedFieldsToUpdate,
...updatedOtherFields
} = formData;
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
objectName: updatedObjectName,
objectRecordId: updatedObjectRecordId ?? '',
objectRecord: updatedOtherFields,
fieldsToUpdate: updatedFieldsToUpdate ?? [],
},
},
});
},
1_000,
);
useEffect(() => {
return () => {
saveAction.flush();
};
}, [saveAction]);
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: 'Update Record',
});
const { getIcon } = useIcons();
const isFormDisabled = actionOptions.readonly === true;
const handleUpdate = (formData: UpdateRecordFormData) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
objectName: formData.objectNameSingular,
objectRecord: formData,
objectRecordId: formData.objectRecordId ?? '',
fieldsToUpdate: formData.fieldsToUpdate,
},
},
});
};
return (
<>
<SidePanelHeader
@@ -176,96 +71,16 @@ export const WorkflowEditActionUpdateRecord = ({
headerType={headerType}
disabled={isFormDisabled}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-action-record-update-object-name"
label="Object"
fullWidth
disabled={isFormDisabled}
value={formData.objectNameSingular}
emptyOption={{ label: 'Select an option', value: '' }}
options={availableMetadata}
onChange={(updatedObjectName) => {
const newFormData: UpdateRecordFormData = {
objectNameSingular: updatedObjectName,
objectRecordId: '',
fieldsToUpdate: [],
};
setFormData(newFormData);
saveAction(newFormData);
}}
withSearchInput
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
<HorizontalSeparator noMargin />
{isDefined(objectNameSingular) && (
<FormSingleRecordPicker
testId="workflow-edit-action-record-update-object-record-id"
label="Record"
onChange={(objectRecordId) =>
handleFieldChange('objectRecordId', objectRecordId)
}
objectNameSingulars={[objectNameSingular]}
defaultValue={formData.objectRecordId}
disabled={isFormDisabled}
VariablePicker={WorkflowVariablePicker}
/>
)}
{isDefined(selectedObjectMetadataItem) && (
<WorkflowFieldsMultiSelect
label="Fields to update"
placeholder="Select fields to update"
objectMetadataItem={selectedObjectMetadataItem}
handleFieldsChange={(fieldsToUpdate) =>
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
}
readonly={isFormDisabled ?? false}
defaultFields={formData.fieldsToUpdate}
/>
)}
<HorizontalSeparator noMargin />
{formData.fieldsToUpdate.map((fieldName) => {
const fieldDefinition = inlineFieldDefinitions?.find((definition) => {
const isFieldRelationManyToOne =
isFieldRelation(definition) &&
definition.metadata.relationType === RelationType.MANY_TO_ONE;
const value = isFieldRelationManyToOne
? `${definition.metadata.fieldName}Id`
: definition.metadata.fieldName;
return value === fieldName;
});
if (!isDefined(fieldDefinition)) {
return null;
}
const currentValue = formData[fieldName] as JsonValue;
return (
<FormFieldInput
key={fieldName}
defaultValue={currentValue}
field={fieldDefinition}
onChange={(value) => {
handleFieldChange(fieldName, value);
}}
VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled}
/>
);
})}
</WorkflowStepBody>
<WorkflowUpdateRecordBody
defaultObjectNameSingular={action.settings.input.objectName}
defaultObjectRecordId={action.settings.input.objectRecordId}
defaultFieldsToUpdate={action.settings.input.fieldsToUpdate}
defaultObjectRecord={action.settings.input.objectRecord}
readonly={isFormDisabled}
actionType="UPDATE_RECORD"
onUpdate={handleUpdate}
shouldPickRecord={true}
/>
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
</>
);
@@ -0,0 +1,85 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow';
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
import { WorkflowUpdateRecordBody } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowUpdateRecordBody';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
import { useIcons } from 'twenty-ui/display';
type WorkflowEditActionUpsertRecordProps = {
action: WorkflowUpsertRecordAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowUpsertRecordAction) => void;
};
};
export const WorkflowEditActionUpsertRecord = ({
action,
actionOptions,
}: WorkflowEditActionUpsertRecordProps) => {
const { headerTitle, headerIcon, headerIconColor, headerType } =
useWorkflowActionHeader({
action,
defaultTitle: 'Create or Update Record',
});
const { getIcon } = useIcons();
const isFormDisabled = actionOptions.readonly === true;
const handleUpdate = (formData: UpdateRecordFormData) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
objectName: formData.objectNameSingular,
objectRecord: formData,
fieldsToUpdate: formData.fieldsToUpdate,
},
},
});
};
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={headerIconColor}
initialTitle={headerTitle}
headerType={headerType}
disabled={isFormDisabled}
/>
<WorkflowUpdateRecordBody
defaultObjectNameSingular={action.settings.input.objectName}
defaultFieldsToUpdate={action.settings.input.fieldsToUpdate}
defaultObjectRecord={action.settings.input.objectRecord}
readonly={isFormDisabled}
actionType="UPSERT_RECORD"
onUpdate={handleUpdate}
shouldPickRecord={false}
/>
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
</>
);
};
@@ -0,0 +1,232 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select } from '@/ui/input/components/Select';
import { useEffect, useState } from 'react';
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
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 WorkflowUpdateRecordBodyProps = {
defaultObjectNameSingular: string;
defaultFieldsToUpdate: string[];
defaultObjectRecord: Record<string, unknown>;
defaultObjectRecordId?: string;
actionType: 'UPDATE_RECORD' | 'UPSERT_RECORD';
readonly: boolean;
onUpdate: (formData: UpdateRecordFormData) => void;
shouldPickRecord: boolean;
};
export const WorkflowUpdateRecordBody = ({
defaultObjectNameSingular,
defaultFieldsToUpdate,
defaultObjectRecord,
defaultObjectRecordId,
readonly,
actionType,
onUpdate,
shouldPickRecord,
}: WorkflowUpdateRecordBodyProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const availableMetadata: Array<SelectOption<string>> =
activeNonSystemObjectMetadataItems
.filter((objectMetadataItem) =>
canObjectBeManagedByWorkflow({
nameSingular: objectMetadataItem.nameSingular,
isSystem: objectMetadataItem.isSystem,
}),
)
.map((item) => ({
Icon: getIcon(item.icon),
label: item.labelPlural,
value: item.nameSingular,
}));
const [formData, setFormData] = useState<UpdateRecordFormData>({
objectNameSingular: defaultObjectNameSingular,
objectRecordId: defaultObjectRecordId,
fieldsToUpdate: defaultFieldsToUpdate ?? [],
...defaultObjectRecord,
});
const handleFieldChange = (
fieldName: keyof UpdateRecordFormData,
updatedValue: JsonValue,
) => {
const newFormData: UpdateRecordFormData = {
...formData,
[fieldName]: updatedValue,
};
setFormData(newFormData);
saveAction(newFormData);
};
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
(item) => item.nameSingular === formData.objectNameSingular,
);
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
.filter((fieldMetadataItem) =>
shouldDisplayFormField({ fieldMetadataItem, actionType }),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
);
const inlineFieldDefinitions = isDefined(selectedObjectMetadataItem)
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem: selectedObjectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
)
: [];
const saveAction = useDebouncedCallback(
async (formData: UpdateRecordFormData) => {
if (readonly === true) {
return;
}
const {
objectNameSingular: updatedObjectName,
objectRecordId: updatedObjectRecordId,
fieldsToUpdate: updatedFieldsToUpdate,
...updatedOtherFields
} = formData;
onUpdate({
objectNameSingular: updatedObjectName,
objectRecordId: updatedObjectRecordId ?? '',
fieldsToUpdate: updatedFieldsToUpdate ?? [],
...updatedOtherFields,
});
},
1_000,
);
useEffect(() => {
return () => {
saveAction.flush();
};
}, [saveAction]);
return (
<WorkflowStepBody>
<Select
dropdownId="workflow-update-record-object-name"
label="Object"
fullWidth
disabled={readonly}
value={formData.objectNameSingular}
emptyOption={{ label: 'Select an option', value: '' }}
options={availableMetadata}
onChange={(updatedObjectName) => {
const newFormData: UpdateRecordFormData = {
objectNameSingular: updatedObjectName,
objectRecordId: '',
fieldsToUpdate: [],
};
setFormData(newFormData);
saveAction(newFormData);
}}
withSearchInput
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
<HorizontalSeparator noMargin />
{isDefined(objectNameSingular) && shouldPickRecord && (
<FormSingleRecordPicker
testId="workflow-update-record-object-record-id"
label="Record"
onChange={(objectRecordId) =>
handleFieldChange('objectRecordId', objectRecordId)
}
objectNameSingulars={[objectNameSingular]}
defaultValue={formData.objectRecordId}
disabled={readonly}
VariablePicker={WorkflowVariablePicker}
/>
)}
{isDefined(selectedObjectMetadataItem) && (
<WorkflowFieldsMultiSelect
label="Fields to update"
placeholder="Select fields to update"
objectMetadataItem={selectedObjectMetadataItem}
handleFieldsChange={(fieldsToUpdate) =>
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
}
readonly={readonly ?? false}
defaultFields={formData.fieldsToUpdate}
/>
)}
<HorizontalSeparator noMargin />
{formData.fieldsToUpdate.map((fieldName) => {
const fieldDefinition = inlineFieldDefinitions?.find((definition) => {
const isFieldRelationManyToOne =
isFieldRelation(definition) &&
definition.metadata.relationType === RelationType.MANY_TO_ONE;
const value = isFieldRelationManyToOne
? `${definition.metadata.fieldName}Id`
: definition.metadata.fieldName;
return value === fieldName;
});
if (!isDefined(fieldDefinition)) {
return null;
}
const currentValue = formData[fieldName] as JsonValue;
return (
<FormFieldInput
key={fieldName}
defaultValue={currentValue}
field={fieldDefinition}
onChange={(value) => {
handleFieldChange(fieldName, value);
}}
VariablePicker={WorkflowVariablePicker}
readonly={readonly}
/>
);
})}
</WorkflowStepBody>
);
};
@@ -111,9 +111,7 @@ export const DisabledWithEmptyValues: Story = {
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
await canvas.findByTestId('workflow-update-record-object-record-id'),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
@@ -179,9 +177,7 @@ export const DisabledWithDefaultStaticValues: Story = {
expect(selectedRecord).toBeVisible();
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
await canvas.findByTestId('workflow-update-record-object-record-id'),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
@@ -241,9 +237,7 @@ export const DisabledWithDefaultVariableValues: Story = {
}
const openRecordSelectButton = within(
await canvas.findByTestId(
'workflow-edit-action-record-update-object-record-id',
),
await canvas.findByTestId('workflow-update-record-object-record-id'),
).queryByRole('button');
expect(openRecordSelectButton).not.toBeInTheDocument();
@@ -4,7 +4,11 @@ export const RECORD_ACTIONS: Array<{
label: string;
type: Extract<
WorkflowActionType,
'CREATE_RECORD' | 'UPDATE_RECORD' | 'DELETE_RECORD' | 'FIND_RECORDS'
| 'CREATE_RECORD'
| 'UPDATE_RECORD'
| 'DELETE_RECORD'
| 'UPSERT_RECORD'
| 'FIND_RECORDS'
>;
icon: string;
}> = [
@@ -28,4 +32,9 @@ export const RECORD_ACTIONS: Array<{
type: 'FIND_RECORDS',
icon: 'IconSearch',
},
{
label: 'Create or Update Record',
type: 'UPSERT_RECORD',
icon: 'IconPencilPlus',
},
];
@@ -0,0 +1,6 @@
export type UpdateRecordFormData = {
objectNameSingular: string;
objectRecordId?: string;
fieldsToUpdate: string[];
[field: string]: unknown;
};
@@ -9,6 +9,7 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
case 'UPSERT_RECORD':
case 'FIND_RECORDS':
case 'FORM':
case 'SEND_EMAIL':
@@ -10,6 +10,7 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
case 'UPSERT_RECORD':
case 'FIND_RECORDS':
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'AI_AGENT':
@@ -17,6 +17,7 @@ export const getActionIconColorOrThrow = ({
case 'CREATE_RECORD':
case 'UPDATE_RECORD':
case 'DELETE_RECORD':
case 'UPSERT_RECORD':
case 'FIND_RECORDS':
return theme.font.color.tertiary;
case 'FORM':
@@ -48,6 +48,7 @@ export const shouldDisplayFormField = ({
fieldMetadataItem.isActive
);
case 'UPDATE_RECORD':
case 'UPSERT_RECORD':
isTypeAllowedForAction =
COMMON_DISPLAYABLE_FIELD_TYPES.includes(fieldMetadataItem.type) ||
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';