diff --git a/packages/web/src/components/EditorNew/Edge/Edge.jsx b/packages/web/src/components/EditorNew/Edge/Edge.jsx
deleted file mode 100644
index ff535fbf..00000000
--- a/packages/web/src/components/EditorNew/Edge/Edge.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { EdgeLabelRenderer, getStraightPath } from 'reactflow';
-import IconButton from '@mui/material/IconButton';
-import AddIcon from '@mui/icons-material/Add';
-
-import PropTypes from 'prop-types';
-import { useContext } from 'react';
-import { EdgesContext } from '../EditorNew';
-
-export default function Edge({
- sourceX,
- sourceY,
- targetX,
- targetY,
- source,
- data: { laidOut },
-}) {
- const { stepCreationInProgress, flowActive, onAddStep } =
- useContext(EdgesContext);
-
- const [edgePath, labelX, labelY] = getStraightPath({
- sourceX,
- sourceY,
- targetX,
- targetY,
- });
-
- return (
- <>
-
- onAddStep(source)}
- color="primary"
- sx={{
- position: 'absolute',
- transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
- pointerEvents: 'all',
- visibility: laidOut ? 'visible' : 'hidden',
- }}
- disabled={stepCreationInProgress || 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({
- laidOut: PropTypes.bool,
- }).isRequired,
-};
diff --git a/packages/web/src/components/EditorNew/Edges/NodeEdge/NodeEdge.js b/packages/web/src/components/EditorNew/Edges/NodeEdge/NodeEdge.js
new file mode 100644
index 00000000..471dc3fd
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Edges/NodeEdge/NodeEdge.js
@@ -0,0 +1,69 @@
+import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
+import IconButton from '@mui/material/IconButton';
+import AddIcon from '@mui/icons-material/Add';
+
+import PropTypes from 'prop-types';
+import { useContext } from 'react';
+import { EdgesContext } from '../../EditorNew';
+import { Tooltip } from '@mui/material';
+
+export default function NodeEdge({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ source,
+ data: { laidOut },
+}) {
+ const { stepCreationInProgress, flowActive, onAddStep } =
+ useContext(EdgesContext);
+
+ const [edgePath, labelX, labelY] = getStraightPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+
+ const handleAddStep = () => {
+ onAddStep(source);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+NodeEdge.propTypes = {
+ sourceX: PropTypes.number.isRequired,
+ sourceY: PropTypes.number.isRequired,
+ targetX: PropTypes.number.isRequired,
+ targetY: PropTypes.number.isRequired,
+ source: PropTypes.string.isRequired,
+ data: PropTypes.shape({
+ laidOut: PropTypes.bool,
+ }).isRequired,
+};
diff --git a/packages/web/src/components/EditorNew/Edges/NodeOrPathsEdge/NodeOrPathsEdge.jsx b/packages/web/src/components/EditorNew/Edges/NodeOrPathsEdge/NodeOrPathsEdge.jsx
new file mode 100644
index 00000000..85712a85
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Edges/NodeOrPathsEdge/NodeOrPathsEdge.jsx
@@ -0,0 +1,95 @@
+import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
+import IconButton from '@mui/material/IconButton';
+import AddIcon from '@mui/icons-material/Add';
+import Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
+
+import PropTypes from 'prop-types';
+import { useContext, useState } from 'react';
+import { EdgesContext } from '../../EditorNew';
+import { Tooltip } from '@mui/material';
+
+export default function NodeOrPathsEdge({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ source,
+ data: { laidOut },
+}) {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const { stepCreationInProgress, flowActive, onAddStep, onAddPaths } =
+ useContext(EdgesContext);
+
+ const [edgePath, labelX, labelY] = getStraightPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+
+ const handleAddStep = () => {
+ onAddStep(source);
+ handleClose();
+ };
+
+ const handleAddPaths = () => {
+ onAddPaths(source);
+ handleClose();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+NodeOrPathsEdge.propTypes = {
+ sourceX: PropTypes.number.isRequired,
+ sourceY: PropTypes.number.isRequired,
+ targetX: PropTypes.number.isRequired,
+ targetY: PropTypes.number.isRequired,
+ source: PropTypes.string.isRequired,
+ data: PropTypes.shape({
+ laidOut: PropTypes.bool,
+ }).isRequired,
+};
diff --git a/packages/web/src/components/EditorNew/Edges/PathsEdge/PathsEdge.js b/packages/web/src/components/EditorNew/Edges/PathsEdge/PathsEdge.js
new file mode 100644
index 00000000..0caaa884
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Edges/PathsEdge/PathsEdge.js
@@ -0,0 +1,69 @@
+import { EdgeLabelRenderer, getStraightPath, BaseEdge } from 'reactflow';
+import IconButton from '@mui/material/IconButton';
+import AddIcon from '@mui/icons-material/Add';
+
+import PropTypes from 'prop-types';
+import { useContext } from 'react';
+import { EdgesContext } from '../../EditorNew';
+import { Tooltip } from '@mui/material';
+
+export default function PathsEdge({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ source,
+ data: { laidOut },
+}) {
+ const { stepCreationInProgress, flowActive, onAddPath } =
+ useContext(EdgesContext);
+
+ const [edgePath, labelX, labelY] = getStraightPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+
+ const handleAddPath = () => {
+ onAddPath(source);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+PathsEdge.propTypes = {
+ sourceX: PropTypes.number.isRequired,
+ sourceY: PropTypes.number.isRequired,
+ targetX: PropTypes.number.isRequired,
+ targetY: PropTypes.number.isRequired,
+ source: PropTypes.string.isRequired,
+ data: PropTypes.shape({
+ laidOut: PropTypes.bool,
+ }).isRequired,
+};
diff --git a/packages/web/src/components/EditorNew/EditorNew.jsx b/packages/web/src/components/EditorNew/EditorNew.jsx
index 066d60a4..f94b42e3 100644
--- a/packages/web/src/components/EditorNew/EditorNew.jsx
+++ b/packages/web/src/components/EditorNew/EditorNew.jsx
@@ -1,25 +1,24 @@
import { useEffect, useCallback, createContext, useRef } from 'react';
-import { useMutation } from '@apollo/client';
-import { useQueryClient } from '@tanstack/react-query';
+// import { useMutation } from '@apollo/client';
+// import { useQueryClient } from '@tanstack/react-query';
import { FlowPropType } from 'propTypes/propTypes';
import ReactFlow, { useNodesState, useEdgesState } from 'reactflow';
import 'reactflow/dist/style.css';
-import { UPDATE_STEP } from 'graphql/mutations/update-step';
-import { CREATE_STEP } from 'graphql/mutations/create-step';
+// import { UPDATE_STEP } from 'graphql/mutations/update-step';
+// import { CREATE_STEP } from 'graphql/mutations/create-step';
import { useAutoLayout } from './useAutoLayout';
-import { useScrollBoundaries } from './useScrollBoundaries';
-import FlowStepNode from './FlowStepNode/FlowStepNode';
-import Edge from './Edge/Edge';
-import InvisibleNode from './InvisibleNode/InvisibleNode';
+import NodeOrPathsEdge from './Edges/NodeOrPathsEdge/NodeOrPathsEdge';
+import FlowStepNode from './Nodes/FlowStepNode/FlowStepNode';
+import InvisibleNode from './Nodes/InvisibleNode/InvisibleNode';
+import PathsNode from './Nodes/PathsNode/PathsNode';
import { EditorWrapper } from './style';
-import {
- generateEdgeId,
- generateInitialEdges,
- generateInitialNodes,
- updatedCollapsedNodes,
-} from './utils';
-import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
+import { generateEdges, generateNodes, updatedCollapsedNodes } from './utils';
+import { EDGE_TYPES, NODE_TYPES } from './constants';
+import { useFlow } from './temp/useFlow';
+import PathNode from './Nodes/PathNode/PathNode';
+import PathsEdge from './Edges/PathsEdge/PathsEdge';
+import NodeEdge from './Edges/NodeEdge/NodeEdge';
export const EdgesContext = createContext();
export const NodesContext = createContext();
@@ -27,248 +26,156 @@ export const NodesContext = createContext();
const nodeTypes = {
[NODE_TYPES.FLOW_STEP]: FlowStepNode,
[NODE_TYPES.INVISIBLE]: InvisibleNode,
+ [NODE_TYPES.PATHS]: PathsNode,
+ [NODE_TYPES.PATH]: PathNode,
};
const edgeTypes = {
- [EDGE_TYPES.ADD_NODE_EDGE]: Edge,
+ [EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE]: NodeOrPathsEdge,
+ [EDGE_TYPES.ADD_PATH_EDGE]: PathsEdge,
+ [EDGE_TYPES.ADD_NODE_EDGE]: NodeEdge,
};
-const EditorNew = ({ flow }) => {
- const [updateStep] = useMutation(UPDATE_STEP);
- const queryClient = useQueryClient();
- const [createStep, { loading: stepCreationInProgress }] =
- useMutation(CREATE_STEP);
+const EditorNew = () =>
+ // { flow }
+ {
+ const { flow, createStep, createPaths, createPath } = useFlow();
+ // const [updateStep] = useMutation(UPDATE_STEP);
+ // const queryClient = useQueryClient();
+ // const [createStep, { loading: stepCreationInProgress }] =
+ // useMutation(CREATE_STEP);
+ const stepCreationInProgress = false;
- const [nodes, setNodes, onNodesChange] = useNodesState(
- generateInitialNodes(flow),
- );
- const [edges, setEdges, onEdgesChange] = useEdgesState(
- generateInitialEdges(flow),
- );
+ const [nodes, setNodes, onNodesChange] = useNodesState(
+ generateNodes({ steps: flow.steps }),
+ );
+ const [edges, setEdges, onEdgesChange] = useEdgesState(
+ generateEdges({ steps: flow.steps }),
+ );
- useAutoLayout();
- useScrollBoundaries();
+ useAutoLayout();
- const createdStepIdRef = useRef(null);
+ const createdStepIdRef = useRef(null);
- const openNextStep = useCallback(
- (currentStepId) => {
- setNodes((nodes) => {
- const currentStepIndex = nodes.findIndex(
- (node) => node.id === currentStepId,
- );
- if (currentStepIndex >= 0) {
- const nextStep = nodes[currentStepIndex + 1];
- return updatedCollapsedNodes(nodes, nextStep.id);
- }
- return nodes;
- });
- },
- [setNodes],
- );
-
- const onStepClose = useCallback(() => {
- setNodes((nodes) => updatedCollapsedNodes(nodes));
- }, [setNodes]);
-
- const onStepOpen = useCallback(
- (stepId) => {
- setNodes((nodes) => updatedCollapsedNodes(nodes, stepId));
- },
- [setNodes],
- );
-
- const onStepChange = useCallback(
- async (step) => {
- const mutationInput = {
- id: step.id,
- key: step.key,
- parameters: step.parameters,
- connection: {
- id: step.connection?.id,
- },
- flow: {
- id: flow.id,
- },
- };
-
- if (step.appKey) {
- mutationInput.appKey = step.appKey;
- }
-
- await updateStep({
- variables: { input: mutationInput },
- });
- await queryClient.invalidateQueries({
- queryKey: ['steps', step.id, 'connection'],
- });
- await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
- },
- [flow.id, updateStep, queryClient],
- );
-
- const onAddStep = useCallback(
- async (previousStepId) => {
- const mutationInput = {
- previousStep: {
- id: previousStepId,
- },
- flow: {
- id: flow.id,
- },
- };
-
- const {
- data: { createStep: createdStep },
- } = await createStep({
- variables: { input: mutationInput },
- });
-
- const createdStepId = createdStep.id;
- await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
- createdStepIdRef.current = createdStepId;
- },
- [flow.id, createStep, queryClient],
- );
-
- useEffect(() => {
- if (flow.steps.length + 1 !== nodes.length) {
- setNodes((nodes) => {
- const newNodes = flow.steps.map((step) => {
- const createdStepId = createdStepIdRef.current;
- const prevNode = nodes.find(({ id }) => id === step.id);
- if (prevNode) {
- return {
- ...prevNode,
- zIndex: createdStepId ? 0 : prevNode.zIndex,
- data: {
- ...prevNode.data,
- collapsed: createdStepId ? true : prevNode.data.collapsed,
- },
- };
- } else {
- return {
- id: step.id,
- type: NODE_TYPES.FLOW_STEP,
- position: {
- x: 0,
- y: 0,
- },
- zIndex: 1,
- data: {
- collapsed: false,
- laidOut: false,
- },
- };
+ const openNextStep = useCallback(
+ (currentStepId) => {
+ setNodes((nodes) => {
+ const currentStepIndex = nodes.findIndex(
+ (node) => node.id === currentStepId,
+ );
+ if (currentStepIndex >= 0) {
+ const nextStep = nodes[currentStepIndex + 1];
+ return updatedCollapsedNodes(nodes, nextStep.id);
}
+ return nodes;
});
+ },
+ [setNodes],
+ );
- const prevInvisible = nodes.find(({ id }) => id === INVISIBLE_NODE_ID);
- return [
- ...newNodes,
- {
- id: INVISIBLE_NODE_ID,
- type: NODE_TYPES.INVISIBLE,
- position: {
- x: prevInvisible?.position.x || 0,
- y: prevInvisible?.position.y || 0,
- },
- },
- ];
- });
+ const onStepClose = useCallback(() => {
+ setNodes((nodes) => updatedCollapsedNodes(nodes));
+ }, [setNodes]);
- setEdges((edges) => {
- const newEdges = flow.steps
- .map((step, i) => {
- const sourceId = step.id;
- const targetId = flow.steps[i + 1]?.id;
- const edge = edges?.find(
- (edge) => edge.id === generateEdgeId(sourceId, targetId),
- );
- if (targetId) {
- return {
- id: generateEdgeId(sourceId, targetId),
- source: sourceId,
- target: targetId,
- type: 'addNodeEdge',
- data: {
- laidOut: edge ? edge?.data.laidOut : false,
- },
- };
- }
- return null;
- })
- .filter((edge) => !!edge);
+ const onStepOpen = useCallback(
+ (stepId) => {
+ setNodes((nodes) => updatedCollapsedNodes(nodes, stepId));
+ },
+ [setNodes],
+ );
- const lastStep = flow.steps[flow.steps.length - 1];
- const lastEdge = edges[edges.length - 1];
+ const onStepChange = useCallback(
+ async (step) => {
+ // const mutationInput = {
+ // id: step.id,
+ // key: step.key,
+ // parameters: step.parameters,
+ // connection: {
+ // id: step.connection?.id,
+ // },
+ // flow: {
+ // id: flow.id,
+ // },
+ // };
+ // if (step.appKey) {
+ // mutationInput.appKey = step.appKey;
+ // }
+ // const updated = await updateStep({
+ // variables: { input: mutationInput },
+ // });
+ // await queryClient.invalidateQueries({
+ // queryKey: ['steps', step.id, 'connection'],
+ // });
+ // await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
+ },
+ // [flow.id, updateStep, queryClient],
+ );
- return lastStep
- ? [
- ...newEdges,
- {
- id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
- source: lastStep.id,
- target: INVISIBLE_NODE_ID,
- type: 'addNodeEdge',
- data: {
- laidOut:
- lastEdge?.id ===
- generateEdgeId(lastStep.id, INVISIBLE_NODE_ID)
- ? lastEdge?.data.laidOut
- : false,
- },
- },
- ]
- : newEdges;
- });
+ const onAddStep = async (previousStepId) => {
+ const createdStepId = createStep(flow, previousStepId);
+ createdStepIdRef.current = createdStepId;
+ };
+
+ console.log({ flow });
+
+ useEffect(() => {
+ // if (flow.steps.length + 1 !== nodes.length) {
+ setNodes((nodes) =>
+ generateNodes({
+ prevNodes: nodes,
+ steps: flow.steps,
+ createdStepId: createdStepIdRef.current,
+ }),
+ );
+
+ setEdges((edges) =>
+ generateEdges({ prevEdges: edges, steps: flow.steps }),
+ );
if (createdStepIdRef.current) {
createdStepIdRef.current = null;
}
- }
- }, [flow.steps]);
+ // }
+ }, [flow.steps]);
- return (
-
-
-
-
-
-
-
- );
-};
+
+
+
+
+
+
+ );
+ };
EditorNew.propTypes = {
flow: FlowPropType.isRequired,
diff --git a/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx b/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx
deleted file mode 100644
index 545331c9..00000000
--- a/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Handle, Position } from 'reactflow';
-import PropTypes from 'prop-types';
-
-import FlowStep from 'components/FlowStep';
-
-import { NodeWrapper, NodeInnerWrapper } from './style.js';
-import { useContext } from 'react';
-import { NodesContext } from '../EditorNew.jsx';
-
-function FlowStepNode({ data: { collapsed, laidOut }, id }) {
- const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
- useContext(NodesContext);
-
- const step = steps.find(({ id: stepId }) => stepId === id);
-
- return (
-
-
-
- {step && (
- onStepOpen(step.id)}
- onClose={onStepClose}
- onChange={onStepChange}
- flowId={flowId}
- onContinue={() => openNextStep(step.id)}
- />
- )}
-
-
-
- );
-}
-
-FlowStepNode.propTypes = {
- id: PropTypes.string,
- data: PropTypes.shape({
- collapsed: PropTypes.bool.isRequired,
- laidOut: PropTypes.bool.isRequired,
- }).isRequired,
-};
-
-export default FlowStepNode;
diff --git a/packages/web/src/components/EditorNew/Nodes/FlowStepNode/FlowStepNode.jsx b/packages/web/src/components/EditorNew/Nodes/FlowStepNode/FlowStepNode.jsx
new file mode 100644
index 00000000..5a4a4abd
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Nodes/FlowStepNode/FlowStepNode.jsx
@@ -0,0 +1,69 @@
+import { Handle, Position } from 'reactflow';
+import PropTypes from 'prop-types';
+
+import FlowStep from 'components/FlowStep';
+
+import { NodeWrapper, NodeInnerWrapper } from './style.js';
+import { useContext } from 'react';
+import { NodesContext } from '../../EditorNew.jsx';
+import { findStepByStepId } from 'components/EditorNew/utils.js';
+
+function FlowStepNode({ data: { collapsed, laidOut }, id }) {
+ const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
+ useContext(NodesContext);
+
+ const step = findStepByStepId({ steps }, id);
+
+ return (
+ //
+
+
+ {step && (
+ onStepOpen(step.id)}
+ onClose={onStepClose}
+ onChange={onStepChange}
+ flowId={flowId}
+ onContinue={() => openNextStep(step.id)}
+ collapseAnimation={false}
+ />
+ )}
+
+
+ //
+ );
+}
+
+FlowStepNode.propTypes = {
+ id: PropTypes.string,
+ data: PropTypes.shape({
+ collapsed: PropTypes.bool.isRequired,
+ laidOut: PropTypes.bool.isRequired,
+ }).isRequired,
+};
+
+export default FlowStepNode;
diff --git a/packages/web/src/components/EditorNew/FlowStepNode/style.js b/packages/web/src/components/EditorNew/Nodes/FlowStepNode/style.js
similarity index 95%
rename from packages/web/src/components/EditorNew/FlowStepNode/style.js
rename to packages/web/src/components/EditorNew/Nodes/FlowStepNode/style.js
index f5d11366..83317064 100644
--- a/packages/web/src/components/EditorNew/FlowStepNode/style.js
+++ b/packages/web/src/components/EditorNew/Nodes/FlowStepNode/style.js
@@ -9,6 +9,6 @@ export const NodeWrapper = styled(Box)(({ theme }) => ({
}));
export const NodeInnerWrapper = styled(Box)(({ theme }) => ({
- maxWidth: 900,
+ width: 900,
flex: 1,
}));
diff --git a/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx b/packages/web/src/components/EditorNew/Nodes/InvisibleNode/InvisibleNode.jsx
similarity index 100%
rename from packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx
rename to packages/web/src/components/EditorNew/Nodes/InvisibleNode/InvisibleNode.jsx
diff --git a/packages/web/src/components/EditorNew/Nodes/PathNode/PathNode.jsx b/packages/web/src/components/EditorNew/Nodes/PathNode/PathNode.jsx
new file mode 100644
index 00000000..14764d04
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Nodes/PathNode/PathNode.jsx
@@ -0,0 +1,98 @@
+import PropTypes from 'prop-types';
+import { Handle, Position } from 'reactflow';
+import { Box, Stack, Typography } from '@mui/material';
+import { useRef, useState } from 'react';
+import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
+import Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
+
+import IconButton from '@mui/material/IconButton';
+import { Wrapper } from './style';
+
+/* TODO
+ - add delete
+ - add rename
+ - add translations
+ - add collapsing?
+*/
+
+function PathNode({ data: { laidOut } }) {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const contextButtonRef = useRef(null);
+
+ const onContextMenuClose = (event) => {
+ event.stopPropagation();
+ setAnchorEl(null);
+ };
+
+ const onContextMenuClick = (event) => {
+ event.stopPropagation();
+ setAnchorEl(contextButtonRef.current);
+ };
+
+ const deletePath = () => {
+ setAnchorEl(null);
+ onContextMenuClose();
+ };
+
+ return (
+ <>
+
+
+
+
+
+ Path
+
+
+
+
+
+
+
+ {anchorEl && (
+
+ )}
+ >
+ );
+}
+
+PathNode.propTypes = {
+ data: PropTypes.shape({
+ laidOut: PropTypes.bool,
+ }).isRequired,
+};
+
+export default PathNode;
diff --git a/packages/web/src/components/EditorNew/Nodes/PathNode/style.js b/packages/web/src/components/EditorNew/Nodes/PathNode/style.js
new file mode 100644
index 00000000..43c39794
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Nodes/PathNode/style.js
@@ -0,0 +1,8 @@
+import { styled } from '@mui/material/styles';
+import Box from '@mui/material/Box';
+
+export const Wrapper = styled(Box)(({ theme }) => ({
+ padding: theme.spacing(1, 2),
+ backgroundColor: '#0059f714',
+ borderRadius: 20,
+}));
diff --git a/packages/web/src/components/EditorNew/Nodes/PathsNode/PathsNode.jsx b/packages/web/src/components/EditorNew/Nodes/PathsNode/PathsNode.jsx
new file mode 100644
index 00000000..157e0f0f
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Nodes/PathsNode/PathsNode.jsx
@@ -0,0 +1,108 @@
+import PropTypes from 'prop-types';
+import { Handle, Position } from 'reactflow';
+import { Avatar, Box, Stack, Typography } from '@mui/material';
+import { useRef, useState } from 'react';
+import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
+import Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
+import CallSplitIcon from '@mui/icons-material/CallSplit';
+
+import IconButton from '@mui/material/IconButton';
+import { Wrapper } from './style';
+
+/* TODO
+ - add delete
+ - add rename
+ - add translations
+ - add collapsing?
+*/
+
+function PathsNode({ data: { laidOut } }) {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const contextButtonRef = useRef(null);
+
+ const onContextMenuClose = (event) => {
+ event.stopPropagation();
+ setAnchorEl(null);
+ };
+
+ const onContextMenuClick = (event) => {
+ event.stopPropagation();
+ setAnchorEl(contextButtonRef.current);
+ };
+
+ const deletePaths = () => {
+ setAnchorEl(null);
+ onContextMenuClose();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* TODO name from path data */}
+ Paths
+
+
+
+
+
+
+
+
+ {anchorEl && (
+
+ )}
+ >
+ );
+}
+
+PathsNode.propTypes = {
+ data: PropTypes.shape({
+ laidOut: PropTypes.bool,
+ }).isRequired,
+};
+
+export default PathsNode;
diff --git a/packages/web/src/components/EditorNew/Nodes/PathsNode/style.js b/packages/web/src/components/EditorNew/Nodes/PathsNode/style.js
new file mode 100644
index 00000000..a03f5160
--- /dev/null
+++ b/packages/web/src/components/EditorNew/Nodes/PathsNode/style.js
@@ -0,0 +1,7 @@
+import { styled } from '@mui/material/styles';
+import Card from '@mui/material/Card';
+
+export const Wrapper = styled(Card)`
+ width: 100%;
+ padding: ${({ theme }) => theme.spacing(2)};
+`;
diff --git a/packages/web/src/components/EditorNew/constants.js b/packages/web/src/components/EditorNew/constants.js
index 1c73c4d5..3a5ce803 100644
--- a/packages/web/src/components/EditorNew/constants.js
+++ b/packages/web/src/components/EditorNew/constants.js
@@ -3,8 +3,12 @@ export const INVISIBLE_NODE_ID = 'invisible-node';
export const NODE_TYPES = {
FLOW_STEP: 'flowStep',
INVISIBLE: 'invisible',
+ PATHS: 'parallelPaths',
+ PATH: 'path',
};
export const EDGE_TYPES = {
+ ADD_NODE_OR_PATHS_EDGE: 'addNodeOrPathsEdge',
+ ADD_PATH_EDGE: 'addPathEdge',
ADD_NODE_EDGE: 'addNodeEdge',
};
diff --git a/packages/web/src/components/EditorNew/style.js b/packages/web/src/components/EditorNew/style.js
index d55ce0a0..2890991c 100644
--- a/packages/web/src/components/EditorNew/style.js
+++ b/packages/web/src/components/EditorNew/style.js
@@ -7,7 +7,7 @@ export const EditorWrapper = styled(Stack)(({ theme }) => ({
flexGrow: 1,
},
- '& .react-flow__pane, & .react-flow__node': {
- cursor: 'auto !important',
- },
+ // '& .react-flow__pane, & .react-flow__node': {
+ // cursor: 'auto !important',
+ // },
}));
diff --git a/packages/web/src/components/EditorNew/temp/useFlow.js b/packages/web/src/components/EditorNew/temp/useFlow.js
new file mode 100644
index 00000000..2e0a801d
--- /dev/null
+++ b/packages/web/src/components/EditorNew/temp/useFlow.js
@@ -0,0 +1,96 @@
+import { useState } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+import { insertStep } from '../utils';
+
+const initFlow = {
+ id: '7c55e6ce-a84a-46e3-ba31-211ec7b5c2cb',
+ name: 'Name your flow',
+ active: false,
+ status: 'draft',
+ createdAt: 1718264916266,
+ updatedAt: 1718264916266,
+ steps: [
+ {
+ id: '82ce34ab-7aab-4e6c-9f62-db5104aa81c6',
+ type: 'trigger',
+ key: null,
+ appKey: null,
+ iconUrl: null,
+ webhookUrl: 'http://localhost:3000/null',
+ status: 'incomplete',
+ position: 1,
+ parameters: {},
+ },
+ {
+ id: '41c60527-eb4f-4f2d-93ec-2fd37e336909',
+ type: 'action',
+ key: null,
+ appKey: null,
+ iconUrl: null,
+ webhookUrl: 'http://localhost:3000/null',
+ status: 'incomplete',
+ position: 2,
+ parameters: {},
+ },
+ ],
+};
+
+const generateStep = () => {
+ return {
+ id: uuidv4(),
+ type: 'action',
+ key: null,
+ appKey: null,
+ parameters: {},
+ iconUrl: null,
+ webhookUrl: 'http://localhost:3000/null',
+ status: 'incomplete',
+ connection: null,
+ position: null,
+ };
+};
+
+const generatePath = (steps) => {
+ return {
+ id: uuidv4(),
+ type: 'path',
+ steps: steps?.length > 0 ? steps : [generateStep()],
+ };
+};
+
+export const generatePaths = (steps) => {
+ return {
+ id: uuidv4(),
+ type: 'parallelPaths',
+ steps: [generatePath(steps), generatePath()],
+ };
+};
+
+export const useFlow = () => {
+ const [flow, setFlow] = useState(initFlow);
+
+ const createStep = (flow, previousStepId) => {
+ const newStep = generateStep();
+ const newFlow = insertStep(flow, previousStepId, newStep);
+
+ setFlow(newFlow);
+ return newStep.id;
+ };
+
+ const createPaths = (previousStepId) => {
+ const newFlow = insertStep(flow, previousStepId, generatePaths());
+ setFlow(newFlow);
+ };
+
+ const createPath = (previousStepId) => {
+ const newFlow = insertStep(flow, previousStepId, generatePath());
+ setFlow(newFlow);
+ };
+
+ return {
+ flow,
+ createStep,
+ createPaths,
+ createPath,
+ };
+};
diff --git a/packages/web/src/components/EditorNew/useAutoLayout.js b/packages/web/src/components/EditorNew/useAutoLayout.js
index a4dec029..049666e6 100644
--- a/packages/web/src/components/EditorNew/useAutoLayout.js
+++ b/packages/web/src/components/EditorNew/useAutoLayout.js
@@ -32,7 +32,7 @@ export const useAutoLayout = () => {
const nodes = useNodes();
const prevNodes = usePrevious(nodes);
const nodesInitialized = useNodesInitialized();
- const { getEdges, setNodes, setEdges } = useReactFlow();
+ const { getEdges, setNodes, setEdges, fitView } = useReactFlow();
const onLayout = useCallback(
(nodes, edges) => {
@@ -62,6 +62,8 @@ export const useAutoLayout = () => {
prevNodes.map(({ width, height }) => ({ width, height })),
);
+ fitView();
+
if (shouldAutoLayout) {
onLayout(nodes, getEdges());
}
diff --git a/packages/web/src/components/EditorNew/utils.js b/packages/web/src/components/EditorNew/utils.js
index cbaede60..8c4f657a 100644
--- a/packages/web/src/components/EditorNew/utils.js
+++ b/packages/web/src/components/EditorNew/utils.js
@@ -1,6 +1,8 @@
-import { INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
+import cloneDeep from 'lodash/cloneDeep.js';
+import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
-export const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`;
+export const generateEdgeId = (sourceId, targetId) =>
+ `${sourceId}--${targetId}`;
export const updatedCollapsedNodes = (nodes, openStepId) => {
return nodes.map((node) => {
@@ -17,72 +19,227 @@ export const updatedCollapsedNodes = (nodes, openStepId) => {
});
};
-export const generateInitialNodes = (flow) => {
- const newNodes = flow.steps.map((step, index) => {
+export const generateNodes = ({ steps, prevNodes, createdStepId }) => {
+ const newNodes = steps.map((step, index) => {
const collapsed = index !== 0;
- return {
- id: step.id,
- type: NODE_TYPES.FLOW_STEP,
- position: {
- x: 0,
- y: 0,
- },
- zIndex: collapsed ? 0 : 1,
- data: {
- collapsed,
- laidOut: false,
- },
- };
+ const prevNode = prevNodes?.find(({ id }) => id === step.id);
+
+ let newNode;
+ let childSteps = [];
+
+ switch (step.type) {
+ case 'trigger':
+ case 'action': {
+ if (prevNode) {
+ newNode = {
+ ...prevNode,
+ zIndex: createdStepId ? 0 : prevNode?.zIndex || 0,
+ data: {
+ ...prevNode.data,
+ collapsed: createdStepId
+ ? true
+ : prevNode?.data?.collapsed || true,
+ },
+ };
+ } else {
+ newNode = {
+ id: step.id,
+ type: NODE_TYPES.FLOW_STEP,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ zIndex: collapsed ? 0 : 1,
+ data: {
+ collapsed,
+ laidOut: false,
+ },
+ };
+ }
+ break;
+ }
+ case 'parallelPaths': {
+ if (prevNode) {
+ newNode = prevNode;
+ } else {
+ newNode = {
+ id: step.id,
+ type: NODE_TYPES.PATHS,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ data: {
+ laidOut: false,
+ },
+ };
+ }
+
+ break;
+ }
+ case 'path': {
+ if (prevNode) {
+ newNode = prevNode;
+ } else {
+ newNode = {
+ id: step.id,
+ type: NODE_TYPES.PATH,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ data: {
+ laidOut: false,
+ },
+ };
+ }
+ break;
+ }
+ default:
+ }
+
+ if (step?.steps?.length > 0) {
+ childSteps = generateNodes({
+ steps: step.steps,
+ prevNodes,
+ createdStepId,
+ });
+ }
+
+ return [newNode, ...childSteps];
});
- return [
- ...newNodes,
- {
- id: INVISIBLE_NODE_ID,
- type: NODE_TYPES.INVISIBLE,
- position: {
- x: 0,
- y: 0,
- },
- },
- ];
+ return newNodes.flat(Infinity);
};
-export const generateInitialEdges = (flow) => {
- const newEdges = flow.steps
- .map((step, i) => {
- const sourceId = step.id;
- const targetId = flow.steps[i + 1]?.id;
- if (targetId) {
- return {
- id: generateEdgeId(sourceId, targetId),
- source: sourceId,
- target: targetId,
- type: 'addNodeEdge',
- data: {
- laidOut: false,
- },
- };
+export const generateEdges = ({ steps }) => {
+ const newEdges = steps.map((step, index) => {
+ switch (step.type) {
+ case 'parallelPaths': {
+ const edges = step.steps.map((childStep) => {
+ const sourceId = step.id;
+ const targetId = childStep.id;
+
+ const newEdge = {
+ id: generateEdgeId(sourceId, targetId),
+ source: sourceId,
+ target: targetId,
+ type: EDGE_TYPES.ADD_PATH_EDGE,
+ data: {
+ laidOut: false,
+ },
+ };
+
+ return newEdge;
+ });
+
+ const childEdges = generateEdges({ steps: step.steps });
+
+ return [...edges, ...childEdges];
}
- return null;
- })
- .filter((edge) => !!edge);
+ case 'path': {
+ console.log({ step });
- const lastStep = flow.steps[flow.steps.length - 1];
+ const sourceId = step.id;
+ const targetId = step.steps?.[0]?.id;
- return lastStep
- ? [
- ...newEdges,
- {
- id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
- source: lastStep.id,
- target: INVISIBLE_NODE_ID,
- type: 'addNodeEdge',
- data: {
- laidOut: false,
- },
- },
- ]
- : newEdges;
+ if (targetId) {
+ const newEdge = {
+ id: generateEdgeId(sourceId, targetId),
+ source: sourceId,
+ target: targetId,
+ type: EDGE_TYPES.ADD_NODE_EDGE,
+ data: {
+ laidOut: false,
+ },
+ };
+ const childEdges = generateEdges({ steps: step.steps });
+
+ return [newEdge, ...childEdges];
+ }
+ return null;
+ }
+ default: {
+ const sourceId = step.id;
+ const targetId = steps[index + 1]?.id;
+
+ if (targetId) {
+ return {
+ id: generateEdgeId(sourceId, targetId),
+ source: sourceId,
+ target: targetId,
+ type: EDGE_TYPES.ADD_NODE_OR_PATHS_EDGE,
+ data: {
+ laidOut: false,
+ },
+ };
+ }
+ return null;
+ }
+ }
+ });
+
+ return newEdges.flat(Infinity).filter((edge) => !!edge);
};
+
+export const findStepByStepId = (obj, id) => {
+ if (Array.isArray(obj.steps)) {
+ for (const step of obj.steps) {
+ if (step.id === id) {
+ return step;
+ }
+ const result = findStepByStepId(step, id);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+};
+
+export function insertStep(parentObj, id, newStep) {
+ function recursiveFindAndInsert(parentObj, id, newStep) {
+ if (parentObj.steps && Array.isArray(parentObj.steps)) {
+ for (let index = 0; index < parentObj.steps.length; index++) {
+ const step = parentObj.steps[index];
+ if (step.id === id) {
+ if (newStep.type === NODE_TYPES.PATHS) {
+ const stepsAfter = parentObj.steps.slice(
+ index + 1,
+ parentObj.steps.length,
+ );
+ parentObj.steps.splice(index + 1);
+ newStep.steps[0].steps = stepsAfter;
+ parentObj.steps.splice(index + 1, 0, newStep);
+ } else if (step.type === NODE_TYPES.PATHS) {
+ step.steps.push(newStep);
+ } else if (step.type === NODE_TYPES.PATH) {
+ step.steps.unshift(newStep);
+ } else {
+ const originalSteps = step.steps || [];
+ step.steps = [];
+
+ const newStepObject = {
+ ...newStep,
+ steps: originalSteps,
+ };
+
+ parentObj.steps.splice(index + 1, 0, newStepObject);
+ }
+ return true;
+ }
+ const found = recursiveFindAndInsert(step, id, newStep);
+ if (found) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Clone the input object to avoid mutating the original
+ const newParentObj = cloneDeep(parentObj);
+ recursiveFindAndInsert(newParentObj, id, newStep);
+ return newParentObj;
+}
diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx
index 853c24bb..040f67e0 100644
--- a/packages/web/src/components/FlowStep/index.jsx
+++ b/packages/web/src/components/FlowStep/index.jsx
@@ -105,7 +105,7 @@ function generateValidationSchema(substeps) {
}
function FlowStep(props) {
- const { collapsed, onChange, onContinue, flowId } = props;
+ const { collapsed, onChange, onContinue, flowId, collapseAnimation } = props;
const editorContext = React.useContext(EditorContext);
const contextButtonRef = React.useRef(null);
const step = props.step;
@@ -259,7 +259,11 @@ function FlowStep(props) {
-
+
@@ -364,6 +368,11 @@ FlowStep.propTypes = {
onClose: PropTypes.func,
onChange: PropTypes.func.isRequired,
onContinue: PropTypes.func,
+ collapseAnimation: PropTypes.bool,
+};
+
+FlowStep.defaultProps = {
+ collapseAnimation: true,
};
export default FlowStep;