diff --git a/src/assets/css/style.css b/src/assets/css/style.css
new file mode 100644
index 0000000..a75a91c
--- /dev/null
+++ b/src/assets/css/style.css
@@ -0,0 +1,107 @@
+.PopupTabBar {
+ position: absolute;
+ bottom: 50px;
+ right: 10px;
+ z-index: 10;
+ background: white;
+ border: 1px solid #ccc;
+ padding: 0 10px 10px 10px;
+ width: 340px;
+ height: 460px;
+ max-height: 70%;
+ overflow-y: auto;
+ border-radius: 8px;
+ box-shadow: 0px 12px 24px rgba(36, 28, 21, 0.16);
+}
+.PopupTabBar .tittle {
+ margin: 0 0 10px 0;
+}
+
+.itemTab {
+ padding: 8px 20px;
+}
+
+.itemTab.grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-gap: 8px;
+}
+
+.PopupTabBar .itemTab .items {
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ height: 48px;
+ border: 1px solid rgba(36, 28, 21, 0.15);
+ border-radius: 4px;
+ padding: 16px 8px;
+ background: #fff;
+}
+
+.itemTab.grid .items {
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+}
+
+.PopupTabBar .itemTab .items:hover {
+ box-shadow: 0px 4px 12px rgba(36, 28, 21, 0.12);
+ border: 1px solid #8c8c8c;
+}
+.PopupTabBar .itemTab span {
+ margin-top: 5px;
+ font-size: 13px;
+ text-align: center;
+}
+
+.header-tab {
+ text-align: center;
+ position: sticky;
+ top: 0;
+ padding: 10px 0;
+ z-index: 2;
+ background: #fff;
+}
+
+.header-tab .close-tabbar {
+ text-align: right;
+ font-size: 20px;
+}
+
+.step {
+ display: flex;
+ align-items: center;
+ height: 30px;
+}
+
+.step .left {
+ width: 40px;
+ margin-right: 10px;
+ border-right: 1px solid #241c156e;
+}
+
+.step .right {
+ width: calc(100% - 30px);
+ text-align: left;
+ font-size: 14px;
+}
+
+.step .left img {
+ margin-top: 5px;
+ height: 20px;
+ width: 20px;
+}
+
+.react-flow__edge-text {
+ font-size: 13px;
+ font-weight: 700;
+ text-transform: capitalize;
+}
diff --git a/src/assets/icons/icon-check.svg b/src/assets/icons/icon-check.svg
new file mode 100644
index 0000000..cd27eeb
--- /dev/null
+++ b/src/assets/icons/icon-check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-email.svg b/src/assets/icons/icon-email.svg
new file mode 100644
index 0000000..5d3349e
--- /dev/null
+++ b/src/assets/icons/icon-email.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-if.svg b/src/assets/icons/icon-if.svg
new file mode 100644
index 0000000..bbda400
--- /dev/null
+++ b/src/assets/icons/icon-if.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-list.svg b/src/assets/icons/icon-list.svg
new file mode 100644
index 0000000..29d2777
--- /dev/null
+++ b/src/assets/icons/icon-list.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-time.svg b/src/assets/icons/icon-time.svg
new file mode 100644
index 0000000..4a6520f
--- /dev/null
+++ b/src/assets/icons/icon-time.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-user.svg b/src/assets/icons/icon-user.svg
new file mode 100644
index 0000000..f366874
--- /dev/null
+++ b/src/assets/icons/icon-user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/icon-wait.svg b/src/assets/icons/icon-wait.svg
new file mode 100644
index 0000000..c3d511e
--- /dev/null
+++ b/src/assets/icons/icon-wait.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/edges/index.ts b/src/edges/index.ts
deleted file mode 100644
index 1f2b021..0000000
--- a/src/edges/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Edge, EdgeTypes } from '@xyflow/react';
-
-export const initialEdges: Edge[] = [
- { id: 'a->c', source: 'a', target: 'c', animated: true },
- { id: 'b->d', source: 'b', target: 'd' },
- { id: 'c->d', source: 'c', target: 'd', animated: true },
-];
-
-export const edgeTypes = {
- // Add your custom edge types here!
-} satisfies EdgeTypes;
diff --git a/src/main.tsx b/src/main.tsx
index 8d624a4..553536f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -10,4 +10,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
-);
\ No newline at end of file
+);
diff --git a/src/nodes/NoteFlow.tsx b/src/nodes/NoteFlow.tsx
index 3f40606..2e04727 100644
--- a/src/nodes/NoteFlow.tsx
+++ b/src/nodes/NoteFlow.tsx
@@ -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: (
-
-
-
Customer signs up for product updates
+
+
+
+
+
Customer signs up to
- ),
+ )
},
- 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: (
-
-
- Send email
-
- ),
- position: { x: 250, y: 200 },
- },
- {
- label: 'Replied to conversation?',
- id: '4',
- type: '',
- html: (
-
-
- Replied to conversation?
-
- ),
- position: { x: 250, y: 290 },
- },
- {
- label: 'Send survey',
- id: '5',
- type: 'output',
- html: (
-
-
- Send survey
-
- ),
- position: { x: 100, y: 400 },
- },
- {
- label: 'Contact Exists',
- id: '6',
- type: 'output',
- html: (
-
-
- Contact Exists
-
- ),
- position: { x: 400, y: 400 },
- },
+ position: { x: 250, y: 250 }
+ }
];
// Nội dung cho mỗi node
const nodeContents = {
- '2': `
+ 'send-email': `
Subject:
Body:
`,
- '1': `
+ 'customer-sign': `
Customer Name:
Customer Email:
`,
- '3': `
+ ifelse: `
Reply Status:
Replied
Not Replied
`,
- '5': `
+ 'contact-exists': `
Contact ID:
`,
- '4': `
+ 'send-survey': `
Survey Question:
Options (comma-separated):
@@ -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: (
-
-
- Send survey
-
- ),
- },
- position: { x: option.position.x - 150, y: option.position.y + 150 },
- };
-
- const noNode = {
- id: '6',
- type: 'output',
- data: {
- label: (
-
-
- Contact Exists
-
- ),
- },
- 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: (
+
+
+
+
+
Contact Exists
+
+ )
+ },
+ position: { x: option.position.x - 150, y: nextY + 150 }
+ };
+
+ const noNode = {
+ id: uuidv4(),
+ type: 'output',
+ key: 'send-survey',
+ data: {
+ label: (
+
+
+
+
+
Send survey
+
+ )
+ },
+ 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 (
-
+
-
+
- {showTabBar && (
-
- {tabBarOptions.map((option) => (
-
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}
-
- ))}
-
- )}
+
+ {showTabBar && }
+
{nodeToDelete && (
{
)}
{showPopup && (
- <>
-
-
- >
+ setShowPopup(false)}
+ />
)}
);
};
-export default NoteFlow;
\ No newline at end of file
+export default NoteFlow;
diff --git a/src/nodes/PositionLoggerNode.tsx b/src/nodes/PositionLoggerNode.tsx
deleted file mode 100644
index 2b93cd3..0000000
--- a/src/nodes/PositionLoggerNode.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Handle, Position, type NodeProps } from '@xyflow/react';
-
-import { type PositionLoggerNode } from './types';
-
-export function PositionLoggerNode({
- positionAbsoluteX,
- positionAbsoluteY,
- data,
-}: NodeProps
) {
- const x = `${Math.round(positionAbsoluteX)}px`;
- const y = `${Math.round(positionAbsoluteY)}px`;
-
- return (
- // We add this class to use the same styles as React Flow's default nodes.
-
- {data.label &&
{data.label}
}
-
-
- {x} {y}
-
-
-
-
- );
-}
diff --git a/src/nodes/ShowPopup.tsx b/src/nodes/ShowPopup.tsx
new file mode 100644
index 0000000..beee81e
--- /dev/null
+++ b/src/nodes/ShowPopup.tsx
@@ -0,0 +1,34 @@
+import { useState, useEffect } from 'react';
+
+const ShowPopup = ({ selectedNodeContent, hidePopup }) => {
+ const [showPopup, setShowPopup] = useState(true);
+
+ const handleOverlayClick = () => {
+ setShowPopup(false);
+ hidePopup(); // Gọi hàm từ props để ẩn popup ở component cha
+ };
+
+ useEffect(() => {
+ if (!showPopup) {
+ hidePopup(); // Đảm bảo khi popup bị ẩn, nó được xử lý ở component cha
+ }
+ }, [showPopup, hidePopup]);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default ShowPopup;
diff --git a/src/nodes/Tabbar.tsx b/src/nodes/Tabbar.tsx
new file mode 100644
index 0000000..4d5df60
--- /dev/null
+++ b/src/nodes/Tabbar.tsx
@@ -0,0 +1,192 @@
+import { useState, useCallback } from 'react';
+import { FiUser, FiMail, FiCheckCircle, FiList } from 'react-icons/fi';
+import { addEdge } from '@xyflow/react';
+
+const initialNodes = [
+ {
+ id: '1',
+ type: 'input',
+ key: 'customer-signs',
+ data: {
+ label: (
+
+
+ Customer signs up for product updates
+
+ )
+ },
+ position: { x: 250, y: 100 }
+ },
+ {
+ id: '2',
+ data: { label: 'Add Note' },
+ position: { x: 250, y: 250 }
+ }
+];
+// nội dung tabbar
+const tabBarOptions = [
+ {
+ key: 'customer-sign',
+ label: 'Send email',
+ id: '3',
+ type: '',
+ html: (
+ <>
+
+ Email
+ >
+ ),
+ htmlNode: (
+
+
+
+
+
Send email
+
+ ),
+ position: { x: 250, y: 0 }
+ },
+ {
+ key: 'ifelse',
+ label: 'Replied to conversation?',
+ id: '4',
+ type: '',
+ html: (
+ <>
+
+ Replied to conversation?
+ >
+ ),
+ htmlNode: (
+
+
+
+
+
Replied to conversation?
+
+ ),
+ position: { x: 250, y: 290 }
+ },
+ {
+ key: 'send-survey',
+ label: 'Send survey',
+ id: '5',
+ type: 'output',
+ html: (
+ <>
+
+ Send survey
+ >
+ ),
+ htmlNode: (
+
+
+
+
+
Send survey
+
+ ),
+ position: { x: 250, y: 400 }
+ },
+ {
+ key: 'contact-exists',
+ label: 'Contact Exists',
+ id: '6',
+ type: 'output',
+ html: (
+ <>
+
+ Contact Exists
+ >
+ ),
+ htmlNode: (
+
+
+
+
+
Contact Exists
+
+ ),
+ position: { x: 250, y: 400 }
+ }
+];
+
+const Tabbar = ({ addNode }) => {
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
+
+ const handleTabClick = (option) => {
+ addNode(option);
+ // Cập nhật selectedNodeId với ID của node được chọn
+ setSelectedNodeId(option.id);
+ };
+
+ return (
+ <>
+
+
+
Rules
+
+ {tabBarOptions.map((option) => (
+
handleTabClick(option)}
+ >
+ {option.html}
+
+ ))}
+
+
+ >
+ );
+};
+
+export default Tabbar;
diff --git a/src/nodes/TextUpdaterNode.tsx b/src/nodes/TextUpdaterNode.tsx
deleted file mode 100644
index 61d1f03..0000000
--- a/src/nodes/TextUpdaterNode.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useCallback } from 'react';
-import { Handle, Position } from '@xyflow/react';
-
-const handleStyle = { left: 10 };
-
-function TextUpdaterNode({ data, isConnectable }) {
- const onChange = useCallback((evt) => {
- console.log(evt.target.value);
- }, []);
-
- return (
-
-
Customer signs up for product updates
-
-
-
- );
-}
-
-export default TextUpdaterNode;
diff --git a/src/nodes/index.ts b/src/nodes/index.ts
deleted file mode 100644
index 3307f46..0000000
--- a/src/nodes/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { NodeTypes } from '@xyflow/react';
-
-import { PositionLoggerNode } from './PositionLoggerNode';
-import { AppNode } from './types';
-
-export const initialNodes: AppNode[] = [
- { id: 'a', type: 'input', position: { x: 0, y: 0 }, data: { label: 'wire' } },
- {
- id: 'b',
- type: 'position-logger',
- position: { x: -100, y: 100 },
- data: { label: 'drag me!' },
- },
- { id: 'c', position: { x: 100, y: 100 }, data: { label: 'your ideas' } },
- {
- id: 'd',
- type: 'output',
- position: { x: 0, y: 200 },
- data: { label: 'with React Flow' },
- },
-];
-
-export const nodeTypes = {
- 'position-logger': PositionLoggerNode,
- // Add any of your custom nodes here!
-} satisfies NodeTypes;