Change workflow node type (#14570)

- Update step endpoint now supports the case where node type is updated
- Added a new page for edition that re-uses the existing select action
component

Additional work:
- moved submitForm function to workflow runner service



https://github.com/user-attachments/assets/1fcb0adc-fe17-41f9-b2ea-4f84318db860
This commit is contained in:
Thomas Trompette
2025-09-17 17:36:42 +02:00
committed by GitHub
parent 0098ac453c
commit 8b345c9fd2
21 changed files with 521 additions and 283 deletions
@@ -5,6 +5,7 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
@@ -12,8 +13,6 @@ import {
type WorkflowAction,
WorkflowActionType,
} 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';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
type MockWorkspaceRepository = Partial<
@@ -125,8 +124,14 @@ describe('WorkflowVersionStepWorkspaceService', () => {
runWorkflowVersionStepDeletionSideEffects: jest.fn(),
},
},
{ provide: WorkflowRunWorkspaceService, useValue: {} },
{ provide: WorkflowRunnerWorkspaceService, useValue: {} },
{
provide: WorkflowCommonWorkspaceService,
useValue: {
getWorkflowVersionOrFail: jest
.fn()
.mockResolvedValue(mockWorkflowVersion),
},
},
],
}).compile();
@@ -9,15 +9,11 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
@Module({
imports: [
WorkflowSchemaModule,
ServerlessFunctionModule,
WorkflowRunnerModule,
WorkflowRunModule,
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity, AgentEntity]),
],
@@ -25,6 +21,9 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
WorkflowVersionStepWorkspaceService,
WorkflowVersionStepOperationsWorkspaceService,
],
exports: [WorkflowVersionStepWorkspaceService],
exports: [
WorkflowVersionStepWorkspaceService,
WorkflowVersionStepOperationsWorkspaceService,
],
})
export class WorkflowVersionStepModule {}
@@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { StepStatus, TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { type CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
@@ -13,24 +12,21 @@ import {
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { computeWorkflowVersionStepChanges } from 'src/modules/workflow/workflow-builder/utils/compute-workflow-version-step-updates.util';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { insertStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/insert-step';
import { removeStep } from 'src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step';
import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service';
import { isWorkflowFormAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/guards/is-workflow-form-action.guard';
import { type WorkflowAction } 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';
import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-runner.workspace-service';
@Injectable()
export class WorkflowVersionStepWorkspaceService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly workflowVersionStepOperationsWorkspaceService: WorkflowVersionStepOperationsWorkspaceService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
async createWorkflowVersionStep({
@@ -65,25 +61,11 @@ export class WorkflowVersionStepWorkspaceService {
workspaceId,
});
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
});
assertWorkflowVersionIsDraft(workflowVersion);
@@ -100,6 +82,13 @@ export class WorkflowVersionStepWorkspaceService {
parentStepConnectionOptions,
});
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await workflowVersionRepository.update(workflowVersion.id, {
trigger: updatedTrigger,
steps: updatedSteps,
@@ -121,25 +110,11 @@ export class WorkflowVersionStepWorkspaceService {
workflowVersionId: string;
step: WorkflowAction;
}): Promise<WorkflowAction> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
});
assertWorkflowVersionIsDraft(workflowVersion);
@@ -150,25 +125,51 @@ export class WorkflowVersionStepWorkspaceService {
);
}
const enrichedNewStep =
await this.workflowSchemaWorkspaceService.enrichOutputSchema({
step,
workspaceId,
});
const existingStep = workflowVersion.steps.find(
(existingStep) => existingStep.id === step.id,
);
if (!isDefined(existingStep)) {
throw new WorkflowVersionStepException(
'Step not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
const isStepTypeChanged = existingStep.type !== step.type;
const updatedStep = isStepTypeChanged
? await this.updateWorkflowVersionStepType({
existingStep,
newStep: step,
workspaceId,
workflowVersionId,
})
: await this.updateWorkflowVersionStepSettings({
newStep: step,
workspaceId,
});
const updatedSteps = workflowVersion.steps.map((existingStep) => {
if (existingStep.id === step.id) {
return enrichedNewStep;
return updatedStep;
} else {
return existingStep;
}
});
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
});
return enrichedNewStep;
return updatedStep;
}
async deleteWorkflowVersionStep({
@@ -180,25 +181,11 @@ export class WorkflowVersionStepWorkspaceService {
workflowVersionId: string;
stepIdToDelete: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
});
assertWorkflowVersionIsDraft(workflowVersion);
@@ -236,6 +223,13 @@ export class WorkflowVersionStepWorkspaceService {
stepToDeleteChildrenIds,
});
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
trigger: updatedTrigger,
@@ -273,25 +267,11 @@ export class WorkflowVersionStepWorkspaceService {
workflowVersionId: string;
stepId: string;
}): Promise<WorkflowVersionStepChangesDTO> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workflowVersionId,
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: {
id: workflowVersionId,
},
});
if (!isDefined(workflowVersion)) {
throw new WorkflowVersionStepException(
'WorkflowVersion not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
});
assertWorkflowVersionIsDraft(workflowVersion);
@@ -320,6 +300,13 @@ export class WorkflowVersionStepWorkspaceService {
insertedStep: duplicatedStep,
});
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await workflowVersionRepository.update(workflowVersion.id, {
steps: updatedSteps,
trigger: updatedTrigger,
@@ -332,70 +319,6 @@ export class WorkflowVersionStepWorkspaceService {
});
}
async submitFormStep({
workspaceId,
stepId,
workflowRunId,
response,
}: {
workspaceId: string;
stepId: string;
workflowRunId: string;
response: object;
}) {
const workflowRun =
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
workflowRunId,
workspaceId,
});
const step = workflowRun.state?.flow?.steps?.find(
(step) => step.id === stepId,
);
if (!isDefined(step)) {
throw new WorkflowVersionStepException(
'Step not found',
WorkflowVersionStepExceptionCode.NOT_FOUND,
);
}
if (!isWorkflowFormAction(step)) {
throw new WorkflowVersionStepException(
'Step is not a form',
WorkflowVersionStepExceptionCode.INVALID_REQUEST,
{
userFriendlyMessage: t`Step is not a form`,
},
);
}
const enrichedResponse =
await this.workflowVersionStepOperationsWorkspaceService.enrichFormStepResponse(
{
workspaceId,
step,
response,
},
);
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
stepId,
stepInfo: {
status: StepStatus.SUCCESS,
result: enrichedResponse,
},
workspaceId,
workflowRunId,
});
await this.workflowRunnerWorkspaceService.resume({
workspaceId,
workflowRunId,
lastExecutedStepId: stepId,
});
}
async createDraftStep({
step,
workspaceId,
@@ -408,4 +331,56 @@ export class WorkflowVersionStepWorkspaceService {
workspaceId,
});
}
private async updateWorkflowVersionStepType({
existingStep,
newStep,
workspaceId,
workflowVersionId,
}: {
existingStep: WorkflowAction;
newStep: WorkflowAction;
workspaceId: string;
workflowVersionId: string;
}): Promise<WorkflowAction> {
await this.workflowVersionStepOperationsWorkspaceService.runWorkflowVersionStepDeletionSideEffects(
{
step: existingStep,
workspaceId,
},
);
const defaultStep =
await this.workflowVersionStepOperationsWorkspaceService.runStepCreationSideEffectsAndBuildStep(
{
type: newStep.type,
workspaceId,
position: newStep.position,
workflowVersionId,
},
);
return this.workflowSchemaWorkspaceService.enrichOutputSchema({
step: {
...defaultStep,
id: existingStep.id,
nextStepIds: existingStep.nextStepIds,
position: existingStep.position,
},
workspaceId,
});
}
private async updateWorkflowVersionStepSettings({
newStep,
workspaceId,
}: {
newStep: WorkflowAction;
workspaceId: string;
}): Promise<WorkflowAction> {
return this.workflowSchemaWorkspaceService.enrichOutputSchema({
step: newStep,
workspaceId,
});
}
}