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:
co-authored by
Félix Malfait
Thomas Trompette
parent
f5f23a9d31
commit
27f50c4f4e
@@ -4508,10 +4508,27 @@ export type WorkflowAction = {
|
||||
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
|
||||
position?: Maybe<WorkflowStepPosition>;
|
||||
settings: Scalars['JSON'];
|
||||
type: Scalars['String'];
|
||||
type: WorkflowActionType;
|
||||
valid: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum WorkflowActionType {
|
||||
AI_AGENT = 'AI_AGENT',
|
||||
CODE = 'CODE',
|
||||
CREATE_RECORD = 'CREATE_RECORD',
|
||||
DELAY = 'DELAY',
|
||||
DELETE_RECORD = 'DELETE_RECORD',
|
||||
EMPTY = 'EMPTY',
|
||||
FILTER = 'FILTER',
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
ITERATOR = 'ITERATOR',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
UPSERT_RECORD = 'UPSERT_RECORD'
|
||||
}
|
||||
|
||||
export type WorkflowRun = {
|
||||
__typename?: 'WorkflowRun';
|
||||
workflowRunId: Scalars['UUID'];
|
||||
@@ -6066,14 +6083,14 @@ export type UpdateWorkflowRunStepMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: string, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<string> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: string, name: string, type: WorkflowActionType, settings: any, valid: boolean, nextStepIds?: Array<string> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
|
||||
export type UpdateWorkflowVersionStepMutationVariables = Exact<{
|
||||
input: UpdateWorkflowVersionStepInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: string, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<string> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: string, name: string, type: WorkflowActionType, settings: any, valid: boolean, nextStepIds?: Array<string> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
|
||||
export type SubmitFormStepMutationVariables = Exact<{
|
||||
input: SubmitFormStepInput;
|
||||
|
||||
@@ -4336,10 +4336,27 @@ export type WorkflowAction = {
|
||||
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
|
||||
position?: Maybe<WorkflowStepPosition>;
|
||||
settings: Scalars['JSON'];
|
||||
type: Scalars['String'];
|
||||
type: WorkflowActionType;
|
||||
valid: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum WorkflowActionType {
|
||||
AI_AGENT = 'AI_AGENT',
|
||||
CODE = 'CODE',
|
||||
CREATE_RECORD = 'CREATE_RECORD',
|
||||
DELAY = 'DELAY',
|
||||
DELETE_RECORD = 'DELETE_RECORD',
|
||||
EMPTY = 'EMPTY',
|
||||
FILTER = 'FILTER',
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
ITERATOR = 'ITERATOR',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
UPSERT_RECORD = 'UPSERT_RECORD'
|
||||
}
|
||||
|
||||
export type WorkflowRun = {
|
||||
__typename?: 'WorkflowRun';
|
||||
workflowRunId: Scalars['UUID'];
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type workflowSendEmailActionSchema,
|
||||
type workflowTriggerSchema,
|
||||
type workflowUpdateRecordActionSchema,
|
||||
type workflowUpsertRecordActionSchema,
|
||||
type workflowWebhookTriggerSchema,
|
||||
type workflowDelayActionSchema,
|
||||
} from 'twenty-shared/workflow';
|
||||
@@ -40,6 +41,9 @@ export type WorkflowUpdateRecordAction = z.infer<
|
||||
export type WorkflowDeleteRecordAction = z.infer<
|
||||
typeof workflowDeleteRecordActionSchema
|
||||
>;
|
||||
export type WorkflowUpsertRecordAction = z.infer<
|
||||
typeof workflowUpsertRecordActionSchema
|
||||
>;
|
||||
export type WorkflowFindRecordsAction = z.infer<
|
||||
typeof workflowFindRecordsActionSchema
|
||||
>;
|
||||
@@ -61,6 +65,7 @@ export type WorkflowAction =
|
||||
| WorkflowCreateRecordAction
|
||||
| WorkflowUpdateRecordAction
|
||||
| WorkflowDeleteRecordAction
|
||||
| WorkflowUpsertRecordAction
|
||||
| WorkflowFindRecordsAction
|
||||
| WorkflowFilterAction
|
||||
| WorkflowFormAction
|
||||
@@ -72,7 +77,6 @@ export type WorkflowAction =
|
||||
|
||||
export type WorkflowActionType = WorkflowAction['type'];
|
||||
export type WorkflowStep = WorkflowAction;
|
||||
export type WorkflowStepType = WorkflowStep['type'];
|
||||
|
||||
export type WorkflowDatabaseEventTrigger = z.infer<
|
||||
typeof workflowDatabaseEventTriggerSchema
|
||||
|
||||
+13
@@ -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
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
+1
@@ -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 };
|
||||
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
|
||||
+5
-3
@@ -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;
|
||||
}
|
||||
|
||||
+38
-223
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
|
||||
+85
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+232
@@ -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>
|
||||
);
|
||||
};
|
||||
+3
-9
@@ -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();
|
||||
|
||||
+10
-1
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export type UpdateRecordFormData = {
|
||||
objectNameSingular: string;
|
||||
objectRecordId?: string;
|
||||
fieldsToUpdate: string[];
|
||||
[field: string]: unknown;
|
||||
};
|
||||
+1
@@ -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':
|
||||
|
||||
+1
@@ -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':
|
||||
|
||||
+1
@@ -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':
|
||||
|
||||
+1
@@ -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';
|
||||
|
||||
+6
-1
@@ -5,7 +5,12 @@ import {
|
||||
import { type RecordActionOutputSchema } from '@/workflow/workflow-variables/types/RecordActionOutputSchema';
|
||||
import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
|
||||
const RECORD_STEP_TYPES = ['CREATE_RECORD', 'UPDATE_RECORD', 'DELETE_RECORD'];
|
||||
const RECORD_STEP_TYPES = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'UPSERT_RECORD',
|
||||
];
|
||||
|
||||
export const isRecordStepOutputSchema = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
|
||||
+1
-2
@@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type UpdateWorkflowVersionPositionsMutation,
|
||||
type UpdateWorkflowVersionPositionsMutationVariables,
|
||||
type WorkflowAction,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
@@ -57,7 +56,7 @@ export const useTidyUpWorkflowVersion = () => {
|
||||
? { ...cachedRecord.trigger, position: triggerPosition.position }
|
||||
: cachedRecord.trigger;
|
||||
|
||||
const updatedSteps = cachedRecord.steps?.map((step: WorkflowAction) => {
|
||||
const updatedSteps = cachedRecord.steps?.map((step) => {
|
||||
const stepPosition = positions.find(
|
||||
(position) => position.id === step.id,
|
||||
);
|
||||
|
||||
+1
@@ -15,5 +15,6 @@ export enum RecordCrudExceptionCode {
|
||||
RECORD_CREATION_FAILED = 'RECORD_CREATION_FAILED',
|
||||
RECORD_UPDATE_FAILED = 'RECORD_UPDATE_FAILED',
|
||||
RECORD_DELETION_FAILED = 'RECORD_DELETION_FAILED',
|
||||
RECORD_UPSERT_FAILED = 'RECORD_UPSERT_FAILED',
|
||||
QUERY_FAILED = 'QUERY_FAILED',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CreateRecordService } from 'src/engine/core-modules/record-crud/service
|
||||
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { UpsertRecordService } from 'src/engine/core-modules/record-crud/services/upsert-record.service';
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -21,12 +22,14 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
UpdateRecordService,
|
||||
DeleteRecordService,
|
||||
FindRecordsService,
|
||||
UpsertRecordService,
|
||||
],
|
||||
exports: [
|
||||
CreateRecordService,
|
||||
UpdateRecordService,
|
||||
DeleteRecordService,
|
||||
FindRecordsService,
|
||||
UpsertRecordService,
|
||||
],
|
||||
})
|
||||
export class RecordCrudModule {}
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { UpsertRecordParams } from 'src/engine/core-modules/record-crud/types/upsert-record-params.type';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class UpsertRecordService {
|
||||
private readonly logger = new Logger(UpsertRecordService.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
) {}
|
||||
|
||||
async execute(params: UpsertRecordParams): Promise<ToolOutput> {
|
||||
const {
|
||||
objectName,
|
||||
objectRecord,
|
||||
fieldsToUpdate,
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
} = params;
|
||||
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to upsert record: Workspace ID is required',
|
||||
error: 'Workspace ID not found',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
!canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
|
||||
})
|
||||
) {
|
||||
throw new RecordCrudException(
|
||||
'Failed to update: Object cannot be updated by workflow',
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce(
|
||||
(acc, key) => {
|
||||
if (fieldsToUpdateArray.includes(key)) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: objectRecord[key],
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const transformedObjectRecord =
|
||||
await this.recordInputTransformerService.process({
|
||||
recordInput: objectRecordWithFilteredFields,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldsMaps,
|
||||
});
|
||||
|
||||
const uniqueFieldsToUpdate = fieldsToUpdateArray
|
||||
.map((field) => objectMetadataItemWithFieldsMaps.fieldIdByName[field])
|
||||
.map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId])
|
||||
.filter((field) => field.isUnique || field.name === 'id');
|
||||
|
||||
const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap(
|
||||
(field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const compositeType = getCompositeTypeOrThrow(field.type);
|
||||
|
||||
const uniqueProperties = compositeType.properties.filter(
|
||||
(prop) => prop.isIncludedInUniqueConstraint,
|
||||
);
|
||||
|
||||
const propertiesToUse =
|
||||
uniqueProperties.length > 0
|
||||
? uniqueProperties
|
||||
: [compositeType.properties[0]];
|
||||
|
||||
return propertiesToUse.map((prop) =>
|
||||
computeCompositeColumnName(field, prop),
|
||||
);
|
||||
}
|
||||
|
||||
return [field.name];
|
||||
},
|
||||
);
|
||||
|
||||
const conflictPaths =
|
||||
conflictPathsUniqueFieldsToUpdate.length > 0
|
||||
? conflictPathsUniqueFieldsToUpdate
|
||||
: ['id'];
|
||||
|
||||
const indexPredicate = uniqueFieldsToUpdate
|
||||
.map((field) =>
|
||||
computeUniqueIndexWhereClause({
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const upsertResult = await repository.upsert(transformedObjectRecord, {
|
||||
conflictPaths: conflictPaths,
|
||||
indexPredicate:
|
||||
indexPredicate.length > 0
|
||||
? `${indexPredicate.join(' AND ')}`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const upsertedRecordId = upsertResult.identifiers?.[0].id;
|
||||
|
||||
if (!isDefined(upsertedRecordId)) {
|
||||
throw new RecordCrudException(
|
||||
`Failed to upsert record in ${objectName}`,
|
||||
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const upsertedRecord = await repository.findOne({
|
||||
where: {
|
||||
id: upsertedRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!upsertedRecord) {
|
||||
throw new RecordCrudException(
|
||||
`Record not found after upsert with id ${upsertedRecordId} in ${objectName}`,
|
||||
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Record upserted successfully in ${objectName}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Record upserted successfully in ${objectName}`,
|
||||
result: upsertedRecord,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof RecordCrudException) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to upsert record in ${objectName}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.error(`Failed to upsert record: ${error}`);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to upsert record in ${objectName}`,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Failed to upsert record',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type UpsertRecordParams = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
fieldsToUpdate?: string[];
|
||||
workspaceId: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
};
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowStepPosition } from 'src/engine/core-modules/workflow/dtos/workflow-step-position.dto';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
registerEnumType(WorkflowActionType, {
|
||||
name: 'WorkflowActionType',
|
||||
});
|
||||
|
||||
@ObjectType('WorkflowAction')
|
||||
export class WorkflowActionDTO {
|
||||
@@ -14,7 +18,7 @@ export class WorkflowActionDTO {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
@Field(() => WorkflowActionType)
|
||||
type: WorkflowActionType;
|
||||
|
||||
@Field(() => graphqlTypeJson)
|
||||
|
||||
+18
-16
@@ -266,7 +266,20 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
),
|
||||
);
|
||||
|
||||
return this.createQueryBuilder(
|
||||
const overwrites = [...conflictColumns, ...overwriteColumns].map(
|
||||
(col) => col.databaseName,
|
||||
);
|
||||
|
||||
const conflictTargets = conflictColumns.map((col) => col.databaseName);
|
||||
|
||||
const upsertOptions = {
|
||||
skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged,
|
||||
indexPredicate: options.indexPredicate,
|
||||
upsertType:
|
||||
options.upsertType || this.connection.driver.supportedUpsertTypes[0],
|
||||
};
|
||||
|
||||
const queryBuilder = this.createQueryBuilder(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -275,21 +288,10 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
.insert()
|
||||
.into(target)
|
||||
.values(entities)
|
||||
.orUpdate(
|
||||
[...conflictColumns, ...overwriteColumns].map(
|
||||
(col) => col.databaseName,
|
||||
),
|
||||
conflictColumns.map((col) => col.databaseName),
|
||||
{
|
||||
skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged,
|
||||
indexPredicate: options.indexPredicate,
|
||||
upsertType:
|
||||
options.upsertType ||
|
||||
this.connection.driver.supportedUpsertTypes[0],
|
||||
},
|
||||
)
|
||||
.returning(selectedColumns)
|
||||
.execute();
|
||||
.orUpdate(overwrites, conflictTargets, upsertOptions)
|
||||
.returning(selectedColumns);
|
||||
|
||||
return queryBuilder.execute();
|
||||
}
|
||||
|
||||
override update<Entity extends ObjectLiteral>(
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
case WorkflowActionType.CREATE_RECORD:
|
||||
case WorkflowActionType.UPDATE_RECORD:
|
||||
case WorkflowActionType.DELETE_RECORD:
|
||||
case WorkflowActionType.UPSERT_RECORD:
|
||||
return this.computeRecordOutputSchema({
|
||||
objectType: step.settings.input.objectName,
|
||||
workspaceId,
|
||||
|
||||
+22
@@ -245,6 +245,28 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
},
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.UPSERT_RECORD: {
|
||||
const activeObjectMetadataItem =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: { workspaceId, isActive: true, isSystem: false },
|
||||
});
|
||||
|
||||
return {
|
||||
builtStep: {
|
||||
...baseStep,
|
||||
name: 'Create or Update Record',
|
||||
type: WorkflowActionType.UPSERT_RECORD,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||
objectRecord: {},
|
||||
fieldsToUpdate: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.FIND_RECORDS: {
|
||||
const activeObjectMetadataItem =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
|
||||
+4
@@ -17,6 +17,7 @@ import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-execut
|
||||
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
|
||||
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
|
||||
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
|
||||
import { UpsertRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action';
|
||||
import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@@ -25,6 +26,7 @@ export class WorkflowActionFactory {
|
||||
constructor(
|
||||
private readonly codeWorkflowAction: CodeWorkflowAction,
|
||||
private readonly createRecordWorkflowAction: CreateRecordWorkflowAction,
|
||||
private readonly upsertRecordWorkflowAction: UpsertRecordWorkflowAction,
|
||||
private readonly updateRecordWorkflowAction: UpdateRecordWorkflowAction,
|
||||
private readonly deleteRecordWorkflowAction: DeleteRecordWorkflowAction,
|
||||
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
|
||||
@@ -45,6 +47,8 @@ export class WorkflowActionFactory {
|
||||
return this.toolExecutorWorkflowAction;
|
||||
case WorkflowActionType.CREATE_RECORD:
|
||||
return this.createRecordWorkflowAction;
|
||||
case WorkflowActionType.UPSERT_RECORD:
|
||||
return this.upsertRecordWorkflowAction;
|
||||
case WorkflowActionType.UPDATE_RECORD:
|
||||
return this.updateRecordWorkflowAction;
|
||||
case WorkflowActionType.DELETE_RECORD:
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
type WorkflowUpsertRecordAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const isWorkflowUpsertRecordAction = (
|
||||
action: WorkflowAction,
|
||||
): action is WorkflowUpsertRecordAction => {
|
||||
return action.type === WorkflowActionType.UPSERT_RECORD;
|
||||
};
|
||||
+5
@@ -4,11 +4,13 @@ import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-cru
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
|
||||
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
|
||||
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
|
||||
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
|
||||
import { UpsertRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action';
|
||||
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
|
||||
|
||||
@Module({
|
||||
@@ -17,17 +19,20 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
|
||||
WorkflowRunModule,
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
providers: [
|
||||
ScopedWorkspaceContextFactory,
|
||||
WorkflowExecutionContextService,
|
||||
CreateRecordWorkflowAction,
|
||||
UpsertRecordWorkflowAction,
|
||||
UpdateRecordWorkflowAction,
|
||||
DeleteRecordWorkflowAction,
|
||||
FindRecordsWorkflowAction,
|
||||
],
|
||||
exports: [
|
||||
CreateRecordWorkflowAction,
|
||||
UpsertRecordWorkflowAction,
|
||||
UpdateRecordWorkflowAction,
|
||||
DeleteRecordWorkflowAction,
|
||||
FindRecordsWorkflowAction,
|
||||
|
||||
+7
@@ -8,6 +8,7 @@ import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud
|
||||
export type WorkflowCreateRecordActionInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
upsert?: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowUpdateRecordActionInput = {
|
||||
@@ -35,3 +36,9 @@ export type WorkflowFindRecordsActionInput = {
|
||||
orderBy?: Partial<ObjectRecordOrderBy>;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type WorkflowUpsertRecordActionInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
fieldsToUpdate?: string[];
|
||||
};
|
||||
|
||||
+5
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
type WorkflowUpsertRecordActionInput,
|
||||
type WorkflowCreateRecordActionInput,
|
||||
type WorkflowDeleteRecordActionInput,
|
||||
type WorkflowFindRecordsActionInput,
|
||||
@@ -21,3 +22,7 @@ export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
|
||||
export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
|
||||
input: WorkflowFindRecordsActionInput;
|
||||
};
|
||||
|
||||
export type WorkflowUpsertRecordActionSettings = BaseWorkflowActionSettings & {
|
||||
input: WorkflowUpsertRecordActionInput;
|
||||
};
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined, resolveInput } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { UpsertRecordService } from 'src/engine/core-modules/record-crud/services/upsert-record.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import { isWorkflowUpsertRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-upsert-record-action.guard';
|
||||
import { WorkflowUpsertRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpsertRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly upsertRecordService: UpsertRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
steps,
|
||||
stepId: currentStepId,
|
||||
});
|
||||
|
||||
if (!isWorkflowUpsertRecordAction(step)) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Step is not an upsert record action',
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowActionInput = resolveInput(
|
||||
step.settings.input,
|
||||
context,
|
||||
) as WorkflowUpsertRecordActionInput;
|
||||
|
||||
if (!isDefined(workflowActionInput.objectName)) {
|
||||
throw new RecordCrudException(
|
||||
'Failed to upsert: Object name is required',
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCrudException(
|
||||
'Failed to update: Workspace ID is required',
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const toolOutput = await this.upsertRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
throw new RecordCrudException(
|
||||
toolOutput.error || toolOutput.message,
|
||||
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
result: toolOutput.result,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
@@ -12,6 +12,7 @@ import {
|
||||
type WorkflowDeleteRecordActionSettings,
|
||||
type WorkflowFindRecordsActionSettings,
|
||||
type WorkflowUpdateRecordActionSettings,
|
||||
type WorkflowUpsertRecordActionSettings,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-settings.type';
|
||||
|
||||
export type BaseWorkflowActionSettings = {
|
||||
@@ -32,6 +33,7 @@ export type WorkflowActionSettings =
|
||||
| WorkflowCreateRecordActionSettings
|
||||
| WorkflowUpdateRecordActionSettings
|
||||
| WorkflowDeleteRecordActionSettings
|
||||
| WorkflowUpsertRecordActionSettings
|
||||
| WorkflowFindRecordsActionSettings
|
||||
| WorkflowFormActionSettings
|
||||
| WorkflowFilterActionSettings
|
||||
|
||||
+8
@@ -6,6 +6,7 @@ import { type WorkflowHttpRequestActionSettings } from 'src/modules/workflow/wor
|
||||
import { type WorkflowIteratorActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import { type WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
|
||||
import {
|
||||
type WorkflowUpsertRecordActionSettings,
|
||||
type WorkflowCreateRecordActionSettings,
|
||||
type WorkflowDeleteRecordActionSettings,
|
||||
type WorkflowFindRecordsActionSettings,
|
||||
@@ -20,6 +21,7 @@ export enum WorkflowActionType {
|
||||
CREATE_RECORD = 'CREATE_RECORD',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
DELETE_RECORD = 'DELETE_RECORD',
|
||||
UPSERT_RECORD = 'UPSERT_RECORD',
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
FILTER = 'FILTER',
|
||||
@@ -68,6 +70,11 @@ export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
|
||||
settings: WorkflowDeleteRecordActionSettings;
|
||||
};
|
||||
|
||||
export type WorkflowUpsertRecordAction = BaseWorkflowAction & {
|
||||
type: WorkflowActionType.UPSERT_RECORD;
|
||||
settings: WorkflowUpsertRecordActionSettings;
|
||||
};
|
||||
|
||||
export type WorkflowFindRecordsAction = BaseWorkflowAction & {
|
||||
type: WorkflowActionType.FIND_RECORDS;
|
||||
settings: WorkflowFindRecordsActionSettings;
|
||||
@@ -113,6 +120,7 @@ export type WorkflowAction =
|
||||
| WorkflowCreateRecordAction
|
||||
| WorkflowUpdateRecordAction
|
||||
| WorkflowDeleteRecordAction
|
||||
| WorkflowUpsertRecordAction
|
||||
| WorkflowFindRecordsAction
|
||||
| WorkflowFormAction
|
||||
| WorkflowFilterAction
|
||||
|
||||
@@ -41,6 +41,8 @@ export { workflowSendEmailActionSchema } from './schemas/send-email-action-schem
|
||||
export { workflowSendEmailActionSettingsSchema } from './schemas/send-email-action-settings-schema';
|
||||
export { workflowUpdateRecordActionSchema } from './schemas/update-record-action-schema';
|
||||
export { workflowUpdateRecordActionSettingsSchema } from './schemas/update-record-action-settings-schema';
|
||||
export { workflowUpsertRecordActionSchema } from './schemas/upsert-record-action-schema';
|
||||
export { workflowUpsertRecordActionSettingsSchema } from './schemas/upsert-record-action-settings-schema';
|
||||
export { workflowWebhookTriggerSchema } from './schemas/webhook-trigger-schema';
|
||||
export { workflowActionSchema } from './schemas/workflow-action-schema';
|
||||
export { workflowDelayActionSchema } from './schemas/workflow-delay-action-schema';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { baseWorkflowActionSchema } from './base-workflow-action-schema';
|
||||
import { workflowUpsertRecordActionSettingsSchema } from './upsert-record-action-settings-schema';
|
||||
|
||||
export const workflowUpsertRecordActionSchema = baseWorkflowActionSchema.extend(
|
||||
{
|
||||
type: z.literal('UPSERT_RECORD'),
|
||||
settings: workflowUpsertRecordActionSettingsSchema,
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema';
|
||||
import { objectRecordSchema } from './object-record-schema';
|
||||
|
||||
export const workflowUpsertRecordActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
objectRecord: objectRecordSchema,
|
||||
fieldsToUpdate: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { workflowHttpRequestActionSchema } from './http-request-action-schema';
|
||||
import { workflowIteratorActionSchema } from './iterator-action-schema';
|
||||
import { workflowSendEmailActionSchema } from './send-email-action-schema';
|
||||
import { workflowUpdateRecordActionSchema } from './update-record-action-schema';
|
||||
import { workflowUpsertRecordActionSchema } from './upsert-record-action-schema';
|
||||
import { workflowDelayActionSchema } from './workflow-delay-action-schema';
|
||||
|
||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
@@ -19,6 +20,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowCreateRecordActionSchema,
|
||||
workflowUpdateRecordActionSchema,
|
||||
workflowDeleteRecordActionSchema,
|
||||
workflowUpsertRecordActionSchema,
|
||||
workflowFindRecordsActionSchema,
|
||||
workflowFormActionSchema,
|
||||
workflowHttpRequestActionSchema,
|
||||
|
||||
Reference in New Issue
Block a user