This commit is contained in:
2024-09-18 22:46:14 +07:00
parent f1f47c9e30
commit 6700e9514e
9 changed files with 793 additions and 77 deletions

View File

@@ -110,3 +110,105 @@
.react-flow__panel { .react-flow__panel {
position: fixed; 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;
}

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" role="presentation"
class="wink-icon icon-ECm3x">
<path d="M13 19a1 1 0 11-2 0 1 1 0 012 0z"></path>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M6 1a2 2 0 00-2 2v18a2 2 0 002 2h12a2 2 0 002-2V3a2 2 0 00-2-2H6zm3 2H6v18h12V3h-3v1a1 1 0 01-1 1h-4a1 1 0 01-1-1V3z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -27,13 +27,11 @@ body,
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 600px; width: 1334px;
height: 600px; background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000; z-index: 1000;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border-radius: 12px;
} }
.popup.hidden { .popup.hidden {
@@ -41,12 +39,10 @@ body,
} }
.popup-content { .popup-content {
background: #fff; padding: 40px;
padding: 20px;
border-radius: 5px; border-radius: 5px;
width: 100%; overflow-y: auto;
max-width: 600px; max-height: 600px;
position: relative;
} }
.popup-close { .popup-close {
@@ -57,21 +53,24 @@ body,
cursor: pointer; cursor: pointer;
} }
form { .conent-form {
height: 450px;
border: 1px solid rgba(36, 28, 21, 0.15);
padding: 20px;
border-radius: 10px;
display: flex; display: flex;
flex-direction: column;
} }
form label { .conent-form label {
font-weight: bold; font-weight: bold;
margin-bottom: 5px; margin-bottom: 5px;
color: #333; color: #333;
display: block; display: block;
} }
form input, .conent-form input,
form textarea, .conent-form textarea,
form select { .conent-form select {
padding: 10px; padding: 10px;
margin-bottom: 15px; margin-bottom: 15px;
border: 1px solid #ddd; border: 1px solid #ddd;
@@ -80,11 +79,11 @@ form select {
width: 97%; width: 97%;
} }
form textarea { .conent-form textarea {
resize: vertical; resize: vertical;
} }
form button { .conent-form button {
padding: 10px 15px; padding: 10px 15px;
background-color: #007bff; background-color: #007bff;
border: none; border: none;
@@ -95,15 +94,15 @@ form button {
transition: background-color 0.3s; transition: background-color 0.3s;
} }
form button:hover { .conent-form button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
form .form-group { .conent-form .form-group {
margin-bottom: 15px; margin-bottom: 15px;
} }
form .form-group:last-child { .conent-form .form-group:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@@ -121,3 +120,52 @@ form .form-group:last-child {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
z-index: 999; 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;
}

View File

@@ -6,6 +6,7 @@ import App from './App';
import NoteFlow from './nodes/NoteFlow'; import NoteFlow from './nodes/NoteFlow';
import './index.css'; import './index.css';
import NoteFlowNew from './nodes/NodeFlowNew'; import NoteFlowNew from './nodes/NodeFlowNew';
import FlowArea from './nodes/NodeMove';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>

View File

@@ -7,17 +7,55 @@ const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, data }) => {
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
targetY targetY,
curvature: 0.5
}); });
return ( return (
<> <>
<defs>
<marker
id={`arrowhead-${id}`}
markerWidth='10'
markerHeight='7'
refX='4'
refY='3'
orient='auto'
markerUnits='strokeWidth'
>
<polygon points='0 0, 5 3.2, 0 7' fill='#222' />
</marker>
</defs>
<path <path
id={id} id={id}
d={path} d={path}
style={{ stroke: '#ccc', strokeWidth: .8, fill: 'transparent' }} style={{ stroke: '#222', strokeWidth: 1.5, fill: 'transparent' }}
markerEnd={`url(#arrowhead-${id})`}
/> />
{data?.label && ( {data?.label1 && data?.label2 ? (
<>
<foreignObject
width={50}
height={8}
x={(sourceX + targetX) / 2 - 20}
y={sourceY - 2}
>
<div
xmlns='http://www.w3.org/1999/xhtml'
style={{
background: 'white',
padding: '3px 5px',
borderRadius: '0.8px',
cursor: 'pointer',
fontSize: '13px',
border: '1px solid #ddd',
textAlign: 'center',
fontWeight: '700'
}}
>
{data.label1}
</div>
</foreignObject>
<foreignObject <foreignObject
width={90} width={90}
height={30} height={30}
@@ -33,9 +71,33 @@ const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, data }) => {
cursor: 'pointer', cursor: 'pointer',
fontSize: '13px' fontSize: '13px'
}} }}
onClick={data.onClick}
> >
{data.label} {data.label2}
</div>
</foreignObject>
</>
) : (
<foreignObject
width={30}
height={30}
x={(sourceX + targetX) / 2 - 15}
y={(sourceY + targetY) / 2 - 15}
>
<div
xmlns='http://www.w3.org/1999/xhtml'
style={{
background: '#007c89',
width: '30px',
height: '30px',
lineHeight: '30px',
borderRadius: '50%',
cursor: 'pointer',
fontSize: '13px',
color: 'white',
textAlign: 'center'
}}
>
{data?.label}
</div> </div>
</foreignObject> </foreignObject>
)} )}

