up
This commit is contained in:
18
.eslintrc.cjs
Normal file
18
.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 webkid GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Cài đặt và chạy dữ án
|
||||||
|
|
||||||
|
Tải dự án về:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://repo.hurasoft.com/tieptk/react-flow
|
||||||
|
```
|
||||||
|
|
||||||
|
Cài đặt file thư viện liên quan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Chạy dự án với lệnh:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="./not-image.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Customer signs up for product updates</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3439
package-lock.json
generated
Normal file
3439
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-react-flow-template",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xyflow/react": "^12.0.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^5.3.0"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
public/not-image.png
Normal file
BIN
public/not-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
128
src/App.tsx
Normal file
128
src/App.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
applyEdgeChanges,
|
||||||
|
applyNodeChanges,
|
||||||
|
Background
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
|
||||||
|
import TextUpdaterNode from './nodes/TextUpdaterNode.js';
|
||||||
|
import { FiUser, FiMail, FiCheckCircle,FiList } from 'react-icons/fi';
|
||||||
|
import {FaShuffle } from "react-icons/fa6";
|
||||||
|
|
||||||
|
const initialNodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'input',
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiUser style={{ marginRight: 5, fontSize: '24px' }} />
|
||||||
|
Customer signs up for product updates
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiMail style={{ marginRight: 5 }} />
|
||||||
|
Send email
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
data: {
|
||||||
|
label:(
|
||||||
|
<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: 200 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'output',
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiCheckCircle style={{ marginRight: 5 }} />
|
||||||
|
Send survey
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
position: { x: 100, y: 300 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'output',
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiList style={{ marginRight: 5 }} />
|
||||||
|
Contact Exits
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
position: { x: 400, y: 300 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges = [
|
||||||
|
{ id: 'e1-2', source: '1', target: '2' },
|
||||||
|
{ id: 'e2-3', source: '2', target: '3' },
|
||||||
|
{ id: 'e3-4', source: '3', target: '4', label: 'No' },
|
||||||
|
{ id: 'e3-5', source: '3', target: '5', label: 'Yes' },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// we define the nodeTypes outside of the component to prevent re-renderings
|
||||||
|
// you could also use useMemo inside the component
|
||||||
|
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||||
|
|
||||||
|
function Flow() {
|
||||||
|
const [nodes, setNodes] = useState(initialNodes);
|
||||||
|
const [edges, setEdges] = useState(initialEdges);
|
||||||
|
|
||||||
|
const onNodesChange = useCallback(
|
||||||
|
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||||
|
[setNodes],
|
||||||
|
);
|
||||||
|
const onEdgesChange = useCallback(
|
||||||
|
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connection) => setEdges((eds) => addEdge(connection, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flow;
|
||||||
|
|
||||||
|
|
||||||
11
src/edges/index.ts
Normal file
11
src/edges/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
122
src/index.css
Normal file
122
src/index.css
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node-input,
|
||||||
|
.react-flow__node-default,
|
||||||
|
.react-flow__node-output,
|
||||||
|
.react-flow__node-group {
|
||||||
|
width: 275px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__attribution {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup Styling */
|
||||||
|
.popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input,
|
||||||
|
form textarea,
|
||||||
|
form select {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 97%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-form-fields {
|
||||||
|
width: 99%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
12
src/main.tsx
Normal file
12
src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
import NoteFlow from './nodes/NoteFlow';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<NoteFlow />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
293
src/nodes/NoteFlow.tsx
Normal file
293
src/nodes/NoteFlow.tsx
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {ReactFlow, addEdge, Controls, Background, MarkerType } 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 { event } from 'jquery';
|
||||||
|
|
||||||
|
|
||||||
|
const initialNode = {
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabBarOptions = [
|
||||||
|
{
|
||||||
|
label: 'Send email',
|
||||||
|
id: '2',
|
||||||
|
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: '3',
|
||||||
|
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: '4',
|
||||||
|
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: '5',
|
||||||
|
type: 'output',
|
||||||
|
html: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiList style={{ marginRight: 5 }} />
|
||||||
|
Contact Exits
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
position: { x: 400, y: 400 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Nội dung cho mỗi node
|
||||||
|
const nodeContents = {
|
||||||
|
'2': `
|
||||||
|
<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': `
|
||||||
|
<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': `
|
||||||
|
<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': `
|
||||||
|
<label for="contact-id">Contact ID:</label>
|
||||||
|
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
||||||
|
`,
|
||||||
|
'4': `
|
||||||
|
<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, ...">
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const NoteFlow = () => {
|
||||||
|
const [nodes, setNodes] = useState([initialNode]);
|
||||||
|
const [edges, setEdges] = useState([]);
|
||||||
|
const [showTabBar, setShowTabBar] = useState(false);
|
||||||
|
const [lastNodeId, setLastNodeId] = useState('1');
|
||||||
|
const [showAdditionalOptions, setShowAdditionalOptions] = useState(false);
|
||||||
|
const [selectedNodeContent, setSelectedNodeContent] = useState('');
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
|
const handleAddNoteClick = () => {
|
||||||
|
setShowTabBar(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabClick = (option) => {
|
||||||
|
setShowTabBar(false);
|
||||||
|
const newNode = {
|
||||||
|
id: option.id,
|
||||||
|
data: { label: option.html },
|
||||||
|
position: { x: option.position.x, y: option.position.y },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => [...nds, newNode]);
|
||||||
|
setEdges((eds) => addEdge({ id: `e${lastNodeId}-${option.id}`, source: lastNodeId, target: option.id, animated: false }, eds));
|
||||||
|
setLastNodeId(option.id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (option.id === '3') {
|
||||||
|
const yesNode = {
|
||||||
|
id: '4',
|
||||||
|
type: 'output',
|
||||||
|
data: { label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiCheckCircle style={{ marginRight: 5 }} />
|
||||||
|
Send survey
|
||||||
|
</div>
|
||||||
|
) },
|
||||||
|
position: { x: 100, y: 400 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const noNode = {
|
||||||
|
id: '5',
|
||||||
|
type: 'output',
|
||||||
|
data: { label: (
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<FiList style={{ marginRight: 5 }} />
|
||||||
|
Contact Exits
|
||||||
|
</div>
|
||||||
|
) },
|
||||||
|
position: { x: 400, y: 400 },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => [...nds, yesNode, noNode]);
|
||||||
|
|
||||||
|
setEdges((eds) => [
|
||||||
|
...eds,
|
||||||
|
{ id: 'e3-4', source: '3', target: '4', label: 'yes', animated: false,},
|
||||||
|
{ id: 'e3-5', source: '3', target: '5', label: 'no', animated: false},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdditionalOptionClick = (option) => {
|
||||||
|
setShowAdditionalOptions(false);
|
||||||
|
|
||||||
|
const surveyNode = {
|
||||||
|
id: option.id,
|
||||||
|
data: { label: 'Send survey' },
|
||||||
|
position: { x: 100, y: 150 * (nodes.length + 1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactExistsNode = {
|
||||||
|
id: '5',
|
||||||
|
data: { label: 'Contact Exists' },
|
||||||
|
position: { x: 400, y: 150 * (nodes.length + 1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => [...nds, surveyNode, contactExistsNode]);
|
||||||
|
|
||||||
|
setEdges((eds) => [
|
||||||
|
...eds,
|
||||||
|
{ id: `e${lastNodeId}-${option.id}`, source: lastNodeId, target: option.id, label: 'yes', animated: true },
|
||||||
|
{ id: `e${option.id}-5`, source: option.id, target: '5', label: 'no', animated: true },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNodeClick = (event, node) => {
|
||||||
|
const content = nodeContents[node.id];
|
||||||
|
setSelectedNodeContent(content);
|
||||||
|
setShowPopup(true);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideenPopup = () => {
|
||||||
|
setShowPopup(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConnect = (params) => setEdges((eds) => addEdge(params, eds));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100vh', width: '100%' }}>
|
||||||
|
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect} onNodeClick={onNodeClick}>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
<button
|
||||||
|
onClick={handleAddNoteClick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
zIndex: 10,
|
||||||
|
padding: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Note
|
||||||
|
</button>
|
||||||
|
{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: 'pointer',
|
||||||
|
borderBottom: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showAdditionalOptions && (
|
||||||
|
<div className='popup'
|
||||||
|
>
|
||||||
|
{additionalOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => handleAdditionalOptionClick(option)}
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderBottom: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoteFlow;
|
||||||
|
|
||||||
|
|
||||||
25
src/nodes/PositionLoggerNode.tsx
Normal file
25
src/nodes/PositionLoggerNode.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/nodes/TextUpdaterNode.tsx
Normal file
31
src/nodes/TextUpdaterNode.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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;
|
||||||
26
src/nodes/index.ts
Normal file
26
src/nodes/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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;
|
||||||
5
src/nodes/types.ts
Normal file
5
src/nodes/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Node, BuiltInNode } from '@xyflow/react';
|
||||||
|
|
||||||
|
export type PositionLoggerNode = Node<{ label: string }, 'position-logger'>;
|
||||||
|
export type AppNode = BuiltInNode | PositionLoggerNode;
|
||||||
|
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user