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:
+2
@@ -30,6 +30,7 @@ describe('addCreateStepNodes', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -48,6 +49,7 @@ describe('addCreateStepNodes', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+160
-4
@@ -52,6 +52,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -70,6 +71,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -118,6 +120,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -136,15 +139,168 @@ describe('generateWorkflowDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateWorkflowDiagram({ trigger, steps });
|
||||
|
||||
expect(result.edges[0].source).toEqual(result.nodes[0].id);
|
||||
expect(result.edges[0].target).toEqual(result.nodes[1].id);
|
||||
expect(result.edges.length).toEqual(2);
|
||||
expect(result.nodes.length).toEqual(3);
|
||||
|
||||
expect(result.edges[1].source).toEqual(result.nodes[1].id);
|
||||
expect(result.edges[1].target).toEqual(result.nodes[2].id);
|
||||
expect(result.edges[0].source).toEqual('trigger');
|
||||
expect(result.edges[0].target).toEqual('step1');
|
||||
|
||||
expect(result.edges[1].source).toEqual('step1');
|
||||
expect(result.edges[1].target).toEqual('step2');
|
||||
});
|
||||
|
||||
it('should take nextStepIds into account', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
name: 'Company created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Step 2',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateWorkflowDiagram({ trigger, steps });
|
||||
|
||||
expect(result.edges.length).toEqual(2);
|
||||
expect(result.nodes.length).toEqual(3);
|
||||
|
||||
expect(result.edges[0].source).toEqual('trigger');
|
||||
expect(result.edges[0].target).toEqual('step2');
|
||||
|
||||
expect(result.edges[1].source).toEqual('step2');
|
||||
expect(result.edges[1].target).toEqual('step1');
|
||||
});
|
||||
|
||||
it('should take nextStepIds into account for complex diagram', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
name: 'Company created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Step 2',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
name: 'Step 3',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateWorkflowDiagram({ trigger, steps });
|
||||
|
||||
expect(result.edges.length).toEqual(4);
|
||||
expect(result.nodes.length).toEqual(4);
|
||||
|
||||
expect(result.edges[0].source).toEqual('trigger');
|
||||
expect(result.edges[0].target).toEqual('step2');
|
||||
|
||||
expect(result.edges[1].source).toEqual('trigger');
|
||||
expect(result.edges[1].target).toEqual('step3');
|
||||
|
||||
expect(result.edges[2].source).toEqual('step2');
|
||||
expect(result.edges[2].target).toEqual('step1');
|
||||
|
||||
expect(result.edges[3].source).toEqual('step3');
|
||||
expect(result.edges[3].target).toEqual('step1');
|
||||
});
|
||||
});
|
||||
|
||||
+36
-14
@@ -21,6 +21,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
@@ -39,6 +40,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -57,6 +59,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
@@ -75,8 +78,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
step1: {
|
||||
result: undefined,
|
||||
@@ -144,7 +149,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -157,7 +162,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step2",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"y": 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -170,7 +175,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step3",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
"y": 450,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -189,6 +194,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
@@ -207,6 +213,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -225,6 +232,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
@@ -243,8 +251,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
step1: {
|
||||
result: {},
|
||||
@@ -322,7 +332,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -335,7 +345,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step2",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"y": 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -348,7 +358,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step3",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
"y": 450,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -367,6 +377,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
@@ -385,6 +396,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -403,6 +415,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
@@ -421,8 +434,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput = undefined;
|
||||
|
||||
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||
@@ -485,7 +500,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -498,7 +513,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step2",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"y": 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -511,7 +526,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step3",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
"y": 450,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -530,6 +545,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
@@ -548,6 +564,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
@@ -566,6 +583,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step3'],
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
@@ -584,6 +602,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step4'],
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
@@ -602,8 +621,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const stepsOutput: WorkflowRunOutputStepsOutput = {
|
||||
step1: {
|
||||
result: {},
|
||||
@@ -681,7 +702,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -694,7 +715,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step2",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"y": 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -707,7 +728,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step3",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
"y": 450,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -720,7 +741,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step4",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 450,
|
||||
"y": 600,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -762,6 +783,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
],
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
const stepsOutput = {
|
||||
@@ -814,7 +836,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"id": "step1",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"y": 150,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const addEdgeOptions = ({
|
||||
nodes,
|
||||
edges,
|
||||
}: WorkflowDiagram): WorkflowDiagram => {
|
||||
return {
|
||||
nodes,
|
||||
edges: edges.map((edge) => ({
|
||||
...edge,
|
||||
data: { shouldDisplayEdgeOptions: true },
|
||||
})),
|
||||
};
|
||||
};
|
||||
+89
-44
@@ -15,6 +15,58 @@ import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerSt
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Groups workflow steps into levels based on their distance from root nodes.
|
||||
*
|
||||
* A root node is one that is not referenced as a `nextStepId` by any other step.
|
||||
* The function performs a breadth-first traversal from all roots and assigns
|
||||
* each step to a level indicating its depth in the graph.
|
||||
*
|
||||
* Returns an array where each sub-array contains all steps at the same level.
|
||||
*/
|
||||
const groupStepsByLevel = (steps: WorkflowStep[]): WorkflowStep[][] => {
|
||||
const stepMap = new Map<string, WorkflowStep>();
|
||||
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
stepMap.set(step.id, step);
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
const rootSteps = steps.filter((step) => !childIds.has(step.id));
|
||||
|
||||
const stepsByLevel: WorkflowStep[][] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
|
||||
const visit = ({ step, level }: { step: WorkflowStep; level: number }) => {
|
||||
if (visited.has(step.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(step.id);
|
||||
|
||||
if (!isDefined(stepsByLevel[level])) {
|
||||
stepsByLevel[level] = [];
|
||||
}
|
||||
|
||||
stepsByLevel[level].push(step);
|
||||
|
||||
step.nextStepIds?.forEach((childId) => {
|
||||
const child = stepMap.get(childId);
|
||||
|
||||
if (isDefined(child)) {
|
||||
visit({ step: child, level: level + 1 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rootSteps.forEach((root) => visit({ step: root, level: 0 }));
|
||||
|
||||
return stepsByLevel;
|
||||
};
|
||||
|
||||
export const generateWorkflowDiagram = ({
|
||||
trigger,
|
||||
steps,
|
||||
@@ -23,6 +75,7 @@ export const generateWorkflowDiagram = ({
|
||||
steps: Array<WorkflowStep>;
|
||||
}): WorkflowDiagram => {
|
||||
const nodes: Array<WorkflowDiagramNode> = [];
|
||||
|
||||
const edges: Array<WorkflowDiagramEdge> = [];
|
||||
|
||||
if (isDefined(trigger)) {
|
||||
@@ -31,58 +84,50 @@ export const generateWorkflowDiagram = ({
|
||||
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
|
||||
}
|
||||
|
||||
const processNode = ({
|
||||
stepIndex,
|
||||
parentNodeId,
|
||||
xPos,
|
||||
yPos,
|
||||
}: {
|
||||
stepIndex: number;
|
||||
parentNodeId: string;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}) => {
|
||||
const step = steps.at(stepIndex);
|
||||
if (!isDefined(step)) {
|
||||
return;
|
||||
const stepsGroupedByLevel = groupStepsByLevel(steps);
|
||||
|
||||
let levelYPos = FIRST_NODE_POSITION.y;
|
||||
|
||||
const xPos = FIRST_NODE_POSITION.x;
|
||||
|
||||
for (const stepsByLevel of stepsGroupedByLevel) {
|
||||
levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
|
||||
|
||||
for (const step of stepsByLevel) {
|
||||
nodes.push({
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: levelYPos,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nodeId = step.id;
|
||||
|
||||
nodes.push({
|
||||
id: nodeId,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||
},
|
||||
});
|
||||
|
||||
for (const firstLevelStep of stepsGroupedByLevel[0] || []) {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
id: v4(),
|
||||
source: parentNodeId,
|
||||
target: nodeId,
|
||||
source: TRIGGER_STEP_ID,
|
||||
target: firstLevelStep.id,
|
||||
});
|
||||
}
|
||||
|
||||
processNode({
|
||||
stepIndex: stepIndex + 1,
|
||||
parentNodeId: nodeId,
|
||||
xPos,
|
||||
yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((child) => {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: child,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
processNode({
|
||||
stepIndex: 0,
|
||||
parentNodeId: TRIGGER_STEP_ID,
|
||||
xPos: FIRST_NODE_POSITION.x,
|
||||
yPos: FIRST_NODE_POSITION.y,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
|
||||
+61
-112
@@ -3,22 +3,16 @@ import {
|
||||
WorkflowStep,
|
||||
WorkflowTrigger,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
|
||||
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
|
||||
import {
|
||||
WorkflowDiagramRunStatus,
|
||||
WorkflowRunDiagram,
|
||||
WorkflowRunDiagramEdge,
|
||||
WorkflowRunDiagramNode,
|
||||
WorkflowRunDiagramNodeData,
|
||||
WorkflowRunDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
||||
|
||||
export const generateWorkflowRunDiagram = ({
|
||||
trigger,
|
||||
@@ -44,124 +38,79 @@ export const generateWorkflowRunDiagram = ({
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
const triggerBase = getWorkflowDiagramTriggerNode({ trigger });
|
||||
const workflowDiagram = generateWorkflowDiagram({ trigger, steps });
|
||||
|
||||
const nodes: Array<WorkflowRunDiagramNode> = [
|
||||
{
|
||||
...triggerBase,
|
||||
data: {
|
||||
...triggerBase.data,
|
||||
runStatus: 'success',
|
||||
},
|
||||
},
|
||||
];
|
||||
const edges: Array<WorkflowRunDiagramEdge> = [];
|
||||
let skippedExecution = false;
|
||||
|
||||
const processNode = ({
|
||||
stepIndex,
|
||||
parentNodeId,
|
||||
parentRunStatus,
|
||||
xPos,
|
||||
yPos,
|
||||
skippedExecution,
|
||||
}: {
|
||||
stepIndex: number;
|
||||
parentNodeId: string;
|
||||
parentRunStatus: WorkflowDiagramRunStatus;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
skippedExecution: boolean;
|
||||
}) => {
|
||||
const step = steps.at(stepIndex);
|
||||
if (!isDefined(step)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeId = step.id;
|
||||
|
||||
if (parentRunStatus === 'success') {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
||||
id: v4(),
|
||||
source: parentNodeId,
|
||||
target: nodeId,
|
||||
});
|
||||
} else {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
id: v4(),
|
||||
source: parentNodeId,
|
||||
target: nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
const runResult = stepsOutput?.[nodeId];
|
||||
const isPendingFormAction =
|
||||
step.type === 'FORM' &&
|
||||
isDefined(runResult?.pendingEvent) &&
|
||||
runResult.pendingEvent;
|
||||
|
||||
let runStatus: WorkflowDiagramRunStatus;
|
||||
if (skippedExecution) {
|
||||
runStatus = 'not-executed';
|
||||
} else if (!isDefined(runResult) || isPendingFormAction) {
|
||||
runStatus = 'running';
|
||||
} else {
|
||||
if (isDefined(runResult.error)) {
|
||||
runStatus = 'failure';
|
||||
} else {
|
||||
runStatus = 'success';
|
||||
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
|
||||
workflowDiagram.nodes.filter(isStepNode).map((node) => {
|
||||
if (node.data.nodeType === 'trigger') {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
runStatus: 'success',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const nodeData: WorkflowRunDiagramNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
runStatus,
|
||||
};
|
||||
const nodeId = node.id;
|
||||
|
||||
nodes.push({
|
||||
id: nodeId,
|
||||
data: nodeData,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
const runResult = stepsOutput?.[nodeId];
|
||||
|
||||
const isPendingFormAction =
|
||||
node.data.nodeType === 'action' &&
|
||||
node.data.actionType === 'FORM' &&
|
||||
isDefined(runResult?.pendingEvent) &&
|
||||
runResult.pendingEvent;
|
||||
|
||||
let runStatus: WorkflowDiagramRunStatus = 'success';
|
||||
|
||||
if (skippedExecution) {
|
||||
runStatus = 'not-executed';
|
||||
} else if (!isDefined(runResult) || isPendingFormAction) {
|
||||
runStatus = 'running';
|
||||
} else if (isDefined(runResult.error)) {
|
||||
runStatus = 'failure';
|
||||
}
|
||||
|
||||
skippedExecution =
|
||||
skippedExecution || runStatus === 'failure' || runStatus === 'running';
|
||||
|
||||
const nodeData = { ...node.data, runStatus };
|
||||
|
||||
if (isPendingFormAction) {
|
||||
stepToOpenByDefault = {
|
||||
id: nodeId,
|
||||
data: nodeData,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: nodeData,
|
||||
};
|
||||
});
|
||||
|
||||
if (isPendingFormAction) {
|
||||
stepToOpenByDefault = {
|
||||
id: nodeId,
|
||||
data: nodeData,
|
||||
const workflowRunDiagramEdges = workflowDiagram.edges.map((edge) => {
|
||||
const parentNode = workflowRunDiagramNodes.find(
|
||||
(node) => node.id === edge.source,
|
||||
);
|
||||
|
||||
if (isDefined(parentNode) && parentNode.data.runStatus === 'success') {
|
||||
return {
|
||||
...edge,
|
||||
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
||||
};
|
||||
}
|
||||
|
||||
processNode({
|
||||
stepIndex: stepIndex + 1,
|
||||
parentNodeId: nodeId,
|
||||
parentRunStatus: runStatus,
|
||||
xPos,
|
||||
yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||
skippedExecution: skippedExecution
|
||||
? true
|
||||
: runStatus === 'failure' || runStatus === 'running',
|
||||
});
|
||||
};
|
||||
|
||||
processNode({
|
||||
stepIndex: 0,
|
||||
parentNodeId: TRIGGER_STEP_ID,
|
||||
parentRunStatus: 'success',
|
||||
xPos: FIRST_NODE_POSITION.x,
|
||||
yPos: FIRST_NODE_POSITION.y,
|
||||
skippedExecution: false,
|
||||
return edge;
|
||||
});
|
||||
|
||||
return {
|
||||
diagram: {
|
||||
nodes,
|
||||
edges,
|
||||
nodes: workflowRunDiagramNodes,
|
||||
edges: workflowRunDiagramEdges,
|
||||
},
|
||||
stepToOpenByDefault,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramStepNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const isStepNode = (
|
||||
node: WorkflowDiagramNode,
|
||||
): node is WorkflowDiagramStepNode => {
|
||||
return node.data.nodeType === 'trigger' || node.data.nodeType === 'action';
|
||||
};
|
||||
Reference in New Issue
Block a user