import { useState, useCallback, useEffect } from 'react'; import { ReactFlow, addEdge, Controls, Background, BackgroundVariant, SelectionMode, applyNodeChanges, applyEdgeChanges, MarkerType } 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'; const initialNodes = [ { key: 'customer-sign', id: '1', type: 'input', data: { label: (
Customer signs up to
) }, position: { x: 250, y: 100 } }, { id: '2', key: 'add-node', type: 'output', data: { label: (
+ Add a journey point
) }, position: { x: 250, y: 220 } } ]; // Nội dung cho mỗi node const nodeContents = { 'send-email': ` `, 'customer-sign': ` `, ifelse: ` `, 'contact-exists': ` `, 'send-survey': ` ` }; const connectEdges = [ { id: 'e1-2', source: '1', target: '2' } ]; const NoteFlow = () => { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(connectEdges); const [showTabBar, setShowTabBar] = useState(false); const [lastNodeId, setLastNodeId] = useState('1'); const [nodeToDelete, setNodeToDelete] = useState(null); const [showPopup, setShowPopup] = useState(false); const [selectedNodeContent, setSelectedNodeContent] = useState(''); const [addNodeCallback, setAddNodeCallback] = useState(() => () => {}); const [isTabbarVisible, setIsTabbarVisible] = 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) => { console.log(node); if (node.key === 'add-node') { console.log(node); // Check if the clicked node is "Add Node" setShowTabBar(true); setIsTabbarVisible(true); } else { // Hiển thị popup khi click vào bất kỳ node nào const content = nodeContents[node.key]; // Lấy nội dung tương ứng của node setSelectedNodeContent(content); // Cập nhật nội dung cho popup setShowPopup(true); // Mở popup } }; const onNodeContextMenu = useCallback((event, node) => { event.preventDefault(); // Ngăn không cho menu chuột phải mặc định hiện ra setNodeToDelete(node); }, []); const handleNodeDelete = () => { if (nodeToDelete) { setNodes((nds) => nds.filter((node) => node.id !== nodeToDelete.id)); setEdges((eds) => eds.filter( (edge) => edge.source !== nodeToDelete.id && edge.target !== nodeToDelete.id ) ); setNodeToDelete(null); } }; // Function to get a unique y position const getUniqueYPosition = (xPosition, baseY, spacing) => { const existingYPositions = nodes .filter((node) => node.position.x === xPosition) .map((node) => node.position.y); let newYPosition = baseY; while (existingYPositions.includes(newYPosition)) { newYPosition += spacing; } return newYPosition; }; 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 removeEdgesRelatedToNode = (nodeId) => { setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId) ); }; 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) => { event.preventDefault(); const type = JSON.parse( event.dataTransfer.getData('application/reactflow') ); const reactFlowBounds = event.target.getBoundingClientRect(); const position = { x: event.clientX - reactFlowBounds.left - 100, y: event.clientY - reactFlowBounds.top + 5 }; const newNodeId = nanoid(8); // Xử lý add-node với id '2' const addNodeId = '2'; const addNode = nodes.find((node) => node.id === addNodeId); // Tìm các edge nối với node có id '2' 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); // Cập nhật vị trí của node mới để cách node đang kết nối 200 đơn vị const newNodePosition = { x: targetNode?.position.x, y: targetNode?.position.y + 110 }; // Tạo node mới const newNode = { id: newNodeId, key: type.key, type: type.type || 'default', data: { label: (
{type.label}
) }, position: newNodePosition }; // Loại bỏ các edge liên quan đến node '2' setEdges((eds) => eds.filter((edge) => edge.source !== '2' && edge.target !== '2') ); // Kiểm tra điều kiện node là 'ifelse' if (type.key === 'ifelse') { setNodes((nds) => nds.filter((node) => node.id !== addNodeId)); const yesNodeId = nanoid(8); const noNodeId = nanoid(8); // Tạo node Yes const yesNode = { id: yesNodeId, key: 'add-node', brand: 'yes', data: { label: (
+ Add a journey point
) }, position: { x: newNode.position.x - 200, // Điều chỉnh vị trí của nhánh Yes y: newNode.position.y + 150 // Điều chỉnh vị trí của nhánh Yes } }; // Tạo node No const noNode = { id: noNodeId, key: 'add-node', brand: 'no', data: { label: (
+ Add a journey point
) }, position: { x: newNode?.position.x + 200, // Điều chỉnh vị trí của nhánh No y: newNode?.position.y + 150 // Điều chỉnh vị trí của nhánh No } }; // Tạo các edge nối từ node ifelse đến Yes và No 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' } } ]; // Thêm node yes, no và edge vào danh sách node và edges setNodes((nds) => [...nds, newNode, yesNode, noNode]); setEdges((eds) => [...eds, ...ifelseEdges]); } else { // Nếu không phải 'ifelse', kiểm tra các điều kiện của node bình thường if (targetNode) { setNodes((nds) => [...nds, newNode]); setEdges((eds) => [ ...eds, { id: nanoid(8), source: targetNodeId, target: newNodeId }, { id: nanoid(8), source: newNodeId, target: '2' } ]); if (addNode) { const addNodePosition = { x: addNode.position.x, y: addNode.position.y + 110 }; setNodes((nds) => nds.map((node) => node.id === addNodeId ? { ...node, position: addNodePosition } : node ) ); setEdges((eds) => eds.filter( (edge) => edge.source !== addNodeId && edge.target !== addNodeId ) ); // Thêm edge mới nối add-node và node mới setEdges((eds) => [ ...eds, { id: nanoid(8), source: newNodeId, target: addNodeId } ]); // Kiểm tra nếu node mới là 'contact-exists', ẩn add-node if (type.key === 'contact-exists') { setNodes((nds) => nds.filter((node) => node.id !== addNodeId)); } } } else { const newNodeNew = { id: newNodeId, key: type.key, type: type.type || 'default', data: { label: (
{type.label}
) }, position: position }; setNodes((nds) => [...nds, newNodeNew]); } } }; const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); const onNodesChange = useCallback( (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [setNodes] ); const onEdgesChange = useCallback( (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [setEdges] ); 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) => { return Math.sqrt( Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2) ); }; // Hàm xử lý khi dừng kéo node const onNodeDragStop = useCallback( (event, node) => { const threshold = 50; // khoảng cách cho phép để node bị "đè" let closestNode = null; let closestDistance = Infinity; nodes.forEach((item) => { if (item.id !== node.id) { const distance = getDistance(node.position, item.position); if (distance < closestDistance && distance <= threshold) { closestNode = item; closestDistance = distance; } } }); if (closestNode) { // Thay thế node cũ bằng node mới const updatedEdges = edges.map((edge) => { if (edge.source === closestNode.id) { return { ...edge, source: node.id }; // Nối node mới làm source } if (edge.target === closestNode.id) { return { ...edge, target: node.id }; // Nối node mới làm target } return edge; }); if (node.type != 'output') { // Tạo một node mới với nội dung "Add a journey point" ngay dưới node thay thế const newNode = { id: nanoid(8), key: 'add-node', brand: node.brand, position: { x: node.position.x, // Giữ nguyên vị trí x y: node.position.y + 100 // Đặt node mới xuống dưới 150 đơn vị y }, data: { label: (
+ Add a journey point
) } }; // Thay thế node cũ bằng node mới setNodes((nds) => [ ...nds.map((n) => n.id === closestNode.id ? { ...closestNode, ...node, position: node.position } : n ), newNode // Thêm node mới ]); // Cập nhật edges setEdges([ ...updatedEdges, { id: nanoid(8), // ID của edge mới source: node.id, // Nối từ node vừa thay thế target: newNode.id // Đến node "Add a journey point" } ]); } else { // Thay thế node cũ bằng node mới setNodes((nds) => [ ...nds.map((n) => n.id === closestNode.id ? { ...closestNode, ...node, position: node.position } : n ) ]); // Cập nhật edges setEdges(updatedEdges); } } else { // Không có node gần, cập nhật node vào vị trí mới setNodes((nds) => nds.map((n) => n.id === node.id ? { ...n, position: node.position } : n ) ); } }, [nodes, edges, setNodes, setEdges] ); return (
{isTabbarVisible && ( )} {nodeToDelete && (
)} {showPopup && ( setShowPopup(false)} /> )}
); }; export default NoteFlow;