flow-canvas-utils.ts•14.7 kB
import { t } from 'i18next';
import { flowRunUtils } from '@/features/flow-runs/lib/flow-run-utils';
import {
FlowAction,
FlowActionType,
FlowOperationType,
FlowRun,
flowStructureUtil,
FlowVersion,
isNil,
LoopOnItemsAction,
RouterAction,
StepLocationRelativeToParent,
FlowTrigger,
FlowTriggerType,
} from '@activepieces/shared';
import { flowUtilConsts } from './consts';
import {
ApBigAddButtonNode,
ApButtonData,
ApEdge,
ApEdgeType,
ApGraph,
ApGraphEndNode,
ApLoopReturnNode,
ApNodeType,
ApStepNode,
ApStraightLineEdge,
} from './types';
const createBigAddButtonGraph: (
parentStep: LoopOnItemsAction | RouterAction,
nodeData: ApBigAddButtonNode['data'],
) => ApGraph = (parentStep, nodeData) => {
const bigAddButtonNode: ApBigAddButtonNode = {
id: `${parentStep.name}-big-add-button-${nodeData.edgeId}`,
type: ApNodeType.BIG_ADD_BUTTON,
position: { x: 0, y: 0 },
data: nodeData,
selectable: false,
style: {
pointerEvents: 'all',
},
};
const graphEndNode: ApGraphEndNode = {
id: `${parentStep.name}-subgraph-end-${nodeData.edgeId}`,
type: ApNodeType.GRAPH_END_WIDGET as const,
position: {
x: flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
y:
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS,
},
data: {},
selectable: false,
};
const straightLineEdge: ApStraightLineEdge = {
id: `big-button-straight-line-for${nodeData.edgeId}`,
source: `${parentStep.name}-big-add-button-${nodeData.edgeId}`,
target: `${parentStep.name}-subgraph-end-${nodeData.edgeId}`,
type: ApEdgeType.STRAIGHT_LINE as const,
data: {
drawArrowHead: false,
hideAddButton: true,
parentStepName: parentStep.name,
},
};
return {
nodes: [bigAddButtonNode, graphEndNode],
edges: [straightLineEdge],
};
};
const createStepGraph: (
step: FlowAction | FlowTrigger,
graphHeight: number,
) => ApGraph = (step, graphHeight) => {
const stepNode: ApStepNode = {
id: step.name,
type: ApNodeType.STEP as const,
position: { x: 0, y: 0 },
data: {
step,
},
selectable: step.name !== 'trigger',
draggable: true,
style: {
pointerEvents: 'all',
},
};
const graphEndNode: ApGraphEndNode = {
id: `${step.name}-subgraph-end`,
type: ApNodeType.GRAPH_END_WIDGET as const,
position: {
x: flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
y: graphHeight,
},
data: {},
selectable: false,
};
const straightLineEdge: ApStraightLineEdge = {
id: `${step.name}-${step.nextAction?.name ?? 'graph-end'}-edge`,
source: step.name,
target: `${step.name}-subgraph-end`,
type: ApEdgeType.STRAIGHT_LINE as const,
data: {
drawArrowHead: !isNil(step.nextAction),
parentStepName: step.name,
},
};
return {
nodes: [stepNode, graphEndNode],
edges:
step.type !== FlowActionType.LOOP_ON_ITEMS &&
step.type !== FlowActionType.ROUTER
? [straightLineEdge]
: [],
};
};
const buildGraph: (step: FlowAction | FlowTrigger | undefined) => ApGraph = (
step,
) => {
if (isNil(step)) {
return {
nodes: [],
edges: [],
};
}
const graph: ApGraph = createStepGraph(
step,
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS,
);
const childGraph =
step.type === FlowActionType.LOOP_ON_ITEMS
? buildLoopChildGraph(step)
: step.type === FlowActionType.ROUTER
? buildRouterChildGraph(step)
: null;
const graphWithChild = childGraph ? mergeGraph(graph, childGraph) : graph;
const nextStepGraph = buildGraph(step.nextAction);
return mergeGraph(
graphWithChild,
offsetGraph(nextStepGraph, {
x: 0,
y: calculateGraphBoundingBox(graphWithChild).height,
}),
);
};
function offsetGraph(
graph: ApGraph,
offset: { x: number; y: number },
): ApGraph {
return {
nodes: graph.nodes.map((node) => ({
...node,
position: {
x: node.position.x + offset.x,
y: node.position.y + offset.y,
},
})),
edges: graph.edges,
};
}
function mergeGraph(graph1: ApGraph, graph2: ApGraph): ApGraph {
return {
nodes: [...graph1.nodes, ...graph2.nodes],
edges: [...graph1.edges, ...graph2.edges],
};
}
function createFocusStepInGraphParams(stepName: string) {
return {
nodes: [{ id: stepName }],
duration: 1000,
maxZoom: 1.25,
minZoom: 1.25,
};
}
const calculateGraphBoundingBox = (graph: ApGraph) => {
const minX = Math.min(
...graph.nodes
.filter((node) => flowUtilConsts.doesNodeAffectBoundingBox(node.type))
.map((node) => node.position.x),
);
const minY = Math.min(...graph.nodes.map((node) => node.position.y));
const maxX = Math.max(
...graph.nodes
.filter((node) => flowUtilConsts.doesNodeAffectBoundingBox(node.type))
.map((node) => node.position.x + flowUtilConsts.AP_NODE_SIZE.STEP.width),
);
const maxY = Math.max(...graph.nodes.map((node) => node.position.y));
const width = maxX - minX;
const height = maxY - minY;
return {
width,
height,
left: -minX + flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
right: maxX - flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
top: minY,
bottom: maxY,
};
};
const buildLoopChildGraph: (step: LoopOnItemsAction) => ApGraph = (step) => {
const childGraph = step.firstLoopAction
? buildGraph(step.firstLoopAction)
: createBigAddButtonGraph(step, {
parentStepName: step.name,
stepLocationRelativeToParent: StepLocationRelativeToParent.INSIDE_LOOP,
edgeId: `${step.name}-loop-start-edge`,
});
const childGraphBoundingBox = calculateGraphBoundingBox(childGraph);
const deltaLeftX =
-(
childGraphBoundingBox.width +
flowUtilConsts.AP_NODE_SIZE.STEP.width +
flowUtilConsts.HORIZONTAL_SPACE_BETWEEN_NODES -
flowUtilConsts.AP_NODE_SIZE.STEP.width / 2 -
childGraphBoundingBox.right
) /
2 -
flowUtilConsts.AP_NODE_SIZE.STEP.width / 2;
const loopReturnNode: ApLoopReturnNode = {
id: `${step.name}-loop-return-node`,
type: ApNodeType.LOOP_RETURN_NODE,
position: {
x: deltaLeftX + flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
y:
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_OFFSET_BETWEEN_LOOP_AND_CHILD +
childGraphBoundingBox.height / 2,
},
data: {},
selectable: false,
};
const childGraphAfterOffset = offsetGraph(childGraph, {
x:
deltaLeftX +
flowUtilConsts.AP_NODE_SIZE.STEP.width +
flowUtilConsts.HORIZONTAL_SPACE_BETWEEN_NODES +
childGraphBoundingBox.left,
y:
flowUtilConsts.VERTICAL_OFFSET_BETWEEN_LOOP_AND_CHILD +
flowUtilConsts.AP_NODE_SIZE.STEP.height,
});
const edges: ApEdge[] = [
{
id: `${step.name}-loop-start-edge`,
source: step.name,
target: `${childGraph.nodes[0].id}`,
type: ApEdgeType.LOOP_START_EDGE as const,
data: {
isLoopEmpty: isNil(step.firstLoopAction),
},
},
{
id: `${step.name}-loop-return-node`,
source: `${childGraph.nodes[childGraph.nodes.length - 1].id}`,
target: `${step.name}-loop-return-node`,
type: ApEdgeType.LOOP_RETURN_EDGE as const,
data: {
parentStepName: step.name,
isLoopEmpty: isNil(step.firstLoopAction),
drawArrowHeadAfterEnd: !isNil(step.nextAction),
verticalSpaceBetweenReturnNodeStartAndEnd:
childGraphBoundingBox.height +
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS,
},
},
];
const subgraphEndSubNode: ApGraphEndNode = {
id: `${step.name}-loop-subgraph-end`,
type: ApNodeType.GRAPH_END_WIDGET,
position: {
x: flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
y:
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_OFFSET_BETWEEN_LOOP_AND_CHILD +
childGraphBoundingBox.height +
flowUtilConsts.ARC_LENGTH +
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS,
},
data: {},
selectable: false,
};
return {
nodes: [loopReturnNode, ...childGraphAfterOffset.nodes, subgraphEndSubNode],
edges: [...edges, ...childGraphAfterOffset.edges],
};
};
const buildRouterChildGraph = (step: RouterAction) => {
const childGraphs = step.children.map((branch, index) => {
return branch
? buildGraph(branch)
: createBigAddButtonGraph(step, {
parentStepName: step.name,
stepLocationRelativeToParent:
StepLocationRelativeToParent.INSIDE_BRANCH,
branchIndex: index,
edgeId: `${step.name}-branch-${index}-start-edge`,
});
});
const childGraphsAfterOffset = offsetRouterChildSteps(childGraphs);
const maxHeight = Math.max(
...childGraphsAfterOffset.map((cg) => calculateGraphBoundingBox(cg).height),
);
const subgraphEndSubNode: ApGraphEndNode = {
id: `${step.name}-branch-subgraph-end`,
type: ApNodeType.GRAPH_END_WIDGET,
position: {
x: flowUtilConsts.AP_NODE_SIZE.STEP.width / 2,
y:
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_OFFSET_BETWEEN_ROUTER_AND_CHILD +
maxHeight +
flowUtilConsts.ARC_LENGTH +
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS,
},
data: {},
selectable: false,
};
const edges: ApEdge[] = childGraphsAfterOffset
.map((childGraph, branchIndex) => {
return [
{
id: `${step.name}-branch-${branchIndex}-start-edge`,
source: step.name,
target: `${childGraph.nodes[0].id}`,
type: ApEdgeType.ROUTER_START_EDGE as const,
data: {
isBranchEmpty: isNil(step.children[branchIndex]),
label:
step.settings.branches[branchIndex]?.branchName ??
`${t('Branch')} ${branchIndex + 1} (missing branch)`,
branchIndex,
stepLocationRelativeToParent:
StepLocationRelativeToParent.INSIDE_BRANCH as const,
drawHorizontalLine:
branchIndex === 0 ||
branchIndex === childGraphsAfterOffset.length - 1,
drawStartingVerticalLine: branchIndex === 0,
},
},
{
id: `${step.name}-branch-${branchIndex}-end-edge`,
source: `${childGraph.nodes.at(-1)!.id}`,
target: subgraphEndSubNode.id,
type: ApEdgeType.ROUTER_END_EDGE as const,
data: {
drawEndingVerticalLine: branchIndex === 0,
verticalSpaceBetweenLastNodeInBranchAndEndLine:
subgraphEndSubNode.position.y -
childGraph.nodes.at(-1)!.position.y -
flowUtilConsts.VERTICAL_SPACE_BETWEEN_STEPS -
flowUtilConsts.ARC_LENGTH,
drawHorizontalLine:
branchIndex === 0 ||
branchIndex === childGraphsAfterOffset.length - 1,
routerOrBranchStepName: step.name,
isNextStepEmpty: isNil(step.nextAction),
},
},
];
})
.flat();
return {
nodes: [
...childGraphsAfterOffset.map((cg) => cg.nodes).flat(),
subgraphEndSubNode,
],
edges: [...childGraphsAfterOffset.map((cg) => cg.edges).flat(), ...edges],
};
};
const offsetRouterChildSteps = (childGraphs: ApGraph[]) => {
const childGraphsBoundingBoxes = childGraphs.map((childGraph) =>
calculateGraphBoundingBox(childGraph),
);
const totalWidth =
childGraphsBoundingBoxes.reduce((acc, current) => acc + current.width, 0) +
flowUtilConsts.HORIZONTAL_SPACE_BETWEEN_NODES * (childGraphs.length - 1);
let deltaLeftX =
-(
totalWidth -
childGraphsBoundingBoxes[0].left -
childGraphsBoundingBoxes[childGraphs.length - 1].right
) /
2 -
childGraphsBoundingBoxes[0].left;
return childGraphsBoundingBoxes.map((childGraphBoundingBox, index) => {
const x = deltaLeftX + childGraphBoundingBox.left;
deltaLeftX +=
childGraphBoundingBox.width +
flowUtilConsts.HORIZONTAL_SPACE_BETWEEN_NODES;
return offsetGraph(childGraphs[index], {
x,
y:
flowUtilConsts.AP_NODE_SIZE.STEP.height +
flowUtilConsts.VERTICAL_OFFSET_BETWEEN_ROUTER_AND_CHILD,
});
});
};
const createAddOperationFromAddButtonData = (data: ApButtonData) => {
if (
data.stepLocationRelativeToParent ===
StepLocationRelativeToParent.INSIDE_BRANCH
) {
return {
type: FlowOperationType.ADD_ACTION,
actionLocation: {
parentStep: data.parentStepName,
stepLocationRelativeToParent: data.stepLocationRelativeToParent,
branchIndex: data.branchIndex,
},
} as const;
}
return {
type: FlowOperationType.ADD_ACTION,
actionLocation: {
parentStep: data.parentStepName,
stepLocationRelativeToParent: data.stepLocationRelativeToParent,
},
} as const;
};
const isSkipped = (stepName: string, trigger: FlowTrigger) => {
const step = flowStructureUtil.getStep(stepName, trigger);
if (
isNil(step) ||
step.type === FlowTriggerType.EMPTY ||
step.type === FlowTriggerType.PIECE
) {
return false;
}
const skippedParents = flowStructureUtil
.findPathToStep(trigger, stepName)
.filter(
(stepInPath) =>
stepInPath.type === FlowActionType.LOOP_ON_ITEMS ||
stepInPath.type === FlowActionType.ROUTER,
)
.filter((routerOrLoop) =>
flowStructureUtil.isChildOf(routerOrLoop, stepName),
)
.filter((parent) => parent.skip);
return skippedParents.length > 0 || !!step.skip;
};
const getStepStatus = (
stepName: string | undefined,
run: FlowRun | null,
loopIndexes: Record<string, number>,
flowVersion: FlowVersion,
) => {
if (isNil(run) || isNil(stepName) || isNil(run.steps)) {
return undefined;
}
const stepOutput = flowRunUtils.extractStepOutput(
stepName,
loopIndexes,
run.steps,
flowVersion.trigger,
);
return stepOutput?.status;
};
export const flowCanvasUtils = {
convertFlowVersionToGraph(version: FlowVersion): ApGraph {
const graph = buildGraph(version.trigger);
const graphEndWidget = graph.nodes.findLast(
(node) => node.type === ApNodeType.GRAPH_END_WIDGET,
) as ApGraphEndNode;
if (graphEndWidget) {
graphEndWidget.data.showWidget = true;
} else {
console.warn('Flow end widget not found');
}
return graph;
},
createFocusStepInGraphParams,
calculateGraphBoundingBox,
createAddOperationFromAddButtonData,
isSkipped,
getStepStatus,
};