diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 4c86ec3..ee659d3 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -110,3 +110,105 @@ .react-flow__panel { position: fixed; } +foreignObject { + overflow: visible; +} + +.add-node { + font-size: 14px; + font-weight: 700; + color: #007c89; +} + +.flex-flow { + display: flex; +} + +.flex-flow .menu-fixed { + background: #fff; + border: initial; + border-radius: initial; + border-right: 1px solid rgba(36, 28, 21, 0.15); + -webkit-transition: -webkit-transform 0.2s ease-in; + transition: -webkit-transform 0.2s ease-in; + transition: transform 0.2s ease-in; + transition: transform 0.2s ease-in, -webkit-transform 0.2s ease-in; + width: 360px; + z-index: 9; + padding: 20px; + height: 100vh; + overflow-y: auto; +} + +.flex-flow .panelContainer { + width: calc(100% - 360px); + position: relative; +} + +.flex-flow .menu-fixed .item-flow { + margin-bottom: 20px; +} +.menu-fixed .item-flow b { + margin-bottom: 10px; + display: block; +} + +.menu-fixed .list-option.flex { + grid-gap: 8px; + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +.menu-fixed .list-option .items { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: 1px solid rgba(36, 28, 21, 0.15); + border-radius: 4px; + padding: 16px 8px; + background: #fff; + cursor: move; + margin-bottom: 10px; +} + +.menu-fixed .list-option.flex .step { + flex-direction: column; + height: 48px; + justify-content: center; +} + +.menu-fixed .list-option.flex .items { + justify-content: center; +} + +.menu-fixed .list-option.flex .left { + border: 0; + width: auto; +} +.menu-fixed .list-option.flex .right { + width: 100%; + text-align: center; +} + +.react-flow__node.btn-add-node { + width: 40px !important; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: rgb(0, 124, 137); + padding: 0; +} + +.react-flow__node.btn-add-node i { + color: #fff; +} + +.react-flow__node.btn-add-node .react-flow__handle { + opacity: 0; +} diff --git a/src/assets/icons/icon-phone.svg b/src/assets/icons/icon-phone.svg new file mode 100644 index 0000000..45707be --- /dev/null +++ b/src/assets/icons/icon-phone.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/index.css b/src/index.css index cbfd7e1..d63bcfa 100644 --- a/src/index.css +++ b/src/index.css @@ -27,13 +27,11 @@ body, position: fixed; top: 50%; left: 50%; - width: 600px; - height: 600px; - display: flex; - align-items: center; - justify-content: center; + width: 1334px; + background: #fff; z-index: 1000; transform: translate(-50%, -50%); + border-radius: 12px; } .popup.hidden { @@ -41,12 +39,10 @@ body, } .popup-content { - background: #fff; - padding: 20px; + padding: 40px; border-radius: 5px; - width: 100%; - max-width: 600px; - position: relative; + overflow-y: auto; + max-height: 600px; } .popup-close { @@ -57,21 +53,24 @@ body, cursor: pointer; } -form { +.conent-form { + height: 450px; + border: 1px solid rgba(36, 28, 21, 0.15); + padding: 20px; + border-radius: 10px; display: flex; - flex-direction: column; } -form label { +.conent-form label { font-weight: bold; margin-bottom: 5px; color: #333; display: block; } -form input, -form textarea, -form select { +.conent-form input, +.conent-form textarea, +.conent-form select { padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; @@ -80,11 +79,11 @@ form select { width: 97%; } -form textarea { +.conent-form textarea { resize: vertical; } -form button { +.conent-form button { padding: 10px 15px; background-color: #007bff; border: none; @@ -95,15 +94,15 @@ form button { transition: background-color 0.3s; } -form button:hover { +.conent-form button:hover { background-color: #0056b3; } -form .form-group { +.conent-form .form-group { margin-bottom: 15px; } -form .form-group:last-child { +.conent-form .form-group:last-child { margin-bottom: 0; } @@ -121,3 +120,52 @@ form .form-group:last-child { background: rgba(0, 0, 0, 0.5); z-index: 999; } + +.popup-header { + border-bottom: 1px solid rgba(36, 28, 21, 0.15); + padding: 10px 20px; + position: relative; +} + +.popup-header .close-popup { + position: absolute; + right: 10px; + top: 0; + font-size: 25px; + font-weight: 600; + cursor: pointer; +} + +.popup-footer { + border-top: 1px solid rgba(36, 28, 21, 0.15); + padding: 12px 16px; + text-align: right; +} + +.conent-form .content-left { + width: 50%; + margin-right: 30px; +} + +.conent-form .conent-right { + width: calc(100% - 50% - 30px); +} + +.btn-save { + margin-right: 16px; + padding: 10px 20px; + border-radius: 50px; + outline: none; + background: #007c89; + color: #fff; + border: 0; +} + +.btn-remove { + padding: 10px 20px; + border-radius: 50px; + outline: none; + background: red; + color: #fff; + border: 0; +} diff --git a/src/main.tsx b/src/main.tsx index 8a731da..dfd9c51 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,7 @@ import App from './App'; import NoteFlow from './nodes/NoteFlow'; import './index.css'; import NoteFlowNew from './nodes/NodeFlowNew'; +import FlowArea from './nodes/NodeMove'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/nodes/CustomEdge.tsx b/src/nodes/CustomEdge.tsx index 286e9d8..cd2075e 100644 --- a/src/nodes/CustomEdge.tsx +++ b/src/nodes/CustomEdge.tsx @@ -7,35 +7,97 @@ const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, data }) => { sourceX, sourceY, targetX, - targetY + targetY, + curvature: 0.5 }); return ( <> + + + + + - {data?.label && ( + {data?.label1 && data?.label2 ? ( + <> + +
+ {data.label1} +
+
+ +
+ {data.label2} +
+
+ + ) : (
- {data.label} + {data?.label}
)} diff --git a/src/nodes/MenuFlow.tsx b/src/nodes/MenuFlow.tsx new file mode 100644 index 0000000..7764f45 --- /dev/null +++ b/src/nodes/MenuFlow.tsx @@ -0,0 +1,121 @@ +import { useState, useEffect } from 'react'; +import { nanoid } from 'nanoid'; + +// nội dung Menu +const MenuOptions = [ + { + option: 'rule', + title: 'Rules', + list_option: [ + { + key: 'time-delay', + label: 'Time delay', + id: nanoid(8), + type: 'default', + icon: '/src/assets/icons/icon-time.svg' + }, + { + key: 'ifelse', + label: 'Replied to conversation?', + id: nanoid(8), + type: 'default', + icon: '/src/assets/icons/icon-if.svg', + position: { x: 400, y: 290 } + }, + { + key: 'wait', + label: 'Wait for trigger', + id: nanoid(8), + type: 'default', + icon: '/src/assets/icons/icon-wait.svg' + } + ] + }, + { + option: 'action', + title: 'Actions', + list_option: [ + { + key: 'send-email', + label: 'Send email', + id: nanoid(8), + type: '', + icon: '/src/assets/icons/icon-email.svg' + }, + { + key: 'send-sms', + label: 'Send sms', + id: nanoid(8), + type: '', + icon: '/src/assets/icons/icon-phone.svg' + }, + { + key: 'send-survey-email', + label: 'Send survey in email', + id: nanoid(8), + type: '', + icon: '/src/assets/icons/icon-list.svg' + }, + { + key: 'contact-exists', + branchKey: 'yes', + label: 'Contact Exists', + id: nanoid(8), + type: 'output', + icon: '/src/assets/icons/icon-check.svg' + }, + { + key: 'send-survey', + branchKey: 'no', + label: 'Send survey', + id: nanoid(8), + type: 'output', + icon: '/src/assets/icons/icon-list.svg' + } + ] + } +]; + +const MenuFlow = () => { + const onDragStart = (event, nodeType) => { + event.dataTransfer.setData( + 'application/reactflow', + JSON.stringify(nodeType) + ); + event.dataTransfer.effectAllowed = 'move'; + }; + + return ( + <> +
+ + {MenuOptions.map((option) => ( +
+ {option.title} +
+ {option.list_option.map((items) => ( +
onDragStart(event, items)} + > +
+
+ +
+
{items.label}
+
+
+ ))} +
+
+ ))} +
+ + ); +}; + +export default MenuFlow; diff --git a/src/nodes/NoteFlow.tsx b/src/nodes/NoteFlow.tsx index adc9b98..984c082 100644 --- a/src/nodes/NoteFlow.tsx +++ b/src/nodes/NoteFlow.tsx @@ -5,16 +5,18 @@ import { Controls, Background, BackgroundVariant, - SelectionMode + SelectionMode, + applyNodeChanges, + applyEdgeChanges, + MarkerType } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { FiUser, FiMail, FiCheckCircle, FiList } from 'react-icons/fi'; -import { FaShuffle } from 'react-icons/fa6'; 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 = [ { @@ -26,7 +28,7 @@ const initialNodes = [
+ + Add a journey point +
+ ) + }, + position: { x: 250, y: 220 } } ]; @@ -78,11 +88,17 @@ const nodeContents = { ` }; +const connectEdges = [ + { + id: 'e1-2', + source: '1', + target: '2' + } +]; + const NoteFlow = () => { const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState([ - { id: 'e1-2', source: '1', target: '2' } // Kết nối node "Add Note" với node mặc định ban đầu - ]); + const [edges, setEdges] = useState(connectEdges); const [showTabBar, setShowTabBar] = useState(false); const [lastNodeId, setLastNodeId] = useState('1'); const [nodeToDelete, setNodeToDelete] = useState(null); @@ -99,13 +115,14 @@ const NoteFlow = () => { }); }; const closeTabbar = () => { - console.log('aaa'); setIsTabbarVisible(false); // Ẩn tabbar khi click close }; const onNodeClick = (event, node) => { - if (node.id === '2') { - // Check if the clicked node is "Add Note" + console.log(node); + if (node.key === 'add-node') { + console.log(node); + // Check if the clicked node is "Add Node" setShowTabBar(true); setIsTabbarVisible(true); } else { @@ -165,14 +182,13 @@ const NoteFlow = () => { const addNode = (option) => { const newNodeId = nanoid(8); // Generate a unique ID for the new node - const addNoteNode = nodes.find((node) => node.id === '2'); - const initialX = addNoteNode.position.x; - console.log(initialX); + const addNoteNode = nodes.find((node) => node.key === 'add-node'); + const initialX = addNoteNode?.position.x; const nextY = nodes.length > 2 - ? getNextYPosition(initialX, 130) - : addNoteNode.position.y; + ? getNextYPosition(initialX, 120) + : addNoteNode?.position.y; // Remove existing edges from addNoteNode setEdges((eds) => @@ -270,14 +286,12 @@ const NoteFlow = () => { setLastNodeId(noNode.id); } else { - console.log(option); - // Add new node logic if (addNoteNode) { const initialX = addNoteNode.position.x; setNodes((nds) => nds.map((node) => - node.id === '2' + node.key === 'add-node' ? { ...node, position: { @@ -303,6 +317,10 @@ const NoteFlow = () => { id: nanoid(8), source: lastNodeId, target: newNodeId + // data: { + // label:
+
, + // onClick: handleAddNoteClick + // } }; setNodes((nds) => [...nds, newNode]); @@ -315,15 +333,17 @@ const NoteFlow = () => { // setEdges((eds) => [ // ...eds, // { - // id: uuidv4(), + // 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); } @@ -340,21 +360,361 @@ const NoteFlow = () => { 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 && ( diff --git a/src/nodes/ShowPopup.tsx b/src/nodes/ShowPopup.tsx index beee81e..0cddbb0 100644 --- a/src/nodes/ShowPopup.tsx +++ b/src/nodes/ShowPopup.tsx @@ -18,14 +18,27 @@ const ShowPopup = ({ selectedNodeContent, hidePopup }) => { <>
+
+

+
+ x +
+
-
-
- - +
+
+
+ + +
); diff --git a/src/nodes/Tabbar.tsx b/src/nodes/Tabbar.tsx index 6871e72..ca293cf 100644 --- a/src/nodes/Tabbar.tsx +++ b/src/nodes/Tabbar.tsx @@ -60,7 +60,7 @@ const tabBarOptions = [
Send email
), - position: { x: 250, y: 0 } + position: { x: 400, y: 0 } }, { key: 'ifelse', @@ -91,10 +91,11 @@ const tabBarOptions = [
Replied to conversation?
), - position: { x: 250, y: 290 } + position: { x: 400, y: 290 } }, { key: 'send-survey', + branchKey: 'no', label: 'Send survey', id: '5', type: 'output', @@ -122,10 +123,11 @@ const tabBarOptions = [
Send survey
), - position: { x: 250, y: 400 } + position: { x: 400, y: 400 } }, { key: 'contact-exists', + branchKey: 'yes', label: 'Contact Exists', id: '6', type: 'output', @@ -153,7 +155,7 @@ const tabBarOptions = [
Contact Exists
), - position: { x: 250, y: 400 } + position: { x: 400, y: 400 } } ]; @@ -175,7 +177,7 @@ const Tabbar = ({ addNode, closeTabbar }) => { ✖
-

Rules

+

Action

{tabBarOptions.map((option) => (