update
This commit is contained in:
@@ -1,108 +1,75 @@
|
||||
import { useState,useCallback } from 'react';
|
||||
import { ReactFlow, addEdge, Controls, Background } from '@xyflow/react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
SelectionMode
|
||||
} 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const initialNodes = [
|
||||
{
|
||||
key: 'customer-sign',
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiUser style={{ marginRight: 5 }} />
|
||||
<span>Customer signs up for product updates</span>
|
||||
<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>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
position: { x: 250, y: 100 },
|
||||
position: { x: 250, y: 100 }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Add Note' },
|
||||
position: { x: 250, y: 250 },
|
||||
},
|
||||
];
|
||||
|
||||
// nội dung tabbar
|
||||
const tabBarOptions = [
|
||||
{
|
||||
label: 'Send email',
|
||||
id: '3',
|
||||
type: '',
|
||||
html: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiMail style={{ marginRight: 5 }} />
|
||||
Send email
|
||||
</div>
|
||||
),
|
||||
position: { x: 250, y: 200 },
|
||||
},
|
||||
{
|
||||
label: 'Replied to conversation?',
|
||||
id: '4',
|
||||
type: '',
|
||||
html: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FaShuffle style={{ marginRight: 5, transform: `rotate(90deg)` }} />
|
||||
<span>Replied to conversation?</span>
|
||||
</div>
|
||||
),
|
||||
position: { x: 250, y: 290 },
|
||||
},
|
||||
{
|
||||
label: 'Send survey',
|
||||
id: '5',
|
||||
type: 'output',
|
||||
html: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiCheckCircle style={{ marginRight: 5 }} />
|
||||
Send survey
|
||||
</div>
|
||||
),
|
||||
position: { x: 100, y: 400 },
|
||||
},
|
||||
{
|
||||
label: 'Contact Exists',
|
||||
id: '6',
|
||||
type: 'output',
|
||||
html: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiList style={{ marginRight: 5 }} />
|
||||
Contact Exists
|
||||
</div>
|
||||
),
|
||||
position: { x: 400, y: 400 },
|
||||
},
|
||||
position: { x: 250, y: 250 }
|
||||
}
|
||||
];
|
||||
|
||||
// Nội dung cho mỗi node
|
||||
const nodeContents = {
|
||||
'2': `
|
||||
'send-email': `
|
||||
<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>
|
||||
`,
|
||||
'1': `
|
||||
'customer-sign': `
|
||||
<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">
|
||||
`,
|
||||
'3': `
|
||||
ifelse: `
|
||||
<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>
|
||||
`,
|
||||
'5': `
|
||||
'contact-exists': `
|
||||
<label for="contact-id">Contact ID:</label>
|
||||
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
||||
`,
|
||||
'4': `
|
||||
'send-survey': `
|
||||
<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>
|
||||
@@ -110,127 +77,35 @@ const nodeContents = {
|
||||
`
|
||||
};
|
||||
|
||||
|
||||
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
|
||||
{ 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 [lastNodeId, setLastNodeId] = useState('1');
|
||||
const [selectedNodeId, setSelectedNodeId] = useState(null); // Theo dõi node đã chọn
|
||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
||||
|
||||
|
||||
|
||||
const handleAddNoteClick = () => {
|
||||
setShowTabBar(true);
|
||||
};
|
||||
|
||||
const handleTabClick = (option) => {
|
||||
setShowTabBar(false);
|
||||
|
||||
|
||||
if (selectedNodeId === option.id) {
|
||||
alert('Node này đã được thêm. Vui lòng chọn một node khác.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newNode = {
|
||||
id: option.id,
|
||||
data: { label: option.html },
|
||||
position: { x: option.position.x, y: option.position.y },
|
||||
const handleNodeSelect = (selectedNode) => {
|
||||
addNodeBetween(selectedNode, sourceId, targetId);
|
||||
};
|
||||
|
||||
const addNoteNode = nodes.find((node) => node.id === '2');
|
||||
|
||||
// Xóa node "Add Note" nếu chọn "Replied to conversation?"
|
||||
if (option.id === '4') {
|
||||
setNodes((nds) => nds.filter((node) => node.id !== '2'));
|
||||
|
||||
// Thêm node "Replied to conversation?"
|
||||
const repliedNode = {
|
||||
id: '4',
|
||||
data: { label: option.html },
|
||||
position: { x: option.position.x, y: option.position.y },
|
||||
};
|
||||
|
||||
// Thêm các nhánh "Send survey" và "Contact Exists"
|
||||
const yesNode = {
|
||||
id: '5',
|
||||
type: 'output',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiCheckCircle style={{ marginRight: 5 }} />
|
||||
Send survey
|
||||
</div>
|
||||
),
|
||||
},
|
||||
position: { x: option.position.x - 150, y: option.position.y + 150 },
|
||||
};
|
||||
|
||||
const noNode = {
|
||||
id: '6',
|
||||
type: 'output',
|
||||
data: {
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<FiList style={{ marginRight: 5 }} />
|
||||
Contact Exists
|
||||
</div>
|
||||
),
|
||||
},
|
||||
position: { x: option.position.x + 150, y: option.position.y + 150 },
|
||||
};
|
||||
|
||||
setNodes((nds) => [...nds, repliedNode, yesNode, noNode]);
|
||||
|
||||
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
{ id: 'e4-5', source: '4', target: '5', label: 'yes', animated: false },
|
||||
{ id: 'e4-6', source: '4', target: '6', label: 'no', animated: false },
|
||||
{ id: `e${lastNodeId}-4`, source: lastNodeId, target: '4' }, // Kết nối node hiện tại với "Replied to conversation?"
|
||||
]);
|
||||
} else {
|
||||
if (addNoteNode) {
|
||||
// Di chuyển node "Add Note" xuống dưới node mới
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === '2'
|
||||
? { ...node, position: { x: option.position.x, y: option.position.y + 150 } }
|
||||
: node
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Thêm node mới vào danh sách node
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
|
||||
// Kết nối node mới với node hiện tại
|
||||
setEdges((eds) => addEdge({ id: `e${lastNodeId}-${option.id}`, source: lastNodeId, target: option.id }, eds));
|
||||
|
||||
// Kết nối node "Add Note" cũ với node mới
|
||||
if (addNoteNode) {
|
||||
setEdges((eds) => addEdge({ id: `e${option.id}-2`, source: option.id, target: '2' }, eds));
|
||||
}
|
||||
|
||||
// Cập nhật lastNodeId
|
||||
setLastNodeId(option.id);
|
||||
}
|
||||
|
||||
// Cập nhật selectedNodeId với ID của node được chọn
|
||||
setSelectedNodeId(option.id);
|
||||
setAddNodeCallback(() => addNodeBetweenWithIds);
|
||||
};
|
||||
|
||||
const onNodeClick = (event, node) => {
|
||||
if (node.id === '2') { // Check if the clicked node is "Add Note"
|
||||
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
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onNodeContextMenu = useCallback((event, node) => {
|
||||
@@ -241,65 +116,203 @@ const NoteFlow = () => {
|
||||
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));
|
||||
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);
|
||||
|
||||
const onNodeDoubleClick = (event, node) => {
|
||||
const content = nodeContents[node.id];
|
||||
setSelectedNodeContent(content);
|
||||
setShowPopup(true);
|
||||
}
|
||||
let newYPosition = baseY;
|
||||
|
||||
const hideenPopup = () => {
|
||||
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 addNode = (option) => {
|
||||
const newNodeId = uuidv4(); // Generate a unique ID for the new node
|
||||
const addNoteNode = nodes.find((node) => node.id === '2');
|
||||
const initialX = addNoteNode.position.x;
|
||||
const nextY =
|
||||
nodes.length > 2
|
||||
? getNextYPosition(initialX, 130)
|
||||
: addNoteNode.position.y;
|
||||
|
||||
if (option.id === '4') {
|
||||
// Handle specific option case
|
||||
setNodes((nds) => nds.filter((node) => node.id !== '2'));
|
||||
|
||||
const repliedNode = {
|
||||
id: newNodeId,
|
||||
key: option.key,
|
||||
data: { label: option.htmlNode },
|
||||
position: { x: option.position.x, y: nextY }
|
||||
};
|
||||
|
||||
const yesNode = {
|
||||
id: uuidv4(),
|
||||
type: 'output',
|
||||
key: 'contact-exists',
|
||||
data: {
|
||||
label: (
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: option.position.x - 150, y: nextY + 150 }
|
||||
};
|
||||
|
||||
const noNode = {
|
||||
id: uuidv4(),
|
||||
type: 'output',
|
||||
key: 'send-survey',
|
||||
data: {
|
||||
label: (
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
position: { x: option.position.x + 150, y: nextY + 150 }
|
||||
};
|
||||
|
||||
setNodes((nds) => [...nds, repliedNode, yesNode, noNode]);
|
||||
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
{
|
||||
id: uuidv4(),
|
||||
source: repliedNode.id,
|
||||
target: yesNode.id,
|
||||
label: 'yes',
|
||||
animated: false
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
source: repliedNode.id,
|
||||
target: noNode.id,
|
||||
label: 'no',
|
||||
animated: false
|
||||
},
|
||||
{ id: uuidv4(), source: lastNodeId, target: repliedNode.id }
|
||||
]);
|
||||
} else {
|
||||
// Add new node logic
|
||||
if (addNoteNode) {
|
||||
const initialX = addNoteNode.position.x;
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === '2'
|
||||
? {
|
||||
...node,
|
||||
position: {
|
||||
x: initialX,
|
||||
y: nextY + 130
|
||||
}
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
}
|
||||
const newNode = {
|
||||
id: newNodeId,
|
||||
key: option.key,
|
||||
data: { label: option.htmlNode },
|
||||
position: {
|
||||
x: option.position.x,
|
||||
y: nodes.length > 2 ? nextY : addNoteNode.position.y
|
||||
}
|
||||
};
|
||||
|
||||
console.log(getNextYPosition(option.position.x, 130));
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
|
||||
setEdges((eds) => [
|
||||
...eds,
|
||||
{
|
||||
id: uuidv4(),
|
||||
source: lastNodeId,
|
||||
target: newNodeId
|
||||
}
|
||||
]);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', width: '100%', position: 'relative' }}>
|
||||
<ReactFlow nodes={nodes} edges={edges} onNodeClick={onNodeClick} onNodeContextMenu={onNodeContextMenu} onNodeDoubleClick={onNodeDoubleClick}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
panOnScroll
|
||||
selectionOnDrag
|
||||
selectionMode={SelectionMode.Partial}
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
<Background variant={BackgroundVariant.Lines} color='#ccccc' />
|
||||
</ReactFlow>
|
||||
{showTabBar && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: 10,
|
||||
zIndex: 10,
|
||||
background: 'white',
|
||||
border: '1px solid #ccc',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
{tabBarOptions.map((option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
onClick={() => handleTabClick(option)}
|
||||
|
||||
style={{
|
||||
padding: '10px',
|
||||
cursor: selectedNodeId === option.id ? 'not-allowed' : 'pointer',
|
||||
opacity: selectedNodeId === option.id ? 0.5 : 1, // Làm mờ các node đã chọn
|
||||
borderBottom: '1px solid #ddd',
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTabBar && <Tabbar addNode={addNode} />}
|
||||
|
||||
{nodeToDelete && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: nodeToDelete.position.y - 12,
|
||||
left: nodeToDelete.position.x + 260,
|
||||
zIndex: 10,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<button
|
||||
@@ -317,24 +330,13 @@ const NoteFlow = () => {
|
||||
</div>
|
||||
)}
|
||||
{showPopup && (
|
||||
<>
|
||||
<div className="popup-overlay" onClick={hideenPopup}></div>
|
||||
<div
|
||||
className={`popup global-popup ${showPopup ? '' : 'hidden'}`}
|
||||
>
|
||||
<div className="popup-content">
|
||||
<div className="conent-form">
|
||||
<form>
|
||||
<div dangerouslySetInnerHTML={{ __html: selectedNodeContent }} />
|
||||
<button onClick={() => setShowPopup(false)}>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<ShowPopup
|
||||
selectedNodeContent={selectedNodeContent}
|
||||
hidePopup={() => setShowPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoteFlow;
|
||||
export default NoteFlow;
|
||||
|
||||
Reference in New Issue
Block a user