Update next step ids on step update (#11605)
When inserting a new step between step 1 et step 2, then step 1 should have the new step as next step id, add stop having step 2. When deleting a step, we link the parent and next steps together. It may change in the future
This commit is contained in:
@ -411,6 +411,10 @@ export type CreateServerlessFunctionInput = {
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionStepInput = {
|
||||
/** Next step ID */
|
||||
nextStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** New step type */
|
||||
stepType: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const generateWorkflowDiagram = ({
|
||||
trigger,
|
||||
|
||||
@ -38,6 +38,8 @@ export const useCreateStep = ({
|
||||
await createWorkflowVersionStep({
|
||||
workflowVersionId,
|
||||
stepType: newStepType,
|
||||
parentStepId: workflowCreateStepFromParentStepId,
|
||||
nextStepId: undefined,
|
||||
})
|
||||
)?.data?.createWorkflowVersionStep;
|
||||
|
||||
|
||||
@ -15,4 +15,16 @@ export class CreateWorkflowVersionStepInput {
|
||||
nullable: false,
|
||||
})
|
||||
stepType: WorkflowActionType;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Parent step ID',
|
||||
nullable: true,
|
||||
})
|
||||
parentStepId?: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Next step ID',
|
||||
nullable: true,
|
||||
})
|
||||
nextStepId?: string;
|
||||
}
|
||||
|
||||
@ -26,12 +26,11 @@ export class WorkflowStepResolver {
|
||||
async createWorkflowVersionStep(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input')
|
||||
{ stepType, workflowVersionId }: CreateWorkflowVersionStepInput,
|
||||
input: CreateWorkflowVersionStepInput,
|
||||
): Promise<WorkflowActionDTO> {
|
||||
return this.workflowVersionStepWorkspaceService.createWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepType,
|
||||
input,
|
||||
});
|
||||
}
|
||||
|
||||
@ -57,7 +56,7 @@ export class WorkflowStepResolver {
|
||||
return this.workflowVersionStepWorkspaceService.deleteWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
stepIdToDelete: stepId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/insert-step';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
describe('insertStep', () => {
|
||||
const createMockAction = (
|
||||
id: string,
|
||||
nextStepIds?: string[],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: `Action ${id}`,
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'test',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
});
|
||||
|
||||
it('should insert a step at the end of the array when no parent or next step is specified', () => {
|
||||
const step1 = createMockAction('1');
|
||||
const step2 = createMockAction('2');
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingSteps: [step1, step2],
|
||||
insertedStep: newStep,
|
||||
});
|
||||
|
||||
expect(result).toEqual([step1, step2, newStep]);
|
||||
});
|
||||
|
||||
it('should update parent step nextStepIds when inserting a step between two steps', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const step2 = createMockAction('2');
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingSteps: [step1, step2],
|
||||
insertedStep: newStep,
|
||||
parentStepId: '1',
|
||||
nextStepId: '2',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ ...step1, nextStepIds: ['new'] },
|
||||
step2,
|
||||
{ ...newStep, nextStepIds: ['2'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle inserting a step at the beginning of the workflow', () => {
|
||||
const step1 = createMockAction('1');
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingSteps: [step1],
|
||||
insertedStep: newStep,
|
||||
parentStepId: undefined,
|
||||
nextStepId: '1',
|
||||
});
|
||||
|
||||
expect(result).toEqual([step1, { ...newStep, nextStepIds: ['1'] }]);
|
||||
});
|
||||
|
||||
it('should handle inserting a step at the end of the workflow', () => {
|
||||
const step1 = createMockAction('1');
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingSteps: [step1],
|
||||
insertedStep: newStep,
|
||||
parentStepId: '1',
|
||||
nextStepId: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ ...step1, nextStepIds: ['new'] }, newStep]);
|
||||
});
|
||||
|
||||
it('should handle inserting a step between two steps with multiple nextStepIds', () => {
|
||||
const step1 = createMockAction('1', ['2', '3']);
|
||||
const step2 = createMockAction('2');
|
||||
const step3 = createMockAction('3');
|
||||
const newStep = createMockAction('new');
|
||||
|
||||
const result = insertStep({
|
||||
existingSteps: [step1, step2, step3],
|
||||
insertedStep: newStep,
|
||||
parentStepId: '1',
|
||||
nextStepId: '2',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ ...step1, nextStepIds: ['3', 'new'] },
|
||||
step2,
|
||||
step3,
|
||||
{ ...newStep, nextStepIds: ['2'] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,108 @@
|
||||
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
describe('removeStep', () => {
|
||||
const createMockAction = (
|
||||
id: string,
|
||||
nextStepIds?: string[],
|
||||
): WorkflowAction => ({
|
||||
id,
|
||||
name: `Action ${id}`,
|
||||
type: WorkflowActionType.CODE,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'test',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
nextStepIds,
|
||||
});
|
||||
|
||||
it('should remove the specified step from the array', () => {
|
||||
const step1 = createMockAction('1');
|
||||
const step2 = createMockAction('2');
|
||||
const step3 = createMockAction('3');
|
||||
|
||||
const result = removeStep({
|
||||
existingSteps: [step1, step2, step3],
|
||||
stepIdToDelete: '2',
|
||||
});
|
||||
|
||||
expect(result).toEqual([step1, step3]);
|
||||
});
|
||||
|
||||
it('should handle removing a step that has no next steps', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const step2 = createMockAction('2');
|
||||
const step3 = createMockAction('3');
|
||||
|
||||
const result = removeStep({
|
||||
existingSteps: [step1, step2, step3],
|
||||
stepIdToDelete: '2',
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ ...step1, nextStepIds: [] }, step3]);
|
||||
});
|
||||
|
||||
it('should update nextStepIds of parent steps to include children of removed step', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const step2 = createMockAction('2', ['3']);
|
||||
const step3 = createMockAction('3');
|
||||
|
||||
const result = removeStep({
|
||||
existingSteps: [step1, step2, step3],
|
||||
stepIdToDelete: '2',
|
||||
stepToDeleteChildrenIds: ['3'],
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ ...step1, nextStepIds: ['3'] }, step3]);
|
||||
});
|
||||
|
||||
it('should handle multiple parent steps pointing to the same step', () => {
|
||||
const step1 = createMockAction('1', ['3']);
|
||||
const step2 = createMockAction('2', ['3']);
|
||||
const step3 = createMockAction('3', ['4']);
|
||||
const step4 = createMockAction('4');
|
||||
|
||||
const result = removeStep({
|
||||
existingSteps: [step1, step2, step3, step4],
|
||||
stepIdToDelete: '3',
|
||||
stepToDeleteChildrenIds: ['4'],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ ...step1, nextStepIds: ['4'] },
|
||||
{ ...step2, nextStepIds: ['4'] },
|
||||
step4,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle removing a step with multiple children', () => {
|
||||
const step1 = createMockAction('1', ['2']);
|
||||
const step2 = createMockAction('2', ['3', '4']);
|
||||
const step3 = createMockAction('3');
|
||||
const step4 = createMockAction('4');
|
||||
|
||||
const result = removeStep({
|
||||
existingSteps: [step1, step2, step3, step4],
|
||||
stepIdToDelete: '2',
|
||||
stepToDeleteChildrenIds: ['3', '4'],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ ...step1, nextStepIds: ['3', '4'] },
|
||||
step3,
|
||||
step4,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const insertStep = ({
|
||||
existingSteps,
|
||||
insertedStep,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
}: {
|
||||
existingSteps: WorkflowAction[];
|
||||
insertedStep: WorkflowAction;
|
||||
parentStepId?: string;
|
||||
nextStepId?: string;
|
||||
}): WorkflowAction[] => {
|
||||
const updatedExistingSteps = existingSteps.map((existingStep) => {
|
||||
if (existingStep.id === parentStepId) {
|
||||
return {
|
||||
...existingStep,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(existingStep.nextStepIds?.filter((id) => id !== nextStepId) ||
|
||||
[]),
|
||||
insertedStep.id,
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return existingStep;
|
||||
});
|
||||
|
||||
return [
|
||||
...updatedExistingSteps,
|
||||
{
|
||||
...insertedStep,
|
||||
nextStepIds: nextStepId ? [nextStepId] : undefined,
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const removeStep = ({
|
||||
existingSteps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds,
|
||||
}: {
|
||||
existingSteps: WorkflowAction[];
|
||||
stepIdToDelete: string;
|
||||
stepToDeleteChildrenIds?: string[];
|
||||
}): WorkflowAction[] => {
|
||||
return existingSteps
|
||||
.filter((step) => step.id !== stepIdToDelete)
|
||||
.map((step) => {
|
||||
if (step.nextStepIds?.includes(stepIdToDelete)) {
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...step.nextStepIds.filter((id) => id !== stepIdToDelete),
|
||||
// We automatically link parent and child steps together
|
||||
...(stepToDeleteChildrenIds || []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return step;
|
||||
});
|
||||
};
|
||||
@ -7,6 +7,7 @@ import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
@ -18,6 +19,8 @@ import {
|
||||
import { StepOutput } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/insert-step';
|
||||
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-step/utils/remove-step';
|
||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
import {
|
||||
WorkflowAction,
|
||||
@ -54,13 +57,13 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
|
||||
async createWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepType,
|
||||
input,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
stepType: WorkflowActionType;
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
}): Promise<WorkflowActionDTO> {
|
||||
const { workflowVersionId, stepType, parentStepId, nextStepId } = input;
|
||||
|
||||
const newStep = await this.getStepDefaultDefinition({
|
||||
type: stepType,
|
||||
workspaceId,
|
||||
@ -87,8 +90,16 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
const existingSteps = workflowVersion.steps || [];
|
||||
const updatedSteps = insertStep({
|
||||
existingSteps,
|
||||
insertedStep: enrichedNewStep,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: [...(workflowVersion.steps || []), enrichedNewStep],
|
||||
steps: updatedSteps,
|
||||
});
|
||||
|
||||
return enrichedNewStep;
|
||||
@ -151,11 +162,11 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
async deleteWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
stepIdToDelete,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
stepId: string;
|
||||
stepIdToDelete: string;
|
||||
}): Promise<WorkflowActionDTO> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
@ -182,9 +193,9 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
const stepToDelete = workflowVersion.steps.filter(
|
||||
(step) => step.id === stepId,
|
||||
)?.[0];
|
||||
const stepToDelete = workflowVersion.steps.find(
|
||||
(step) => step.id === stepIdToDelete,
|
||||
);
|
||||
|
||||
if (!isDefined(stepToDelete)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
@ -194,9 +205,15 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
}
|
||||
|
||||
const workflowVersionUpdates =
|
||||
stepId === TRIGGER_STEP_ID
|
||||
stepIdToDelete === TRIGGER_STEP_ID
|
||||
? { trigger: null }
|
||||
: { steps: workflowVersion.steps.filter((step) => step.id !== stepId) };
|
||||
: {
|
||||
steps: removeStep({
|
||||
existingSteps: workflowVersion.steps,
|
||||
stepIdToDelete,
|
||||
stepToDeleteChildrenIds: stepToDelete.nextStepIds,
|
||||
}),
|
||||
};
|
||||
|
||||
await workflowVersionRepository.update(
|
||||
workflowVersion.id,
|
||||
|
||||
Reference in New Issue
Block a user