@canmingir/link 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/context/reducer.js +1 -4
- package/src/http/index.js +11 -8
- package/src/http/user.js +48 -16
- package/src/layouts/DashboardLayout/nav-mini.jsx +34 -14
- package/src/layouts/FullScreenLayout/nav-horizontal.jsx +6 -1
- package/src/layouts/common/ProjectBar/index.jsx +13 -13
- package/src/layouts/common/account-popover.jsx +2 -3
- package/src/lib/Flow/DraggableNode.jsx +62 -0
- package/src/lib/Flow/DynamicConnector.jsx +172 -0
- package/src/lib/Flow/Flow.jsx +34 -0
- package/src/lib/Flow/FlowNode.jsx +266 -0
- package/src/lib/Flow/flowUtils.js +111 -0
- package/src/lib/Flow/index.js +1 -0
- package/src/lib/Flow/layouts/ActionNode.jsx +78 -0
- package/src/lib/Flow/layouts/AnimatedNode.jsx +22 -0
- package/src/lib/Flow/layouts/CardLayout.jsx +313 -0
- package/src/lib/Flow/layouts/InfoNode.jsx +196 -0
- package/src/lib/Flow/layouts/LoadingNode.jsx +37 -0
- package/src/lib/Flow/styles.js +114 -0
- package/src/lib/index.js +13 -0
- package/src/lib/useChart/useChart.js +3 -3
- package/src/pages/Callback.jsx +9 -9
- package/src/pages/LoginPage.jsx +4 -1
- package/src/stories/FlowChart.stories.jsx +333 -0
- package/src/widgets/Login/Login.jsx +4 -1
- package/src/widgets/LoginForm/LoginForm.jsx +5 -5
package/package.json
CHANGED
package/src/context/reducer.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import config from "../config/config";
|
|
2
1
|
import { jwtDecode } from "jwt-decode";
|
|
3
2
|
import { storage } from "@nucleoidjs/webstorage";
|
|
4
3
|
|
|
5
|
-
const { name } = config();
|
|
6
|
-
|
|
7
4
|
let login = true;
|
|
8
5
|
const itemId = storage.get("itemId");
|
|
9
6
|
try {
|
|
10
|
-
const token = storage.get(
|
|
7
|
+
const token = storage.get("link", "accessToken");
|
|
11
8
|
const decodedToken = jwtDecode(token);
|
|
12
9
|
|
|
13
10
|
if (decodedToken.exp * 1000 < Date.now()) {
|
package/src/http/index.js
CHANGED
|
@@ -15,8 +15,8 @@ const instance = axios.create({
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
instance.interceptors.request.use((request) => {
|
|
18
|
-
const {
|
|
19
|
-
const accessToken = storage.get(
|
|
18
|
+
const { base } = config();
|
|
19
|
+
const accessToken = storage.get("link", "accessToken");
|
|
20
20
|
|
|
21
21
|
if (!accessToken) {
|
|
22
22
|
window.location.href = base === "/" ? "/login" : `${base}/login`;
|
|
@@ -85,14 +85,17 @@ export const fetcher = (url) => instance.get(url).then((res) => res.data);
|
|
|
85
85
|
|
|
86
86
|
const refreshAuthLogic = async (failedRequest) => {
|
|
87
87
|
try {
|
|
88
|
-
const {
|
|
88
|
+
const { appId } = config();
|
|
89
89
|
|
|
90
90
|
const projectId = storage.get("projectId");
|
|
91
91
|
|
|
92
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
93
|
+
|
|
92
94
|
const { data } = await oauth.post("/oauth", {
|
|
93
|
-
refreshToken: storage.get(
|
|
95
|
+
refreshToken: storage.get("link", "refreshToken"),
|
|
94
96
|
appId,
|
|
95
97
|
projectId,
|
|
98
|
+
identityProvider,
|
|
96
99
|
});
|
|
97
100
|
|
|
98
101
|
const accessToken = data.accessToken;
|
|
@@ -100,13 +103,13 @@ const refreshAuthLogic = async (failedRequest) => {
|
|
|
100
103
|
failedRequest.response.config.headers["Authorization"] =
|
|
101
104
|
"Bearer " + accessToken;
|
|
102
105
|
|
|
103
|
-
storage.set(
|
|
106
|
+
storage.set("link", "accessToken", accessToken);
|
|
104
107
|
return Promise.resolve();
|
|
105
108
|
} catch (error) {
|
|
106
109
|
const { name, base } = config();
|
|
107
110
|
|
|
108
|
-
storage.remove(
|
|
109
|
-
storage.remove(
|
|
111
|
+
storage.remove("link", "accessToken");
|
|
112
|
+
storage.remove("link", "refreshToken");
|
|
110
113
|
|
|
111
114
|
window.location.href = `${window.location.origin}${base}/login`;
|
|
112
115
|
return Promise.reject(error);
|
|
@@ -122,7 +125,7 @@ refreshInterceptor(instance, refreshAuthLogic, {
|
|
|
122
125
|
statusCodes: [401, 403],
|
|
123
126
|
shouldRefresh: () => {
|
|
124
127
|
const { name, base } = config();
|
|
125
|
-
const token = storage.get(
|
|
128
|
+
const token = storage.get("link", "accessToken");
|
|
126
129
|
|
|
127
130
|
if (!token) {
|
|
128
131
|
window.location.href = `${window.location.origin}${base}/login`;
|
package/src/http/user.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import config from "../config/config.js";
|
|
3
3
|
import http from "./index";
|
|
4
|
+
import { jwtDecode } from "jwt-decode";
|
|
5
|
+
import oauth from "./oauth";
|
|
4
6
|
import { storage } from "@nucleoidjs/webstorage";
|
|
5
7
|
|
|
6
8
|
const instance = axios.create({
|
|
@@ -11,15 +13,8 @@ const instance = axios.create({
|
|
|
11
13
|
},
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
function getProjectName() {
|
|
15
|
-
const { name } = config();
|
|
16
|
-
if (name) {
|
|
17
|
-
return name;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
instance.interceptors.request.use(async (request) => {
|
|
22
|
-
const refreshToken = await storage.get(
|
|
17
|
+
const refreshToken = await storage.get("link", "refreshToken");
|
|
23
18
|
if (refreshToken) {
|
|
24
19
|
request.headers["Authorization"] = `Bearer ${refreshToken}`;
|
|
25
20
|
}
|
|
@@ -28,25 +23,62 @@ instance.interceptors.request.use(async (request) => {
|
|
|
28
23
|
|
|
29
24
|
instance.getUserDetails = async () => {
|
|
30
25
|
try {
|
|
31
|
-
const refreshToken = await storage.get(
|
|
26
|
+
const refreshToken = await storage.get("link", "refreshToken");
|
|
27
|
+
|
|
32
28
|
if (!refreshToken) {
|
|
33
29
|
console.log("No refresh token found");
|
|
34
30
|
return null;
|
|
35
31
|
}
|
|
36
32
|
|
|
33
|
+
let accessToken = await storage.get("link", "accessToken");
|
|
34
|
+
|
|
35
|
+
if (!accessToken) {
|
|
36
|
+
console.log("No access token found");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const decodedToken = jwtDecode(accessToken);
|
|
42
|
+
const isExpired = decodedToken.exp * 1000 < Date.now();
|
|
43
|
+
|
|
44
|
+
if (isExpired) {
|
|
45
|
+
console.log(
|
|
46
|
+
"Access token expired, refreshing before fetching user details..."
|
|
47
|
+
);
|
|
48
|
+
const { appId } = config();
|
|
49
|
+
const projectId = storage.get("projectId");
|
|
50
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
51
|
+
|
|
52
|
+
const { data } = await oauth.post("/oauth", {
|
|
53
|
+
refreshToken,
|
|
54
|
+
appId,
|
|
55
|
+
projectId,
|
|
56
|
+
identityProvider,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
accessToken = data.accessToken;
|
|
60
|
+
storage.set("link", "accessToken", accessToken);
|
|
61
|
+
console.log(
|
|
62
|
+
"Access token refreshed successfully, now fetching user details"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Error checking or refreshing token:", error);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
const response = await http.get("/oauth/user", {
|
|
38
71
|
headers: {
|
|
39
|
-
|
|
40
|
-
}
|
|
72
|
+
"X-Refresh-Token": refreshToken,
|
|
73
|
+
},
|
|
41
74
|
});
|
|
42
|
-
|
|
75
|
+
|
|
43
76
|
if (response.data && response.data.user) {
|
|
44
77
|
return response.data.user;
|
|
45
78
|
}
|
|
46
|
-
|
|
79
|
+
|
|
47
80
|
console.log("No user data received from server");
|
|
48
81
|
return null;
|
|
49
|
-
|
|
50
82
|
} catch (error) {
|
|
51
83
|
console.error("Error fetching user details from server:", error);
|
|
52
84
|
return null;
|
|
@@ -55,7 +87,7 @@ instance.getUserDetails = async () => {
|
|
|
55
87
|
|
|
56
88
|
instance.getPermittedUsers = async () => {
|
|
57
89
|
const userIds = [];
|
|
58
|
-
const refreshToken = await storage.get(
|
|
90
|
+
const refreshToken = await storage.get("link", "refreshToken");
|
|
59
91
|
const response = await http.get("/permissions");
|
|
60
92
|
|
|
61
93
|
response.data.forEach((permission) => {
|
|
@@ -75,4 +107,4 @@ instance.getPermittedUsers = async () => {
|
|
|
75
107
|
return users;
|
|
76
108
|
};
|
|
77
109
|
|
|
78
|
-
export default instance;
|
|
110
|
+
export default instance;
|
|
@@ -9,15 +9,16 @@ import SettingsDialog from "../../widgets/SettingsDialog";
|
|
|
9
9
|
import Stack from "@mui/material/Stack";
|
|
10
10
|
import config from "../../config/config";
|
|
11
11
|
import { hideScroll } from "../../theme/css";
|
|
12
|
+
import { useResponsive } from "../../hooks/use-responsive";
|
|
12
13
|
import { useUser } from "../../hooks/use-user";
|
|
13
14
|
|
|
14
15
|
import React, { useState } from "react";
|
|
15
16
|
|
|
16
17
|
export default function NavMini({ only }) {
|
|
17
18
|
const { user } = useUser();
|
|
18
|
-
const { sideMenu } = config().menu;
|
|
19
|
+
const { sideMenu, actionButtons } = config().menu;
|
|
19
20
|
const [openSettings, setOpenSettings] = useState(false);
|
|
20
|
-
|
|
21
|
+
const lgUp = useResponsive("up", "lg");
|
|
21
22
|
const handleCloseSettings = () => {
|
|
22
23
|
setOpenSettings(false);
|
|
23
24
|
};
|
|
@@ -57,21 +58,40 @@ export default function NavMini({ only }) {
|
|
|
57
58
|
}}
|
|
58
59
|
/>
|
|
59
60
|
<Box sx={{ flexGrow: 1 }} />
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
<Stack
|
|
62
|
+
direction={"column"}
|
|
63
|
+
alignItems={"center"}
|
|
64
|
+
justifyItems={"center"}
|
|
65
|
+
sx={{
|
|
66
|
+
marginBottom: lgUp ? 3 : 0,
|
|
67
|
+
position: lgUp ? "static" : "fixed",
|
|
68
|
+
bottom: lgUp ? "auto" : 66,
|
|
69
|
+
width: "100%",
|
|
70
|
+
}}
|
|
71
|
+
gap={2}
|
|
64
72
|
>
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
{actionButtons &&
|
|
74
|
+
actionButtons.map((Action, index) => (
|
|
75
|
+
<Box key={index} component={Action}></Box>
|
|
76
|
+
))}
|
|
77
|
+
<Button
|
|
78
|
+
onClick={() => setOpenSettings(true)}
|
|
67
79
|
sx={{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
mx: "auto",
|
|
80
|
+
position: lgUp ? "static" : "fixed",
|
|
81
|
+
bottom: lgUp ? "auto" : 16,
|
|
82
|
+
width: "100%",
|
|
72
83
|
}}
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
>
|
|
85
|
+
<Iconify
|
|
86
|
+
icon={"ic:baseline-settings"}
|
|
87
|
+
sx={{
|
|
88
|
+
width: 32,
|
|
89
|
+
height: 32,
|
|
90
|
+
color: "text.secondary",
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</Button>
|
|
94
|
+
</Stack>
|
|
75
95
|
<SettingsDialog open={openSettings} handleClose={handleCloseSettings} />
|
|
76
96
|
</Stack>
|
|
77
97
|
</Box>
|
|
@@ -16,7 +16,7 @@ function NavHorizontal() {
|
|
|
16
16
|
|
|
17
17
|
const { user } = useUser();
|
|
18
18
|
|
|
19
|
-
const { sideMenu } = config().menu;
|
|
19
|
+
const { sideMenu, actionButtons } = config().menu;
|
|
20
20
|
return (
|
|
21
21
|
<AppBar component="div" data-cy="nav-horizontal">
|
|
22
22
|
<Toolbar
|
|
@@ -35,6 +35,11 @@ function NavHorizontal() {
|
|
|
35
35
|
...theme.mixins.toolbar,
|
|
36
36
|
}}
|
|
37
37
|
/>
|
|
38
|
+
|
|
39
|
+
{actionButtons &&
|
|
40
|
+
actionButtons.map((Action, index) => (
|
|
41
|
+
<Box key={index} component={Action}></Box>
|
|
42
|
+
))}
|
|
38
43
|
</Toolbar>
|
|
39
44
|
|
|
40
45
|
<HeaderShadow />
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import { Button, DialogActions } from "@mui/material";
|
|
2
|
-
import Dialog, { dialogClasses } from "@mui/material/Dialog";
|
|
3
|
-
import React, { useCallback, useState } from "react";
|
|
4
|
-
import { publish, useEvent } from "@nucleoidai/react-event";
|
|
5
|
-
import { storage, useStorage } from "@nucleoidjs/webstorage";
|
|
6
|
-
import { useMediaQuery, useTheme } from "@mui/material";
|
|
7
|
-
|
|
8
1
|
import Box from "@mui/material/Box";
|
|
9
2
|
import IconButton from "@mui/material/IconButton";
|
|
10
3
|
import Iconify from "../../../components/Iconify";
|
|
@@ -29,9 +22,16 @@ import { useEventListener } from "../../../hooks/use-event-listener";
|
|
|
29
22
|
import { useNavigate } from "react-router-dom";
|
|
30
23
|
import useProjects from "../../../hooks/useProjects";
|
|
31
24
|
|
|
25
|
+
import { Button, DialogActions } from "@mui/material";
|
|
26
|
+
import Dialog, { dialogClasses } from "@mui/material/Dialog";
|
|
27
|
+
import React, { useCallback, useState } from "react";
|
|
28
|
+
import { publish, useEvent } from "@nucleoidai/react-event";
|
|
29
|
+
import { storage, useStorage } from "@nucleoidjs/webstorage";
|
|
30
|
+
import { useMediaQuery, useTheme } from "@mui/material";
|
|
31
|
+
|
|
32
32
|
function ProjectBar() {
|
|
33
33
|
const label = config().template?.projectBar?.label;
|
|
34
|
-
const { appId
|
|
34
|
+
const { appId } = config();
|
|
35
35
|
const theme = useTheme();
|
|
36
36
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down(435));
|
|
37
37
|
const { loading, projects, getProjects } = useProjects();
|
|
@@ -91,15 +91,15 @@ function ProjectBar() {
|
|
|
91
91
|
const handleSelect = (project) => {
|
|
92
92
|
const { id: projectId } = project;
|
|
93
93
|
|
|
94
|
-
const refreshToken = storage.get(
|
|
95
|
-
const
|
|
94
|
+
const refreshToken = storage.get("link", "refreshToken");
|
|
95
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
96
96
|
|
|
97
97
|
oauth
|
|
98
|
-
.post("/oauth", { appId, refreshToken, projectId,
|
|
98
|
+
.post("/oauth", { appId, refreshToken, projectId, identityProvider })
|
|
99
99
|
.then(({ data }) => {
|
|
100
100
|
const { refreshToken, accessToken } = data;
|
|
101
|
-
storage.set(
|
|
102
|
-
storage.set(
|
|
101
|
+
storage.set("link", "accessToken", accessToken);
|
|
102
|
+
storage.set("link", "refreshToken", refreshToken);
|
|
103
103
|
storage.set("projectId", projectId);
|
|
104
104
|
})
|
|
105
105
|
.finally(() => {
|
|
@@ -19,7 +19,6 @@ import CustomPopover, { usePopover } from "../../components/custom-popover";
|
|
|
19
19
|
// ----------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
export default function AccountPopover() {
|
|
22
|
-
const { name } = config();
|
|
23
22
|
const { options } = config().menu;
|
|
24
23
|
const router = useRouter();
|
|
25
24
|
const { user } = useUser();
|
|
@@ -28,8 +27,8 @@ export default function AccountPopover() {
|
|
|
28
27
|
|
|
29
28
|
const handleLogout = async () => {
|
|
30
29
|
try {
|
|
31
|
-
storage.remove(
|
|
32
|
-
storage.remove(
|
|
30
|
+
storage.remove("link", "accessToken");
|
|
31
|
+
storage.remove("link", "refreshToken");
|
|
33
32
|
popover.onClose();
|
|
34
33
|
router.replace("/login");
|
|
35
34
|
} catch (error) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
const DraggableNode = ({ children, registerRef, onDrag }) => {
|
|
6
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
7
|
+
const localRef = useRef(null);
|
|
8
|
+
|
|
9
|
+
const setRef = (el) => {
|
|
10
|
+
localRef.current = el;
|
|
11
|
+
if (registerRef) registerRef(el);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const handleMouseDown = (e) => {
|
|
15
|
+
if (e.button !== 0) return;
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
|
|
18
|
+
const startX = e.clientX;
|
|
19
|
+
const startY = e.clientY;
|
|
20
|
+
const startOffset = { ...offset };
|
|
21
|
+
|
|
22
|
+
const onMove = (ev) => {
|
|
23
|
+
const dx = ev.clientX - startX;
|
|
24
|
+
const dy = ev.clientY - startY;
|
|
25
|
+
setOffset({
|
|
26
|
+
x: startOffset.x + dx,
|
|
27
|
+
y: startOffset.y + dy,
|
|
28
|
+
});
|
|
29
|
+
if (onDrag) onDrag();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const onUp = () => {
|
|
33
|
+
window.removeEventListener("mousemove", onMove);
|
|
34
|
+
window.removeEventListener("mouseup", onUp);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
window.addEventListener("mousemove", onMove);
|
|
38
|
+
window.addEventListener("mouseup", onUp);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Box
|
|
43
|
+
ref={setRef}
|
|
44
|
+
onMouseDown={handleMouseDown}
|
|
45
|
+
sx={{
|
|
46
|
+
display: "inline-flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
position: "relative",
|
|
50
|
+
transform: `translate(${offset.x}px, ${offset.y}px)`,
|
|
51
|
+
cursor: "grab",
|
|
52
|
+
"&:active": {
|
|
53
|
+
cursor: "grabbing",
|
|
54
|
+
},
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default DraggableNode;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const DynamicConnector = ({
|
|
4
|
+
containerEl,
|
|
5
|
+
parentEl,
|
|
6
|
+
childEls,
|
|
7
|
+
stroke,
|
|
8
|
+
strokeWidth,
|
|
9
|
+
lineStyle,
|
|
10
|
+
connectorType = "default",
|
|
11
|
+
tick = 0,
|
|
12
|
+
}) => {
|
|
13
|
+
const [dims, setDims] = useState(null);
|
|
14
|
+
const [points, setPoints] = useState({
|
|
15
|
+
parent: null,
|
|
16
|
+
children: [],
|
|
17
|
+
yMid: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
if (!containerEl || !parentEl || !childEls?.length) return;
|
|
22
|
+
|
|
23
|
+
const update = () => {
|
|
24
|
+
const cRect = containerEl.getBoundingClientRect();
|
|
25
|
+
const pRect = parentEl.getBoundingClientRect();
|
|
26
|
+
|
|
27
|
+
const parent = {
|
|
28
|
+
x: pRect.left + pRect.width / 2 - cRect.left,
|
|
29
|
+
y: pRect.bottom - cRect.top,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const children = childEls.map((el) => {
|
|
33
|
+
const r = el.getBoundingClientRect();
|
|
34
|
+
return {
|
|
35
|
+
x: r.left + r.width / 2 - cRect.left,
|
|
36
|
+
y: r.top - cRect.top,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const firstTop = Math.min(...children.map((c) => c.y));
|
|
41
|
+
const yMid =
|
|
42
|
+
parent.y + Math.max(12, Math.min(24, (firstTop - parent.y) * 0.4));
|
|
43
|
+
|
|
44
|
+
setPoints({ parent, children, yMid });
|
|
45
|
+
setDims({ w: cRect.width, h: cRect.height });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ro = new ResizeObserver(update);
|
|
49
|
+
ro.observe(containerEl);
|
|
50
|
+
ro.observe(parentEl);
|
|
51
|
+
childEls.forEach((el) => el && ro.observe(el));
|
|
52
|
+
|
|
53
|
+
update();
|
|
54
|
+
return () => ro.disconnect();
|
|
55
|
+
}, [containerEl, parentEl, childEls, tick]);
|
|
56
|
+
|
|
57
|
+
if (!dims || !points.parent || !points.children.length) return null;
|
|
58
|
+
|
|
59
|
+
const dash =
|
|
60
|
+
lineStyle === "dashed"
|
|
61
|
+
? `${strokeWidth * 3},${strokeWidth * 2}`
|
|
62
|
+
: lineStyle === "dotted"
|
|
63
|
+
? `${strokeWidth},${strokeWidth * 1.5}`
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
const onlyOne = points.children.length === 1;
|
|
67
|
+
|
|
68
|
+
if (connectorType === "curved" || connectorType === "n8n") {
|
|
69
|
+
const createN8nPath = (from, to) => {
|
|
70
|
+
const v = Math.max(32, Math.abs(to.y - from.y) * 0.35);
|
|
71
|
+
const h = Math.max(24, Math.abs(to.x - from.x) * 0.35);
|
|
72
|
+
|
|
73
|
+
const c1 = {
|
|
74
|
+
x: to.x > from.x ? from.x + h : from.x - h,
|
|
75
|
+
y: from.y + v,
|
|
76
|
+
};
|
|
77
|
+
const c2 = {
|
|
78
|
+
x: to.x > from.x ? to.x - h : to.x + h,
|
|
79
|
+
y: to.y - v,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return `M ${from.x} ${from.y} C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${to.x} ${to.y}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<svg
|
|
87
|
+
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
|
88
|
+
width="100%"
|
|
89
|
+
height="100%"
|
|
90
|
+
viewBox={`0 0 ${dims.w} ${dims.h}`}
|
|
91
|
+
>
|
|
92
|
+
{points.children.map((child, i) => (
|
|
93
|
+
<path
|
|
94
|
+
key={i}
|
|
95
|
+
d={createN8nPath(points.parent, child)}
|
|
96
|
+
fill="none"
|
|
97
|
+
stroke={stroke}
|
|
98
|
+
strokeWidth={strokeWidth}
|
|
99
|
+
strokeDasharray={dash}
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
strokeLinejoin="round"
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</svg>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<svg
|
|
110
|
+
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
|
111
|
+
width="100%"
|
|
112
|
+
height="100%"
|
|
113
|
+
viewBox={`0 0 ${dims.w} ${dims.h}`}
|
|
114
|
+
>
|
|
115
|
+
{onlyOne ? (
|
|
116
|
+
<line
|
|
117
|
+
x1={points.parent.x}
|
|
118
|
+
y1={points.parent.y}
|
|
119
|
+
x2={points.children[0].x}
|
|
120
|
+
y2={points.children[0].y}
|
|
121
|
+
stroke={stroke}
|
|
122
|
+
strokeWidth={strokeWidth}
|
|
123
|
+
strokeDasharray={dash}
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<>
|
|
127
|
+
<line
|
|
128
|
+
x1={points.parent.x}
|
|
129
|
+
y1={points.parent.y}
|
|
130
|
+
x2={points.parent.x}
|
|
131
|
+
y2={points.yMid}
|
|
132
|
+
stroke={stroke}
|
|
133
|
+
strokeWidth={strokeWidth}
|
|
134
|
+
strokeDasharray={dash}
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
{(() => {
|
|
138
|
+
const xs = points.children.map((c) => c.x);
|
|
139
|
+
const xMin = Math.min(...xs);
|
|
140
|
+
const xMax = Math.max(...xs);
|
|
141
|
+
return (
|
|
142
|
+
<line
|
|
143
|
+
x1={xMin}
|
|
144
|
+
y1={points.yMid}
|
|
145
|
+
x2={xMax}
|
|
146
|
+
y2={points.yMid}
|
|
147
|
+
stroke={stroke}
|
|
148
|
+
strokeWidth={strokeWidth}
|
|
149
|
+
strokeDasharray={dash}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
})()}
|
|
153
|
+
|
|
154
|
+
{points.children.map((c, i) => (
|
|
155
|
+
<line
|
|
156
|
+
key={i}
|
|
157
|
+
x1={c.x}
|
|
158
|
+
y1={points.yMid}
|
|
159
|
+
x2={c.x}
|
|
160
|
+
y2={c.y}
|
|
161
|
+
stroke={stroke}
|
|
162
|
+
strokeWidth={strokeWidth}
|
|
163
|
+
strokeDasharray={dash}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
</svg>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default DynamicConnector;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import FlowNode from "./FlowNode";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from "react";
|
|
4
|
+
import { assertLinkedGraph, buildTreeFromLinked } from "./flowUtils";
|
|
5
|
+
|
|
6
|
+
export const Flow = ({ data, variant = "simple", style, plugin }) => {
|
|
7
|
+
const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
|
|
8
|
+
|
|
9
|
+
const treeData = useMemo(() => {
|
|
10
|
+
if (!roots?.length)
|
|
11
|
+
return { id: "__empty__", label: "(empty)", children: [] };
|
|
12
|
+
|
|
13
|
+
if (roots.length === 1) {
|
|
14
|
+
return (
|
|
15
|
+
buildTreeFromLinked(roots[0], nodesById) || {
|
|
16
|
+
id: roots[0],
|
|
17
|
+
children: [],
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const children = roots
|
|
23
|
+
.map((r) => buildTreeFromLinked(r, nodesById))
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
return { id: "__root__", label: "Root", children };
|
|
27
|
+
}, [nodesById, roots]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<FlowNode node={treeData} variant={variant} style={style} plugin={plugin} />
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default Flow;
|