@canmingir/link 1.2.50 → 1.2.52
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/http/index.js +39 -10
- package/src/http/user.js +89 -19
- package/src/layouts/DashboardLayout/header.jsx +1 -5
- package/src/layouts/common/nav-toggle-button.jsx +2 -2
- package/src/lib/Flow/connectors/DynamicConnector.jsx +19 -12
- package/src/lib/Flow/core/Flow.jsx +7 -3
- package/src/lib/Flow/core/FlowBoard.jsx +78 -0
- package/src/lib/Flow/core/FlowBoardItem.jsx +144 -0
- package/src/lib/Flow/core/FlowNode.jsx +2 -0
- package/src/lib/Flow/core/FlowViewport.jsx +3 -2
- package/src/lib/Flow/index.js +1 -0
- package/src/lib/index.js +1 -1
- package/src/widgets/SettingsDialog.jsx +75 -40
package/package.json
CHANGED
package/src/http/index.js
CHANGED
|
@@ -40,6 +40,20 @@ instance.interceptors.response.use(
|
|
|
40
40
|
async (error) => {
|
|
41
41
|
publish("LOADED", { loading: false });
|
|
42
42
|
const statusCode = error.response?.status;
|
|
43
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
44
|
+
|
|
45
|
+
if (statusCode === 500 && identityProvider === "DEMO") {
|
|
46
|
+
const token = storage.get("link", "accessToken");
|
|
47
|
+
try {
|
|
48
|
+
const decodedToken = jwtDecode(token);
|
|
49
|
+
if (decodedToken.exp * 1000 < Date.now()) {
|
|
50
|
+
return Promise.reject(error);
|
|
51
|
+
}
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return Promise.reject(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
switch (statusCode) {
|
|
44
58
|
case 400:
|
|
45
59
|
publish("GLOBAL_MESSAGE_POSTED", {
|
|
@@ -78,7 +92,7 @@ instance.interceptors.response.use(
|
|
|
78
92
|
break;
|
|
79
93
|
}
|
|
80
94
|
return Promise.reject(error);
|
|
81
|
-
}
|
|
95
|
+
},
|
|
82
96
|
);
|
|
83
97
|
|
|
84
98
|
export const fetcher = (url) => instance.get(url).then((res) => res.data);
|
|
@@ -128,26 +142,41 @@ const refreshInterceptor =
|
|
|
128
142
|
: createAuthRefreshInterceptor.default;
|
|
129
143
|
|
|
130
144
|
refreshInterceptor(instance, refreshAuthLogic, {
|
|
131
|
-
statusCodes: [401, 403],
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
statusCodes: [401, 403, 500],
|
|
146
|
+
pauseInstanceWhileRefreshing: true,
|
|
147
|
+
shouldRefresh: (error) => {
|
|
148
|
+
const { base } = config();
|
|
134
149
|
const token = storage.get("link", "accessToken");
|
|
150
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
151
|
+
const statusCode = error.response?.status;
|
|
135
152
|
|
|
136
153
|
if (!token) {
|
|
137
154
|
window.location.href = `${window.location.origin}${base}/login`;
|
|
138
155
|
return false;
|
|
139
156
|
}
|
|
140
157
|
|
|
158
|
+
if (identityProvider === "DEMO") {
|
|
159
|
+
if (statusCode !== 500) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const decodedToken = jwtDecode(token);
|
|
165
|
+
return decodedToken.exp * 1000 < Date.now();
|
|
166
|
+
} catch (_) {
|
|
167
|
+
// If we can't decode, attempt refresh once.
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (statusCode === 500) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
try {
|
|
142
177
|
const decodedToken = jwtDecode(token);
|
|
143
178
|
if (decodedToken.exp * 1000 < Date.now()) {
|
|
144
179
|
return true;
|
|
145
|
-
} else {
|
|
146
|
-
publish("GLOBAL_MESSAGE_POSTED", {
|
|
147
|
-
status: true,
|
|
148
|
-
message: "UNAUTHORIZED",
|
|
149
|
-
severity: "warning",
|
|
150
|
-
});
|
|
151
180
|
}
|
|
152
181
|
} catch (err) {
|
|
153
182
|
window.location.href = `${window.location.origin}${base}/login`;
|
package/src/http/user.js
CHANGED
|
@@ -43,7 +43,7 @@ instance.getUserDetails = async () => {
|
|
|
43
43
|
|
|
44
44
|
if (isExpired) {
|
|
45
45
|
console.log(
|
|
46
|
-
"Access token expired, refreshing before fetching user details..."
|
|
46
|
+
"Access token expired, refreshing before fetching user details...",
|
|
47
47
|
);
|
|
48
48
|
const { appId } = config();
|
|
49
49
|
const projectId = storage.get("projectId");
|
|
@@ -54,12 +54,16 @@ instance.getUserDetails = async () => {
|
|
|
54
54
|
appId,
|
|
55
55
|
projectId,
|
|
56
56
|
identityProvider,
|
|
57
|
+
...(identityProvider === "DEMO" && {
|
|
58
|
+
username: "admin",
|
|
59
|
+
password: "admin",
|
|
60
|
+
}),
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
accessToken = data.accessToken;
|
|
60
64
|
storage.set("link", "accessToken", accessToken);
|
|
61
65
|
console.log(
|
|
62
|
-
"Access token refreshed successfully, now fetching user details"
|
|
66
|
+
"Access token refreshed successfully, now fetching user details",
|
|
63
67
|
);
|
|
64
68
|
}
|
|
65
69
|
} catch (error) {
|
|
@@ -86,25 +90,91 @@ instance.getUserDetails = async () => {
|
|
|
86
90
|
};
|
|
87
91
|
|
|
88
92
|
instance.getPermittedUsers = async () => {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
93
|
+
const { appId } = config();
|
|
94
|
+
const projectId = storage.get("projectId");
|
|
95
|
+
const identityProvider = storage.get("link", "identityProvider");
|
|
96
|
+
|
|
91
97
|
const response = await http.get("/permissions");
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
const uniqueUserIds = [
|
|
100
|
+
...new Set(
|
|
101
|
+
response.data
|
|
102
|
+
.filter((p) => p.appId === appId && p.projectId === projectId)
|
|
103
|
+
.map((p) => p.userId),
|
|
104
|
+
),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const UUID_REGEX =
|
|
108
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
109
|
+
|
|
110
|
+
if (identityProvider?.toUpperCase() === "GITHUB") {
|
|
111
|
+
const results = await Promise.allSettled(
|
|
112
|
+
uniqueUserIds.map(async (userId) => {
|
|
113
|
+
if (UUID_REGEX.test(userId)) {
|
|
114
|
+
return {
|
|
115
|
+
id: String(userId),
|
|
116
|
+
identityProvider,
|
|
117
|
+
name: String(userId),
|
|
118
|
+
displayName: null,
|
|
119
|
+
avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${userId}`,
|
|
120
|
+
email: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const { data } = await axios.get(
|
|
124
|
+
`https://api.github.com/user/${userId}`,
|
|
125
|
+
{ headers: { Accept: "application/vnd.github+json" } },
|
|
126
|
+
);
|
|
127
|
+
return {
|
|
128
|
+
id: String(data.id),
|
|
129
|
+
identityProvider: "GITHUB",
|
|
130
|
+
name: data.login,
|
|
131
|
+
displayName: data.name || null,
|
|
132
|
+
avatarUrl: data.avatar_url || null,
|
|
133
|
+
email: data.email || null,
|
|
134
|
+
};
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return results
|
|
139
|
+
.filter((r) => r.status === "fulfilled" && r.value !== null)
|
|
140
|
+
.map((r) => r.value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (identityProvider?.toUpperCase() === "DEMO") {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
id: "1001",
|
|
147
|
+
identityProvider: "DEMO",
|
|
148
|
+
name: "admin",
|
|
149
|
+
displayName: "Demo Admin",
|
|
150
|
+
avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=1001`,
|
|
151
|
+
email: "admin@demo.local",
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (identityProvider?.toUpperCase() === "COGNITO") {
|
|
157
|
+
const accessToken = await storage.get("link", "accessToken");
|
|
158
|
+
let currentUserId = null;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const decoded = jwtDecode(accessToken);
|
|
162
|
+
currentUserId = decoded.sub;
|
|
163
|
+
} catch (_) {}
|
|
164
|
+
|
|
165
|
+
return uniqueUserIds
|
|
166
|
+
.filter((id) => !UUID_REGEX.test(id))
|
|
167
|
+
.map((id) => ({
|
|
168
|
+
id: String(id),
|
|
169
|
+
identityProvider: "COGNITO",
|
|
170
|
+
name: id === currentUserId ? "You" : "Cognito User",
|
|
171
|
+
displayName: id === currentUserId ? "Cognito Admin" : null,
|
|
172
|
+
avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${id}`,
|
|
173
|
+
email: null,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return [];
|
|
108
178
|
};
|
|
109
179
|
|
|
110
180
|
export default instance;
|
|
@@ -26,7 +26,7 @@ export default function NavToggleButton({ sx, ...other }) {
|
|
|
26
26
|
onClick={() =>
|
|
27
27
|
settings.onUpdate(
|
|
28
28
|
"themeLayout",
|
|
29
|
-
settings.themeLayout === "vertical" ? "mini" : "vertical"
|
|
29
|
+
settings.themeLayout === "vertical" ? "mini" : "vertical",
|
|
30
30
|
)
|
|
31
31
|
}
|
|
32
32
|
sx={{
|
|
@@ -34,7 +34,7 @@ export default function NavToggleButton({ sx, ...other }) {
|
|
|
34
34
|
top: 32,
|
|
35
35
|
position: "fixed",
|
|
36
36
|
left: NAV.W_VERTICAL - 12,
|
|
37
|
-
zIndex:
|
|
37
|
+
zIndex: theme.zIndex.appBar + 1,
|
|
38
38
|
border: `dashed 1px ${theme.palette.divider}`,
|
|
39
39
|
...bgBlur({ opacity: 0.48, color: theme.palette.background.default }),
|
|
40
40
|
"&:hover": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Box } from "@mui/material";
|
|
2
|
-
|
|
3
1
|
import React, { useId, useLayoutEffect, useMemo, useState } from "react";
|
|
4
2
|
|
|
3
|
+
import { Box } from "@mui/material";
|
|
4
|
+
|
|
5
5
|
const DynamicConnector = ({
|
|
6
6
|
containerEl,
|
|
7
7
|
parentEl,
|
|
@@ -43,41 +43,48 @@ const DynamicConnector = ({
|
|
|
43
43
|
const cRect = containerEl.getBoundingClientRect();
|
|
44
44
|
const pRect = parentEl.getBoundingClientRect();
|
|
45
45
|
|
|
46
|
+
const scaleX = containerEl.offsetWidth
|
|
47
|
+
? cRect.width / containerEl.offsetWidth || 1
|
|
48
|
+
: 1;
|
|
49
|
+
const scaleY = containerEl.offsetHeight
|
|
50
|
+
? cRect.height / containerEl.offsetHeight || 1
|
|
51
|
+
: 1;
|
|
52
|
+
|
|
46
53
|
let parentPoint;
|
|
47
54
|
let childPoints = [];
|
|
48
55
|
|
|
49
56
|
if (isHorizontal) {
|
|
50
57
|
parentPoint = {
|
|
51
|
-
x: pRect.right - cRect.left,
|
|
52
|
-
y: pRect.top + pRect.height / 2 - cRect.top,
|
|
58
|
+
x: (pRect.right - cRect.left) / scaleX,
|
|
59
|
+
y: (pRect.top + pRect.height / 2 - cRect.top) / scaleY,
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
childPoints = childEls.map((el) => {
|
|
56
63
|
if (!el) return { x: parentPoint.x + 100, y: parentPoint.y };
|
|
57
64
|
const r = el.getBoundingClientRect();
|
|
58
65
|
return {
|
|
59
|
-
x: r.left - cRect.left,
|
|
60
|
-
y: r.top + r.height / 2 - cRect.top,
|
|
66
|
+
x: (r.left - cRect.left) / scaleX,
|
|
67
|
+
y: (r.top + r.height / 2 - cRect.top) / scaleY,
|
|
61
68
|
};
|
|
62
69
|
});
|
|
63
70
|
} else {
|
|
64
71
|
parentPoint = {
|
|
65
|
-
x: pRect.left + pRect.width / 2 - cRect.left,
|
|
66
|
-
y: pRect.bottom - cRect.top,
|
|
72
|
+
x: (pRect.left + pRect.width / 2 - cRect.left) / scaleX,
|
|
73
|
+
y: (pRect.bottom - cRect.top) / scaleY,
|
|
67
74
|
};
|
|
68
75
|
|
|
69
76
|
childPoints = childEls.map((el) => {
|
|
70
77
|
if (!el) return { x: parentPoint.x, y: parentPoint.y + 100 };
|
|
71
78
|
const r = el.getBoundingClientRect();
|
|
72
79
|
return {
|
|
73
|
-
x: r.left + r.width / 2 - cRect.left,
|
|
74
|
-
y: r.top - cRect.top,
|
|
80
|
+
x: (r.left + r.width / 2 - cRect.left) / scaleX,
|
|
81
|
+
y: (r.top - cRect.top) / scaleY,
|
|
75
82
|
};
|
|
76
83
|
});
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
setPoints({ parent: parentPoint, children: childPoints });
|
|
80
|
-
setDims({ w: cRect.width, h: cRect.height });
|
|
87
|
+
setDims({ w: cRect.width / scaleX, h: cRect.height / scaleY });
|
|
81
88
|
};
|
|
82
89
|
|
|
83
90
|
update();
|
|
@@ -95,7 +102,7 @@ const DynamicConnector = ({
|
|
|
95
102
|
gradient: `gradient-${uniqueId}`,
|
|
96
103
|
arrow: `arrow-${uniqueId}`,
|
|
97
104
|
}),
|
|
98
|
-
[uniqueId]
|
|
105
|
+
[uniqueId],
|
|
99
106
|
);
|
|
100
107
|
|
|
101
108
|
const getPath = (from, to) => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import FlowNode from "./FlowNode";
|
|
2
|
-
import { useGraphOperations } from "../hooks/useGraphOperations";
|
|
3
|
-
|
|
4
1
|
import { Box, alpha } from "@mui/material";
|
|
5
2
|
import React, { useMemo, useState } from "react";
|
|
6
3
|
import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
|
|
7
4
|
|
|
5
|
+
import FlowNode from "./FlowNode";
|
|
6
|
+
import { useGraphOperations } from "../hooks/useGraphOperations";
|
|
7
|
+
|
|
8
8
|
export const Flow = ({
|
|
9
9
|
data,
|
|
10
10
|
variant = "simple",
|
|
@@ -13,6 +13,7 @@ export const Flow = ({
|
|
|
13
13
|
editable = false,
|
|
14
14
|
onChange,
|
|
15
15
|
height,
|
|
16
|
+
initialZoom,
|
|
16
17
|
}) => {
|
|
17
18
|
const [floatingNodes, setFloatingNodes] = useState([]);
|
|
18
19
|
|
|
@@ -65,6 +66,8 @@ export const Flow = ({
|
|
|
65
66
|
return (
|
|
66
67
|
<Box
|
|
67
68
|
sx={{
|
|
69
|
+
height,
|
|
70
|
+
flexShrink: 0,
|
|
68
71
|
backgroundImage: (theme) => `
|
|
69
72
|
radial-gradient(
|
|
70
73
|
${alpha(theme.palette.divider, 0.08)} 1px,
|
|
@@ -86,6 +89,7 @@ export const Flow = ({
|
|
|
86
89
|
onConnect={editable ? handleConnect : undefined}
|
|
87
90
|
floatingNodes={floatingNodes}
|
|
88
91
|
height={height}
|
|
92
|
+
initialZoom={initialZoom}
|
|
89
93
|
/>
|
|
90
94
|
</Box>
|
|
91
95
|
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import FlowBoardItem from "./FlowBoardItem";
|
|
4
|
+
import FlowViewport from "./FlowViewport";
|
|
5
|
+
import { SelectionProvider } from "../selection/SelectionContext";
|
|
6
|
+
import { getBaseStyleForVariant } from "../styles";
|
|
7
|
+
|
|
8
|
+
export const FlowBoard = ({
|
|
9
|
+
flows = [],
|
|
10
|
+
variant = "simple",
|
|
11
|
+
style,
|
|
12
|
+
plugin,
|
|
13
|
+
initialZoom = 1,
|
|
14
|
+
height = "100vh",
|
|
15
|
+
gap = 480,
|
|
16
|
+
onFlowPositionChange,
|
|
17
|
+
}) => {
|
|
18
|
+
const baseStyle = getBaseStyleForVariant(variant);
|
|
19
|
+
const selectionColor = baseStyle.selectionColor ?? "#64748b";
|
|
20
|
+
|
|
21
|
+
const positionedFlows = useMemo(() => {
|
|
22
|
+
const count = flows.length;
|
|
23
|
+
return flows.map((flow, index) => {
|
|
24
|
+
const fallback = {
|
|
25
|
+
x: (index - (count - 1) / 2) * gap,
|
|
26
|
+
y: 0,
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
flow,
|
|
30
|
+
id: flow?.id ?? index,
|
|
31
|
+
position: flow?.position ?? fallback,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}, [flows, gap]);
|
|
35
|
+
|
|
36
|
+
const mergedNodesById = useMemo(() => {
|
|
37
|
+
const merged = {};
|
|
38
|
+
for (const { flow } of positionedFlows) {
|
|
39
|
+
if (flow?.nodes) Object.assign(merged, flow.nodes);
|
|
40
|
+
}
|
|
41
|
+
return merged;
|
|
42
|
+
}, [positionedFlows]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SelectionProvider>
|
|
46
|
+
<FlowViewport
|
|
47
|
+
selectionColor={selectionColor}
|
|
48
|
+
nodesById={mergedNodesById}
|
|
49
|
+
variant={variant}
|
|
50
|
+
style={style}
|
|
51
|
+
plugin={plugin}
|
|
52
|
+
height={height}
|
|
53
|
+
initialZoom={initialZoom}
|
|
54
|
+
>
|
|
55
|
+
{positionedFlows.map(({ flow, id, position }) => (
|
|
56
|
+
<FlowBoardItem
|
|
57
|
+
key={id}
|
|
58
|
+
flow={flow}
|
|
59
|
+
flowId={id}
|
|
60
|
+
label={flow?.label}
|
|
61
|
+
divider={flow?.divider}
|
|
62
|
+
position={position}
|
|
63
|
+
onPositionChange={
|
|
64
|
+
onFlowPositionChange
|
|
65
|
+
? (pos) => onFlowPositionChange(id, pos)
|
|
66
|
+
: undefined
|
|
67
|
+
}
|
|
68
|
+
variant={flow?.variant ?? variant}
|
|
69
|
+
style={style}
|
|
70
|
+
plugin={plugin}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</FlowViewport>
|
|
74
|
+
</SelectionProvider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default FlowBoard;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
|
|
3
|
+
|
|
4
|
+
import { Box } from "@mui/material";
|
|
5
|
+
import FlowNodeView from "../nodes/FlowNodeView";
|
|
6
|
+
|
|
7
|
+
const FlowBoardItem = ({
|
|
8
|
+
flow,
|
|
9
|
+
flowId,
|
|
10
|
+
position: initialPosition,
|
|
11
|
+
onPositionChange,
|
|
12
|
+
variant,
|
|
13
|
+
style,
|
|
14
|
+
plugin,
|
|
15
|
+
label,
|
|
16
|
+
divider,
|
|
17
|
+
}) => {
|
|
18
|
+
const [position, setPosition] = useState(
|
|
19
|
+
() => initialPosition || { x: 0, y: 0 },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (initialPosition) setPosition({ ...initialPosition });
|
|
24
|
+
}, [initialPosition?.x, initialPosition?.y]);
|
|
25
|
+
|
|
26
|
+
const didDragRef = useRef(false);
|
|
27
|
+
|
|
28
|
+
const namespacedFlow = useMemo(() => {
|
|
29
|
+
if (flowId == null) return flow;
|
|
30
|
+
const prefix = `${flowId}::`;
|
|
31
|
+
const ns = (id) =>
|
|
32
|
+
typeof id === "string" && !id.startsWith(prefix) ? `${prefix}${id}` : id;
|
|
33
|
+
|
|
34
|
+
const nodes = {};
|
|
35
|
+
for (const [id, node] of Object.entries(flow?.nodes || {})) {
|
|
36
|
+
const nextArr = Array.isArray(node.next)
|
|
37
|
+
? node.next.map((n) => (typeof n === "string" ? ns(n) : n))
|
|
38
|
+
: node.next != null
|
|
39
|
+
? ns(node.next)
|
|
40
|
+
: undefined;
|
|
41
|
+
nodes[ns(id)] = {
|
|
42
|
+
...node,
|
|
43
|
+
id: ns(id),
|
|
44
|
+
next: nextArr,
|
|
45
|
+
previous: node.previous != null ? ns(node.previous) : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const roots = Array.isArray(flow?.roots) ? flow.roots.map(ns) : flow?.roots;
|
|
49
|
+
return { ...flow, nodes, roots };
|
|
50
|
+
}, [flow, flowId]);
|
|
51
|
+
|
|
52
|
+
const { nodesById, roots } = useMemo(
|
|
53
|
+
() => assertLinkedGraph(namespacedFlow),
|
|
54
|
+
[namespacedFlow],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const treesData = useMemo(() => {
|
|
58
|
+
if (!roots?.length) return [];
|
|
59
|
+
return roots
|
|
60
|
+
.map((rootId) => buildTreeFromLinked(rootId, nodesById))
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}, [nodesById, roots]);
|
|
63
|
+
|
|
64
|
+
const handleMouseDown = (e) => {
|
|
65
|
+
if (e.button !== 0) return;
|
|
66
|
+
if (e.target?.closest?.(".MuiCard-root") || e.target?.closest?.("button"))
|
|
67
|
+
return;
|
|
68
|
+
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
didDragRef.current = false;
|
|
71
|
+
|
|
72
|
+
const startX = e.clientX;
|
|
73
|
+
const startY = e.clientY;
|
|
74
|
+
const startPosition = { ...position };
|
|
75
|
+
let lastPosition = startPosition;
|
|
76
|
+
|
|
77
|
+
const onMove = (ev) => {
|
|
78
|
+
const dx = ev.clientX - startX;
|
|
79
|
+
const dy = ev.clientY - startY;
|
|
80
|
+
if (!didDragRef.current && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
|
|
81
|
+
didDragRef.current = true;
|
|
82
|
+
}
|
|
83
|
+
lastPosition = { x: startPosition.x + dx, y: startPosition.y + dy };
|
|
84
|
+
setPosition(lastPosition);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onUp = () => {
|
|
88
|
+
window.removeEventListener("mousemove", onMove);
|
|
89
|
+
window.removeEventListener("mouseup", onUp);
|
|
90
|
+
if (didDragRef.current) onPositionChange?.(lastPosition);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
window.addEventListener("mousemove", onMove);
|
|
94
|
+
window.addEventListener("mouseup", onUp);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!treesData.length) return null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Box
|
|
101
|
+
data-flow-id={label}
|
|
102
|
+
onMouseDown={handleMouseDown}
|
|
103
|
+
sx={{
|
|
104
|
+
position: "absolute",
|
|
105
|
+
left: "50%",
|
|
106
|
+
top: "50%",
|
|
107
|
+
transform: `translate(${position.x}px, ${position.y}px)`,
|
|
108
|
+
cursor: "grab",
|
|
109
|
+
"&:active": { cursor: "grabbing" },
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{label != null && (
|
|
113
|
+
<Box
|
|
114
|
+
sx={{
|
|
115
|
+
position: "absolute",
|
|
116
|
+
top: -28,
|
|
117
|
+
left: 0,
|
|
118
|
+
fontSize: 12,
|
|
119
|
+
fontWeight: 600,
|
|
120
|
+
opacity: 0.6,
|
|
121
|
+
pointerEvents: "none",
|
|
122
|
+
whiteSpace: "nowrap",
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{label}
|
|
126
|
+
</Box>
|
|
127
|
+
)}
|
|
128
|
+
{divider != null && <Box sx={{ width: "100%", mb: 1 }}>{divider}</Box>}
|
|
129
|
+
<Box sx={{ display: "flex", gap: 4, alignItems: "flex-start" }}>
|
|
130
|
+
{treesData.map((tree, idx) => (
|
|
131
|
+
<FlowNodeView
|
|
132
|
+
key={tree.id || `tree-${idx}`}
|
|
133
|
+
node={tree}
|
|
134
|
+
variant={variant}
|
|
135
|
+
style={style}
|
|
136
|
+
plugin={plugin}
|
|
137
|
+
/>
|
|
138
|
+
))}
|
|
139
|
+
</Box>
|
|
140
|
+
</Box>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default FlowBoardItem;
|
|
@@ -17,6 +17,7 @@ const FlowNode = ({
|
|
|
17
17
|
plugin,
|
|
18
18
|
node,
|
|
19
19
|
height,
|
|
20
|
+
initialZoom,
|
|
20
21
|
...props
|
|
21
22
|
}) => {
|
|
22
23
|
if (!isRoot) {
|
|
@@ -50,6 +51,7 @@ const FlowNode = ({
|
|
|
50
51
|
style={style}
|
|
51
52
|
plugin={plugin}
|
|
52
53
|
height={height}
|
|
54
|
+
initialZoom={initialZoom}
|
|
53
55
|
>
|
|
54
56
|
{node && (
|
|
55
57
|
<FlowNodeView
|
|
@@ -17,13 +17,14 @@ const FlowViewport = ({
|
|
|
17
17
|
style,
|
|
18
18
|
plugin,
|
|
19
19
|
height = "100vh",
|
|
20
|
+
initialZoom = 1,
|
|
20
21
|
sx = {},
|
|
21
22
|
...rest
|
|
22
23
|
}) => {
|
|
23
24
|
const clampZoom = (zoom) => Math.min(2.5, Math.max(0.25, zoom));
|
|
24
25
|
|
|
25
26
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
26
|
-
const [zoom, setZoom] = useState(
|
|
27
|
+
const [zoom, setZoom] = useState(initialZoom);
|
|
27
28
|
const [isDragging, setIsDragging] = useState(false);
|
|
28
29
|
const [selectionBox, setSelectionBox] = useState(null);
|
|
29
30
|
const [shouldCenter, setShouldCenter] = useState(true);
|
|
@@ -291,7 +292,7 @@ const FlowViewport = ({
|
|
|
291
292
|
onContextMenu={(e) => e.preventDefault()}
|
|
292
293
|
sx={{
|
|
293
294
|
width: "100%",
|
|
294
|
-
height:
|
|
295
|
+
height: height,
|
|
295
296
|
overflow: "hidden",
|
|
296
297
|
bgcolor: "none",
|
|
297
298
|
cursor: isDragging ? "grabbing" : "default",
|
package/src/lib/Flow/index.js
CHANGED
package/src/lib/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export { default as useTable } from "./useTable/useTable";
|
|
|
23
23
|
export { default as useChart } from "./useChart/useChart";
|
|
24
24
|
|
|
25
25
|
export { default as Flow } from "./Flow/core/Flow";
|
|
26
|
-
|
|
26
|
+
export { default as FlowBoard } from "./Flow/core/FlowBoard";
|
|
27
27
|
export {
|
|
28
28
|
HeaderCard,
|
|
29
29
|
MediaAvatarCard,
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import Iconify from "../components/Iconify";
|
|
2
|
-
import config from "../config/config";
|
|
3
|
-
import { useEvent } from "@nucleoidai/react-event";
|
|
4
|
-
import useSettings from "../hooks/useSettings";
|
|
5
|
-
import { useSettingsContext } from "../components/settings/context";
|
|
6
|
-
import { useUser } from "../hooks/use-user";
|
|
7
|
-
|
|
8
1
|
import {
|
|
9
2
|
Avatar,
|
|
10
3
|
Box,
|
|
@@ -30,6 +23,13 @@ import {
|
|
|
30
23
|
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
|
|
31
24
|
import React, { useEffect, useState } from "react";
|
|
32
25
|
|
|
26
|
+
import Iconify from "../components/Iconify";
|
|
27
|
+
import config from "../config/config";
|
|
28
|
+
import { useEvent } from "@nucleoidai/react-event";
|
|
29
|
+
import useSettings from "../hooks/useSettings";
|
|
30
|
+
import { useSettingsContext } from "../components/settings/context";
|
|
31
|
+
import { useUser } from "../hooks/use-user";
|
|
32
|
+
|
|
33
33
|
let pkg = {
|
|
34
34
|
name: "",
|
|
35
35
|
version: "",
|
|
@@ -173,7 +173,7 @@ function SettingsDialog({ handleClose, open }) {
|
|
|
173
173
|
color: "white",
|
|
174
174
|
minHeight: 600,
|
|
175
175
|
},
|
|
176
|
-
}
|
|
176
|
+
},
|
|
177
177
|
}}
|
|
178
178
|
>
|
|
179
179
|
<DialogContent>
|
|
@@ -211,16 +211,24 @@ const Permission = () => {
|
|
|
211
211
|
|
|
212
212
|
useEffect(() => {
|
|
213
213
|
getPermittedUsers();
|
|
214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
215
|
}, [event, event2]);
|
|
215
216
|
|
|
216
217
|
return (
|
|
217
|
-
<Stack
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
<Stack
|
|
219
|
+
direction="column"
|
|
220
|
+
spacing={2}
|
|
221
|
+
sx={{
|
|
222
|
+
p: 2,
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
220
225
|
<Typography variant="h6">Users</Typography>
|
|
221
|
-
<Typography
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
<Typography
|
|
227
|
+
variant="subtitle2"
|
|
228
|
+
sx={{
|
|
229
|
+
color: "text.secondary",
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
224
232
|
Users with access to the project.
|
|
225
233
|
</Typography>
|
|
226
234
|
<List>
|
|
@@ -238,7 +246,7 @@ const Permission = () => {
|
|
|
238
246
|
}}
|
|
239
247
|
>
|
|
240
248
|
<ListItemAvatar>
|
|
241
|
-
<Avatar src={user.
|
|
249
|
+
<Avatar src={user.avatarUrl} alt={user.name} />
|
|
242
250
|
</ListItemAvatar>
|
|
243
251
|
<ListItemText primary={user.name} />
|
|
244
252
|
<ListItemSecondaryAction>
|
|
@@ -306,13 +314,20 @@ const Settings = () => {
|
|
|
306
314
|
};
|
|
307
315
|
|
|
308
316
|
return (
|
|
309
|
-
<Stack
|
|
310
|
-
|
|
311
|
-
|
|
317
|
+
<Stack
|
|
318
|
+
direction="column"
|
|
319
|
+
spacing={2}
|
|
320
|
+
sx={{
|
|
321
|
+
p: 2,
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
312
324
|
<Typography variant="h6">Settings</Typography>
|
|
313
|
-
<Typography
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
<Typography
|
|
326
|
+
variant="subtitle2"
|
|
327
|
+
sx={{
|
|
328
|
+
color: "text.secondary",
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
316
331
|
Configure your application settings.
|
|
317
332
|
</Typography>
|
|
318
333
|
<List>
|
|
@@ -373,9 +388,13 @@ const About = () => {
|
|
|
373
388
|
const description = pkg.description;
|
|
374
389
|
|
|
375
390
|
return (
|
|
376
|
-
<Stack
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
<Stack
|
|
392
|
+
direction="column"
|
|
393
|
+
spacing={2}
|
|
394
|
+
sx={{
|
|
395
|
+
p: 2,
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
379
398
|
<Box
|
|
380
399
|
sx={{
|
|
381
400
|
p: 3,
|
|
@@ -386,9 +405,13 @@ const About = () => {
|
|
|
386
405
|
"linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.01))",
|
|
387
406
|
}}
|
|
388
407
|
>
|
|
389
|
-
<Stack
|
|
390
|
-
|
|
391
|
-
|
|
408
|
+
<Stack
|
|
409
|
+
direction="row"
|
|
410
|
+
spacing={2.5}
|
|
411
|
+
sx={{
|
|
412
|
+
alignItems: "center",
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
392
415
|
<Avatar
|
|
393
416
|
src={iconSrc}
|
|
394
417
|
variant="rounded"
|
|
@@ -404,9 +427,12 @@ const About = () => {
|
|
|
404
427
|
</Avatar>
|
|
405
428
|
|
|
406
429
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
407
|
-
<Typography
|
|
408
|
-
|
|
409
|
-
|
|
430
|
+
<Typography
|
|
431
|
+
variant="h5"
|
|
432
|
+
sx={{
|
|
433
|
+
fontWeight: 600,
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
410
436
|
{appName.toUpperCase()}
|
|
411
437
|
</Typography>
|
|
412
438
|
|
|
@@ -416,8 +442,9 @@ const About = () => {
|
|
|
416
442
|
sx={{
|
|
417
443
|
color: "text.secondary",
|
|
418
444
|
mt: 0.5,
|
|
419
|
-
maxWidth: 520
|
|
420
|
-
}}
|
|
445
|
+
maxWidth: 520,
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
421
448
|
{description}
|
|
422
449
|
</Typography>
|
|
423
450
|
)}
|
|
@@ -460,9 +487,12 @@ const About = () => {
|
|
|
460
487
|
textAlign: "center",
|
|
461
488
|
}}
|
|
462
489
|
>
|
|
463
|
-
<Typography
|
|
464
|
-
|
|
465
|
-
|
|
490
|
+
<Typography
|
|
491
|
+
variant="caption"
|
|
492
|
+
sx={{
|
|
493
|
+
color: "text.secondary",
|
|
494
|
+
}}
|
|
495
|
+
>
|
|
466
496
|
© 2026 greycollar.ai. All rights reserved.
|
|
467
497
|
</Typography>
|
|
468
498
|
</Box>
|
|
@@ -473,15 +503,20 @@ const About = () => {
|
|
|
473
503
|
|
|
474
504
|
const InfoRow = ({ label, value, link }) => {
|
|
475
505
|
return (
|
|
476
|
-
<Stack
|
|
477
|
-
|
|
478
|
-
|
|
506
|
+
<Stack
|
|
507
|
+
direction="row"
|
|
508
|
+
spacing={1.5}
|
|
509
|
+
sx={{
|
|
510
|
+
alignItems: "center",
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
479
513
|
<Typography
|
|
480
514
|
variant="body2"
|
|
481
515
|
sx={{
|
|
482
516
|
color: "text.secondary",
|
|
483
|
-
minWidth: 110
|
|
484
|
-
}}
|
|
517
|
+
minWidth: 110,
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
485
520
|
{label}
|
|
486
521
|
</Typography>
|
|
487
522
|
{link ? (
|