update
107
src/assets/css/style.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
1
src/assets/icons/icon-check.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 447 B |
1
src/assets/icons/icon-email.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="wink-icon icon-3SKgm"><path fill-rule="evenodd" clip-rule="evenodd" d="M21 3H3a2 2 0 00-2 2v14a2 2 0 002 2h18a2 2 0 002-2V5a2 2 0 00-2-2zM3 5h18v.57L12.101 14h-.203L3 5.57V5zm0 3.325v7.891l4.219-3.894L3 8.325zM3 19v-.062L8.674 13.7l2.428 2.3h1.796l2.428-2.3L21 18.938V19H3zm18-2.784V8.325l-4.219 3.997L21 16.216z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
1
src/assets/icons/icon-if.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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 2v2c0 1.316.323 2.097.738 2.65.443.591 1.045 1.004 1.817 1.518l.05.033c.719.48 1.591 1.06 2.258 1.949.71.947 1.137 2.166 1.137 3.85v2h3l-4 6-4-6h3v-2c0-1.316-.323-2.097-.738-2.65-.443-.591-1.045-1.004-1.817-1.518l-.05-.033c-.719-.48-1.591-1.06-2.258-1.949C11.427 6.903 11 5.684 11 4V2h2zM9.685 9.291c.184-.173.372-.353.562-.544L8.832 7.332c-.167.169-.34.335-.517.502-.121.114-.246.228-.372.345C6.585 9.43 5 10.89 5 13.667V16H2l4 6 4-6H7v-2.333c0-1.873.95-2.758 2.347-4.06l.338-.316z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 656 B |
1
src/assets/icons/icon-list.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 144a48 48 0 1 0 0-96 48 48 0 1 0 0 96zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM64 464a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm48-208a48 48 0 1 0 -96 0 48 48 0 1 0 96 0z"/></svg>
|
||||||
|
After Width: | Height: | Size: 658 B |
1
src/assets/icons/icon-time.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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="M11 5v7.5a1 1 0 00.486.857l5 3 1.029-1.714L13 11.933V5h-2z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M23 12c0 6.075-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1s11 4.925 11 11zm-2 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
src/assets/icons/icon-user.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="28" viewBox="0 0 448 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z"/></svg>
|
||||||
|
After Width: | Height: | Size: 437 B |
1
src/assets/icons/icon-wait.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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="M11 8H9v8h2V8zm4 0h-2v8h2V8z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M23 12c0 6.075-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1s11 4.925 11 11zm-2 0a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
@@ -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;
|
|
||||||
@@ -1,108 +1,75 @@
|
|||||||
import { useState,useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { ReactFlow, addEdge, Controls, Background } from '@xyflow/react';
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
SelectionMode
|
||||||
|
} 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 { FiUser, FiMail, FiCheckCircle, FiList } from 'react-icons/fi';
|
||||||
import { FaShuffle } from 'react-icons/fa6';
|
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 = [
|
const initialNodes = [
|
||||||
{
|
{
|
||||||
|
key: 'customer-sign',
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
data: {
|
data: {
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div className='step'>
|
||||||
<FiUser style={{ marginRight: 5 }} />
|
<div className='left'>
|
||||||
<span>Customer signs up for product updates</span>
|
<img
|
||||||
|
src='/src/assets/icons/icon-user.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
<div className='right'>Customer signs up to</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
position: { x: 250, y: 100 },
|
position: { x: 250, y: 100 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Add Note' },
|
data: { label: 'Add Note' },
|
||||||
position: { x: 250, y: 250 },
|
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 },
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Nội dung cho mỗi node
|
// Nội dung cho mỗi node
|
||||||
const nodeContents = {
|
const nodeContents = {
|
||||||
'2': `
|
'send-email': `
|
||||||
<label for="email-subject">Subject:</label>
|
<label for="email-subject">Subject:</label>
|
||||||
<input type="text" id="email-subject" name="email-subject" placeholder="Enter subject">
|
<input type="text" id="email-subject" name="email-subject" placeholder="Enter subject">
|
||||||
<label for="email-body">Body:</label>
|
<label for="email-body">Body:</label>
|
||||||
<textarea id="email-body" name="email-body" rows="4" placeholder="Enter email body"></textarea>
|
<textarea id="email-body" name="email-body" rows="4" placeholder="Enter email body"></textarea>
|
||||||
`,
|
`,
|
||||||
'1': `
|
'customer-sign': `
|
||||||
<label for="customer-name">Customer Name:</label>
|
<label for="customer-name">Customer Name:</label>
|
||||||
<input type="text" id="customer-name" name="customer-name" placeholder="Enter customer name">
|
<input type="text" id="customer-name" name="customer-name" placeholder="Enter customer name">
|
||||||
<label for="customer-email">Customer Email:</label>
|
<label for="customer-email">Customer Email:</label>
|
||||||
<input type="email" id="customer-email" name="customer-email" placeholder="Enter customer email">
|
<input type="email" id="customer-email" name="customer-email" placeholder="Enter customer email">
|
||||||
`,
|
`,
|
||||||
'3': `
|
ifelse: `
|
||||||
<label for="reply-status">Reply Status:</label>
|
<label for="reply-status">Reply Status:</label>
|
||||||
<select id="reply-status" name="reply-status">
|
<select id="reply-status" name="reply-status">
|
||||||
<option value="replied">Replied</option>
|
<option value="replied">Replied</option>
|
||||||
<option value="not-replied">Not Replied</option>
|
<option value="not-replied">Not Replied</option>
|
||||||
</select>
|
</select>
|
||||||
`,
|
`,
|
||||||
'5': `
|
'contact-exists': `
|
||||||
<label for="contact-id">Contact ID:</label>
|
<label for="contact-id">Contact ID:</label>
|
||||||
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
||||||
`,
|
`,
|
||||||
'4': `
|
'send-survey': `
|
||||||
<label for="survey-question">Survey Question:</label>
|
<label for="survey-question">Survey Question:</label>
|
||||||
<input type="text" id="survey-question" name="survey-question" placeholder="Enter survey question">
|
<input type="text" id="survey-question" name="survey-question" placeholder="Enter survey question">
|
||||||
<label for="survey-options">Options (comma-separated):</label>
|
<label for="survey-options">Options (comma-separated):</label>
|
||||||
@@ -110,127 +77,35 @@ const nodeContents = {
|
|||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const NoteFlow = () => {
|
const NoteFlow = () => {
|
||||||
const [nodes, setNodes] = useState(initialNodes);
|
const [nodes, setNodes] = useState(initialNodes);
|
||||||
const [edges, setEdges] = useState([
|
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 [showTabBar, setShowTabBar] = useState(false);
|
||||||
const [lastNodeId, setLastNodeId] = useState('1');
|
const [lastNodeId, setLastNodeId] = useState('1');
|
||||||
const [selectedNodeId, setSelectedNodeId] = useState(null); // Theo dõi node đã chọn
|
|
||||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleAddNoteClick = () => {
|
const handleAddNoteClick = () => {
|
||||||
setShowTabBar(true);
|
setShowTabBar(true);
|
||||||
|
const handleNodeSelect = (selectedNode) => {
|
||||||
|
addNodeBetween(selectedNode, sourceId, targetId);
|
||||||
};
|
};
|
||||||
|
setAddNodeCallback(() => addNodeBetweenWithIds);
|
||||||
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 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNodeClick = (event, node) => {
|
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);
|
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) => {
|
const onNodeContextMenu = useCallback((event, node) => {
|
||||||
@@ -241,65 +116,203 @@ const NoteFlow = () => {
|
|||||||
const handleNodeDelete = () => {
|
const handleNodeDelete = () => {
|
||||||
if (nodeToDelete) {
|
if (nodeToDelete) {
|
||||||
setNodes((nds) => nds.filter((node) => node.id !== nodeToDelete.id));
|
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);
|
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) => {
|
let newYPosition = baseY;
|
||||||
const content = nodeContents[node.id];
|
|
||||||
setSelectedNodeContent(content);
|
while (existingYPositions.includes(newYPosition)) {
|
||||||
setShowPopup(true);
|
newYPosition += spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideenPopup = () => {
|
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);
|
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 (
|
return (
|
||||||
<div style={{ height: '100vh', width: '100%', position: 'relative' }}>
|
<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 />
|
<Controls />
|
||||||
<Background />
|
<Background variant={BackgroundVariant.Lines} color='#ccccc' />
|
||||||
</ReactFlow>
|
</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={{
|
{showTabBar && <Tabbar addNode={addNode} />}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
{nodeToDelete && (
|
{nodeToDelete && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: nodeToDelete.position.y - 12,
|
top: nodeToDelete.position.y - 12,
|
||||||
left: nodeToDelete.position.x + 260,
|
left: nodeToDelete.position.x + 260,
|
||||||
zIndex: 10,
|
zIndex: 10
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -317,21 +330,10 @@ const NoteFlow = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showPopup && (
|
{showPopup && (
|
||||||
<>
|
<ShowPopup
|
||||||
<div className="popup-overlay" onClick={hideenPopup}></div>
|
selectedNodeContent={selectedNodeContent}
|
||||||
<div
|
hidePopup={() => setShowPopup(false)}
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Handle, Position, type NodeProps } from '@xyflow/react';
|
|
||||||
|
|
||||||
import { type PositionLoggerNode } from './types';
|
|
||||||
|
|
||||||
export function PositionLoggerNode({
|
|
||||||
positionAbsoluteX,
|
|
||||||
positionAbsoluteY,
|
|
||||||
data,
|
|
||||||
}: NodeProps<PositionLoggerNode>) {
|
|
||||||
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.
|
|
||||||
<div className="react-flow__node-default">
|
|
||||||
{data.label && <div>{data.label}</div>}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{x} {y}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Handle type="source" position={Position.Bottom} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
34
src/nodes/ShowPopup.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className='popup-overlay' onClick={handleOverlayClick}></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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowPopup;
|
||||||
192
src/nodes/Tabbar.tsx
Normal file
@@ -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: (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiUser style={{ marginRight: 5 }} />
|
||||||
|
<span>Customer signs up for product updates</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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: (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-email.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
<span>Email</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
htmlNode: (
|
||||||
|
<div className='step'>
|
||||||
|
<div className='left'>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-email.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='right'>Send email</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
position: { x: 250, y: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ifelse',
|
||||||
|
label: 'Replied to conversation?',
|
||||||
|
id: '4',
|
||||||
|
type: '',
|
||||||
|
html: (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-if.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
<span>Replied to conversation?</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
htmlNode: (
|
||||||
|
<div className='step'>
|
||||||
|
<div className='left'>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-if.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='right'>Replied to conversation?</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
position: { x: 250, y: 290 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send-survey',
|
||||||
|
label: 'Send survey',
|
||||||
|
id: '5',
|
||||||
|
type: 'output',
|
||||||
|
html: (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-list.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
<span>Send survey</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
htmlNode: (
|
||||||
|
<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: 250, y: 400 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'contact-exists',
|
||||||
|
label: 'Contact Exists',
|
||||||
|
id: '6',
|
||||||
|
type: 'output',
|
||||||
|
html: (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src='/src/assets/icons/icon-check.svg'
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
<span>Contact Exists</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
htmlNode: (
|
||||||
|
<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: 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 (
|
||||||
|
<>
|
||||||
|
<div className='PopupTabBar'>
|
||||||
|
<div className='header-tab'>
|
||||||
|
<div className='close-tabbar'>✖</div>
|
||||||
|
</div>
|
||||||
|
<h3 className='tittle'>Rules</h3>
|
||||||
|
<div className='itemTab grid'>
|
||||||
|
{tabBarOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
className='items'
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => handleTabClick(option)}
|
||||||
|
>
|
||||||
|
{option.html}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabbar;
|
||||||
@@ -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 (
|
|
||||||
<div className="text-updater-node">
|
|
||||||
<div className="customer-sign-up node" data-node="customerSignsUpNode"><p><i className="fa-regular fa-circle-play"></i> Customer signs up for product updates</p></div>
|
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
id="a"
|
|
||||||
style={handleStyle}
|
|
||||||
isConnectable={isConnectable}
|
|
||||||
/>
|
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
id="b"
|
|
||||||
isConnectable={isConnectable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextUpdaterNode;
|
|
||||||
@@ -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;
|
|
||||||