121
src/nodes/MenuFlow.tsx Normal file
View File

@@ -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 (
<>
<div className='menu-fixed'>
<b></b>
{MenuOptions.map((option) => (
<div className='item-flow' key={option.option}>
<b>{option.title}</b>
<div
className={`list-option ${option.option == 'rule' ? 'flex' : ''}`}
>
{option.list_option.map((items) => (
<div
className='items'
draggable='true'
key={items.id}
onDragStart={(event) => onDragStart(event, items)}
>
<div className='step'>
<div className='left'>
<img src={items.icon} width='24px' height='24px' alt='' />
</div>
<div className='right'>{items.label}</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};
export default MenuFlow;

View File

@@ -5,16 +5,18 @@ import {
Controls, Controls,
Background, Background,
BackgroundVariant, BackgroundVariant,
SelectionMode SelectionMode,
applyNodeChanges,
applyEdgeChanges,
MarkerType
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; 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 '../assets/css/style.css';
import Tabbar from './Tabbar.tsx'; import Tabbar from './Tabbar.tsx';
import ShowPopup from './ShowPopup.tsx'; import ShowPopup from './ShowPopup.tsx';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import CustomEdge from './CustomEdge'; import CustomEdge from './CustomEdge';
import MenuFlow from './MenuFlow';
const initialNodes = [ const initialNodes = [
{ {
@@ -26,7 +28,7 @@ const initialNodes = [
<div className='step'> <div className='step'>
<div className='left'> <div className='left'>
<img <img
src='/src/assets/icons/icon-user.svg' src='/src/assets/icons/icon-time.svg'
width='24px' width='24px'
height='24px' height='24px'
alt='' alt=''
@@ -40,8 +42,16 @@ const initialNodes = [
}, },
{ {
id: '2', id: '2',
data: { label: 'Add Note' }, key: 'add-node',
position: { x: 250, y: 250 } type: 'output',
data: {
label: (
<div className='add-node'>
<i>+</i> <span>Add a journey point</span>
</div>
)
},
position: { x: 250, y: 220 }
} }
]; ];
@@ -78,11 +88,17 @@ const nodeContents = {
` `
}; };
const connectEdges = [
{
id: 'e1-2',
source: '1',
target: '2'
}
];
const NoteFlow = () => { const NoteFlow = () => {
const [nodes, setNodes] = useState(initialNodes); const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState([ const [edges, setEdges] = useState(connectEdges);
{ id: 'e1-2', source: '1', target: '2' } // Kết nối node "Add Note" với node mặc định ban đầu
]);
const [showTabBar, setShowTabBar] = useState(false); const [showTabBar, setShowTabBar] = useState(false);
const [lastNodeId, setLastNodeId] = useState('1'); const [lastNodeId, setLastNodeId] = useState('1');
const [nodeToDelete, setNodeToDelete] = useState(null); const [nodeToDelete, setNodeToDelete] = useState(null);
@@ -99,13 +115,14 @@ const NoteFlow = () => {
}); });
}; };
const closeTabbar = () => { const closeTabbar = () => {
console.log('aaa');
setIsTabbarVisible(false); // Ẩn tabbar khi click close setIsTabbarVisible(false); // Ẩn tabbar khi click close
}; };
const onNodeClick = (event, node) => { const onNodeClick = (event, node) => {
if (node.id === '2') { console.log(node);
// Check if the clicked node is "Add Note" if (node.key === 'add-node') {
console.log(node);
// Check if the clicked node is "Add Node"
setShowTabBar(true); setShowTabBar(true);
setIsTabbarVisible(true); setIsTabbarVisible(true);
} else { } else {
@@ -165,14 +182,13 @@ const NoteFlow = () => {
const addNode = (option) => { const addNode = (option) => {
const newNodeId = nanoid(8); // Generate a unique ID for the new node const newNodeId = nanoid(8); // Generate a unique ID for the new node
const addNoteNode = nodes.find((node) => node.id === '2'); const addNoteNode = nodes.find((node) => node.key === 'add-node');
const initialX = addNoteNode.position.x; const initialX = addNoteNode?.position.x;
console.log(initialX);
const nextY = const nextY =
nodes.length > 2 nodes.length > 2
? getNextYPosition(initialX, 130) ? getNextYPosition(initialX, 120)
: addNoteNode.position.y; : addNoteNode?.position.y;
// Remove existing edges from addNoteNode // Remove existing edges from addNoteNode
setEdges((eds) => setEdges((eds) =>
@@ -270,14 +286,12 @@ const NoteFlow = () => {
setLastNodeId(noNode.id); setLastNodeId(noNode.id);
} else { } else {
console.log(option);
// Add new node logic // Add new node logic
if (addNoteNode) { if (addNoteNode) {
const initialX = addNoteNode.position.x; const initialX = addNoteNode.position.x;
setNodes((nds) => setNodes((nds) =>
nds.map((node) => nds.map((node) =>
node.id === '2' node.key === 'add-node'
? { ? {
...node, ...node,
position: { position: {
@@ -303,6 +317,10 @@ const NoteFlow = () => {
id: nanoid(8), id: nanoid(8),
source: lastNodeId, source: lastNodeId,
target: newNodeId target: newNodeId
// data: {
// label: <div onClick={handleAddNoteClick}>+</div>,
// onClick: handleAddNoteClick
// }
}; };
setNodes((nds) => [...nds, newNode]); setNodes((nds) => [...nds, newNode]);
@@ -315,15 +333,17 @@ const NoteFlow = () => {
// setEdges((eds) => [ // setEdges((eds) => [
// ...eds, // ...eds,
// { // {
// id: uuidv4(), // id: nanoid(8),
// source: lastNodeId, // source: lastNodeId,
// data: { // data: {
// label: <div onClick={handleAddNoteClick}>Add Note</div>, // Use HTML or React component // label: <div onClick={handleAddNoteClick}>Add Note</div>, // Use HTML or React component
// onClick: handleAddNoteClick // onClick: handleAddNoteClick
// }, // },
// target: newNodeId // target: newNodeId
// } // },
// { id: nanoid(8), source: newNodeId, target: addNoteNode.id }
// ]); // ]);
// Cuộn tới node mới thêm // Cuộn tới node mới thêm
setLastNodeId(newNodeId); setLastNodeId(newNodeId);
} }
@@ -340,21 +360,361 @@ const NoteFlow = () => {
setSelectedNodeContent(content); // Cập nhật nội dung được chọn 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: (
<div className='step'>
<div className='left'>
<img src={type.icon} width='24px' height='24px' alt='' />
</div>
<div className='right'>{type.label}</div>
</div>
)
},
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: (
<div className='add-node'>
<i>+</i> <span>Add a journey point</span>
</div>
)
},
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: (
<div className='add-node'>
<i>+</i> <span>Add a journey point</span>
</div>
)
},
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: (
<div className='step'>
<div className='left'>
<img src={type.icon} width='24px' height='24px' alt='' />
</div>
<div className='right'>{type.label}</div>
</div>
)
},
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: (
<div className='add-node'>
<i>+</i> <span>Add a journey point</span>
</div>
)
}
};
// 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 ( return (
<div style={{ height: '100vh', width: '100%', position: 'relative' }}> <div style={{ overflowY: 'auto', position: 'relative' }}>
<div className='flex-flow'>
<MenuFlow />
<div className='panelContainer'>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
edgeTypes={{ custom: CustomEdge }} edgeTypes={{ custom: CustomEdge }}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeContextMenu={onNodeContextMenu} onNodeContextMenu={onNodeContextMenu}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onDrop={onDrop}
onDragOver={onDragOver}
panOnScroll panOnScroll
selectionOnDrag defaultEdgeOptions={defaultEdgeOptions}
selectionMode={SelectionMode.Partial} onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
> >
<Controls /> <Controls position='right-bottom' />
<Background variant={BackgroundVariant.Lines} color='#ccccc' /> <Background variant={BackgroundVariant.Lines} color='#ccccc' />
</ReactFlow> </ReactFlow>
</div>
</div>
{isTabbarVisible && ( {isTabbarVisible && (
<Tabbar addNode={addNode} closeTabbar={closeTabbar} /> <Tabbar addNode={addNode} closeTabbar={closeTabbar} />

View File

@@ -18,14 +18,27 @@ const ShowPopup = ({ selectedNodeContent, hidePopup }) => {
<> <>
<div className='popup-overlay' onClick={handleOverlayClick}></div> <div className='popup-overlay' onClick={handleOverlayClick}></div>
<div className={`popup global-popup ${showPopup ? '' : 'hidden'}`}> <div className={`popup global-popup ${showPopup ? '' : 'hidden'}`}>
<div className='popup-header'>
<h1 className='title'></h1>
<div className='close-popup' onClick={handleOverlayClick}>
x
</div>
</div>
<div className='popup-content'> <div className='popup-content'>
<div className='conent-form'> <div className='conent-form'>
<form> <div
<div dangerouslySetInnerHTML={{ __html: selectedNodeContent }} /> className='content-left'
<button onClick={() => setShowPopup(false)}>Close</button> dangerouslySetInnerHTML={{ __html: selectedNodeContent }}
</form> />
<div className='content-right'></div>
</div> </div>
</div> </div>
<div className='popup-footer '>
<button onClick={() => setShowPopup(false)} className='btn-save'>
Lưu lại
</button>
<button className='btn-remove'>Xóa</button>
</div>
</div> </div>
</> </>
); );

View File

@@ -60,7 +60,7 @@ const tabBarOptions = [
<div className='right'>Send email</div> <div className='right'>Send email</div>
</div> </div>
), ),
position: { x: 250, y: 0 } position: { x: 400, y: 0 }
}, },
{ {
key: 'ifelse', key: 'ifelse',
@@ -91,10 +91,11 @@ const tabBarOptions = [
<div className='right'>Replied to conversation?</div> <div className='right'>Replied to conversation?</div>
</div> </div>
), ),
position: { x: 250, y: 290 } position: { x: 400, y: 290 }
}, },
{ {
key: 'send-survey', key: 'send-survey',
branchKey: 'no',
label: 'Send survey', label: 'Send survey',
id: '5', id: '5',
type: 'output', type: 'output',
@@ -122,10 +123,11 @@ const tabBarOptions = [
<div className='right'>Send survey</div> <div className='right'>Send survey</div>
</div> </div>
), ),
position: { x: 250, y: 400 } position: { x: 400, y: 400 }
}, },
{ {
key: 'contact-exists', key: 'contact-exists',
branchKey: 'yes',
label: 'Contact Exists', label: 'Contact Exists',
id: '6', id: '6',
type: 'output', type: 'output',
@@ -153,7 +155,7 @@ const tabBarOptions = [
<div className='right'>Contact Exists</div> <div className='right'>Contact Exists</div>
</div> </div>
), ),
position: { x: 250, y: 400 } position: { x: 400, y: 400 }
} }
]; ];
@@ -175,7 +177,7 @@ const Tabbar = ({ addNode, closeTabbar }) => {
</div> </div>
</div> </div>
<h3 className='tittle'>Rules</h3> <h3 className='tittle'>Action</h3>
<div className='itemTab grid'> <div className='itemTab grid'>
{tabBarOptions.map((option) => ( {tabBarOptions.map((option) => (
<div <div