import { useEffect, useCallback, createContext, useRef, useState, useMemo, } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { ReactFlow, useEdgesState, applyNodeChanges } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { v4 as uuidv4 } from 'uuid'; import { debounce } from 'lodash'; import useUpdateStep from 'hooks/useUpdateStep'; import useCreateStep from 'hooks/useCreateStep'; import { FlowPropType } from 'propTypes/propTypes'; import { useScrollBoundaries } from './useScrollBoundaries'; import FlowStepNode from './FlowStepNode/FlowStepNode'; import Edge from './Edge/Edge'; import InvisibleNode from './InvisibleNode/InvisibleNode'; import { getLaidOutElements, updatedCollapsedNodes } from './utils'; import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants'; import { EditorWrapper } from './style'; export const EdgesContext = createContext(); export const NodesContext = createContext(); const nodeTypes = { [NODE_TYPES.FLOW_STEP]: FlowStepNode, [NODE_TYPES.INVISIBLE]: InvisibleNode, }; const edgeTypes = { [EDGE_TYPES.ADD_NODE_EDGE]: Edge, }; const EditorNew = ({ flow }) => { const { mutateAsync: updateStep } = useUpdateStep(); const queryClient = useQueryClient(); const [nodes, setNodes] = useState([]); const [edges, setEdges, onEdgesChange] = useEdgesState(); const [containerHeight, setContainerHeight] = useState(null); const containerRef = useRef(null); const { mutateAsync: createStep, isPending: isCreateStepPending } = useCreateStep(flow?.id); useScrollBoundaries(containerHeight); const onStepDelete = useCallback( (nodeId) => { const prevEdge = edges.find((edge) => edge.target === nodeId); const edgeToDelete = edges.find((edge) => edge.source === nodeId); const newEdges = edges .map((edge) => { if ( edge.id === edgeToDelete?.id || (edge.id === prevEdge?.id && !edgeToDelete) ) { return null; } else if (edge.id === prevEdge?.id) { return { ...prevEdge, target: edgeToDelete?.target, }; } return edge; }) .filter((edge) => !!edge); setNodes((nodes) => { const newNodes = nodes.filter((node) => node.id !== nodeId); const laidOutElements = getLaidOutElements(newNodes, newEdges); setEdges([...laidOutElements.edges]); return [...laidOutElements.nodes]; }); }, [edges, setEdges], ); const onStepAdd = useCallback( async (previousStepId) => { const { data: createdStep } = await createStep({ previousStepId }); setNodes((nodes) => { const newNode = { id: createdStep.id, type: NODE_TYPES.FLOW_STEP, position: { x: 0, y: 0, }, data: { laidOut: false, }, }; const newNodes = nodes.flatMap((node) => { if (node.id === previousStepId) { return [node, newNode]; } return node; }); return updatedCollapsedNodes(newNodes, createdStep.id); }); setEdges((edges) => { const newEdges = edges .map((edge) => { if (edge.source === previousStepId) { const previousTarget = edge.target; return [ { ...edge, target: createdStep.id }, { id: uuidv4(), source: createdStep.id, target: previousTarget, type: EDGE_TYPES.ADD_NODE_EDGE, data: { laidOut: false, }, }, ]; } return edge; }) .flat(); return newEdges; }); }, [createStep, setEdges], ); const onStepAddDebounced = useMemo( () => debounce(onStepAdd, 300), [onStepAdd], ); const onNodesChange = useCallback( (changes) => { setNodes((oldNodes) => { const newNodes = applyNodeChanges(changes, oldNodes); if (changes?.some((change) => change.type === 'dimensions')) { const laidOutElements = getLaidOutElements(newNodes, edges); setEdges([...laidOutElements.edges]); return [...laidOutElements.nodes]; } else { return newNodes; } }); }, [setNodes, setEdges, edges], ); 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 payload = { id: step.id, key: step.key, parameters: step.parameters, connectionId: step.connection?.id, }; if (step.name || step.keyLabel) { payload.name = step.name || step.keyLabel; } if (step.appKey) { payload.appKey = step.appKey; } await updateStep(payload); await queryClient.invalidateQueries({ queryKey: ['steps', step.id, 'connection'], }); }, [updateStep, queryClient], ); useEffect(function initiateNodesAndEdges() { const newNodes = flow?.steps.map((step, index) => { return { id: step.id, type: NODE_TYPES.FLOW_STEP, position: { x: 0, y: 0, }, zIndex: index !== 0 ? 0 : 1, data: { collapsed: index !== 0, laidOut: false, }, }; }); newNodes.push({ id: INVISIBLE_NODE_ID, type: NODE_TYPES.INVISIBLE, position: { x: 0, y: 0, }, }); const newEdges = newNodes .map((node, i) => { const sourceId = node.id; const targetId = newNodes[i + 1]?.id; if (targetId) { return { id: uuidv4(), source: sourceId, target: targetId, type: 'addNodeEdge', data: { laidOut: false, }, }; } return null; }) .filter((edge) => !!edge); setNodes(newNodes); setEdges(newEdges); }, []); useEffect(function updateContainerHeightOnResize() { const updateHeight = () => { if (containerRef.current) { setContainerHeight(containerRef.current.clientHeight); } }; updateHeight(); window.addEventListener('resize', updateHeight); return () => { window.removeEventListener('resize', updateHeight); }; }, []); return ( ); }; EditorNew.propTypes = { flow: FlowPropType.isRequired, }; export default EditorNew;