Skip to main content
Glama

mcp-google-sheets

flow-canvas-utils.ts14.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, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server