Allow to insert into loop step ids (#14425)
- Create empty node on iterator creation - Add options on step creation to insert in loop - Handle properly when a step is removed from loop - Remove loopNextStepIds from frontend action - Add a frontend skeleton for empty action https://github.com/user-attachments/assets/281a8c15-8062-4702-afb4-0d9a50902252
This commit is contained in:
@@ -849,6 +849,8 @@ export type CreateWorkflowVersionStepInput = {
|
||||
nextStepId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Step creation options */
|
||||
parentStepOptions?: InputMaybe<Scalars['JSON']>;
|
||||
/** Step position */
|
||||
position?: InputMaybe<WorkflowStepPositionInput>;
|
||||
/** New step type */
|
||||
|
||||
@@ -813,6 +813,8 @@ export type CreateWorkflowVersionStepInput = {
|
||||
nextStepId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Step creation options */
|
||||
parentStepOptions?: InputMaybe<Scalars['JSON']>;
|
||||
/** Step position */
|
||||
position?: InputMaybe<WorkflowStepPositionInput>;
|
||||
/** New step type */
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type workflowCronTriggerSchema,
|
||||
type workflowDatabaseEventTriggerSchema,
|
||||
type workflowDeleteRecordActionSchema,
|
||||
type workflowEmptyActionSchema,
|
||||
type workflowFilterActionSchema,
|
||||
type workflowFindRecordsActionSchema,
|
||||
type workflowFormActionSchema,
|
||||
@@ -47,6 +48,7 @@ export type WorkflowIteratorAction = z.infer<
|
||||
typeof workflowIteratorActionSchema
|
||||
>;
|
||||
export type WorkflowAiAgentAction = z.infer<typeof workflowAiAgentActionSchema>;
|
||||
export type WorkflowEmptyAction = z.infer<typeof workflowEmptyActionSchema>;
|
||||
|
||||
export type WorkflowAction =
|
||||
| WorkflowCodeAction
|
||||
@@ -59,7 +61,8 @@ export type WorkflowAction =
|
||||
| WorkflowFormAction
|
||||
| WorkflowHttpRequestAction
|
||||
| WorkflowAiAgentAction
|
||||
| WorkflowIteratorAction;
|
||||
| WorkflowIteratorAction
|
||||
| WorkflowEmptyAction;
|
||||
|
||||
export type WorkflowActionType = WorkflowAction['type'];
|
||||
export type WorkflowStep = WorkflowAction;
|
||||
|
||||
+12
@@ -8,6 +8,7 @@ import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-ac
|
||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
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 { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
@@ -226,6 +227,17 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'EMPTY': {
|
||||
return (
|
||||
<WorkflowEditActionEmpty
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={{
|
||||
readonly: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -7,6 +7,7 @@ import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-ac
|
||||
import { WorkflowActionServerlessFunction } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionServerlessFunction';
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
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 { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
@@ -206,6 +207,15 @@ export const WorkflowStepDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'EMPTY': {
|
||||
return (
|
||||
<WorkflowEditActionEmpty
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(
|
||||
stepDefinition.definition,
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type WorkflowEmptyAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionEmptyProps = {
|
||||
action: WorkflowEmptyAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowEmptyAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditActionEmpty = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionEmptyProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'Empty Node',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>{'Empty Node'}</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-14
@@ -101,20 +101,6 @@ export const WorkflowEditActionIterator = ({
|
||||
readonly={actionOptions.readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
{/* TODO: remove once the UI is implemented */}
|
||||
<FormTextFieldInput
|
||||
label={t`Initial loop step IDs`}
|
||||
placeholder={t`Enter array of step IDs`}
|
||||
defaultValue={
|
||||
Array.isArray(formData.initialLoopStepIds)
|
||||
? JSON.stringify(formData.initialLoopStepIds)
|
||||
: (formData.initialLoopStepIds as string) || ''
|
||||
}
|
||||
onChange={(value: string) =>
|
||||
handleFieldChange('initialLoopStepIds', value)
|
||||
}
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
|
||||
+3
@@ -23,6 +23,9 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
||||
case 'ITERATOR': {
|
||||
return msg`Iterator`;
|
||||
}
|
||||
case 'EMPTY': {
|
||||
return msg`Empty Node`;
|
||||
}
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ export const getActionIconColorOrThrow = ({
|
||||
return theme.color.orange;
|
||||
case 'ITERATOR':
|
||||
case 'FILTER':
|
||||
case 'EMPTY':
|
||||
return theme.font.color.tertiary;
|
||||
case 'AI_AGENT':
|
||||
return theme.color.pink;
|
||||
|
||||
+10
-1
@@ -1,8 +1,11 @@
|
||||
import { Field, InputType } 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 { WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
import { WorkflowStepCreationOptions } from 'src/modules/workflow/workflow-builder/workflow-version-step/types/WorkflowStepCreationOptions';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@InputType()
|
||||
export class CreateWorkflowVersionStepInput {
|
||||
@@ -25,6 +28,12 @@ export class CreateWorkflowVersionStepInput {
|
||||
})
|
||||
parentStepId?: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, {
|
||||
description: 'Step creation options',
|
||||
nullable: true,
|
||||
})
|
||||
parentStepOptions?: WorkflowStepCreationOptions;
|
||||
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'Next step ID',
|
||||
nullable: true,
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
type WorkflowIteratorStepCreationOptions = {
|
||||
parentStepType: WorkflowActionType.ITERATOR;
|
||||
settings: {
|
||||
shouldInsertToLoop: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowStepCreationOptions = WorkflowIteratorStepCreationOptions;
|
||||
+70
@@ -3,6 +3,7 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/insert-step';
|
||||
import {
|
||||
type WorkflowAction,
|
||||
type WorkflowIteratorAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
@@ -10,6 +11,24 @@ import {
|
||||
WorkflowTriggerType,
|
||||
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
const mockIteratorStep: WorkflowIteratorAction = {
|
||||
id: '1',
|
||||
name: 'Iterator 1',
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['existing-loop-step'],
|
||||
items: [],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
};
|
||||
|
||||
describe('insertStep', () => {
|
||||
const createMockAction = (
|
||||
id: string,
|
||||
@@ -158,4 +177,55 @@ describe('insertStep', () => {
|
||||
nextStepIds: ['1', 'new'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add step to iterator initialLoopStepIds when shouldInsertToLoop is true', () => {
|
||||
const existingTrigger = createMockTrigger(['1']);
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingTrigger,
|
||||
existingSteps: [mockIteratorStep],
|
||||
insertedStep: newStep,
|
||||
parentStepId: '1',
|
||||
parentStepOptions: {
|
||||
parentStepType: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
shouldInsertToLoop: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const updatedIteratorStep = result
|
||||
.updatedSteps[0] as WorkflowIteratorAction;
|
||||
|
||||
expect(updatedIteratorStep.settings.input.initialLoopStepIds).toEqual([
|
||||
'existing-loop-step',
|
||||
'new',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not add step to iterator initialLoopStepIds when shouldInsertToLoop is false', () => {
|
||||
const existingTrigger = createMockTrigger(['1']);
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingTrigger,
|
||||
existingSteps: [mockIteratorStep],
|
||||
insertedStep: newStep,
|
||||
parentStepId: '1',
|
||||
parentStepOptions: {
|
||||
parentStepType: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
shouldInsertToLoop: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const updatedIteratorStep = result
|
||||
.updatedSteps[0] as WorkflowIteratorAction;
|
||||
|
||||
expect(updatedIteratorStep.settings.input.initialLoopStepIds).toEqual([
|
||||
'existing-loop-step',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+50
@@ -255,4 +255,54 @@ describe('removeStep', () => {
|
||||
expect(result.updatedTrigger).toEqual(null);
|
||||
expect(result.updatedSteps).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle removing a step that is part of iteratorLoopStepIds', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const iteratorStep = {
|
||||
id: '2',
|
||||
name: 'Iterator Step',
|
||||
type: WorkflowActionType.ITERATOR,
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['3'],
|
||||
iterableValue: { value: [] },
|
||||
iteratorKey: 'item',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds: ['4'],
|
||||
} as WorkflowAction;
|
||||
const step3 = createMockAction('3', ['5']);
|
||||
const step4 = createMockAction('4');
|
||||
const step5 = createMockAction('5');
|
||||
|
||||
const result = removeStep({
|
||||
existingTrigger: mockTrigger,
|
||||
existingSteps: [step1, iteratorStep, step3, step4, step5],
|
||||
stepIdToDelete: '3',
|
||||
stepToDeleteChildrenIds: ['5'],
|
||||
});
|
||||
|
||||
expect(result.updatedSteps).toEqual([
|
||||
step1,
|
||||
{
|
||||
...iteratorStep,
|
||||
settings: {
|
||||
...iteratorStep.settings,
|
||||
input: {
|
||||
...iteratorStep.settings.input,
|
||||
initialLoopStepIds: ['5'],
|
||||
},
|
||||
},
|
||||
},
|
||||
step4,
|
||||
step5,
|
||||
]);
|
||||
expect(result.updatedTrigger).toEqual(mockTrigger);
|
||||
});
|
||||
});
|
||||
|
||||
+186
-41
@@ -1,62 +1,50 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
WorkflowVersionStepException,
|
||||
WorkflowVersionStepExceptionCode,
|
||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
||||
import { type WorkflowStepCreationOptions } from 'src/modules/workflow/workflow-builder/workflow-version-step/types/WorkflowStepCreationOptions';
|
||||
import { type WorkflowIteratorActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import {
|
||||
WorkflowActionType,
|
||||
type WorkflowAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
export const insertStep = ({
|
||||
existingSteps,
|
||||
existingTrigger,
|
||||
insertedStep,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
parentStepId,
|
||||
parentStepOptions,
|
||||
}: {
|
||||
existingSteps: WorkflowAction[];
|
||||
existingTrigger: WorkflowTrigger | null;
|
||||
insertedStep: WorkflowAction;
|
||||
parentStepId?: string;
|
||||
nextStepId?: string;
|
||||
parentStepId?: string;
|
||||
parentStepOptions?: WorkflowStepCreationOptions;
|
||||
}): {
|
||||
updatedSteps: WorkflowAction[];
|
||||
updatedInsertedStep: WorkflowAction;
|
||||
updatedTrigger: WorkflowTrigger | null;
|
||||
} => {
|
||||
let updatedTrigger = existingTrigger;
|
||||
|
||||
let updatedExistingSteps = existingSteps;
|
||||
|
||||
if (parentStepId === TRIGGER_STEP_ID) {
|
||||
if (!existingTrigger) {
|
||||
throw new Error('Cannot insert step from undefined trigger');
|
||||
}
|
||||
|
||||
updatedTrigger = {
|
||||
...existingTrigger,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(existingTrigger.nextStepIds?.filter((id) => id !== nextStepId) ||
|
||||
[]),
|
||||
insertedStep.id,
|
||||
]),
|
||||
],
|
||||
};
|
||||
} else {
|
||||
updatedExistingSteps = existingSteps.map((existingStep) => {
|
||||
if (existingStep.id === parentStepId) {
|
||||
return {
|
||||
...existingStep,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(existingStep.nextStepIds?.filter((id) => id !== nextStepId) ||
|
||||
[]),
|
||||
insertedStep.id,
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return existingStep;
|
||||
});
|
||||
}
|
||||
let { updatedSteps, updatedTrigger } = isDefined(parentStepId)
|
||||
? updateParentStep({
|
||||
trigger: existingTrigger,
|
||||
steps: existingSteps,
|
||||
parentStepId,
|
||||
insertedStepId: insertedStep.id,
|
||||
nextStepId,
|
||||
parentStepOptions,
|
||||
})
|
||||
: {
|
||||
updatedSteps: existingSteps,
|
||||
updatedTrigger: existingTrigger,
|
||||
};
|
||||
|
||||
const updatedInsertedStep = {
|
||||
...insertedStep,
|
||||
@@ -64,8 +52,165 @@ export const insertStep = ({
|
||||
};
|
||||
|
||||
return {
|
||||
updatedSteps: [...updatedExistingSteps, updatedInsertedStep],
|
||||
updatedSteps: [...updatedSteps, updatedInsertedStep],
|
||||
updatedTrigger,
|
||||
updatedInsertedStep,
|
||||
};
|
||||
};
|
||||
|
||||
const updateParentStep = ({
|
||||
steps,
|
||||
trigger,
|
||||
parentStepId,
|
||||
insertedStepId,
|
||||
nextStepId,
|
||||
parentStepOptions,
|
||||
}: {
|
||||
steps: WorkflowAction[];
|
||||
trigger: WorkflowTrigger | null;
|
||||
parentStepId: string;
|
||||
insertedStepId: string;
|
||||
nextStepId?: string;
|
||||
parentStepOptions?: WorkflowStepCreationOptions;
|
||||
}): {
|
||||
updatedSteps: WorkflowAction[];
|
||||
updatedTrigger: WorkflowTrigger | null;
|
||||
} => {
|
||||
if (isDefined(parentStepOptions)) {
|
||||
return updateStepsWithOptions({
|
||||
steps,
|
||||
parentStepId,
|
||||
insertedStepId,
|
||||
parentStepOptions,
|
||||
trigger,
|
||||
});
|
||||
} else {
|
||||
return updateParentStepNextStepIds({
|
||||
steps,
|
||||
trigger,
|
||||
parentStepId,
|
||||
insertedStepId,
|
||||
nextStepId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateParentStepNextStepIds = ({
|
||||
steps,
|
||||
trigger,
|
||||
parentStepId,
|
||||
insertedStepId,
|
||||
nextStepId,
|
||||
}: {
|
||||
steps: WorkflowAction[];
|
||||
trigger: WorkflowTrigger | null;
|
||||
parentStepId: string;
|
||||
insertedStepId: string;
|
||||
nextStepId?: string;
|
||||
}): {
|
||||
updatedSteps: WorkflowAction[];
|
||||
updatedTrigger: WorkflowTrigger | null;
|
||||
} => {
|
||||
let updatedTrigger = trigger;
|
||||
|
||||
let updatedSteps = steps;
|
||||
|
||||
if (parentStepId === TRIGGER_STEP_ID) {
|
||||
if (!trigger) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Cannot insert step from undefined trigger',
|
||||
WorkflowVersionStepExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
updatedTrigger = {
|
||||
...trigger,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(trigger.nextStepIds?.filter((id) => id !== nextStepId) || []),
|
||||
insertedStepId,
|
||||
]),
|
||||
],
|
||||
};
|
||||
} else {
|
||||
updatedSteps = steps.map((step) => {
|
||||
if (step.id === parentStepId) {
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(step.nextStepIds?.filter((id) => id !== nextStepId) || []),
|
||||
insertedStepId,
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return step;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
updatedSteps,
|
||||
updatedTrigger,
|
||||
};
|
||||
};
|
||||
|
||||
const updateStepsWithOptions = ({
|
||||
parentStepId,
|
||||
insertedStepId,
|
||||
steps,
|
||||
parentStepOptions,
|
||||
trigger,
|
||||
}: {
|
||||
parentStepId: string;
|
||||
insertedStepId: string;
|
||||
steps: WorkflowAction[];
|
||||
parentStepOptions: WorkflowStepCreationOptions;
|
||||
trigger: WorkflowTrigger | null;
|
||||
}) => {
|
||||
let updatedSteps = steps;
|
||||
|
||||
switch (parentStepOptions.parentStepType) {
|
||||
case WorkflowActionType.ITERATOR:
|
||||
if (!parentStepOptions.settings.shouldInsertToLoop) {
|
||||
break;
|
||||
}
|
||||
|
||||
updatedSteps = steps.map((step) => {
|
||||
if (step.id === parentStepId) {
|
||||
if (step.type !== WorkflowActionType.ITERATOR) {
|
||||
throw new WorkflowVersionStepException(
|
||||
`Step ${step.id} is not an iterator`,
|
||||
WorkflowVersionStepExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...step,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: {
|
||||
...step.settings.input,
|
||||
initialLoopStepIds: [
|
||||
...(step.settings.input.initialLoopStepIds || []),
|
||||
insertedStepId,
|
||||
],
|
||||
},
|
||||
} satisfies WorkflowIteratorActionSettings,
|
||||
};
|
||||
}
|
||||
|
||||
return step;
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedSteps,
|
||||
updatedTrigger: trigger,
|
||||
};
|
||||
};
|
||||
|
||||
+21
@@ -59,6 +59,27 @@ const removeOneStep = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
step.type === WorkflowActionType.ITERATOR &&
|
||||
isDefined(step.settings.input.initialLoopStepIds) &&
|
||||
step.settings.input.initialLoopStepIds.includes(stepIdToDelete)
|
||||
) {
|
||||
return {
|
||||
...step,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: {
|
||||
...step.settings.input,
|
||||
initialLoopStepIds: computeUpdatedNextStepIds({
|
||||
existingNextStepIds: step.settings.input.initialLoopStepIds,
|
||||
stepIdToRemove: stepIdToDelete,
|
||||
stepToDeleteChildrenIds: stepToDeleteChildrenIds,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return step;
|
||||
}) ?? [];
|
||||
|
||||
|
||||
+72
-5
@@ -31,6 +31,7 @@ import { type BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-e
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
WorkflowEmptyAction,
|
||||
type WorkflowFormAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
@@ -72,13 +73,20 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
workspaceId: string;
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
}): Promise<WorkflowVersionStepChangesDTO> {
|
||||
const { workflowVersionId, stepType, parentStepId, nextStepId, position } =
|
||||
input;
|
||||
const {
|
||||
workflowVersionId,
|
||||
stepType,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
position,
|
||||
parentStepOptions,
|
||||
} = input;
|
||||
|
||||
const newStep = await this.getStepDefaultDefinition({
|
||||
const newStep = await this.runStepCreationSideEffectsAndBuildStep({
|
||||
type: stepType,
|
||||
workspaceId,
|
||||
position,
|
||||
workflowVersionId,
|
||||
});
|
||||
|
||||
const enrichedNewStep = await this.enrichOutputSchema({
|
||||
@@ -118,6 +126,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
insertedStep: enrichedNewStep,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
parentStepOptions,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
@@ -513,14 +522,16 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getStepDefaultDefinition({
|
||||
private async runStepCreationSideEffectsAndBuildStep({
|
||||
type,
|
||||
workspaceId,
|
||||
position,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
type: WorkflowActionType;
|
||||
workspaceId: string;
|
||||
position?: WorkflowStepPositionInput;
|
||||
workflowVersionId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
const newStepId = v4();
|
||||
|
||||
@@ -722,6 +733,12 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.ITERATOR: {
|
||||
const emptyNodeStep = await this.createEmptyNodeForIteratorStep({
|
||||
iteratorStepId: baseStep.id,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...baseStep,
|
||||
name: 'Iterator',
|
||||
@@ -730,7 +747,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
items: [],
|
||||
initialLoopStepIds: [],
|
||||
initialLoopStepIds: [emptyNodeStep.id],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -861,4 +878,54 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createEmptyNodeForIteratorStep({
|
||||
iteratorStepId,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
}: {
|
||||
iteratorStepId: string;
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const existingSteps = workflowVersion.steps ?? [];
|
||||
|
||||
const emptyNodeStep: WorkflowEmptyAction = {
|
||||
id: v4(),
|
||||
name: 'Empty Node',
|
||||
type: WorkflowActionType.EMPTY,
|
||||
valid: true,
|
||||
nextStepIds: [iteratorStepId],
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {},
|
||||
},
|
||||
};
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: [...existingSteps, emptyNodeStep],
|
||||
});
|
||||
|
||||
return emptyNodeStep;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -50,11 +50,6 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
|
||||
const { items, initialLoopStepIds } = iteratorInput;
|
||||
|
||||
// TODO: remove once the UI is implemented
|
||||
const parsedInitialLoopStepIds = isString(initialLoopStepIds)
|
||||
? JSON.parse(initialLoopStepIds)
|
||||
: initialLoopStepIds;
|
||||
|
||||
const parsedItems = isString(items) ? JSON.parse(items) : items;
|
||||
|
||||
if (!Array.isArray(parsedItems)) {
|
||||
@@ -64,7 +59,11 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedInitialLoopStepIds.length === 0 || parsedItems.length === 0) {
|
||||
if (
|
||||
!isDefined(initialLoopStepIds) ||
|
||||
initialLoopStepIds.length === 0 ||
|
||||
parsedItems.length === 0
|
||||
) {
|
||||
return {
|
||||
result: {
|
||||
currentItemIndex: 0,
|
||||
@@ -109,7 +108,7 @@ export class IteratorWorkflowAction implements WorkflowActionInterface {
|
||||
if (!hasProcessedAllItems) {
|
||||
await this.resetStepsInLoop({
|
||||
iteratorStepId,
|
||||
initialLoopStepIds: parsedInitialLoopStepIds,
|
||||
initialLoopStepIds,
|
||||
workflowRunId: runInfo.workflowRunId,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
steps,
|
||||
|
||||
+1
-2
@@ -3,8 +3,7 @@ import { type BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-e
|
||||
export type WorkflowIteratorActionInput = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?: Array<any> | string;
|
||||
// testing purpose, should never be a string nor undefined
|
||||
initialLoopStepIds?: string[] | string;
|
||||
initialLoopStepIds?: string[];
|
||||
};
|
||||
|
||||
export type WorkflowIteratorActionSettings = BaseWorkflowActionSettings & {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ const { getAllStepIdsInLoop } = jest.requireMock(
|
||||
describe('canExecuteIteratorStep', () => {
|
||||
const createMockIteratorStep = (
|
||||
id: string,
|
||||
initialLoopStepIds: string[] | string = [],
|
||||
initialLoopStepIds: string[] = [],
|
||||
): WorkflowIteratorAction => ({
|
||||
id,
|
||||
name: 'Iterator Step',
|
||||
|
||||
+8
-13
@@ -1,4 +1,3 @@
|
||||
import { isString } from 'class-validator';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
|
||||
@@ -29,19 +28,15 @@ export const canExecuteIteratorStep = ({
|
||||
(step) => stepInfos[step.id]?.status === StepStatus.SUCCESS,
|
||||
);
|
||||
} else {
|
||||
// On the first iteration, the loop steps will not have been started yet.
|
||||
// TODO: remove parsing once the UI is implemented
|
||||
const parsedInitialLoopStepIds = isString(
|
||||
step.settings.input.initialLoopStepIds,
|
||||
)
|
||||
? JSON.parse(step.settings.input.initialLoopStepIds)
|
||||
: step.settings.input.initialLoopStepIds;
|
||||
const initialLoopStepIds = step.settings.input.initialLoopStepIds;
|
||||
|
||||
const stepIdsInLoop = getAllStepIdsInLoop({
|
||||
iteratorStepId: step.id,
|
||||
initialLoopStepIds: parsedInitialLoopStepIds,
|
||||
steps,
|
||||
});
|
||||
const stepIdsInLoop = isDefined(initialLoopStepIds)
|
||||
? getAllStepIdsInLoop({
|
||||
iteratorStepId: step.id,
|
||||
initialLoopStepIds,
|
||||
steps,
|
||||
})
|
||||
: [];
|
||||
|
||||
const parentSteps = stepsTargetingIterator.filter(
|
||||
(step) => !stepIdsInLoop.includes(step.id),
|
||||
|
||||
+1
-5
@@ -33,11 +33,7 @@ const traverseSteps = ({
|
||||
const nestedIteratorInput = step.settings
|
||||
.input as WorkflowIteratorActionInput;
|
||||
|
||||
if (
|
||||
nestedIteratorInput.initialLoopStepIds &&
|
||||
// TODO: To remove once we remove the string input for initialLoopStepIds
|
||||
Array.isArray(nestedIteratorInput.initialLoopStepIds)
|
||||
) {
|
||||
if (nestedIteratorInput.initialLoopStepIds) {
|
||||
const nestedLoopStepIds = getAllStepIdsInLoop({
|
||||
iteratorStepId: stepId,
|
||||
initialLoopStepIds: nestedIteratorInput.initialLoopStepIds,
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
workflowAiAgentActionSettingsSchema,
|
||||
workflowFilterActionSettingsSchema,
|
||||
workflowIteratorActionSettingsSchema,
|
||||
workflowEmptyActionSettingsSchema,
|
||||
workflowCodeActionSchema,
|
||||
workflowSendEmailActionSchema,
|
||||
workflowCreateRecordActionSchema,
|
||||
@@ -36,6 +37,7 @@ export {
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
workflowEmptyActionSchema,
|
||||
workflowActionSchema,
|
||||
workflowDatabaseEventTriggerSchema,
|
||||
workflowManualTriggerSchema,
|
||||
|
||||
@@ -223,11 +223,15 @@ export const workflowIteratorActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
items: z.union([z.array(z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.any()), z.any()])), z.string()]).optional(),
|
||||
// TODO: should never be a string once fix the UI
|
||||
initialLoopStepIds: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
initialLoopStepIds: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const workflowEmptyActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({}),
|
||||
});
|
||||
|
||||
// Action schemas
|
||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('CODE'),
|
||||
@@ -284,6 +288,11 @@ export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
|
||||
settings: workflowIteratorActionSettingsSchema,
|
||||
});
|
||||
|
||||
export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('EMPTY'),
|
||||
settings: workflowEmptyActionSettingsSchema,
|
||||
});
|
||||
|
||||
// Combined action schema
|
||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowCodeActionSchema,
|
||||
@@ -297,6 +306,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
workflowEmptyActionSchema,
|
||||
]);
|
||||
|
||||
// Trigger schemas
|
||||
|
||||
Reference in New Issue
Block a user