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
@@ -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',
@@ -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,
@@ -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');
});
});
@@ -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;
};