965 flow control arrow menu 1/3 add insert step button (#12519)

Add insert step button to workflow edges



https://github.com/user-attachments/assets/7144f722-f1c7-450f-a8eb-c902071986a1



Also fixes `iconButtonGroup` UI component

## Before


https://github.com/user-attachments/assets/7b5f0245-d0e8-48af-9aa5-a29388a1caea


## After



https://github.com/user-attachments/assets/1820874f-aa99-41ae-8254-c76c275ee3ae
This commit is contained in:
martmull
2025-06-12 14:14:21 +02:00
committed by GitHub
parent a189f15313
commit cf01faf276
31 changed files with 755 additions and 291 deletions

View File

@ -38,7 +38,8 @@ describe('insertStep', () => {
insertedStep: newStep,
});
expect(result).toEqual([step1, step2, newStep]);
expect(result.updatedSteps).toEqual([step1, step2, newStep]);
expect(result.updatedInsertedStep).toEqual(newStep);
});
it('should update parent step nextStepIds when inserting a step between two steps', () => {
@ -53,7 +54,7 @@ describe('insertStep', () => {
nextStepId: '2',
});
expect(result).toEqual([
expect(result.updatedSteps).toEqual([
{ ...step1, nextStepIds: ['new'] },
step2,
{ ...newStep, nextStepIds: ['2'] },
@ -71,7 +72,10 @@ describe('insertStep', () => {
nextStepId: '1',
});
expect(result).toEqual([step1, { ...newStep, nextStepIds: ['1'] }]);
expect(result.updatedSteps).toEqual([
step1,
{ ...newStep, nextStepIds: ['1'] },
]);
});
it('should handle inserting a step at the end of the workflow', () => {
@ -85,7 +89,10 @@ describe('insertStep', () => {
nextStepId: undefined,
});
expect(result).toEqual([{ ...step1, nextStepIds: ['new'] }, newStep]);
expect(result.updatedSteps).toEqual([
{ ...step1, nextStepIds: ['new'] },
newStep,
]);
});
it('should handle inserting a step between two steps with multiple nextStepIds', () => {
@ -101,7 +108,7 @@ describe('insertStep', () => {
nextStepId: '2',
});
expect(result).toEqual([
expect(result.updatedSteps).toEqual([
{ ...step1, nextStepIds: ['3', 'new'] },
step2,
step3,

View File

@ -10,7 +10,7 @@ export const insertStep = ({
insertedStep: WorkflowAction;
parentStepId?: string;
nextStepId?: string;
}): WorkflowAction[] => {
}): { updatedSteps: WorkflowAction[]; updatedInsertedStep: WorkflowAction } => {
const updatedExistingSteps = existingSteps.map((existingStep) => {
if (existingStep.id === parentStepId) {
return {
@ -28,11 +28,13 @@ export const insertStep = ({
return existingStep;
});
return [
...updatedExistingSteps,
{
...insertedStep,
nextStepIds: nextStepId ? [nextStepId] : undefined,
},
];
const updatedInsertedStep = {
...insertedStep,
nextStepIds: nextStepId ? [nextStepId] : undefined,
};
return {
updatedSteps: [...updatedExistingSteps, updatedInsertedStep],
updatedInsertedStep,
};
};

View File

@ -96,7 +96,8 @@ export class WorkflowVersionStepWorkspaceService {
assertWorkflowVersionIsDraft(workflowVersion);
const existingSteps = workflowVersion.steps || [];
const updatedSteps = insertStep({
const { updatedSteps, updatedInsertedStep } = insertStep({
existingSteps,
insertedStep: enrichedNewStep,
parentStepId,
@ -107,7 +108,7 @@ export class WorkflowVersionStepWorkspaceService {
steps: updatedSteps,
});
return enrichedNewStep;
return updatedInsertedStep;
}
async updateWorkflowVersionStep({

View File

@ -8,6 +8,7 @@ export class WorkflowRunException extends CustomException {
export enum WorkflowRunExceptionCode {
WORKFLOW_RUN_NOT_FOUND = 'WORKFLOW_RUN_NOT_FOUND',
WORKFLOW_ROOT_STEP_NOT_FOUND = 'WORKFLOW_ROOT_STEP_NOT_FOUND',
INVALID_OPERATION = 'INVALID_OPERATION',
INVALID_INPUT = 'INVALID_INPUT',
WORKFLOW_RUN_LIMIT_REACHED = 'WORKFLOW_RUN_LIMIT_REACHED',

View File

@ -14,6 +14,7 @@ import {
WorkflowRunExceptionCode,
} from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
import { getRootSteps } from 'src/modules/workflow/workflow-runner/utils/getRootSteps.utils';
export type RunWorkflowJobData = {
workspaceId: string;
@ -114,9 +115,11 @@ export class RunWorkflowJob {
await this.throttleExecution(workflowVersion.workflowId);
const rootSteps = getRootSteps(workflowVersion.steps);
await this.executeWorkflow({
workflowRunId,
currentStepId: workflowVersion.steps[0].id,
currentStepId: rootSteps[0].id,
steps: workflowVersion.steps,
context,
workspaceId,

View File

@ -0,0 +1,85 @@
import { getRootSteps } from 'src/modules/workflow/workflow-runner/utils/getRootSteps.utils';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
describe('getRootSteps', () => {
it('should return the root steps', () => {
const steps = [
{
id: 'step1',
nextStepIds: ['step2'],
},
{ id: 'step2', nextStepIds: undefined },
] as WorkflowAction[];
const expectedRootSteps = [
{
id: 'step1',
nextStepIds: ['step2'],
},
] as WorkflowAction[];
expect(getRootSteps(steps)).toEqual(expectedRootSteps);
});
it('should not consider step order', () => {
const steps = [
{ id: 'step2', nextStepIds: undefined },
{
id: 'step1',
nextStepIds: ['step2'],
},
] as WorkflowAction[];
const expectedRootSteps = [
{
id: 'step1',
nextStepIds: ['step2'],
},
] as WorkflowAction[];
expect(getRootSteps(steps)).toEqual(expectedRootSteps);
});
it('should handle multiple root steps', () => {
const steps = [
{
id: 'step1',
nextStepIds: ['step3'],
},
{
id: 'step2',
nextStepIds: ['step3'],
},
{ id: 'step3', nextStepIds: ['step4'] },
{ id: 'step4', nextStepIds: undefined },
] as WorkflowAction[];
const expectedRootSteps = [
{
id: 'step1',
nextStepIds: ['step3'],
},
{
id: 'step2',
nextStepIds: ['step3'],
},
] as WorkflowAction[];
expect(getRootSteps(steps)).toEqual(expectedRootSteps);
});
it('should throw if buggy steps provided', () => {
const steps = [
{
id: 'step1',
nextStepIds: ['step2'],
},
{
id: 'step2',
nextStepIds: ['step1'],
},
] as WorkflowAction[];
expect(() => getRootSteps(steps)).toThrow('No root step found');
});
});

View File

@ -0,0 +1,24 @@
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
WorkflowRunException,
WorkflowRunExceptionCode,
} from 'src/modules/workflow/workflow-runner/exceptions/workflow-run.exception';
export const getRootSteps = (steps: WorkflowAction[]): WorkflowAction[] => {
const childIds = new Set<string>();
for (const step of steps) {
step.nextStepIds?.forEach((id) => childIds.add(id));
}
const rootSteps = steps.filter((step) => !childIds.has(step.id));
if (rootSteps.length === 0) {
throw new WorkflowRunException(
'No root step found',
WorkflowRunExceptionCode.WORKFLOW_ROOT_STEP_NOT_FOUND,
);
}
return rootSteps;
};