2024-09-09 16:00:58 +07:00
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
ReactFlow,
|
|
|
|
|
addEdge,
|
|
|
|
|
Controls,
|
|
|
|
|
Background,
|
|
|
|
|
BackgroundVariant,
|
|
|
|
|
SelectionMode
|
|
|
|
|
} from '@xyflow/react';
|
2024-08-16 15:08:06 +07:00
|
|
|
import '@xyflow/react/dist/style.css';
|
2024-08-23 14:19:10 +07:00
|
|
|
import { FiUser, FiMail, FiCheckCircle, FiList } from 'react-icons/fi';
|
|
|
|
|
import { FaShuffle } from 'react-icons/fa6';
|
2024-09-09 16:00:58 +07:00
|
|
|
import '../assets/css/style.css';
|
|
|
|
|
import Tabbar from './Tabbar.tsx';
|
|
|
|
|
import ShowPopup from './ShowPopup.tsx';
|
2024-09-10 10:56:15 +07:00
|
|
|
import { nanoid } from 'nanoid';
|
|
|
|
|
import CustomEdge from './CustomEdge';
|
2024-08-23 14:19:10 +07:00
|
|
|
|
|
|
|
|
const initialNodes = [
|
|
|
|
|
{
|
2024-09-09 16:00:58 +07:00
|
|
|
key: 'customer-sign',
|
2024-08-23 14:19:10 +07:00
|
|
|
id: '1',
|
|
|
|
|
type: 'input',
|
|
|
|
|
data: {
|
2024-08-16 15:08:06 +07:00
|
|
|
label: (
|
2024-09-09 16:00:58 +07:00
|
|
|
<div className='step'>
|
|
|
|
|
<div className='left'>
|
|
|
|
|
<img
|
|
|
|
|
src='/src/assets/icons/icon-user.svg'
|
|
|
|
|
width='24px'
|
|
|
|
|
height='24px'
|
|
|
|
|
alt=''
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='right'>Customer signs up to</div>
|
2024-08-16 15:08:06 +07:00
|
|
|
</div>
|
2024-09-09 16:00:58 +07:00
|
|
|
)
|
2024-08-16 15:08:06 +07:00
|
|
|
},
|
2024-09-09 16:00:58 +07:00
|
|
|
position: { x: 250, y: 100 }
|
2024-08-23 14:19:10 +07:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '2',
|
|
|
|
|
data: { label: 'Add Note' },
|
2024-09-09 16:00:58 +07:00
|
|
|
position: { x: 250, y: 250 }
|
|
|
|
|
}
|
2024-08-16 15:08:06 +07:00
|
|
|
];
|
|
|
|
|
|
2024-08-24 11:01:05 +07:00
|
|
|
// Nội dung cho mỗi node
|
|
|
|
|
const nodeContents = {
|
2024-09-09 16:00:58 +07:00
|
|
|
'send-email': `
|
2024-08-24 11:01:05 +07:00
|
|
|
<label for="email-subject">Subject:</label>
|
|
|
|
|
<input type="text" id="email-subject" name="email-subject" placeholder="Enter subject">
|
|
|
|
|
<label for="email-body">Body:</label>
|
|
|
|
|
<textarea id="email-body" name="email-body" rows="4" placeholder="Enter email body"></textarea>
|
|
|
|
|
`,
|
2024-09-09 16:00:58 +07:00
|
|
|
'customer-sign': `
|
2024-08-24 11:01:05 +07:00
|
|
|
<label for="customer-name">Customer Name:</label>
|
|
|
|
|
<input type="text" id="customer-name" name="customer-name" placeholder="Enter customer name">
|
|
|
|
|
<label for="customer-email">Customer Email:</label>
|
|
|
|
|
<input type="email" id="customer-email" name="customer-email" placeholder="Enter customer email">
|
|
|
|
|
`,
|
2024-09-09 16:00:58 +07:00
|
|
|
ifelse: `
|
2024-08-24 11:01:05 +07:00
|
|
|
<label for="reply-status">Reply Status:</label>
|
|
|
|
|
<select id="reply-status" name="reply-status">
|
|
|
|
|
<option value="replied">Replied</option>
|
|
|
|
|
<option value="not-replied">Not Replied</option>
|
|
|
|
|
</select>
|
|
|
|
|
`,
|
2024-09-09 16:00:58 +07:00
|
|
|
'contact-exists': `
|
2024-08-24 11:01:05 +07:00
|
|
|
<label for="contact-id">Contact ID:</label>
|
|
|
|
|
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
|
|
|
|
`,
|
2024-09-09 16:00:58 +07:00
|
|
|
'send-survey': `
|
2024-08-24 11:01:05 +07:00
|
|
|
<label for="survey-question">Survey Question:</label>
|
|
|
|
|
<input type="text" id="survey-question" name="survey-question" placeholder="Enter survey question">
|
|
|
|
|
<label for="survey-options">Options (comma-separated):</label>
|
|
|
|
|
<input type="text" id="survey-options" name="survey-options" placeholder="Option 1, Option 2, ...">
|
|
|
|
|
`
|
|
|
|
|
};
|
|
|
|
|
|
2024-08-16 15:08:06 +07:00
|
|
|
const NoteFlow = () => {
|
2024-08-23 14:19:10 +07:00
|
|
|
const [nodes, setNodes] = useState(initialNodes);
|
|
|
|
|
const [edges, setEdges] = useState([
|
2024-09-09 16:00:58 +07:00
|
|
|
{ id: 'e1-2', source: '1', target: '2' } // Kết nối node "Add Note" với node mặc định ban đầu
|
2024-08-23 14:19:10 +07:00
|
|
|
]);
|
2024-08-16 15:08:06 +07:00
|
|
|
const [showTabBar, setShowTabBar] = useState(false);
|
|
|
|
|
const [lastNodeId, setLastNodeId] = useState('1');
|
2024-08-24 11:01:05 +07:00
|
|
|
const [nodeToDelete, setNodeToDelete] = useState(null);
|
|
|
|
|
const [showPopup, setShowPopup] = useState(false);
|
|
|
|
|
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
2024-09-10 10:56:15 +07:00
|
|
|
const [addNodeCallback, setAddNodeCallback] = useState(() => () => {});
|
2024-08-24 11:01:05 +07:00
|
|
|
|
2024-08-16 15:08:06 +07:00
|
|
|
const handleAddNoteClick = () => {
|
|
|
|
|
setShowTabBar(true);
|
2024-09-10 10:56:15 +07:00
|
|
|
setAddNodeCallback(() => (selectedNode) => {
|
2024-09-09 16:00:58 +07:00
|
|
|
addNodeBetween(selectedNode, sourceId, targetId);
|
2024-09-10 10:56:15 +07:00
|
|
|
});
|
2024-08-16 15:08:06 +07:00
|
|
|
};
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
const onNodeClick = (event, node) => {
|
|
|
|
|
if (node.id === '2') {
|
|
|
|
|
// Check if the clicked node is "Add Note"
|
|
|
|
|
setShowTabBar(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
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-08-23 14:19:10 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
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);
|
|
|
|
|
}, []);
|
2024-08-24 21:28:13 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
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);
|
2024-08-24 21:28:13 +07:00
|
|
|
}
|
2024-09-09 16:00:58 +07:00
|
|
|
};
|
2024-08-24 21:28:13 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
// 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;
|
2024-08-16 15:08:06 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
const removeEdgesRelatedToNode = (nodeId) => {
|
|
|
|
|
setEdges((eds) =>
|
|
|
|
|
eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
const addNode = (option) => {
|
2024-09-10 10:56:15 +07:00
|
|
|
const newNodeId = nanoid(8); // Generate a unique ID for the new node
|
2024-08-23 14:19:10 +07:00
|
|
|
const addNoteNode = nodes.find((node) => node.id === '2');
|
2024-09-09 16:00:58 +07:00
|
|
|
const initialX = addNoteNode.position.x;
|
2024-09-10 10:56:15 +07:00
|
|
|
console.log(initialX);
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
const nextY =
|
|
|
|
|
nodes.length > 2
|
|
|
|
|
? getNextYPosition(initialX, 130)
|
|
|
|
|
: addNoteNode.position.y;
|
2024-08-16 15:08:06 +07:00
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
// Remove existing edges from addNoteNode
|
|
|
|
|
setEdges((eds) =>
|
|
|
|
|
eds.filter((edge) => edge.source !== '2' && edge.target !== '2')
|
|
|
|
|
);
|
2024-08-16 15:08:06 +07:00
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
if (option.id === '4') {
|
2024-08-23 14:19:10 +07:00
|
|
|
const repliedNode = {
|
2024-09-09 16:00:58 +07:00
|
|
|
id: newNodeId,
|
|
|
|
|
key: option.key,
|
|
|
|
|
data: { label: option.htmlNode },
|
2024-09-10 10:56:15 +07:00
|
|
|
position: { x: initialX, y: nextY }
|
2024-08-16 15:08:06 +07:00
|
|
|
};
|
|
|
|
|
|
2024-08-23 14:19:10 +07:00
|
|
|
const yesNode = {
|
2024-09-10 10:56:15 +07:00
|
|
|
id: nanoid(8),
|
2024-08-23 14:42:10 +07:00
|
|
|
type: 'output',
|
2024-09-09 16:00:58 +07:00
|
|
|
key: 'contact-exists',
|
2024-08-23 14:19:10 +07:00
|
|
|
data: {
|
|
|
|
|
label: (
|
2024-09-09 16:00:58 +07:00
|
|
|
<div className='step'>
|
|
|
|
|
<div className='left'>
|
|
|
|
|
<img
|
|
|
|
|
src='/src/assets/icons/icon-check.svg'
|
|
|
|
|
width='24px'
|
|
|
|
|
height='24px'
|
|
|
|
|
alt=''
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='right'>Contact Exists</div>
|
2024-08-23 14:19:10 +07:00
|
|
|
</div>
|
2024-09-09 16:00:58 +07:00
|
|
|
)
|
2024-08-23 14:19:10 +07:00
|
|
|
},
|
2024-09-10 10:56:15 +07:00
|
|
|
position: { x: initialX - 150, y: nextY + 150 }
|
2024-08-16 15:08:06 +07:00
|
|
|
};
|
|
|
|
|
|
2024-08-23 14:19:10 +07:00
|
|
|
const noNode = {
|
2024-09-10 10:56:15 +07:00
|
|
|
id: nanoid(8),
|
|
|
|
|
type: '',
|
2024-09-09 16:00:58 +07:00
|
|
|
key: 'send-survey',
|
2024-08-23 14:19:10 +07:00
|
|
|
data: {
|
|
|
|
|
label: (
|
2024-09-09 16:00:58 +07:00
|
|
|
<div className='step'>
|
|
|
|
|
<div className='left'>
|
|
|
|
|
<img
|
|
|
|
|
src='/src/assets/icons/icon-list.svg'
|
|
|
|
|
width='24px'
|
|
|
|
|
height='24px'
|
|
|
|
|
alt=''
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className='right'>Send survey</div>
|
2024-08-23 14:19:10 +07:00
|
|
|
</div>
|
2024-09-09 16:00:58 +07:00
|
|
|
)
|
2024-08-23 14:19:10 +07:00
|
|
|
},
|
2024-09-10 10:56:15 +07:00
|
|
|
position: { x: initialX + 150, y: nextY + 150 }
|
2024-08-23 14:19:10 +07:00
|
|
|
};
|
|
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
// 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)
|
2024-08-23 14:19:10 +07:00
|
|
|
setNodes((nds) => [...nds, repliedNode, yesNode, noNode]);
|
2024-08-16 15:08:06 +07:00
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
// Kết nối các edges giữa các node
|
2024-08-16 15:08:06 +07:00
|
|
|
setEdges((eds) => [
|
|
|
|
|
...eds,
|
2024-09-09 16:00:58 +07:00
|
|
|
{
|
2024-09-10 10:56:15 +07:00
|
|
|
id: nanoid(8),
|
2024-09-09 16:00:58 +07:00
|
|
|
source: repliedNode.id,
|
|
|
|
|
target: yesNode.id,
|
2024-09-10 10:56:15 +07:00
|
|
|
label: 'yes'
|
2024-09-09 16:00:58 +07:00
|
|
|
},
|
|
|
|
|
{
|
2024-09-10 10:56:15 +07:00
|
|
|
id: nanoid(8),
|
2024-09-09 16:00:58 +07:00
|
|
|
source: repliedNode.id,
|
|
|
|
|
target: noNode.id,
|
2024-09-10 10:56:15 +07:00
|
|
|
label: 'no'
|
2024-09-09 16:00:58 +07:00
|
|
|
},
|
2024-09-10 10:56:15 +07:00
|
|
|
{ id: nanoid(8), source: lastNodeId, target: repliedNode.id },
|
|
|
|
|
{ id: nanoid(8), source: noNode.id, target: addNoteNode.id }
|
2024-08-16 15:08:06 +07:00
|
|
|
]);
|
2024-09-10 10:56:15 +07:00
|
|
|
|
|
|
|
|
setLastNodeId(noNode.id);
|
2024-08-23 14:19:10 +07:00
|
|
|
} else {
|
2024-09-10 10:56:15 +07:00
|
|
|
console.log(option);
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
// Add new node logic
|
2024-08-23 14:19:10 +07:00
|
|
|
if (addNoteNode) {
|
2024-09-09 16:00:58 +07:00
|
|
|
const initialX = addNoteNode.position.x;
|
2024-08-23 14:19:10 +07:00
|
|
|
setNodes((nds) =>
|
|
|
|
|
nds.map((node) =>
|
|
|
|
|
node.id === '2'
|
2024-09-09 16:00:58 +07:00
|
|
|
? {
|
|
|
|
|
...node,
|
|
|
|
|
position: {
|
|
|
|
|
x: initialX,
|
|
|
|
|
y: nextY + 130
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-23 14:19:10 +07:00
|
|
|
: node
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-09 16:00:58 +07:00
|
|
|
const newNode = {
|
|
|
|
|
id: newNodeId,
|
|
|
|
|
key: option.key,
|
|
|
|
|
data: { label: option.htmlNode },
|
|
|
|
|
position: {
|
2024-09-10 10:56:15 +07:00
|
|
|
x: initialX,
|
2024-09-09 16:00:58 +07:00
|
|
|
y: nodes.length > 2 ? nextY : addNoteNode.position.y
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-08-23 14:19:10 +07:00
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
const newEdge = {
|
|
|
|
|
id: nanoid(8),
|
|
|
|
|
source: lastNodeId,
|
|
|
|
|
target: newNodeId
|
|
|
|
|
};
|
2024-08-23 14:19:10 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
setNodes((nds) => [...nds, newNode]);
|
|
|
|
|
setEdges((eds) => [
|
|
|
|
|
...eds,
|
2024-09-10 10:56:15 +07:00
|
|
|
newEdge,
|
|
|
|
|
{ id: nanoid(8), source: newNodeId, target: addNoteNode.id }
|
2024-09-09 16:00:58 +07:00
|
|
|
]);
|
2024-08-23 14:19:10 +07:00
|
|
|
|
2024-09-10 10:56:15 +07:00
|
|
|
// setEdges((eds) => [
|
|
|
|
|
// ...eds,
|
|
|
|
|
// {
|
|
|
|
|
// id: uuidv4(),
|
|
|
|
|
// source: lastNodeId,
|
|
|
|
|
// data: {
|
|
|
|
|
// label: <div onClick={handleAddNoteClick}>Add Note</div>, // Use HTML or React component
|
|
|
|
|
// onClick: handleAddNoteClick
|
|
|
|
|
// },
|
|
|
|
|
// target: newNodeId
|
|
|
|
|
// }
|
|
|
|
|
// ]);
|
|
|
|
|
// Cuộn tới node mới thêm
|
2024-09-09 16:00:58 +07:00
|
|
|
setLastNodeId(newNodeId);
|
2024-08-16 15:08:06 +07:00
|
|
|
}
|
2024-08-23 14:42:10 +07:00
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
setShowTabBar(false);
|
2024-08-16 15:08:06 +07:00
|
|
|
};
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
const hidePopup = () => {
|
|
|
|
|
setShowPopup(false);
|
2024-08-22 16:42:25 +07:00
|
|
|
};
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
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
|
2024-08-24 11:01:05 +07:00
|
|
|
};
|
|
|
|
|
|
2024-08-16 15:08:06 +07:00
|
|
|
return (
|
2024-08-23 14:19:10 +07:00
|
|
|
<div style={{ height: '100vh', width: '100%', position: 'relative' }}>
|
2024-09-09 16:00:58 +07:00
|
|
|
<ReactFlow
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
edges={edges}
|
2024-09-10 10:56:15 +07:00
|
|
|
edgeTypes={{ custom: CustomEdge }}
|
2024-09-09 16:00:58 +07:00
|
|
|
onNodeClick={onNodeClick}
|
|
|
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
|
|
|
panOnScroll
|
|
|
|
|
selectionOnDrag
|
|
|
|
|
selectionMode={SelectionMode.Partial}
|
|
|
|
|
>
|
2024-08-16 15:08:06 +07:00
|
|
|
<Controls />
|
2024-09-09 16:00:58 +07:00
|
|
|
<Background variant={BackgroundVariant.Lines} color='#ccccc' />
|
2024-08-16 15:08:06 +07:00
|
|
|
</ReactFlow>
|
2024-09-09 16:00:58 +07:00
|
|
|
|
|
|
|
|
{showTabBar && <Tabbar addNode={addNode} />}
|
|
|
|
|
|
2024-08-24 11:01:05 +07:00
|
|
|
{nodeToDelete && (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: nodeToDelete.position.y - 12,
|
|
|
|
|
left: nodeToDelete.position.x + 260,
|
2024-09-09 16:00:58 +07:00
|
|
|
zIndex: 10
|
2024-08-24 11:01:05 +07:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleNodeDelete}
|
|
|
|
|
style={{
|
|
|
|
|
background: 'red',
|
|
|
|
|
color: 'white',
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
cursor: 'pointer'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
X
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{showPopup && (
|
2024-09-09 16:00:58 +07:00
|
|
|
<ShowPopup
|
|
|
|
|
selectedNodeContent={selectedNodeContent}
|
|
|
|
|
hidePopup={() => setShowPopup(false)}
|
|
|
|
|
/>
|
2024-08-24 11:01:05 +07:00
|
|
|
)}
|
2024-08-16 15:08:06 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-09 16:00:58 +07:00
|
|
|
export default NoteFlow;
|