up
19
.eslintrc.cjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||||
|
settings: { react: { version: "18.2" } },
|
||||||
|
plugins: ["react-refresh"],
|
||||||
|
rules: {
|
||||||
|
"react-refresh/only-export-components": "warn",
|
||||||
|
},
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
COLORS: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
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?
|
||||||
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
# React Flow Drag
|
||||||
|
|
||||||
|
# **[View the Live Demo](https://react-flow-drag.vercel.app/)**
|
||||||
|
|
||||||
|
Experience the versatility of React Flow Drag, a robust tool for effortless drag-and-drop functionality in web applications. Explore its potential for creating highly interactive user interfaces.
|
||||||
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5140
package-lock.json
generated
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
"@contactlab/ds-tokens": "^3.3.0",
|
||||||
|
"antd": "^5.4.7",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-flow-renderer": "^10.3.17",
|
||||||
|
"react-router-dom": "^6.26.2",
|
||||||
|
"reactflow": "^11.7.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"zustand": "^4.3.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
85
src/App.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import "antd/dist/reset.css";
|
||||||
|
import "./index.scss";
|
||||||
|
import { getIncomers, getOutgoers } from "react-flow-renderer";
|
||||||
|
// File Importing from folder
|
||||||
|
import Layout from "./Automation.jsx";
|
||||||
|
import { initialElements } from "./Data/Elements1.jsx";
|
||||||
|
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils.jsx";
|
||||||
|
import Sidebar from "./Sidebar/Sidebar";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [elements, setElements] = React.useState([]);
|
||||||
|
|
||||||
|
const onAddNodeCallback = ({ id, type }) => {
|
||||||
|
console.log(type);
|
||||||
|
setElements((elements) =>
|
||||||
|
getUpdatedElementsAfterNodeAddition({
|
||||||
|
elements,
|
||||||
|
targetEdgeId: id,
|
||||||
|
type,
|
||||||
|
onDeleteNodeCallback,
|
||||||
|
onNodeClickCallback,
|
||||||
|
onAddNodeCallback,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteNodeCallback = (id) => {
|
||||||
|
setElements((elements) => {
|
||||||
|
const clonedElements = _.cloneDeep(elements);
|
||||||
|
const incomingEdges = clonedElements.filter((x) => x.target === id);
|
||||||
|
const outgoingEdges = clonedElements.filter((x) => x.source === id);
|
||||||
|
const updatedIncomingEdges = incomingEdges.map((x) => ({
|
||||||
|
...x,
|
||||||
|
target: outgoingEdges[0].target,
|
||||||
|
}));
|
||||||
|
const filteredElements = clonedElements.filter(
|
||||||
|
(x) =>
|
||||||
|
x.id !== id &&
|
||||||
|
x.target !== incomingEdges[0].target &&
|
||||||
|
x.source !== outgoingEdges[0].source
|
||||||
|
);
|
||||||
|
filteredElements.push(...updatedIncomingEdges);
|
||||||
|
return filteredElements;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNodeClickCallback = (id) => {
|
||||||
|
setElements((elements) => {
|
||||||
|
const currentNode = elements.find((x) => x.id === id);
|
||||||
|
const nodes = elements.filter((x) => x.position);
|
||||||
|
const edges = elements.filter((x) => !x.position);
|
||||||
|
console.error({
|
||||||
|
incomers: getIncomers(currentNode, nodes, edges),
|
||||||
|
outgoers: getOutgoers(currentNode, nodes, edges),
|
||||||
|
});
|
||||||
|
return elements;
|
||||||
|
});
|
||||||
|
alert(`You clicked the "${id}" node`);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const nodes = initialElements
|
||||||
|
.filter((x) => !x.target)
|
||||||
|
.map((x) => ({
|
||||||
|
...x,
|
||||||
|
data: { ...x.data, onDeleteNodeCallback, onNodeClickCallback },
|
||||||
|
}));
|
||||||
|
const edges = initialElements
|
||||||
|
.filter((x) => x.target)
|
||||||
|
.map((x) => ({ ...x, data: { ...x.data, onAddNodeCallback } }));
|
||||||
|
setElements([...nodes, ...edges]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<Sidebar />
|
||||||
|
<Layout elements={elements} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
184
src/Automation.css
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.menu-fixed .item-flow b {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-fixed .list-option.flex {
|
||||||
|
grid-gap: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-fixed .list-option .items {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(36, 28, 21, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px 8px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: move;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-fixed .list-option.flex .step {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-fixed .list-option.flex .items {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-fixed .list-option.flex .left {
|
||||||
|
border: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.menu-fixed .list-option.flex .right {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.AutomationCanvas {
|
||||||
|
--edges-stroke-color: green;
|
||||||
|
width: calc(100% - 360px);
|
||||||
|
background-color: var(--color-base100);
|
||||||
|
}
|
||||||
|
.menu-fixed {
|
||||||
|
background: #fff;
|
||||||
|
border: initial;
|
||||||
|
border-radius: initial;
|
||||||
|
border-right: 1px solid rgba(36, 28, 21, 0.15);
|
||||||
|
-webkit-transition: -webkit-transform 0.2s ease-in;
|
||||||
|
transition: -webkit-transform 0.2s ease-in;
|
||||||
|
transition: transform 0.2s ease-in;
|
||||||
|
transition: transform 0.2s ease-in, -webkit-transform 0.2s ease-in;
|
||||||
|
width: 360px;
|
||||||
|
z-index: 9;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-path {
|
||||||
|
stroke: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Controls {
|
||||||
|
bottom: 70px;
|
||||||
|
left: auto !important;
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactflow-wrapper {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--color-base100);
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* export const AutomationCanvas = css`
|
||||||
|
--edges-stroke-color: green;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--color-base100);
|
||||||
|
|
||||||
|
.react-flow__edge-path {
|
||||||
|
stroke: var(--edges-stroke-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Controls = css`
|
||||||
|
top: 10px;
|
||||||
|
bottom: auto;
|
||||||
|
`; */
|
||||||
|
|
||||||
|
.node-dropzone {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid crimson;
|
||||||
|
background-color: rgb(255, 238, 0);
|
||||||
|
border-radius: 50px;
|
||||||
|
color: crimson;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-dropzone:hover {
|
||||||
|
background-color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: auto;
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SidebarItem {
|
||||||
|
background-color: blue;
|
||||||
|
padding: 10px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SidebarItem:hover {
|
||||||
|
background-color: rgb(153, 0, 255);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.NodeIcon svg {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
137
src/Automation.jsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import ReactFlow, {
|
||||||
|
Controls,
|
||||||
|
MiniMap,
|
||||||
|
ReactFlowProvider,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
} from "reactflow";
|
||||||
|
|
||||||
|
// File imports
|
||||||
|
import "./Automation.css";
|
||||||
|
|
||||||
|
import { nodeTypes } from "./Nodes/index.jsx";
|
||||||
|
import { edgeTypes } from "./Edges/index.jsx";
|
||||||
|
import { getLayoutedElements } from "./Utils/WorkflowLayoutUtils.jsx";
|
||||||
|
import Sidebar from "./Sidebar/Sidebar";
|
||||||
|
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils";
|
||||||
|
|
||||||
|
export const Automation = (props) => {
|
||||||
|
const { elements } = props;
|
||||||
|
|
||||||
|
const reactFlowWrapper = useRef(null);
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState();
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState();
|
||||||
|
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const layoutElements = getLayoutedElements(elements);
|
||||||
|
const layoutNodes = layoutElements.filter((x) => x.position);
|
||||||
|
const layoutEdges = layoutElements.filter((x) => !x.position);
|
||||||
|
setNodes(layoutNodes);
|
||||||
|
setEdges(layoutEdges);
|
||||||
|
}, [elements]);
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(params) => setEdges((eds) => eds.concat(params)), // Modified: Concatenate edges
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================>
|
||||||
|
|
||||||
|
// ==============================>
|
||||||
|
let id = 0;
|
||||||
|
const getId = () => `dndnode_${id++}`;
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
console.log(reactFlowInstance);
|
||||||
|
|
||||||
|
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||||
|
const type = event.dataTransfer.getData("application/reactflow");
|
||||||
|
|
||||||
|
// check if the dropped element is valid
|
||||||
|
if (typeof type === "undefined" || !type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = reactFlowInstance.project({
|
||||||
|
x: event.clientX - reactFlowBounds.left,
|
||||||
|
y: event.clientY - reactFlowBounds.top,
|
||||||
|
});
|
||||||
|
const newNode = {
|
||||||
|
id: getId(),
|
||||||
|
type,
|
||||||
|
position,
|
||||||
|
data: { label: `${type} node` },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => nds.concat(newNode));
|
||||||
|
|
||||||
|
// ===========
|
||||||
|
|
||||||
|
// setNodes((elements) =>
|
||||||
|
// getUpdatedElementsAfterNodeAddition({
|
||||||
|
// elements,
|
||||||
|
// newNode: newNode,
|
||||||
|
// targetEdgeId: "e1-2",
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ===========
|
||||||
|
},
|
||||||
|
[reactFlowInstance, setNodes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDragOver = useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// =================================>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="AutomationCanvas">
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<div ref={reactFlowWrapper} className="reactflow-wrapper">
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
nodesDraggable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
// zoomOnScroll={false}
|
||||||
|
// zoomOnPinch={false}
|
||||||
|
// panOnScroll
|
||||||
|
// panOnDrag
|
||||||
|
// preventScrolling
|
||||||
|
onConnect={onConnect}
|
||||||
|
panOnScroll
|
||||||
|
onInit={setReactFlowInstance}
|
||||||
|
// onDrop={onDrop}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
>
|
||||||
|
<Controls
|
||||||
|
// showInteractive={false}
|
||||||
|
className="Controls"
|
||||||
|
position="right-bottom"
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Layout = (props) => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<Automation {...props} />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
124
src/BuildFlow.jsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { useState, useCallback } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import "./assets/css/style.css";
|
||||||
|
import imageStart from "./assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png";
|
||||||
|
import PopupListFlow from "./Popups/PopupListFlow";
|
||||||
|
import { mainNode } from "./data/data";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
const BuildFlow = () => {
|
||||||
|
const [showPopup, setShowPopup] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
const [selectedFlow, setSelectedFlow] = useState(null);
|
||||||
|
const query = new URLSearchParams(location.search);
|
||||||
|
const flowName = query.get("flowName");
|
||||||
|
|
||||||
|
const handleAddStartPointClick = () => {
|
||||||
|
setShowPopup(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hidePopup = () => {
|
||||||
|
setShowPopup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFlow = (flow) => {
|
||||||
|
setSelectedFlow(flow); // Lưu flow đã chọn
|
||||||
|
hidePopup(); // Ẩn popup sau khi chọn flow
|
||||||
|
|
||||||
|
// Tìm node khớp với flow trong mainNode
|
||||||
|
const selectedMainNode = mainNode.find((node) => node.key === flow);
|
||||||
|
|
||||||
|
if (selectedMainNode) {
|
||||||
|
// Tạo một node từ mainNode
|
||||||
|
const newNode = {
|
||||||
|
id: `${selectedMainNode.id}`, // Sử dụng id từ mainNode
|
||||||
|
type: "input",
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div className="step">
|
||||||
|
<div className="left">
|
||||||
|
<img
|
||||||
|
src={selectedMainNode.icon}
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="right"> {selectedMainNode.name}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
position: { ...selectedMainNode.position }, // Vị trí từ mainNode
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tạo node add-node
|
||||||
|
const addNode = {
|
||||||
|
id: "2",
|
||||||
|
key: "add-node",
|
||||||
|
type: "default",
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div className="add-node">
|
||||||
|
<i>+</i> <span>Add a journey point</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: selectedMainNode.position.x,
|
||||||
|
y: selectedMainNode.position.y + 180,
|
||||||
|
},
|
||||||
|
className: "contact-exits",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kết nối giữa node và add-node
|
||||||
|
const newEdge = {
|
||||||
|
id: `edge-${selectedMainNode.id}-add-node`,
|
||||||
|
source: `${selectedMainNode.id}`,
|
||||||
|
target: "2",
|
||||||
|
type: "custom",
|
||||||
|
data: { onAddNodeCallback },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cập nhật state với node mới, add-node, và edge
|
||||||
|
setNodes((nds) => [newNode, addNode]);
|
||||||
|
setEdges((eds) => [newEdge]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!selectedFlow ? (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
<h2>Building Flow: {flowName}</h2>
|
||||||
|
|
||||||
|
<div className="content-start">
|
||||||
|
<img src={imageStart} className="imageStart" alt="" />
|
||||||
|
<h2 className="title">How will a contact start their journey?</h2>
|
||||||
|
<p className="note">
|
||||||
|
This is what kicks off your contact's journey. You choose the
|
||||||
|
starting point, then contacts who meet the criteria will enter
|
||||||
|
your map and begin their journey.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleAddStartPointClick} className="btn-start">
|
||||||
|
Choose A Starting Point
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showPopup && (
|
||||||
|
<PopupListFlow
|
||||||
|
onSelectFlow={handleSelectFlow}
|
||||||
|
hidePopup={() => setShowPopup(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<App />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BuildFlow;
|
||||||
14
src/Buttons/AddButton/AddButton.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export const AddButton = (props) => {
|
||||||
|
const { onClick } = props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => onClick(props)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
76
src/Buttons/EdgeAddButton/DropzoneNode.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import { useState, DragEvent, memo } from "react";
|
||||||
|
import { Handle, Position } from "react-flow-renderer";
|
||||||
|
|
||||||
|
import styles from "./nodes.module.scss";
|
||||||
|
|
||||||
|
const DropzoneNode = (props) => {
|
||||||
|
const [isDropzoneActive, setDropzoneActive] = useState(false);
|
||||||
|
|
||||||
|
const onDrop = () => {
|
||||||
|
setDropzoneActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragOver = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnter = () => {
|
||||||
|
setDropzoneActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragLeave = () => {
|
||||||
|
setDropzoneActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.NextStepComponent} ${
|
||||||
|
isDropzoneActive ? styles.dropzoneNode : ""
|
||||||
|
}`}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragEnter={onDragEnter}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
id="b"
|
||||||
|
className={styles.handle}
|
||||||
|
position={Position.Top}
|
||||||
|
style={{
|
||||||
|
background: "#ffff",
|
||||||
|
border: "1px dotted gray",
|
||||||
|
zIndex: "-1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
id="a"
|
||||||
|
position={Position.Bottom}
|
||||||
|
style={{
|
||||||
|
background: "#ffff",
|
||||||
|
border: "1px dotted gray",
|
||||||
|
padding: 0,
|
||||||
|
zIndex: "-1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isDropzoneActive && (
|
||||||
|
<p style={{ textAlign: "center" }}>Drop here</p>
|
||||||
|
)}
|
||||||
|
<div className={styles.cancelButton}>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
margin: "-1px -19px",
|
||||||
|
padding: "0 0",
|
||||||
|
zIndex: "10",
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DropzoneNode);
|
||||||
62
src/Buttons/EdgeAddButton/EdgeAddButton.jsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// import { AddButton } from "../AddButton/AddButton.jsx";
|
||||||
|
// import { Menu, Dropdown } from "antd";
|
||||||
|
// import "./EdgeAddButton.scss";
|
||||||
|
|
||||||
|
// const EdgeAddButton = (props) => {
|
||||||
|
// const { style, data, id } = props;
|
||||||
|
// console.log(id);
|
||||||
|
|
||||||
|
// const content = (
|
||||||
|
// <Menu
|
||||||
|
// onClick={(event) => data.onAddNodeCallback({ id, type: event.key })}
|
||||||
|
// >
|
||||||
|
// <Menu.Item key="email">Email</Menu.Item>
|
||||||
|
// <Menu.Item key="sms">SMS</Menu.Item>
|
||||||
|
// <Menu.Item key="waitThenCheck">Rule</Menu.Item>
|
||||||
|
// <Menu.Item key="end">End</Menu.Item>
|
||||||
|
// </Menu>
|
||||||
|
// );
|
||||||
|
// return (
|
||||||
|
// <div className="EdgeAddButton" style={style}>
|
||||||
|
// <Dropdown overlay={content} trigger={["click"]}>
|
||||||
|
// <AddButton {...props} />
|
||||||
|
// </Dropdown>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default EdgeAddButton;
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
// import { AddButton } from "../AddButton/AddButton.jsx";
|
||||||
|
import { Menu, Dropdown, Button } from "antd";
|
||||||
|
import "./EdgeAddButton.scss";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Handle, Position } from "reactflow";
|
||||||
|
import styles from "./nodes.module.scss";
|
||||||
|
|
||||||
|
// import DropzoneNode from "./DropzoneNode.jsx";
|
||||||
|
|
||||||
|
const EdgeAddButton = (props) => {
|
||||||
|
const { style, data, id } = props;
|
||||||
|
|
||||||
|
const dropzone = <div className="node-dropzone"> +</div>;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="EdgeAddButton"
|
||||||
|
style={style}
|
||||||
|
onDrop={(event) =>
|
||||||
|
data.onAddNodeCallback({
|
||||||
|
id,
|
||||||
|
type: event.dataTransfer.getData("nodeType"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{dropzone}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EdgeAddButton;
|
||||||
5
src/Buttons/EdgeAddButton/EdgeAddButton.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.EdgeAddButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
0
src/Buttons/EdgeAddButton/nodes.module.scss
Normal file
46
src/CreateFlow.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "./assets/css/style.css";
|
||||||
|
import Customer from "./assets/images/Customer-Journey_Animation.gif";
|
||||||
|
|
||||||
|
const CreateFlow = () => {
|
||||||
|
const [flowName, setFlowName] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSave = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (flowName.trim() !== "") {
|
||||||
|
// Chuyển hướng đến trang build với tham số build=1
|
||||||
|
navigate(`/build?build=1&flowName=${encodeURIComponent(flowName)}`);
|
||||||
|
} else {
|
||||||
|
alert("Bạn chưa nhập tên cho tiến trình");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content-create">
|
||||||
|
<img src={Customer} className="image-header" alt="" />
|
||||||
|
<h2 className="title">Create a map of your contact's journey</h2>
|
||||||
|
<p className="note">
|
||||||
|
Put your contacts on a path that’s right for them. With a customer
|
||||||
|
journey, you can always be there for your contacts when they need you
|
||||||
|
most.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSave} className="style-form">
|
||||||
|
<label>Flow Name:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={flowName}
|
||||||
|
onChange={(e) => setFlowName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="btn-submit">
|
||||||
|
Start Building
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateFlow;
|
||||||
146
src/Data/Elements1.jsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
const position = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "source",
|
||||||
|
data: {
|
||||||
|
title: "Source",
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 150,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "2",
|
||||||
|
// type: "email",
|
||||||
|
// data: {
|
||||||
|
// title: "Email",
|
||||||
|
// description: "Send message to contacts.",
|
||||||
|
// stats: {
|
||||||
|
// running: 18,
|
||||||
|
// error: 1,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "3",
|
||||||
|
// type: "waitThenCheck",
|
||||||
|
// data: {
|
||||||
|
// title: "Replied to conversation?",
|
||||||
|
// description: "Replied to conversation?",
|
||||||
|
// stats: {
|
||||||
|
// running: 17,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// style: {
|
||||||
|
// width: 250,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "4",
|
||||||
|
// type: "email",
|
||||||
|
// data: {
|
||||||
|
// title: "Email",
|
||||||
|
// description: "Send message to contacts.",
|
||||||
|
// stats: {
|
||||||
|
// running: 3,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "5",
|
||||||
|
// type: "sms",
|
||||||
|
// data: {
|
||||||
|
// title: "SMS",
|
||||||
|
// description: "Send SMS to contacts.",
|
||||||
|
// stats: {
|
||||||
|
// running: 14,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
type: "end",
|
||||||
|
data: {
|
||||||
|
title: "Contact Exits",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 150,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "8",
|
||||||
|
// type: "end",
|
||||||
|
// data: {
|
||||||
|
// title: "Contact Exits",
|
||||||
|
// description: "",
|
||||||
|
// stats: {
|
||||||
|
// completed: 14,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const edges = [
|
||||||
|
{
|
||||||
|
id: "e1-7",
|
||||||
|
source: "1",
|
||||||
|
target: "7",
|
||||||
|
type: "condition",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "e2-3",
|
||||||
|
// source: "2",
|
||||||
|
// target: "3",
|
||||||
|
// type: "condition",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e3-4",
|
||||||
|
// source: "3",
|
||||||
|
// target: "4",
|
||||||
|
// type: "condition",
|
||||||
|
// sourceHandle: "yes",
|
||||||
|
// data: {
|
||||||
|
// title: "Default condition",
|
||||||
|
// disabled: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e3-5",
|
||||||
|
// source: "3",
|
||||||
|
// target: "5",
|
||||||
|
// type: "condition",
|
||||||
|
// sourceHandle: "no",
|
||||||
|
// data: {
|
||||||
|
// title: "Editable branch",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e6-7",
|
||||||
|
// source: "5",
|
||||||
|
// target: "7",
|
||||||
|
// sourceHandle: "yes",
|
||||||
|
// type: "condition",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e4-8",
|
||||||
|
// source: "4",
|
||||||
|
// target: "8",
|
||||||
|
// sourceHandle: "no",
|
||||||
|
// type: "condition",
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const initialElements = [...nodes, ...edges];
|
||||||
511
src/Data/Elements2.jsx
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
const position = { x: 0, y: 0 };
|
||||||
|
const data = {
|
||||||
|
layoutNodes: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "source",
|
||||||
|
data: {
|
||||||
|
title: "Source",
|
||||||
|
description: "Automations Database contacts",
|
||||||
|
stats: {
|
||||||
|
started: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 380.00037444075423,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
type: "waitThenCheck",
|
||||||
|
data: {
|
||||||
|
title: "Wait then Check",
|
||||||
|
description: "Check behaviour of the contacts.",
|
||||||
|
stats: {
|
||||||
|
running: 17,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 380.00030212384667,
|
||||||
|
y: 372,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
type: "sms",
|
||||||
|
data: {
|
||||||
|
title: "SMS",
|
||||||
|
description: "Send SMS to contacts.",
|
||||||
|
stats: {
|
||||||
|
running: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 0.0006895893374535293,
|
||||||
|
y: 1246,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 380.00084311022334,
|
||||||
|
y: 1376,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
mergeNodeOfParentId: "3",
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
type: "end",
|
||||||
|
data: {
|
||||||
|
title: "End",
|
||||||
|
description: "Automation ends.",
|
||||||
|
stats: {
|
||||||
|
completed: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 380.0000154657833,
|
||||||
|
y: 1432,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
|
||||||
|
type: "sms",
|
||||||
|
data: {
|
||||||
|
title: "Sms",
|
||||||
|
description: "Send sms to contacts.",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 530.0003565002155,
|
||||||
|
y: 502,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b288f125-ff39-466d-bab6-934c2fa07c03",
|
||||||
|
type: "waitThenCheck",
|
||||||
|
data: {
|
||||||
|
title: "New Rule",
|
||||||
|
description: "Check behaviour of the Rule",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 530.000649770617,
|
||||||
|
y: 632,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 380.000880810274,
|
||||||
|
y: 1134,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "977c989f-9657-41e6-9617-08bd3c6fbd43",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 760.0009293479523,
|
||||||
|
y: 1134,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "316366e7-8c56-49c9-9803-5c30d937634e",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 530.0002802883737,
|
||||||
|
y: 1190,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
|
||||||
|
type: "sms",
|
||||||
|
data: {
|
||||||
|
title: "Sms",
|
||||||
|
description: "Send sms to contacts.",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 530.0009417226739,
|
||||||
|
y: 1246,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "571a5ddf-f865-486b-9374-0196e842a9f9",
|
||||||
|
type: "waitThenCheck",
|
||||||
|
data: {
|
||||||
|
title: "New Rule",
|
||||||
|
description: "Check behaviour of the Rule",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 380.0009084120236,
|
||||||
|
y: 762,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8c804560-48f1-47fd-9937-bc62124b8de4",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 230.00023449374996,
|
||||||
|
y: 1022,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3d4f48ac-1dc7-4aff-8739-15659058e878",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 530.0000020924601,
|
||||||
|
y: 1022,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d7651a34-9478-409a-a38c-64312d73e8d3",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 380.0006605416041,
|
||||||
|
y: 1078,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
|
||||||
|
type: "sms",
|
||||||
|
data: {
|
||||||
|
title: "Sms",
|
||||||
|
description: "Send sms to contacts.",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 530.0004075241991,
|
||||||
|
y: 892,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
|
||||||
|
type: "sms",
|
||||||
|
data: {
|
||||||
|
title: "Sms",
|
||||||
|
description: "Send sms to contacts.",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 230.00060068647215,
|
||||||
|
y: 892,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
|
||||||
|
type: "waitThenCheck",
|
||||||
|
data: {
|
||||||
|
title: "New Rule",
|
||||||
|
description: "Check behaviour of the Rule",
|
||||||
|
},
|
||||||
|
_position: {
|
||||||
|
x: 380.0001113668592,
|
||||||
|
y: 130,
|
||||||
|
},
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cd485fbb-3d6f-4015-8717-822d3c433725",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 230.00041223048268,
|
||||||
|
y: 260,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 530.0006256305971,
|
||||||
|
y: 260,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2202a162-e008-4653-ab23-1b780c839a26",
|
||||||
|
type: "empty",
|
||||||
|
data: {},
|
||||||
|
_position: {
|
||||||
|
x: 380.0006922939385,
|
||||||
|
y: 316,
|
||||||
|
},
|
||||||
|
height: 6,
|
||||||
|
target_position: "top",
|
||||||
|
source_position: "bottom",
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layoutEdges: [
|
||||||
|
{
|
||||||
|
id: "e3-4",
|
||||||
|
source: "3",
|
||||||
|
target: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
title: "Default condition",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e3-5",
|
||||||
|
source: "3",
|
||||||
|
target: "5",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
title: "Editable branch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e5-6",
|
||||||
|
source: "5",
|
||||||
|
target: "6",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e6-7",
|
||||||
|
source: "6",
|
||||||
|
target: "7",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "53706f28-e86a-4687-8e4e-9c41b0ab4cbc",
|
||||||
|
source: "b288f125-ff39-466d-bab6-934c2fa07c03",
|
||||||
|
target: "571a5ddf-f865-486b-9374-0196e842a9f9",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6a5ea441-efa8-45a5-a0a5-e89ea942a714",
|
||||||
|
source: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
|
||||||
|
target: "316366e7-8c56-49c9-9803-5c30d937634e",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "047285c3-4f2c-4d2d-b7e5-34be0cd03cd8",
|
||||||
|
source: "b288f125-ff39-466d-bab6-934c2fa07c03",
|
||||||
|
target: "977c989f-9657-41e6-9617-08bd3c6fbd43",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ebd9bc4f-d5c9-47d5-b667-548c9f63ed35",
|
||||||
|
source: "977c989f-9657-41e6-9617-08bd3c6fbd43",
|
||||||
|
target: "316366e7-8c56-49c9-9803-5c30d937634e",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c1f453c1-0d79-4bdd-bb3e-4995b43851c9",
|
||||||
|
source: "316366e7-8c56-49c9-9803-5c30d937634e",
|
||||||
|
target: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
mergeNodeOfParentId: "b288f125-ff39-466d-bab6-934c2fa07c03",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3ce6e537-8efe-491c-bd67-bd8dd2c22453",
|
||||||
|
source: "78919ba2-5f9e-4d1a-b729-444a738ea1f5",
|
||||||
|
target: "6",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2178e8ad-4da2-43b5-95fd-d7b8e813bb99",
|
||||||
|
source: "8c804560-48f1-47fd-9937-bc62124b8de4",
|
||||||
|
target: "d7651a34-9478-409a-a38c-64312d73e8d3",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e6d0ce27-60c3-4fa9-a855-78d3eaf8e653",
|
||||||
|
source: "571a5ddf-f865-486b-9374-0196e842a9f9",
|
||||||
|
target: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1b2e68f6-cada-45a6-9333-4d25bc3ec018",
|
||||||
|
source: "3d4f48ac-1dc7-4aff-8739-15659058e878",
|
||||||
|
target: "d7651a34-9478-409a-a38c-64312d73e8d3",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fe2db513-d113-468e-b884-2be2385f0c74",
|
||||||
|
source: "d7651a34-9478-409a-a38c-64312d73e8d3",
|
||||||
|
target: "7301b7b0-eaba-49dc-bf57-0c5fec2b192f",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
mergeNodeOfParentId: "571a5ddf-f865-486b-9374-0196e842a9f9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "07773a27-04d1-470b-b7e0-36911475aece",
|
||||||
|
source: "4acdd729-39b9-42ce-adfe-a8d1068f821a",
|
||||||
|
target: "3d4f48ac-1dc7-4aff-8739-15659058e878",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "726de0f3-6c82-41a6-af03-c5ee43eaf195",
|
||||||
|
source: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
|
||||||
|
target: "8c804560-48f1-47fd-9937-bc62124b8de4",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e1-2",
|
||||||
|
source: "1",
|
||||||
|
target: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9ef8742a-2d31-44bc-abbe-137ce9bdee57",
|
||||||
|
source: "848c3dec-1b01-4a3f-8b72-706d66a8cd70",
|
||||||
|
target: "b288f125-ff39-466d-bab6-934c2fa07c03",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d3db8ba7-c6e6-4ac2-b024-be2b04468de4",
|
||||||
|
source: "571a5ddf-f865-486b-9374-0196e842a9f9",
|
||||||
|
target: "e70a497c-1b42-4f9f-b92c-c4d6b1cc5d57",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d5693be9-7995-4b97-831f-928a9de65cdd",
|
||||||
|
source: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
|
||||||
|
target: "cd485fbb-3d6f-4015-8717-822d3c433725",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d21e4de4-b3f2-47ca-a597-cfcb94151f3b",
|
||||||
|
source: "cd485fbb-3d6f-4015-8717-822d3c433725",
|
||||||
|
target: "2202a162-e008-4653-ab23-1b780c839a26",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "d535bd4f-8e24-4530-9499-db8d220a7267",
|
||||||
|
source: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
|
||||||
|
target: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "68d0415b-959d-42f3-be7e-9e70b4611929",
|
||||||
|
source: "8f37b5f7-9cea-4085-89c9-bfa7db971e17",
|
||||||
|
target: "2202a162-e008-4653-ab23-1b780c839a26",
|
||||||
|
type: "condition",
|
||||||
|
data: {
|
||||||
|
isAddButtonHidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "383b4cf8-e73f-416d-93e6-753f5f14d0cb",
|
||||||
|
source: "2202a162-e008-4653-ab23-1b780c839a26",
|
||||||
|
target: "3",
|
||||||
|
type: "condition",
|
||||||
|
data: {},
|
||||||
|
mergeNodeOfParentId: "528ba05d-9c5b-413e-a59f-7ca6914020c4",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const initialElements = [...data.layoutNodes, ...data.layoutEdges];
|
||||||
|
export { initialElements };
|
||||||
92
src/Data/Menu.jsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { nanoid } from "nanoid";
|
||||||
|
// nội dung Menu
|
||||||
|
|
||||||
|
const MenuOptions = [
|
||||||
|
{
|
||||||
|
option: "rule",
|
||||||
|
title: "Rules",
|
||||||
|
list_option: [
|
||||||
|
{
|
||||||
|
key: "time-delay",
|
||||||
|
label: "Time delay",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-time.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "waitThenCheck",
|
||||||
|
label: "Replied to conversation?",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "selectorNode",
|
||||||
|
icon: "./src/assets/images/icon-if.svg",
|
||||||
|
position: { x: 400, y: 290 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wait",
|
||||||
|
label: "Wait for trigger",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-wait.svg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
option: "action",
|
||||||
|
title: "Actions",
|
||||||
|
list_option: [
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "Send email",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-email.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sms",
|
||||||
|
label: "Send sms",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-phone.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "slack",
|
||||||
|
label: "Send Slack",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-slack.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "zalo",
|
||||||
|
label: "Send Zalo",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-zalo.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "survey-email",
|
||||||
|
label: "Send survey in email",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-list.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "end",
|
||||||
|
branchKey: "yes",
|
||||||
|
label: "Contact Exits",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "output",
|
||||||
|
icon: "./src/assets/images/icon-check.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "survey",
|
||||||
|
branchKey: "no",
|
||||||
|
label: "Send survey",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "output",
|
||||||
|
icon: "./src/assets/images/icon-list.svg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { MenuOptions };
|
||||||
85
src/Data/data.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export const dataListFlow = [
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
name: "Tag added",
|
||||||
|
key: "tag",
|
||||||
|
icon: "./src/assets/images/icon-tag.svg",
|
||||||
|
description: `<ul>
|
||||||
|
<li>Send more relevant content</ li >
|
||||||
|
<li>Track people's interests</li>
|
||||||
|
<li> Turn new shoppers into regulars </li></ul>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
name: "Signs up for Email",
|
||||||
|
key: "sign-up",
|
||||||
|
icon: "./src/assets/images/icon-signs.svg",
|
||||||
|
description: `<ul><li>Welcome new signups</li><li>Introduce your brand</li><li>Turn new contacts into new customers</li></ul>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
name: "Birthday",
|
||||||
|
key: "birthday",
|
||||||
|
icon: "./src/assets/images/icon-birthday.svg",
|
||||||
|
description: `<ul class="description-1lOza"><li>Send a special discount</li><li>Offer a free gift</li><li>Just say 'happy birthday'</li></ul>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
name: "Sent an email",
|
||||||
|
key: "send-an-email",
|
||||||
|
icon: "./src/assets/images/icon-email.svg",
|
||||||
|
description: "Contacts will enter the map when sent a bulk email.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
name: "Buys a specific product",
|
||||||
|
key: "buy-product",
|
||||||
|
icon: "./src/assets/images/icon-buy.svg",
|
||||||
|
description: "You haven’t connected your store.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mainNode = [
|
||||||
|
{
|
||||||
|
key: "sign-up",
|
||||||
|
id: nanoid(8),
|
||||||
|
type: "input",
|
||||||
|
icon: "./src/assets/images/icon-signs.svg",
|
||||||
|
name: "Signs up for Email",
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
key: "tag",
|
||||||
|
type: "input",
|
||||||
|
icon: "./src/assets/images/icon-tag.svg",
|
||||||
|
name: "Set a tag",
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
key: "birthday",
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-birthday.svg",
|
||||||
|
name: "Contact Birthday",
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
key: "send-an-email",
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-email.svg",
|
||||||
|
name: "Sent an email",
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: nanoid(8),
|
||||||
|
key: "buy-product",
|
||||||
|
type: "default",
|
||||||
|
icon: "./src/assets/images/icon-buy.svg",
|
||||||
|
name: "Buys a specific product",
|
||||||
|
position: { x: 250, y: 100 },
|
||||||
|
},
|
||||||
|
];
|
||||||
71
src/Edges/Edges.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import EdgeAddButton from "../Buttons/EdgeAddButton/EdgeAddButton.jsx";
|
||||||
|
|
||||||
|
import "./Style.scss";
|
||||||
|
import {
|
||||||
|
getEdgeCenter,
|
||||||
|
getBezierPath,
|
||||||
|
getMarkerEnd,
|
||||||
|
} from "react-flow-renderer";
|
||||||
|
|
||||||
|
const [buttonWidth, buttonHeight] = [100, 40];
|
||||||
|
|
||||||
|
export const Condition = (props) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
arrowHeadType,
|
||||||
|
markerEndId,
|
||||||
|
data,
|
||||||
|
} = props;
|
||||||
|
const edgePath = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
});
|
||||||
|
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||||
|
|
||||||
|
const [edgeCenterX, edgeCenterY] = getEdgeCenter({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAddButtonHidden } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
id={id}
|
||||||
|
d={edgePath}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
className="react-flow__edge-path"
|
||||||
|
/>
|
||||||
|
{isAddButtonHidden ? null : (
|
||||||
|
<>
|
||||||
|
<foreignObject
|
||||||
|
width={buttonWidth}
|
||||||
|
height={buttonHeight}
|
||||||
|
x={edgeCenterX - buttonWidth / 2}
|
||||||
|
y={edgeCenterY - buttonHeight / 2}
|
||||||
|
requiredExtensions="http://www.w3.org/1999/xhtml"
|
||||||
|
>
|
||||||
|
<EdgeAddButton
|
||||||
|
{...props}
|
||||||
|
onClick={() => console.log("clicked")}
|
||||||
|
style={{ width: buttonWidth, height: buttonHeight }}
|
||||||
|
/>
|
||||||
|
</foreignObject>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
src/Edges/Style.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.EdgeButton {
|
||||||
|
padding: 2px;
|
||||||
|
border: 0;
|
||||||
|
background-color: var(--color-base100);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-accent);
|
||||||
|
transform: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-accent700);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: var(--color-base600);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/Edges/index.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as Edge from "./Edges";
|
||||||
|
|
||||||
|
export const edgeTypes = {
|
||||||
|
condition: Edge.Condition,
|
||||||
|
};
|
||||||
98
src/Nodes/Base.jsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* eslint-disable default-case */
|
||||||
|
import * as colors from "@contactlab/ds-tokens/constants/colors";
|
||||||
|
import {
|
||||||
|
DatabaseOutlined,
|
||||||
|
MailOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
FlagOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
CheckCircleFilled,
|
||||||
|
SlackOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
|
import "./Style.scss";
|
||||||
|
|
||||||
|
export const BaseNode = ({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
selected,
|
||||||
|
disabled,
|
||||||
|
onNodeClick,
|
||||||
|
onCloseIconClick,
|
||||||
|
additionalClassName,
|
||||||
|
}) => {
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{getIconSrc(type)}
|
||||||
|
<div className="NodeContent">
|
||||||
|
<div className="NodeTitle">{data.title}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-selected={selected}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
className={`NodeInnerWrapper ${additionalClassName}`}
|
||||||
|
style={{ color: getColor(type) }}
|
||||||
|
{...(onNodeClick && { onClick: () => onNodeClick(type, data) })}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
<CloseOutlined className="closeIcon" onClick={onCloseIconClick} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyBaseNode = () => {
|
||||||
|
return <div className="EmptyNodeInnerWrapper"></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helpers
|
||||||
|
const getColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "source":
|
||||||
|
return colors.success;
|
||||||
|
case "email":
|
||||||
|
return colors.accent;
|
||||||
|
case "sms":
|
||||||
|
return colors.accent;
|
||||||
|
case "slack":
|
||||||
|
return colors.accent;
|
||||||
|
case "zalo":
|
||||||
|
return colors.accent;
|
||||||
|
case "waitThenCheck":
|
||||||
|
return colors.warning;
|
||||||
|
case "end":
|
||||||
|
return colors.base;
|
||||||
|
default:
|
||||||
|
return colors.base;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconSrc = (type) => {
|
||||||
|
const color = getColor(type);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "source":
|
||||||
|
return <DatabaseOutlined className="NodeIcon" style={{ color }} />;
|
||||||
|
case "email":
|
||||||
|
return <MailOutlined className="NodeIcon" style={{ color }} />;
|
||||||
|
case "sms":
|
||||||
|
return <CommentOutlined className="NodeIcon" style={{ color }} />;
|
||||||
|
case "slack":
|
||||||
|
return <SlackOutlined className="NodeIcon" style={{ color }} />;
|
||||||
|
case "zalo":
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className="NodeIcon"
|
||||||
|
src="./src/assets/images/icon-zalo.svg"
|
||||||
|
style={{ width: 40, height: 40 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "waitThenCheck":
|
||||||
|
return <CheckCircleOutlined className="NodeIcon" style={{ color }} />;
|
||||||
|
case "end":
|
||||||
|
return <CheckCircleFilled className="NodeIcon" style={{ color }} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
125
src/Nodes/Nodes.jsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Handle, Position } from "reactflow";
|
||||||
|
import { BaseNode, EmptyBaseNode } from "./Base";
|
||||||
|
// import { Handle, Position } from "react-flow-renderer";
|
||||||
|
|
||||||
|
import "./Style.scss";
|
||||||
|
|
||||||
|
const handleNodeClick = (props) => {
|
||||||
|
const { data, id } = props;
|
||||||
|
data.onNodeClickCallback(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseIconClick = (event, props) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const { data, id } = props;
|
||||||
|
data.onDeleteNodeCallback(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Source = (props) => (
|
||||||
|
<div className="NodeWrapper">
|
||||||
|
<BaseNode
|
||||||
|
{...props}
|
||||||
|
onNodeClick={() => handleNodeClick(props)}
|
||||||
|
onCloseIconClick={(event) => onCloseIconClick(event, props)}
|
||||||
|
/>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="NodePort" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Action = (props) => (
|
||||||
|
<div className="NodeWrapper">
|
||||||
|
<Handle type="target" position={Position.Top} className="NodePort" />
|
||||||
|
<BaseNode
|
||||||
|
{...props}
|
||||||
|
onNodeClick={() => handleNodeClick(props)}
|
||||||
|
onCloseIconClick={(event) => onCloseIconClick(event, props)}
|
||||||
|
/>
|
||||||
|
<Handle type="source" position={Position.Bottom} className="NodePort" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Condition = (props) => (
|
||||||
|
<div className="NodeWrapper">
|
||||||
|
<Handle type="target" position={Position.Top} className="NodePort" />
|
||||||
|
<BaseNode
|
||||||
|
{...props}
|
||||||
|
additionalClassName="ConditionNode"
|
||||||
|
onNodeClick={() => handleNodeClick(props)}
|
||||||
|
onCloseIconClick={(event) => onCloseIconClick(event, props)}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
id="yes"
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
style={{ bottom: 0, left: 45, background: "#555" }}
|
||||||
|
className="NodePort"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 30,
|
||||||
|
bottom: -9,
|
||||||
|
background: "#000",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 11,
|
||||||
|
paddingLeft: 5,
|
||||||
|
paddingRight: 5,
|
||||||
|
paddingTop: 3,
|
||||||
|
paddingBottom: 3,
|
||||||
|
borderRadius: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
id="no"
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
style={{ bottom: 0, left: 220, background: "#555" }}
|
||||||
|
className="NodePort"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 25,
|
||||||
|
bottom: -10,
|
||||||
|
background: "#000",
|
||||||
|
color: "#fff",
|
||||||
|
paddingLeft: 5,
|
||||||
|
paddingRight: 5,
|
||||||
|
paddingTop: 3,
|
||||||
|
paddingBottom: 3,
|
||||||
|
fontSize: 11,
|
||||||
|
borderRadius: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const End = (props) => (
|
||||||
|
<div className="NodeWrapper">
|
||||||
|
<Handle type="target" position={Position.Top} className="NodePort" />
|
||||||
|
<BaseNode {...props} disabled={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Empty = (props) => (
|
||||||
|
<div className="NodeWrapper">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
className="NodePort"
|
||||||
|
// style={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
<EmptyBaseNode {...props} disabled={true} />
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
className="NodePort"
|
||||||
|
style={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
88
src/Nodes/Style.scss
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.NodeWrapper {
|
||||||
|
width: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodeInnerWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border-left-width: 3px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
box-shadow: var(--elevation-z1);
|
||||||
|
transition: box-shadow 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
&[aria-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([aria-disabled="true"]):hover {
|
||||||
|
box-shadow: var(--elevation-z2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([aria-disabled="true"])[data-selected="true"] {
|
||||||
|
box-shadow:
|
||||||
|
inset 0px 0px 0px 1px currentColor,
|
||||||
|
var(--elevation-z3);
|
||||||
|
}
|
||||||
|
&.ConditionNode {
|
||||||
|
// width: 350px;
|
||||||
|
}
|
||||||
|
> .closeIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
> svg {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.EmptyNodeInnerWrapper {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--edges-stroke-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodeContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodeIcon {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodeTitle {
|
||||||
|
color: var(--color-base900);
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: upppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodeDesc {
|
||||||
|
color: var(--color-base700);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NodePort {
|
||||||
|
&.react-flow__handle {
|
||||||
|
border: 0;
|
||||||
|
background-color: var(--edges-stroke-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// &[data-handlepos="bottom"] {
|
||||||
|
// width: 1px;
|
||||||
|
// background-color: var(--edges-stroke-color);
|
||||||
|
// }
|
||||||
|
}
|
||||||
12
src/Nodes/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as Node from "./Nodes";
|
||||||
|
|
||||||
|
export const nodeTypes = {
|
||||||
|
source: Node.Source,
|
||||||
|
email: Node.Action,
|
||||||
|
sms: Node.Action,
|
||||||
|
slack: Node.Action,
|
||||||
|
zalo: Node.Action,
|
||||||
|
waitThenCheck: Node.Condition,
|
||||||
|
end: Node.End,
|
||||||
|
empty: Node.Empty,
|
||||||
|
};
|
||||||
61
src/Popups/PopupListFlow.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { dataListFlow } from "../Data/data";
|
||||||
|
|
||||||
|
const PopupListFlow = ({ hidePopup, onSelectFlow }) => {
|
||||||
|
const [showPopup, setShowPopup] = useState(true);
|
||||||
|
|
||||||
|
const handleOverlayClick = () => {
|
||||||
|
setShowPopup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFlowClick = (flow) => {
|
||||||
|
onSelectFlow(flow); // Gọi callback với flow đã chọn
|
||||||
|
setShowPopup(false); // Ẩn popup
|
||||||
|
};
|
||||||
|
|
||||||
|
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-list-flow ">
|
||||||
|
<div className="popup-content">
|
||||||
|
<button onClick={handleOverlayClick} className="close">
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
<h3 className="title">
|
||||||
|
These starting points are popular with other Mailchimp customers.
|
||||||
|
</h3>
|
||||||
|
<p className="note">
|
||||||
|
Some starting points are coming soon or require additional setup.
|
||||||
|
</p>
|
||||||
|
<div className="ListNodeMain d-flex flex-wrap">
|
||||||
|
{dataListFlow.map((option) => (
|
||||||
|
<div
|
||||||
|
className="item-node"
|
||||||
|
key={option.id}
|
||||||
|
onClick={() => handleFlowClick(option.key)}
|
||||||
|
>
|
||||||
|
<div className="triggerName d-flex align-items">
|
||||||
|
<img className="icon" src={option.icon} alt={option.name} />
|
||||||
|
<p className="name">{option.name}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="description"
|
||||||
|
dangerouslySetInnerHTML={{ __html: option.description }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupListFlow;
|
||||||
17
src/RouterFlow.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import CreateFlow from "./CreateFlow";
|
||||||
|
import BuildFlow from "./BuildFlow";
|
||||||
|
|
||||||
|
function RouterFlow() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<CreateFlow />} />
|
||||||
|
<Route path="/build" element={<BuildFlow />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouterFlow;
|
||||||
57
src/Sidebar/Sidebar.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.dndflow {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow aside {
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
padding: 15px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fcfcfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow aside .description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow .dndnode {
|
||||||
|
height: 20px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #1a192b;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow .dndnode.input {
|
||||||
|
border-color: #0041d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow .dndnode.output {
|
||||||
|
border-color: #ff0072;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow .selectall {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.dndflow {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dndflow aside {
|
||||||
|
width: 20%;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Sidebar/Sidebar.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// =================================================================================================
|
||||||
|
import React from "react";
|
||||||
|
import { MenuOptions } from "../Data/Menu";
|
||||||
|
|
||||||
|
const Sidebar = ({ onAddNodeCallback }) => {
|
||||||
|
// const onDragStart = (event, nodeType) => {
|
||||||
|
// event.dataTransfer.setData("application/reactflow", nodeType);
|
||||||
|
// };
|
||||||
|
const onDragStart = (event, nodeType) => {
|
||||||
|
// event.dataTransfer.setData("application/reactflow", "custom"); // Set a custom dataTransfer type
|
||||||
|
event.dataTransfer.setData("nodeType", nodeType); // Store the node type in a custom data attribute
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="menu-fixed">
|
||||||
|
{MenuOptions.map((option) => (
|
||||||
|
<div className="item-flow" key={option.option}>
|
||||||
|
<b>{option.title}</b>
|
||||||
|
<div
|
||||||
|
className={`list-option ${option.option == "rule" ? "flex" : ""}`}
|
||||||
|
>
|
||||||
|
{option.list_option.map((items) => (
|
||||||
|
<div
|
||||||
|
className="items"
|
||||||
|
draggable="true"
|
||||||
|
key={items.id}
|
||||||
|
onDragStart={(event) => onDragStart(event, items.key)}
|
||||||
|
>
|
||||||
|
<div className="step">
|
||||||
|
<div className="left">
|
||||||
|
<img src={items.icon} width="24px" height="24px" alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="right">{items.label}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
224
src/Utils/WorkflowElementUtils.jsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { MenuOptions } from "../Data/Menu";
|
||||||
|
|
||||||
|
const position = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const getTitleAndDescription = (type) => {
|
||||||
|
let title = [];
|
||||||
|
|
||||||
|
MenuOptions.forEach((option) => {
|
||||||
|
option.list_option.forEach((item) => {
|
||||||
|
if (type === item.key) {
|
||||||
|
title.push(item.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { title };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatedElementsAfterActionNodeAddition = ({
|
||||||
|
elements,
|
||||||
|
newNodeId,
|
||||||
|
targetNodeId,
|
||||||
|
onAddNodeCallback,
|
||||||
|
}) => {
|
||||||
|
const clonedElements = _.cloneDeep(elements);
|
||||||
|
const newEdge = {
|
||||||
|
id: uuidv4(),
|
||||||
|
source: newNodeId,
|
||||||
|
target: targetNodeId,
|
||||||
|
type: "condition",
|
||||||
|
data: { onAddNodeCallback },
|
||||||
|
};
|
||||||
|
clonedElements.push(newEdge);
|
||||||
|
return clonedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatedElementsAfterEndNodeAddition = () => {};
|
||||||
|
|
||||||
|
const getUpdatedElementsAfterRuleNodeAdditon = ({
|
||||||
|
elements,
|
||||||
|
newNodeId,
|
||||||
|
targetNodeId,
|
||||||
|
onAddNodeCallback,
|
||||||
|
}) => {
|
||||||
|
const clonedElements = _.cloneDeep(elements);
|
||||||
|
const emptyNode1Id = uuidv4();
|
||||||
|
const emptyNode2Id = uuidv4();
|
||||||
|
const endNodeId = uuidv4();
|
||||||
|
|
||||||
|
// Define the end node for 'no' branch
|
||||||
|
const endNode = {
|
||||||
|
id: endNodeId,
|
||||||
|
type: "end",
|
||||||
|
data: {
|
||||||
|
title: "End",
|
||||||
|
description: "Automation ends.",
|
||||||
|
stats: {
|
||||||
|
completed: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
position, // Node end sẽ ở vị trí của nhánh no
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move the existing target node to the 'yes' branch
|
||||||
|
const ruleNodeToYesBranchEdge = {
|
||||||
|
id: uuidv4(),
|
||||||
|
source: newNodeId,
|
||||||
|
target: targetNodeId, // Existing target node moves to yes branch
|
||||||
|
type: "condition",
|
||||||
|
sourceHandle: "yes",
|
||||||
|
data: { onAddNodeCallback },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connect the rule node to the empty node in 'no' branch, which ends in the end node
|
||||||
|
const ruleNodeToNoBranchEdge = {
|
||||||
|
id: uuidv4(),
|
||||||
|
source: newNodeId,
|
||||||
|
target: endNodeId, // This goes to the 'no' branch
|
||||||
|
type: "condition",
|
||||||
|
sourceHandle: "no",
|
||||||
|
data: { onAddNodeCallback },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add everything to cloned elements
|
||||||
|
clonedElements.push(ruleNodeToYesBranchEdge, ruleNodeToNoBranchEdge, endNode);
|
||||||
|
|
||||||
|
return clonedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatedElementsAfterNodeAddition = ({
|
||||||
|
elements,
|
||||||
|
targetEdgeId,
|
||||||
|
type,
|
||||||
|
onDeleteNodeCallback,
|
||||||
|
onNodeClickCallback,
|
||||||
|
onAddNodeCallback,
|
||||||
|
position,
|
||||||
|
}) => {
|
||||||
|
const newNodeId = uuidv4();
|
||||||
|
const { title } = getTitleAndDescription(type);
|
||||||
|
const newNode = {
|
||||||
|
id: newNodeId,
|
||||||
|
type,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
onNodeClickCallback,
|
||||||
|
onDeleteNodeCallback,
|
||||||
|
},
|
||||||
|
|
||||||
|
position,
|
||||||
|
};
|
||||||
|
const clonedElements = _.cloneDeep(elements);
|
||||||
|
const targetEdgeIndex = clonedElements.findIndex(
|
||||||
|
(x) => x.id === targetEdgeId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetEdgeIndex === -1) {
|
||||||
|
console.error("Target edge is undefined.");
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetEdge = elements[targetEdgeIndex];
|
||||||
|
|
||||||
|
// Check if targetEdge is defined before accessing its properties
|
||||||
|
if (targetEdge) {
|
||||||
|
const { target: targetNodeId } = targetEdge;
|
||||||
|
const updatedTargetEdge = { ...targetEdge, target: newNodeId };
|
||||||
|
clonedElements[targetEdgeIndex] = updatedTargetEdge;
|
||||||
|
clonedElements.push(newNode);
|
||||||
|
|
||||||
|
console.log(position);
|
||||||
|
|
||||||
|
console.log(updatedTargetEdge);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "end":
|
||||||
|
// return getUpdatedElementsAfterEndNodeAddition();
|
||||||
|
|
||||||
|
return getUpdatedElementsAfterActionNodeAddition({
|
||||||
|
elements: clonedElements,
|
||||||
|
newNodeId,
|
||||||
|
newNode,
|
||||||
|
targetNodeId,
|
||||||
|
onAddNodeCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
case "waitThenCheck":
|
||||||
|
return getUpdatedElementsAfterRuleNodeAdditon({
|
||||||
|
elements: clonedElements,
|
||||||
|
newNodeId,
|
||||||
|
targetNodeId,
|
||||||
|
onAddNodeCallback,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return getUpdatedElementsAfterActionNodeAddition({
|
||||||
|
elements: clonedElements,
|
||||||
|
newNodeId,
|
||||||
|
newNode,
|
||||||
|
targetNodeId,
|
||||||
|
onAddNodeCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle the case when targetEdge is undefined
|
||||||
|
console.error("Target edge is undefined.");
|
||||||
|
return elements; // Return the original elements array
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const getUpdatedElementsAfterNodeAddition = ({
|
||||||
|
// elements,
|
||||||
|
// targetEdgeId,
|
||||||
|
// type,
|
||||||
|
// onDeleteNodeCallback,
|
||||||
|
// onNodeClickCallback,
|
||||||
|
// onAddNodeCallback,
|
||||||
|
// }) => {
|
||||||
|
// const newNodeId = uuidv4();
|
||||||
|
// const { title, description } = getTitleAndDescription(type);
|
||||||
|
// const newNode = {
|
||||||
|
// id: newNodeId,
|
||||||
|
// type,
|
||||||
|
// data: {
|
||||||
|
// title,
|
||||||
|
// description,
|
||||||
|
// onNodeClickCallback,
|
||||||
|
// onDeleteNodeCallback,
|
||||||
|
// },
|
||||||
|
// position,
|
||||||
|
// };
|
||||||
|
// const clonedElements = _.cloneDeep(elements);
|
||||||
|
// const targetEdgeIndex = clonedElements.findIndex(
|
||||||
|
// (x) => x.id === targetEdgeId
|
||||||
|
// );
|
||||||
|
// const targetEdge = elements[targetEdgeIndex];
|
||||||
|
// const { target: targetNodeId } = targetEdge;
|
||||||
|
// const updatedTargetEdge = { ...targetEdge, target: newNodeId };
|
||||||
|
// clonedElements[targetEdgeIndex] = updatedTargetEdge;
|
||||||
|
// clonedElements.push(newNode);
|
||||||
|
|
||||||
|
// switch (type) {
|
||||||
|
// case "end":
|
||||||
|
// return getUpdatedElementsAfterEndNodeAddition();
|
||||||
|
// case "waitThenCheck":
|
||||||
|
// return getUpdatedElementsAfterRuleNodeAdditon({
|
||||||
|
// elements: clonedElements,
|
||||||
|
// newNodeId,
|
||||||
|
// targetNodeId,
|
||||||
|
// onAddNodeCallback,
|
||||||
|
// });
|
||||||
|
// default:
|
||||||
|
// return getUpdatedElementsAfterActionNodeAddition({
|
||||||
|
// elements: clonedElements,
|
||||||
|
// newNodeId,
|
||||||
|
// newNode,
|
||||||
|
// targetNodeId,
|
||||||
|
// onAddNodeCallback,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
export { getUpdatedElementsAfterNodeAddition };
|
||||||
45
src/Utils/WorkflowLayoutUtils.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import dagre from "dagre";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { isNode } from "reactflow";
|
||||||
|
|
||||||
|
const nodeWidth = 250;
|
||||||
|
const nodeHeight = 80;
|
||||||
|
|
||||||
|
const getLayoutedElements = (_elements) => {
|
||||||
|
const elements = _.cloneDeep(_elements);
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
|
|
||||||
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
|
dagreGraph.setGraph({ rankdir: "TB" });
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (isNode(el)) {
|
||||||
|
dagreGraph.setNode(el.id, {
|
||||||
|
width: el.width || nodeWidth,
|
||||||
|
height: el.height || nodeHeight,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dagreGraph.setEdge(el.source, el.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
return elements.map((el) => {
|
||||||
|
if (isNode(el)) {
|
||||||
|
const nodeWithPosition = dagreGraph.node(el.id);
|
||||||
|
el.targetPosition = "top";
|
||||||
|
el.sourcePosition = "bottom";
|
||||||
|
el.position = {
|
||||||
|
x:
|
||||||
|
nodeWithPosition.x -
|
||||||
|
(el.width || nodeWidth) / 2 +
|
||||||
|
Math.random() / 1000,
|
||||||
|
y: nodeWithPosition.y - (el.height || nodeHeight) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getLayoutedElements };
|
||||||
226
src/assets/css/style.css
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
.container {
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-create {
|
||||||
|
width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-create .title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-create .note {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-form {
|
||||||
|
border: 1px solid rgba(36, 28, 21, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: 0px 12px 24px rgba(36, 28, 21, 0.16);
|
||||||
|
box-shadow: 0px 12px 24px rgba(36, 28, 21, 0.16);
|
||||||
|
padding: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-form label {
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-form input {
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #222;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-form .btn-submit {
|
||||||
|
background-color: #007c89;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 120px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 50px;
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-create .image-header {
|
||||||
|
width: 200px;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-start {
|
||||||
|
width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-start .imageStart {
|
||||||
|
width: 200px;
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-start h2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-start .note {
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin: 30px auto 0 auto;
|
||||||
|
width: 300px;
|
||||||
|
height: 50px;
|
||||||
|
background: #007c89;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50px;
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
left: 50%;
|
||||||
|
bottom: -50px;
|
||||||
|
width: 2px;
|
||||||
|
height: 50px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-list-flow {
|
||||||
|
left: 50%;
|
||||||
|
position: fixed;
|
||||||
|
width: 1000px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-list-flow .popup-content {
|
||||||
|
background: #efeeea;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.popup-list-flow .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 25px;
|
||||||
|
outline: 0;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-list-flow .title,
|
||||||
|
.popup-list-flow .note {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.popup-list-flow .title {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.d-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.align-items {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListNodeMain {
|
||||||
|
padding: 20px;
|
||||||
|
margin-right: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListNodeMain .item-node {
|
||||||
|
width: calc(100% / 3 - 16px);
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid rgba(36, 28, 21, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 20px;
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListNodeMain .item-node .icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.ListNodeMain .triggerName {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.ListNodeMain .item-node .name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListNodeMain .item-node .description {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.ListNodeMain .item-node ul li {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
border-bottom: 1px solid rgba(36, 28, 21, 0.15);
|
||||||
|
padding: 10px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header .close-popup {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-footer {
|
||||||
|
border-top: 1px solid rgba(36, 28, 21, 0.15);
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
BIN
src/assets/images/Customer-Journey_Animation.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/images/ILLO_MiniSpot_2-Paths-v2_1080x1080.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
1
src/assets/images/icon-birthday.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 iconPositioning-2AitU icon-3YgtG"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1l1.106 2.211a1.236 1.236 0 11-2.212 0L8 1zm4 0l1.106 2.211a1.236 1.236 0 11-2.212 0L12 1zM7 9V6h2v3h2V6h2v3h2V6h2v3h3.75A2.25 2.25 0 0123 11.25V14a3.99 3.99 0 01-1 2.646V22a1 1 0 01-1 1H3a1 1 0 01-1-1v-5.354A3.985 3.985 0 011 14v-2.75A2.25 2.25 0 013.25 9H7zM4 21h16v-3.126A4.01 4.01 0 0115.535 16h-.07A3.998 3.998 0 0112 18a3.998 3.998 0 01-3.465-2h-.07A3.998 3.998 0 014 17.874V21zM17.106 3.211L16 1l-1.106 2.211a1.236 1.236 0 102.212 0zM7 14a2 2 0 11-4 0v-2.75a.25.25 0 01.25-.25h17.5a.25.25 0 01.25.25V14a2 2 0 11-4 0h-3a2 2 0 11-4 0H7z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 756 B |
1
src/assets/images/icon-buy.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 iconPositioning-2AitU icon-3YgtG" disabled=""><path fill-rule="evenodd" clip-rule="evenodd" d="M14 8V5a4 4 0 10-8 0v3H1v15h22V8h-4V5a4 4 0 00-5.076-3.854c.518.527.93 1.158 1.203 1.858A2 2 0 0117 5v3h-3zm-4-5a2 2 0 00-2 2v3h4V5a2 2 0 00-2-2zm7 7v11H3V10h3v2h2v-2h4v2h2v-2h3z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
1
src/assets/images/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/images/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/images/icon-exits.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 exitIndicatorIcon-1gdYp"><path d="M7 2a2 2 0 00-2 2v5h2V4h12v16H7v-5H5v5a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H7z"></path><path d="M11.743 6.331l-1.486 1.338 3.181 3.535L2 11v2l11.438-.204-3.181 3.535 1.486 1.338L16.845 12l-5.102-5.669z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 369 B |
1
src/assets/images/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/images/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 |
7
src/assets/images/icon-phone.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<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 19a1 1 0 11-2 0 1 1 0 012 0z"></path>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M6 1a2 2 0 00-2 2v18a2 2 0 002 2h12a2 2 0 002-2V3a2 2 0 00-2-2H6zm3 2H6v18h12V3h-3v1a1 1 0 01-1 1h-4a1 1 0 01-1-1V3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 404 B |
1
src/assets/images/icon-signs.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 iconPositioning-2AitU icon-3YgtG"><g clip-path="url(#svg__d4767__clip0)"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 8.5c0 3.13-1.547 5.814-3.94 6.937.219.734 1.01 1.027 1.897 1.356C14.359 17.313 16 17.922 16 20.5v2H1v-2c0-2.578 1.641-3.187 3.043-3.707.887-.329 1.678-.622 1.897-1.356C3.547 14.314 2 11.63 2 8.5 2 4.358 4.364 1 8.5 1S15 4.358 15 8.5zM8.5 14c2.39 0 4.5-2.14 4.5-5.5 0-1.71-.489-3.108-1.248-4.039C11.027 3.573 9.972 3 8.5 3c-1.473 0-2.527.573-3.252 1.461C4.488 5.391 4 6.791 4 8.5 4 11.86 6.11 14 8.5 14z"></path><path d="M21 4v3h3v2h-3v3h-2V9h-3V7h3V4h2z"></path></g><defs><clipPath id="svg__d4767__clip0"><path d="M0 0h24v24H0z"></path></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 800 B |
18
src/assets/images/icon-slack.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="800px" height="800px" viewBox="0 0 31.444 31.443"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M31.202,16.369c-0.62-1.388-2.249-2.011-3.637-1.391l-1.325,0.594l-3.396-7.591l1.325-0.592
|
||||||
|
c1.388-0.622,2.01-2.25,1.389-3.637c-0.62-1.389-2.248-2.012-3.637-1.39l-1.324,0.593l-0.593-1.326
|
||||||
|
c-0.621-1.388-2.249-2.009-3.637-1.388c-1.388,0.62-2.009,2.247-1.389,3.637l0.593,1.325L7.98,8.598L7.388,7.273
|
||||||
|
c-0.621-1.39-2.249-2.009-3.637-1.39C2.363,6.504,1.742,8.132,2.362,9.52l0.592,1.324L1.63,11.438
|
||||||
|
c-1.388,0.621-2.01,2.247-1.389,3.636c0.62,1.388,2.249,2.01,3.637,1.39l1.325-0.594l3.394,7.592l-1.325,0.592
|
||||||
|
c-1.388,0.621-2.009,2.25-1.389,3.637c0.621,1.389,2.249,2.011,3.637,1.391l1.324-0.593l0.593,1.325
|
||||||
|
c0.621,1.389,2.249,2.01,3.637,1.389c1.387-0.62,2.009-2.248,1.388-3.636l-0.591-1.326l7.591-3.394l0.592,1.321
|
||||||
|
c0.621,1.391,2.248,2.013,3.637,1.392c1.388-0.619,2.01-2.248,1.389-3.637l-0.592-1.324l1.323-0.594
|
||||||
|
C31.201,19.384,31.823,17.757,31.202,16.369z M13.623,21.215l-3.395-7.593l7.591-3.394l3.395,7.591L13.623,21.215z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/images/icon-tag.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true" class="wink-icon iconPositioning-2AitU icon-3YgtG"><path d="M8 10a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M12.414 2H3.586L2 3.586v8.828l11 11L23.414 13l-11-11zm-8 2L4 4.414v7.172l9 9L20.586 13l-9-9H4.414z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 358 B |
1
src/assets/images/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/images/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/images/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
src/assets/images/icon-wink.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"><path d="M14 5a2 2 0 11-4 0 2 2 0 014 0zm0 7a2 2 0 11-4 0 2 2 0 014 0zm-2 9a2 2 0 100-4 2 2 0 000 4z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 230 B |
1
src/assets/images/icon-zalo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 9 4 C 6.2504839 4 4 6.2504839 4 9 L 4 41 C 4 43.749516 6.2504839 46 9 46 L 41 46 C 43.749516 46 46 43.749516 46 41 L 46 9 C 46 6.2504839 43.749516 4 41 4 L 9 4 z M 9 6 L 15.580078 6 C 12.00899 9.7156859 10 14.518083 10 19.5 C 10 24.66 12.110156 29.599844 15.910156 33.339844 C 16.030156 33.549844 16.129922 34.579531 15.669922 35.769531 C 15.379922 36.519531 14.799687 37.499141 13.679688 37.869141 C 13.249688 38.009141 12.97 38.430859 13 38.880859 C 13.03 39.330859 13.360781 39.710781 13.800781 39.800781 C 16.670781 40.370781 18.529297 39.510078 20.029297 38.830078 C 21.379297 38.210078 22.270625 37.789609 23.640625 38.349609 C 26.440625 39.439609 29.42 40 32.5 40 C 36.593685 40 40.531459 39.000731 44 37.113281 L 44 41 C 44 42.668484 42.668484 44 41 44 L 9 44 C 7.3315161 44 6 42.668484 6 41 L 6 9 C 6 7.3315161 7.3315161 6 9 6 z M 33 15 C 33.55 15 34 15.45 34 16 L 34 25 C 34 25.55 33.55 26 33 26 C 32.45 26 32 25.55 32 25 L 32 16 C 32 15.45 32.45 15 33 15 z M 18 16 L 23 16 C 23.36 16 23.700859 16.199531 23.880859 16.519531 C 24.050859 16.829531 24.039609 17.219297 23.849609 17.529297 L 19.800781 24 L 23 24 C 23.55 24 24 24.45 24 25 C 24 25.55 23.55 26 23 26 L 18 26 C 17.64 26 17.299141 25.800469 17.119141 25.480469 C 16.949141 25.170469 16.960391 24.780703 17.150391 24.470703 L 21.199219 18 L 18 18 C 17.45 18 17 17.55 17 17 C 17 16.45 17.45 16 18 16 z M 27.5 19 C 28.11 19 28.679453 19.169219 29.189453 19.449219 C 29.369453 19.189219 29.65 19 30 19 C 30.55 19 31 19.45 31 20 L 31 25 C 31 25.55 30.55 26 30 26 C 29.65 26 29.369453 25.810781 29.189453 25.550781 C 28.679453 25.830781 28.11 26 27.5 26 C 25.57 26 24 24.43 24 22.5 C 24 20.57 25.57 19 27.5 19 z M 38.5 19 C 40.43 19 42 20.57 42 22.5 C 42 24.43 40.43 26 38.5 26 C 36.57 26 35 24.43 35 22.5 C 35 20.57 36.57 19 38.5 19 z M 27.5 21 C 27.39625 21 27.29502 21.011309 27.197266 21.03125 C 27.001758 21.071133 26.819727 21.148164 26.660156 21.255859 C 26.500586 21.363555 26.363555 21.500586 26.255859 21.660156 C 26.148164 21.819727 26.071133 22.001758 26.03125 22.197266 C 26.011309 22.29502 26 22.39625 26 22.5 C 26 22.60375 26.011309 22.70498 26.03125 22.802734 C 26.051191 22.900488 26.079297 22.994219 26.117188 23.083984 C 26.155078 23.17375 26.202012 23.260059 26.255859 23.339844 C 26.309707 23.419629 26.371641 23.492734 26.439453 23.560547 C 26.507266 23.628359 26.580371 23.690293 26.660156 23.744141 C 26.819727 23.851836 27.001758 23.928867 27.197266 23.96875 C 27.29502 23.988691 27.39625 24 27.5 24 C 27.60375 24 27.70498 23.988691 27.802734 23.96875 C 28.487012 23.82916 29 23.22625 29 22.5 C 29 21.67 28.33 21 27.5 21 z M 38.5 21 C 38.39625 21 38.29502 21.011309 38.197266 21.03125 C 38.099512 21.051191 38.005781 21.079297 37.916016 21.117188 C 37.82625 21.155078 37.739941 21.202012 37.660156 21.255859 C 37.580371 21.309707 37.507266 21.371641 37.439453 21.439453 C 37.303828 21.575078 37.192969 21.736484 37.117188 21.916016 C 37.079297 22.005781 37.051191 22.099512 37.03125 22.197266 C 37.011309 22.29502 37 22.39625 37 22.5 C 37 22.60375 37.011309 22.70498 37.03125 22.802734 C 37.051191 22.900488 37.079297 22.994219 37.117188 23.083984 C 37.155078 23.17375 37.202012 23.260059 37.255859 23.339844 C 37.309707 23.419629 37.371641 23.492734 37.439453 23.560547 C 37.507266 23.628359 37.580371 23.690293 37.660156 23.744141 C 37.739941 23.797988 37.82625 23.844922 37.916016 23.882812 C 38.005781 23.920703 38.099512 23.948809 38.197266 23.96875 C 38.29502 23.988691 38.39625 24 38.5 24 C 38.60375 24 38.70498 23.988691 38.802734 23.96875 C 39.487012 23.82916 40 23.22625 40 22.5 C 40 21.67 39.33 21 38.5 21 z"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
106
src/index.scss
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* prettier-ignore */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
/* 1. The Contactlab Marketing Cloud color palette */
|
||||||
|
--color-accent: #1890ff;
|
||||||
|
--color-accent100: #e6f7ff;
|
||||||
|
--color-accent200: #bae7ff;
|
||||||
|
--color-accent300: #91d5ff;
|
||||||
|
--color-accent400: #69c0ff;
|
||||||
|
--color-accent500: #40a9ff;
|
||||||
|
--color-accent600: #1890ff;
|
||||||
|
--color-accent700: #096dd9;
|
||||||
|
--color-accent800: #0050b3;
|
||||||
|
--color-accent900: #003a8c;
|
||||||
|
--color-base: #141414;
|
||||||
|
--color-base100: #fafafa;
|
||||||
|
--color-base200: #f5f5f5;
|
||||||
|
--color-base300: #f0f0f0;
|
||||||
|
--color-base400: #d9d9d9;
|
||||||
|
--color-base500: #bfbfbf;
|
||||||
|
--color-base600: #8c8c8c;
|
||||||
|
--color-base700: #595959;
|
||||||
|
--color-base800: #434343;
|
||||||
|
--color-base900: #141414;
|
||||||
|
--color-info: #1890ff;
|
||||||
|
--color-success: #52c41a;
|
||||||
|
--color-warning: #faad14;
|
||||||
|
--color-error: #f5222d;
|
||||||
|
--color-white: #fff;
|
||||||
|
--color-black: #000;
|
||||||
|
--color-fluo-base300: #e9e8ff;
|
||||||
|
--color-fluo-base600: #b4afff;
|
||||||
|
--color-fluo-base900: #7168ff;
|
||||||
|
--color-fluo-accent300: #e1f9ef;
|
||||||
|
--color-fluo-accent600: #9bebd0;
|
||||||
|
--color-fluo-accent900: #00c9bd;
|
||||||
|
|
||||||
|
/* 2. The Contactlab Marketing Cloud elevations */
|
||||||
|
--elevation-z1: 0 1px 3px 0 rgba(0, 20, 32, 0.12);
|
||||||
|
--elevation-z2: 0 3px 6px 0 rgba(0, 20, 32, 0.04), 0 3px 6px 0 rgba(0, 20, 32, 0.12);
|
||||||
|
--elevation-z3: 0 6px 6px 0 rgba(0, 20, 32, 0.09), 0 10px 20px 0 rgba(0, 20, 32, 0.13);
|
||||||
|
--elevation-z4: 0 10px 10px 0 rgba(0, 20, 32, 0.05), 0 14px 28px 0 rgba(0, 20, 32, 0.16);
|
||||||
|
--elevation-z5: 0 19px 38px 0 rgba(0, 20, 32, 0.16), 0 15px 12px 0 rgba(0, 20, 32, 0.12);
|
||||||
|
--elevation-accent-z1: 0 1px 3px 0 rgba(3, 145, 236, 0.12);
|
||||||
|
--elevation-accent-z2: 0 3px 6px 0 rgba(3, 145, 236, 0.04), 0 3px 6px 0 rgba(3, 145, 236, 0.12);
|
||||||
|
--elevation-accent-z3: 0 6px 6px 0 rgba(3, 145, 236, 0.09), 0 10px 20px 0 rgba(3, 145, 236, 0.13);
|
||||||
|
--elevation-accent-z4: 0 10px 10px 0 rgba(3, 145, 236, 0.05), 0 14px 28px 0 rgba(3, 145, 236, 0.16);
|
||||||
|
--elevation-accent-z5: 0 19px 38px 0 rgba(3, 145, 236, 0.16), 0 15px 12px 0 rgba(3, 145, 236, 0.12);
|
||||||
|
|
||||||
|
/* 3. The Contactlab Marketing Cloud transitions */
|
||||||
|
--elevation-transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* 4. Document styles */
|
||||||
|
--mc-body-foreground: var(--color-base);
|
||||||
|
--mc-body-background: var(--color-base100);
|
||||||
|
--mc-scroll-padding: 100px;
|
||||||
|
--mc-scroll-behavior: smooth;
|
||||||
|
|
||||||
|
/* 5. a11y */
|
||||||
|
--mc-selection-foreground: var(--color-white);
|
||||||
|
--mc-selection-background: rgba(24, 144, 255, 0.8);
|
||||||
|
|
||||||
|
/* 6. Headings */
|
||||||
|
--mc-heading-foreground: var(--color-base);
|
||||||
|
--mc-subheading-foreground: var(--color-base800);
|
||||||
|
|
||||||
|
/* 7. Links */
|
||||||
|
--mc-link-foreground: var(--color-accent);
|
||||||
|
--mc-link-active-foreground: var(--color-accent500);
|
||||||
|
|
||||||
|
/* 8. <code> */
|
||||||
|
--mc-code-border-radius: 2px;
|
||||||
|
--mc-code-background: var(--color-base300);
|
||||||
|
|
||||||
|
/* 9. <hr> */
|
||||||
|
--mc-hr-background: var(--color-base300);
|
||||||
|
|
||||||
|
/* 10. <mark> */
|
||||||
|
--mc-mark-foreground: var(--color-white);
|
||||||
|
--mc-mark-background: var(--color-accent);
|
||||||
|
|
||||||
|
/* 11. <small> */
|
||||||
|
--mc-small-foreground: var(--color-base600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
21
src/main.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client"; // Sử dụng từ react-dom/client trong React 18
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
import RouterFlow from "./RouterFlow";
|
||||||
|
|
||||||
|
// Lấy root element từ file HTML
|
||||||
|
const rootElement = document.getElementById("root");
|
||||||
|
|
||||||
|
// Sử dụng createRoot thay vì ReactDOM.render
|
||||||
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
|
||||||
|
// Render ứng dụng
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<RouterFlow />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
7
vite.config.js
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()],
|
||||||
|
})
|
||||||
10
vite.config.js.timestamp-1727964485729-baa5cfaa2c2d6.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from "file:///C:/Users/tiepb/Downloads/Company/work_2024/react-flow-drag/node_modules/vite/dist/node/index.js";
|
||||||
|
import react from "file:///C:/Users/tiepb/Downloads/Company/work_2024/react-flow-drag/node_modules/@vitejs/plugin-react/dist/index.mjs";
|
||||||
|
var vite_config_default = defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
vite_config_default as default
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0aWVwYlxcXFxEb3dubG9hZHNcXFxcQ29tcGFueVxcXFx3b3JrXzIwMjRcXFxccmVhY3QtZmxvdy1kcmFnXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0aWVwYlxcXFxEb3dubG9hZHNcXFxcQ29tcGFueVxcXFx3b3JrXzIwMjRcXFxccmVhY3QtZmxvdy1kcmFnXFxcXHZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90aWVwYi9Eb3dubG9hZHMvQ29tcGFueS93b3JrXzIwMjQvcmVhY3QtZmxvdy1kcmFnL3ZpdGUuY29uZmlnLmpzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcclxuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0J1xyXG5cclxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cclxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcclxuICBwbHVnaW5zOiBbcmVhY3QoKV0sXHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOFcsU0FBUyxvQkFBb0I7QUFDM1ksT0FBTyxXQUFXO0FBR2xCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxNQUFNLENBQUM7QUFDbkIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K
|
||||||