diff --git a/packages/web/src/components/EditorNew/Edge/Edge.jsx b/packages/web/src/components/EditorNew/Edge/Edge.jsx
new file mode 100644
index 00000000..980e80db
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Edge/Edge.jsx
@@ -0,0 +1,79 @@
+import { EdgeLabelRenderer, getStraightPath } from 'reactflow';
+import IconButton from '@mui/material/IconButton';
+import AddIcon from '@mui/icons-material/Add';
+import { useMutation } from '@apollo/client';
+import { CREATE_STEP } from 'graphql/mutations/create-step';
+import { useQueryClient } from '@tanstack/react-query';
+import PropTypes from 'prop-types';
+
+export default function Edge({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ source,
+ data: { flowId, setCurrentStepId, flowActive, layouted },
+}) {
+ const [createStep, { loading: creationInProgress }] =
+ useMutation(CREATE_STEP);
+ const queryClient = useQueryClient();
+ const [edgePath, labelX, labelY] = getStraightPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+
+ const addStep = async (previousStepId) => {
+ const mutationInput = {
+ previousStep: {
+ id: previousStepId,
+ },
+ flow: {
+ id: flowId,
+ },
+ };
+
+ const createdStep = await createStep({
+ variables: { input: mutationInput },
+ });
+
+ const createdStepId = createdStep.data.createStep.id;
+ setCurrentStepId(createdStepId);
+ await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
+ };
+
+ return (
+ <>
+
+ addStep(source)}
+ color="primary"
+ sx={{
+ position: 'absolute',
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
+ pointerEvents: 'all',
+ visibility: layouted ? 'visible' : 'hidden',
+ }}
+ disabled={creationInProgress || flowActive}
+ >
+
+
+
+ >
+ );
+}
+
+Edge.propTypes = {
+ sourceX: PropTypes.number.isRequired,
+ sourceY: PropTypes.number.isRequired,
+ targetX: PropTypes.number.isRequired,
+ targetY: PropTypes.number.isRequired,
+ source: PropTypes.string.isRequired,
+ data: PropTypes.shape({
+ flowId: PropTypes.string.isRequired,
+ setCurrentStepId: PropTypes.func.isRequired,
+ flowActive: PropTypes.bool.isRequired,
+ layouted: PropTypes.bool,
+ }).isRequired,
+};
diff --git a/packages/web/src/components/EditorNew/EditorNew.jsx b/packages/web/src/components/EditorNew/EditorNew.jsx
index 87a434a1..31220a31 100644
--- a/packages/web/src/components/EditorNew/EditorNew.jsx
+++ b/packages/web/src/components/EditorNew/EditorNew.jsx
@@ -5,12 +5,22 @@ import { FlowPropType } from 'propTypes/propTypes';
import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
import { Stack } from '@mui/material';
-
import { UPDATE_STEP } from 'graphql/mutations/update-step';
-import FlowStep from './FlowStep/FlowStep';
-import { useAutoLayout } from './useAutoLayout';
-const nodeTypes = { flowStep: FlowStep };
+import { useAutoLayout } from './useAutoLayout';
+import FlowStepNode from './FlowStepNode/FlowStepNode';
+import Edge from './Edge/Edge';
+import InvisibleNode from './InvisibleNode/InvisibleNode';
+
+const nodeTypes = { flowStep: FlowStepNode, invisible: InvisibleNode };
+
+const edgeTypes = {
+ addNodeEdge: Edge,
+};
+
+const INVISIBLE_NODE_ID = 'invisible-node';
+
+const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`;
const EditorNew = ({ flow }) => {
const [triggerStep] = flow.steps;
@@ -64,53 +74,138 @@ const EditorNew = ({ flow }) => {
[flow.id, updateStep, queryClient],
);
+ const generateEdges = useCallback((flow, prevEdges) => {
+ const newEdges =
+ flow.steps
+ .map((step, i) => {
+ const sourceId = step.id;
+ const targetId = flow.steps[i + 1]?.id;
+ const edge = prevEdges?.find(
+ (edge) => edge.id === generateEdgeId(sourceId, targetId),
+ );
+ if (targetId) {
+ return {
+ id: generateEdgeId(sourceId, targetId),
+ source: sourceId,
+ target: targetId,
+ type: 'addNodeEdge',
+ data: {
+ flowId: flow.id,
+ flowActive: flow.active,
+ setCurrentStepId,
+ layouted: !!edge,
+ },
+ };
+ }
+ })
+ .filter((edge) => !!edge) || [];
+
+ const lastStep = flow.steps[flow.steps.length - 1];
+
+ return lastStep
+ ? [
+ ...newEdges,
+ {
+ id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
+ source: lastStep.id,
+ target: INVISIBLE_NODE_ID,
+ type: 'addNodeEdge',
+ data: {
+ flowId: flow.id,
+ flowActive: flow.active,
+ setCurrentStepId,
+ layouted: false,
+ },
+ },
+ ]
+ : newEdges;
+ }, []);
+
+ const generateNodes = useCallback(
+ (flow, prevNodes) => {
+ const newNodes = flow.steps.map((step, index) => {
+ const node = prevNodes?.find(({ id }) => id === step.id);
+ return {
+ id: step.id,
+ type: 'flowStep',
+ position: {
+ x: node ? node.position.x : 0,
+ y: node ? node.position.y : 0,
+ },
+ data: {
+ step,
+ index: index,
+ flowId: flow.id,
+ collapsed: currentStepId !== step.id,
+ openNextStep: openNextStep(flow.steps[index + 1]),
+ onOpen: () => setCurrentStepId(step.id),
+ onClose: () => setCurrentStepId(null),
+ onChange: onStepChange,
+ layouted: !!node,
+ },
+ };
+ });
+
+ const prevInvisibleNode = nodes.find((node) => node.type === 'invisible');
+
+ return [
+ ...newNodes,
+ {
+ id: INVISIBLE_NODE_ID,
+ type: 'invisible',
+ position: {
+ x: prevInvisibleNode ? prevInvisibleNode.position.x : 0,
+ y: prevInvisibleNode ? prevInvisibleNode.position.y : 0,
+ },
+ },
+ ];
+ },
+ [currentStepId, nodes, onStepChange, openNextStep],
+ );
+
+ const updateNodesData = useCallback(
+ (steps) => {
+ setNodes((nodes) =>
+ nodes.map((node) => {
+ const step = steps.find((step) => step.id === node.id);
+ if (step) {
+ return { ...node, data: { ...node.data, step: { ...step } } };
+ }
+ return node;
+ }),
+ );
+ },
+ [setNodes],
+ );
+
useEffect(() => {
setNodes(
- nodes.map((node) => ({
- ...node,
- data: { ...node.data, collapsed: currentStepId !== node.data.step.id },
- })),
+ nodes.map((node) => {
+ if (node.type === 'flowStep') {
+ return {
+ ...node,
+ data: {
+ ...node.data,
+ collapsed: currentStepId !== node.data.step.id,
+ },
+ };
+ }
+ return node;
+ }),
);
}, [currentStepId]);
useEffect(() => {
- const getInitialNodes = () => {
- return flow?.steps?.map((step, index) => ({
- id: step.id,
- position: { x: 0, y: 0 },
- data: {
- step,
- index: index,
- flowId: flow.id,
- collapsed: currentStepId !== step.id,
- openNextStep: openNextStep(flow?.steps[index + 1]),
- onOpen: () => setCurrentStepId(step.id),
- onClose: () => setCurrentStepId(null),
- onChange: onStepChange,
- },
- type: 'flowStep',
- }));
- };
+ if (flow.steps.length + 1 !== nodes.length) {
+ const newNodes = generateNodes(flow, nodes);
+ const newEdges = generateEdges(flow, edges);
- const getInitialEdges = () => {
- return flow?.steps?.map((step, i) => {
- const sourceId = step.id;
- const targetId = flow.steps[i + 1]?.id;
- return {
- id: i,
- source: sourceId,
- target: targetId,
- animated: false,
- };
- });
- };
-
- const nodes = getInitialNodes();
- const edges = getInitialEdges();
-
- setNodes(nodes);
- setEdges(edges);
- }, []);
+ setNodes(newNodes);
+ setEdges(newEdges);
+ } else {
+ updateNodesData(flow.steps);
+ }
+ }, [flow]);
return (
{
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
+ edgeTypes={edgeTypes}
panOnScroll
panOnScrollMode="vertical"
panOnDrag={false}
@@ -136,7 +232,7 @@ const EditorNew = ({ flow }) => {
zoomOnPinch={false}
zoomOnDoubleClick={false}
panActivationKeyCode={null}
- >
+ />
);
};
diff --git a/packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx b/packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx
deleted file mode 100644
index 2f717a53..00000000
--- a/packages/web/src/components/EditorNew/FlowStep/FlowStep.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Handle, Position } from 'reactflow';
-import FlowStepBase from 'components/FlowStep';
-import { Box } from '@mui/material';
-
-function FlowStep({
- data: {
- step,
- index,
- flowId,
- collapsed,
- openNextStep,
- onOpen,
- onClose,
- onChange,
- currentStepId,
- },
- selected,
-}) {
- return (
-
-
-
-
-
- );
-}
-
-export default FlowStep;
diff --git a/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx b/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx
new file mode 100644
index 00000000..2b173dcb
--- /dev/null
+++ b/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx
@@ -0,0 +1,68 @@
+import { Handle, Position } from 'reactflow';
+import { Box } from '@mui/material';
+import PropTypes from 'prop-types';
+
+import FlowStep from 'components/FlowStep';
+import { StepPropType } from 'propTypes/propTypes';
+
+function FlowStepNode({
+ data: {
+ step,
+ index,
+ flowId,
+ collapsed,
+ openNextStep,
+ onOpen,
+ onClose,
+ onChange,
+ layouted,
+ },
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+FlowStepNode.propTypes = {
+ data: PropTypes.shape({
+ step: StepPropType.isRequired,
+ index: PropTypes.number.isRequired,
+ flowId: PropTypes.string.isRequired,
+ collapsed: PropTypes.bool.isRequired,
+ openNextStep: PropTypes.func.isRequired,
+ onOpen: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ layouted: PropTypes.bool.isRequired,
+ }).isRequired,
+};
+
+export default FlowStepNode;
diff --git a/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx b/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx
new file mode 100644
index 00000000..f9db694f
--- /dev/null
+++ b/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx
@@ -0,0 +1,30 @@
+import { Handle, Position } from 'reactflow';
+import { Box } from '@mui/material';
+
+// This node is used for adding an edge with add node button after the last flow step node
+function InvisibleNode() {
+ return (
+
+
+ Invisible node
+
+
+ );
+}
+
+export default InvisibleNode;
diff --git a/packages/web/src/components/EditorNew/useAutoLayout.js b/packages/web/src/components/EditorNew/useAutoLayout.js
index b6c603a3..d97bab87 100644
--- a/packages/web/src/components/EditorNew/useAutoLayout.js
+++ b/packages/web/src/components/EditorNew/useAutoLayout.js
@@ -1,56 +1,59 @@
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect } from 'react';
import Dagre from '@dagrejs/dagre';
import { usePrevious } from 'hooks/usePrevious';
import { isEqual } from 'lodash';
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
+const getLayoutedElements = (nodes, edges) => {
+ const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
+ graph.setGraph({
+ rankdir: 'TB',
+ marginy: 60,
+ marginx: 60,
+ universalSep: true,
+ ranksep: 64,
+ });
+ edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
+ nodes.forEach((node) => graph.setNode(node.id, node));
+
+ Dagre.layout(graph);
+
+ return {
+ nodes: nodes.map((node) => {
+ const { x, y, width, height } = graph.node(node.id);
+ return {
+ ...node,
+ position: { x: x - width / 2, y: y - height / 2 },
+ };
+ }),
+ edges,
+ };
+};
+
export const useAutoLayout = () => {
const nodes = useNodes();
const prevNodes = usePrevious(nodes);
const nodesInitialized = useNodesInitialized();
const { getEdges, setNodes, setEdges } = useReactFlow();
- const graph = useMemo(
- () => new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})),
- [],
- );
-
- const getLayoutedElements = useCallback(
- (nodes, edges) => {
- graph.setGraph({
- rankdir: 'TB',
- marginy: 60,
- marginx: 60,
- universalSep: true,
- });
-
- edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
- nodes.forEach((node) => graph.setNode(node.id, node));
-
- Dagre.layout(graph);
-
- return {
- nodes: nodes.map((node) => {
- const { x, y, width, height } = graph.node(node.id);
- return {
- ...node,
- position: { x: x - width / 2, y: y - height / 2 },
- };
- }),
- edges,
- };
- },
- [graph],
- );
-
const onLayout = useCallback(
(nodes, edges) => {
- const layouted = getLayoutedElements(nodes, edges);
+ const layoutedElements = getLayoutedElements(nodes, edges);
- setNodes([...layouted.nodes]);
- setEdges([...layouted.edges]);
+ setNodes([
+ ...layoutedElements.nodes.map((node) => ({
+ ...node,
+ data: { ...node.data, layouted: true },
+ })),
+ ]);
+ setEdges([
+ ...layoutedElements.edges.map((edge) => ({
+ ...edge,
+ data: { ...edge.data, layouted: true },
+ })),
+ ]);
},
- [setEdges, setNodes, getLayoutedElements],
+ [setEdges, setNodes],
);
useEffect(() => {