import { useState, useCallback, useEffect } from 'react'; import { ReactFlow, addEdge, Controls, Background, BackgroundVariant, MarkerType, useReactFlow, ReactFlowProvider, useNodesState, useEdgesState, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import '../assets/css/style.css'; import Tabbar from './Tabbar.tsx'; import ShowPopup from './ShowPopup.tsx'; import { nanoid } from 'nanoid'; import CustomEdge from './CustomEdge'; import MenuFlow from './MenuFlow'; // Nội dung cho mỗi node const nodeContents = { 'send-email': ` `, 'customer-sign': ` `, ifelse: ` `, 'contact-exists': ` `, 'send-survey': ` `, }; const NodeFlow = ({ initialNodes, initialEdges }) => { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); const [showTabBar, setShowTabBar] = useState(false); const [lastNodeId, setLastNodeId] = useState(nodes[0].id); const [showPopup, setShowPopup] = useState(false); const [selectedNodeContent, setSelectedNodeContent] = useState(''); const [addNodeCallback, setAddNodeCallback] = useState(() => () => {}); const [isTabbarVisible, setIsTabbarVisible] = useState(false); const { screenToFlowPosition } = useReactFlow(); const [showActions, setShowActions] = useState(null); const [showPopupSuccess, setShowPopupSuccess] = useState(false); const handleAddNoteClick = () => { setShowTabBar(true); setIsTabbarVisible(true); setAddNodeCallback(() => (selectedNode) => { addNodeBetween(selectedNode, sourceId, targetId); }); }; const closeTabbar = () => { setIsTabbarVisible(false); // Ẩn tabbar khi click close }; const onNodeClick = (event, node) => { if (node.key === 'add-node') { // Check if the clicked node is "Add Node" setShowTabBar(true); setIsTabbarVisible(true); } }; const getNextYPosition = (xPosition, spacing) => { const nodesInColumn = nodes.filter((node) => node.position.x === xPosition); if (nodesInColumn.length === 0) return 0; // Return 0 if there are no nodes const lastNode = nodesInColumn[nodesInColumn.length - 1]; return lastNode.position.y + spacing; }; const addNode = (option) => { const newNodeId = nanoid(8); // Generate a unique ID for the new node const addNoteNode = nodes.find((node) => node.key === 'add-node'); const initialX = addNoteNode?.position.x; const nextY = nodes.length > 2 ? getNextYPosition(initialX, 120) : addNoteNode?.position.y; // Remove existing edges from addNoteNode setEdges((eds) => eds.filter((edge) => edge.source !== '2' && edge.target !== '2') ); if (option.id === '4') { const repliedNode = { id: newNodeId, key: option.key, data: { label: option.htmlNode }, position: { x: initialX, y: nextY }, }; const yesNode = { id: nanoid(8), type: 'output', key: 'contact-exists', data: { label: (
Contact Exists
), }, position: { x: initialX - 150, y: nextY + 150 }, }; const noNode = { id: nanoid(8), type: '', key: 'send-survey', data: { label: (
Send survey
), }, position: { x: initialX + 150, y: nextY + 150 }, }; // Di chuyển node add-note sang nhánh no setNodes((nds) => nds.map((node) => node.id === '2' ? { ...node, position: { x: noNode.position.x, y: noNode.position.y + 150, }, } : node ) ); // Thêm các node mới (replied, yes, no) setNodes((nds) => [...nds, repliedNode, yesNode, noNode]); // Kết nối các edges giữa các node setEdges((eds) => [ ...eds, { id: nanoid(8), source: repliedNode.id, target: yesNode.id, label: 'yes', }, { id: nanoid(8), source: repliedNode.id, target: noNode.id, label: 'no', }, { id: nanoid(8), source: lastNodeId, target: repliedNode.id }, { id: nanoid(8), source: noNode.id, target: addNoteNode.id }, ]); setLastNodeId(noNode.id); } else { // Add new node logic if (addNoteNode) { const initialX = addNoteNode.position.x; setNodes((nds) => nds.map((node) => node.key === 'add-node' ? { ...node, position: { x: initialX, y: nextY + 130, }, } : node ) ); } const newNode = { id: newNodeId, key: option.key, data: { label: option.htmlNode }, position: { x: initialX, y: nodes.length > 2 ? nextY : addNoteNode.position.y, }, }; const newEdge = { id: nanoid(8), source: lastNodeId, target: newNodeId, // data: { // label:
+
, // onClick: handleAddNoteClick // } }; setNodes((nds) => [...nds, newNode]); setEdges((eds) => [ ...eds, newEdge, { id: nanoid(8), source: newNodeId, target: addNoteNode.id }, ]); // setEdges((eds) => [ // ...eds, // { // id: nanoid(8), // source: lastNodeId, // data: { // label:
Add Note
, // Use HTML or React component // onClick: handleAddNoteClick // }, // target: newNodeId // }, // { id: nanoid(8), source: newNodeId, target: addNoteNode.id } // ]); // Cuộn tới node mới thêm setLastNodeId(newNodeId); } setShowTabBar(false); }; const hidePopup = () => { setShowPopup(false); }; const addPopup = (node) => { const content = nodeContents[node.id]; // Lấy nội dung từ nodeContents dựa trên ID node setSelectedNodeContent(content); // Cập nhật nội dung được chọn }; const onDrop = (event: React.DragEvent) => { event.preventDefault(); const type = JSON.parse( event.dataTransfer.getData('application/reactflow') ); const position = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); const newNodeId = nanoid(8); const addNodeId = '2'; const addNode = nodes.find((node) => node.id === addNodeId); const targetEdge = edges.find( (edge) => edge?.source === '2' || edge?.target === '2' ); const targetNodeId = targetEdge?.source === '2' ? targetEdge?.target : targetEdge?.source; const targetNode = nodes.find((node) => node.id === targetNodeId); const newNodePosition = targetNode ? { x: targetNode.position.x, y: targetNode.position.y + 110 } : position; const createNewNode = ( nodeId: string, nodePosition: any, nodeLabel: JSX.Element ) => ({ id: nodeId, key: type.key, type: type.type || 'default', data: { label: nodeLabel }, position: nodePosition, }); const newNode = createNewNode( newNodeId, newNodePosition,
{type.label}
); setEdges((eds) => eds.filter((edge) => edge.source !== '2' && edge.target !== '2') ); if (type.key === 'ifelse') { const yesNodeId = nanoid(8); const noNodeId = nanoid(8); const createBranchNode = (id: string, xOffset: number) => ({ id, key: 'add-node', data: { label: (
+ Add a journey point
), }, position: { x: newNode.position.x + xOffset, y: newNode.position.y + 150, }, }); const yesNode = createBranchNode(yesNodeId, -200); const noNode = createBranchNode(noNodeId, 200); const ifelseEdges = [ { id: nanoid(8), source: targetNodeId, target: newNodeId }, { id: nanoid(8), source: newNodeId, target: yesNodeId, type: 'custom', data: { label: 'Yes' }, }, { id: nanoid(8), source: newNodeId, target: noNodeId, type: 'custom', data: { label: 'No' }, }, ]; setNodes((nds) => [ ...nds.filter((node) => node.id !== addNodeId), newNode, yesNode, noNode, ]); setEdges((eds) => [...eds, ...ifelseEdges]); } else { if (targetNode) { const updatedEdges = [ { id: nanoid(8), source: targetNodeId, target: newNodeId }, { id: nanoid(8), source: newNodeId, target: '2' }, ]; // Cập nhật nodes setNodes((nds) => { const updatedNodes = [...nds, newNode]; if (addNode) { // Tính toán vị trí cho addNode nằm dưới newNode const addNodePosition = { x: newNode.position.x, y: newNode.position.y + 110, // Đặt add-node dưới newNode }; // Cập nhật vị trí của addNode const addNodeUpdated = updatedNodes.map((node) => node.id === addNodeId ? { ...node, position: addNodePosition } : node ); // Đảm bảo rằng add-node nằm dưới newNode return addNodeUpdated.sort((a, b) => { if (a.id === newNodeId) return -1; // Đặt newNode lên trước if (b.id === newNodeId) return 1; // Đặt addNode xuống dưới return 0; // Giữ nguyên thứ tự cho các node khác }); } return updatedNodes; }); // Cập nhật edges setEdges((eds) => [...eds, ...updatedEdges]); if (addNode && type.key === 'contact-exists') { // Nếu key là 'contact-exists', xóa addNode setNodes((nds) => nds.filter((node) => node.id !== addNodeId)); } } else { const newNodeNew = createNewNode( newNodeId, position, newNode.data.label ); let closestNode = null; let closestDistance = Infinity; const threshold = 150; nodes.forEach((node) => { const distance = getDistance(newNodeNew.position, node.position); if (distance < closestDistance && distance <= threshold) { closestDistance = distance; closestNode = node; } }); if (closestNode) { const updatedEdges = edges.map((edge) => { if (edge.source === closestNode.id) return { ...edge, source: newNodeNew.id }; if (edge.target === closestNode.id) return { ...edge, target: newNodeNew.id }; return edge; }); if (newNodeNew.type !== 'output') { const addNodeNew = createNewNode( nanoid(8), { x: closestNode.position.x, y: closestNode.position.y + 100 },
+ Add a journey point
); setNodes((nds) => [ ...nds.map((n) => n.id === closestNode.id ? { ...newNodeNew, position: closestNode.position } : n ), addNodeNew, ]); setEdges([ ...updatedEdges, { id: nanoid(8), source: newNodeNew.id, target: addNodeNew.id }, ]); } else { setNodes((nds) => [ ...nds.map((n) => n.id === closestNode.id ? { ...newNodeNew, position: closestNode.position } : n ), ]); setEdges(updatedEdges); } } else { setNodes((nds) => [...nds, newNodeNew]); } } } console.log('Updated nodes:', nodes); }; const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); const defaultEdgeOptions = { style: { strokeWidth: 1.5, stroke: 'black' }, type: 'floating', markerEnd: { type: MarkerType.ArrowClosed, color: 'black', }, }; const onConnect = useCallback( (params) => setEdges((eds) => addEdge(params, eds)), [setEdges] ); // Hàm tính khoảng cách giữa hai node const getDistance = (pos1, pos2) => { const dx = pos1.x - pos2.x; const dy = pos1.y - pos2.y; return Math.sqrt(dx * dx + dy * dy); }; const toggleActions = (nodeId: string) => { setShowActions((prev) => (prev === nodeId ? null : nodeId)); const actionsElement = document.querySelector(`.actions-${nodeId}`); if (actionsElement) { // Toggle class 'show' cho phần tử actions actionsElement.classList.toggle('staticNode'); } }; // Hàm xóa node const handleDeleteNode = useCallback( (nodeId) => { // Tìm tất cả các edges có nodeId là source hoặc target const connectedEdges = edges.filter( (edge) => edge.source === nodeId || edge.target === nodeId ); console.log(connectedEdges); // Lấy các nodes đang nối với node bị xóa const sourceNode = connectedEdges.find( (edge) => edge.target === nodeId )?.source; const targetNode = connectedEdges.find( (edge) => edge.source === nodeId )?.target; // Cập nhật nodes: xóa node bị xóa setNodes((nds) => nds.filter((node) => node.id !== nodeId)); // Cập nhật edges: xóa các cạnh kết nối với node bị xóa setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId) ); // Nếu có cả sourceNode và targetNode, tạo một edge mới nối 2 node này if (sourceNode && targetNode) { setEdges((eds) => [ ...eds, { id: `new-edge-${sourceNode}-${targetNode}`, source: sourceNode, target: targetNode, }, ]); } }, [edges, setNodes, setEdges] ); // Hàm sửa node const handleEditNode = (nodeId: string) => { // Logic chỉnh sửa node alert(`Sửa node: ${nodeId}`); }; const handleSave = () => { setShowPopupSuccess(true); setTimeout(() => { setShowPopupSuccess(false); }, 2000); }; return (
{isTabbarVisible && ( )} {showPopup && ( setShowPopup(false)} /> )} {showPopupSuccess && (
Đã lưu lại tiến trình thành công !
)}
); }; export default NodeFlow;