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 ( + <> + + + + + + + + + Step + Paths + + + + ); +} + +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 && ( + + Delete + + )} + + ); +} + +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 && ( + + Delete + + )} + + ); +} + +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;