@canmingir/link 1.2.10 → 1.2.12
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 +3 -2
- package/src/config/schemas.js +6 -0
- package/src/http/index.js +21 -10
- package/src/layouts/auth/modern.jsx +26 -10
- package/src/layouts/common/ProjectBar/index.jsx +17 -4
- package/src/lib/Flow/core/Flow.jsx +79 -0
- package/src/lib/Flow/core/FlowNode.jsx +68 -0
- package/src/lib/Flow/core/FlowViewport.jsx +259 -0
- package/src/lib/Flow/graph/FloatingGraph.jsx +44 -0
- package/src/lib/Flow/hooks/useGraphOperations.js +362 -0
- package/src/lib/Flow/hooks/useNodeStyle.js +56 -0
- package/src/lib/Flow/index.js +1 -1
- package/src/lib/Flow/nodes/DefaultCard.jsx +107 -0
- package/src/lib/Flow/nodes/DraggableNode.jsx +162 -0
- package/src/lib/Flow/nodes/FlowNodeView.jsx +214 -0
- package/src/lib/Flow/selection/SelectionContext.jsx +259 -0
- package/src/lib/Flow/selection/SelectionOverlay.jsx +31 -0
- package/src/lib/Flow/utils/flowUtils.js +268 -0
- package/src/lib/index.js +1 -1
- package/src/pages/LoginPage.jsx +15 -1
- package/src/widgets/Login/CognitoLogin.jsx +45 -0
- package/src/widgets/Login/DemoLogin.jsx +178 -0
- package/src/widgets/Login/cognitoAuth.jsx +44 -0
- package/src/lib/Flow/DraggableNode.jsx +0 -128
- package/src/lib/Flow/Flow.jsx +0 -40
- package/src/lib/Flow/FlowNode.jsx +0 -519
- package/src/lib/Flow/SelectionContext.jsx +0 -123
- package/src/lib/Flow/flowUtils.js +0 -111
- /package/src/lib/Flow/{DynamicConnector.jsx → connectors/DynamicConnector.jsx} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canmingir/link",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"cypress": "cypress open --browser chrome"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.968.0",
|
|
24
25
|
"@chatscope/chat-ui-kit-react": "^1.10.1",
|
|
25
26
|
"@emoji-mart/data": "^1.1.2",
|
|
26
27
|
"@emoji-mart/react": "^1.1.1",
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
"playwright": "^1.55.1",
|
|
102
103
|
"prettier": "^2.8.8",
|
|
103
104
|
"storybook": "^9.1.8",
|
|
104
|
-
"vite": "^7.1
|
|
105
|
+
"vite": "^7.3.1",
|
|
105
106
|
"vite-plugin-checker": "^0.9.0",
|
|
106
107
|
"vite-plugin-svgr": "^4.2.0",
|
|
107
108
|
"vitest": "^3.2.4"
|
package/src/config/schemas.js
CHANGED
|
@@ -10,6 +10,12 @@ export const ConfigSchema = Joi.object({
|
|
|
10
10
|
host: Joi.string().uri().required(),
|
|
11
11
|
path: Joi.string().required(),
|
|
12
12
|
}).optional(),
|
|
13
|
+
credentials: Joi.object({
|
|
14
|
+
provider: Joi.string().valid("DEMO", "COGNITO").required(),
|
|
15
|
+
region: Joi.string().optional(),
|
|
16
|
+
userPoolId: Joi.string().optional(),
|
|
17
|
+
clientId: Joi.string().optional(),
|
|
18
|
+
}).optional(),
|
|
13
19
|
project: Joi.object({
|
|
14
20
|
nucleoid: Joi.object().optional(),
|
|
15
21
|
github: Joi.object({
|
package/src/http/index.js
CHANGED
|
@@ -86,27 +86,38 @@ export const fetcher = (url) => instance.get(url).then((res) => res.data);
|
|
|
86
86
|
const refreshAuthLogic = async (failedRequest) => {
|
|
87
87
|
try {
|
|
88
88
|
const { appId } = config();
|
|
89
|
-
|
|
90
89
|
const projectId = storage.get("projectId");
|
|
91
|
-
|
|
92
90
|
const identityProvider = storage.get("link", "identityProvider");
|
|
93
91
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
const isDemo = identityProvider?.toUpperCase() === "DEMO";
|
|
93
|
+
|
|
94
|
+
const { data } = isDemo
|
|
95
|
+
? await oauth.post("/oauth/demo", {
|
|
96
|
+
appId,
|
|
97
|
+
projectId,
|
|
98
|
+
username: "admin",
|
|
99
|
+
password: "admin",
|
|
100
|
+
})
|
|
101
|
+
: await oauth.post("/oauth", {
|
|
102
|
+
refreshToken: storage.get("link", "refreshToken"),
|
|
103
|
+
appId,
|
|
104
|
+
projectId,
|
|
105
|
+
identityProvider,
|
|
106
|
+
});
|
|
100
107
|
|
|
101
|
-
const accessToken = data
|
|
108
|
+
const { accessToken, refreshToken } = data;
|
|
102
109
|
|
|
103
110
|
failedRequest.response.config.headers["Authorization"] =
|
|
104
111
|
"Bearer " + accessToken;
|
|
105
112
|
|
|
106
113
|
storage.set("link", "accessToken", accessToken);
|
|
114
|
+
if (refreshToken) {
|
|
115
|
+
storage.set("link", "refreshToken", refreshToken);
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
return Promise.resolve();
|
|
108
119
|
} catch (error) {
|
|
109
|
-
const {
|
|
120
|
+
const { base } = config();
|
|
110
121
|
|
|
111
122
|
storage.remove("link", "accessToken");
|
|
112
123
|
storage.remove("link", "refreshToken");
|
|
@@ -4,6 +4,7 @@ import Logo from "../../components/logo";
|
|
|
4
4
|
import { Outlet } from "react-router";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import Stack from "@mui/material/Stack";
|
|
7
|
+
import { alpha } from "@mui/material/styles";
|
|
7
8
|
import { useResponsive } from "../../hooks/use-responsive";
|
|
8
9
|
|
|
9
10
|
// ----------------------------------------------------------------------
|
|
@@ -16,28 +17,37 @@ export default function AuthModernLayout({ image }) {
|
|
|
16
17
|
sx={{
|
|
17
18
|
width: 1,
|
|
18
19
|
mx: "auto",
|
|
19
|
-
maxWidth:
|
|
20
|
-
px: { xs:
|
|
20
|
+
maxWidth: 600,
|
|
21
|
+
px: { xs: 3, md: 10 },
|
|
22
|
+
py: { xs: 4, md: 0 },
|
|
21
23
|
height: "100vh",
|
|
22
24
|
justifyContent: "center",
|
|
23
25
|
alignItems: "center",
|
|
24
26
|
}}
|
|
25
27
|
>
|
|
26
28
|
<Logo
|
|
27
|
-
maxSize={
|
|
29
|
+
maxSize={140}
|
|
28
30
|
sx={{
|
|
29
|
-
|
|
30
|
-
mb: { xs: 10, md: 8 },
|
|
31
|
+
mb: { xs: 1, md: 2 },
|
|
31
32
|
}}
|
|
32
33
|
/>
|
|
33
34
|
|
|
34
35
|
<Card
|
|
35
36
|
sx={{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
width: 1,
|
|
38
|
+
py: { xs: 6, md: 8 },
|
|
39
|
+
px: { xs: 4, md: 6 },
|
|
40
|
+
boxShadow: {
|
|
41
|
+
xs: (theme) =>
|
|
42
|
+
`0 0 2px ${alpha(
|
|
43
|
+
theme.palette.grey[500],
|
|
44
|
+
0.16
|
|
45
|
+
)}, 0 12px 24px -4px ${alpha(theme.palette.grey[500], 0.12)}`,
|
|
46
|
+
md: "none",
|
|
47
|
+
},
|
|
39
48
|
overflow: { md: "unset" },
|
|
40
|
-
bgcolor: { md: "
|
|
49
|
+
bgcolor: { md: "transparent" },
|
|
50
|
+
borderRadius: 2,
|
|
41
51
|
}}
|
|
42
52
|
>
|
|
43
53
|
<Outlet />
|
|
@@ -46,7 +56,12 @@ export default function AuthModernLayout({ image }) {
|
|
|
46
56
|
);
|
|
47
57
|
|
|
48
58
|
const renderSection = (
|
|
49
|
-
<Stack
|
|
59
|
+
<Stack
|
|
60
|
+
flexGrow={1}
|
|
61
|
+
sx={{
|
|
62
|
+
position: "relative",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
50
65
|
<Box
|
|
51
66
|
component="img"
|
|
52
67
|
alt="auth"
|
|
@@ -59,6 +74,7 @@ export default function AuthModernLayout({ image }) {
|
|
|
59
74
|
position: "absolute",
|
|
60
75
|
width: "calc(60% - 32px)",
|
|
61
76
|
height: "calc(60% - 32px)",
|
|
77
|
+
borderRadius: 3,
|
|
62
78
|
}}
|
|
63
79
|
/>
|
|
64
80
|
</Stack>
|
|
@@ -94,8 +94,23 @@ function ProjectBar() {
|
|
|
94
94
|
const refreshToken = storage.get("link", "refreshToken");
|
|
95
95
|
const identityProvider = storage.get("link", "identityProvider");
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const isDemo = identityProvider?.toUpperCase() === "DEMO";
|
|
98
|
+
|
|
99
|
+
const request = isDemo
|
|
100
|
+
? oauth.post("/oauth/demo", {
|
|
101
|
+
appId,
|
|
102
|
+
projectId,
|
|
103
|
+
username: "admin",
|
|
104
|
+
password: "admin",
|
|
105
|
+
})
|
|
106
|
+
: oauth.post("/oauth", {
|
|
107
|
+
appId,
|
|
108
|
+
refreshToken,
|
|
109
|
+
projectId,
|
|
110
|
+
identityProvider,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
request
|
|
99
114
|
.then(({ data }) => {
|
|
100
115
|
const { refreshToken, accessToken } = data;
|
|
101
116
|
storage.set("link", "accessToken", accessToken);
|
|
@@ -104,9 +119,7 @@ function ProjectBar() {
|
|
|
104
119
|
})
|
|
105
120
|
.finally(() => {
|
|
106
121
|
setSelectedProject(project);
|
|
107
|
-
|
|
108
122
|
publish("PROJECT_SELECTED", { projectId });
|
|
109
|
-
|
|
110
123
|
search.onFalse();
|
|
111
124
|
setSearchQuery("");
|
|
112
125
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import FlowNode from "./FlowNode";
|
|
2
|
+
import { useGraphOperations } from "../hooks/useGraphOperations";
|
|
3
|
+
|
|
4
|
+
import React, { useMemo, useState } from "react";
|
|
5
|
+
import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
|
|
6
|
+
|
|
7
|
+
export const Flow = ({
|
|
8
|
+
data,
|
|
9
|
+
variant = "simple",
|
|
10
|
+
style,
|
|
11
|
+
plugin,
|
|
12
|
+
editable = false,
|
|
13
|
+
onChange,
|
|
14
|
+
}) => {
|
|
15
|
+
const [floatingNodes, setFloatingNodes] = useState([]);
|
|
16
|
+
|
|
17
|
+
const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
|
|
18
|
+
|
|
19
|
+
const { handleCut, handlePaste, handleConnect } = useGraphOperations({
|
|
20
|
+
nodesById,
|
|
21
|
+
roots,
|
|
22
|
+
onChange,
|
|
23
|
+
floatingNodes,
|
|
24
|
+
setFloatingNodes,
|
|
25
|
+
editable,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const allNodesById = useMemo(() => {
|
|
29
|
+
if (!floatingNodes.length) return nodesById;
|
|
30
|
+
|
|
31
|
+
const merged = { ...nodesById };
|
|
32
|
+
|
|
33
|
+
for (const structure of floatingNodes) {
|
|
34
|
+
if (structure?.nodes) {
|
|
35
|
+
Object.assign(merged, structure.nodes);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return merged;
|
|
40
|
+
}, [nodesById, floatingNodes]);
|
|
41
|
+
|
|
42
|
+
const treeData = useMemo(() => {
|
|
43
|
+
if (!roots?.length) return null;
|
|
44
|
+
|
|
45
|
+
if (roots.length === 1) {
|
|
46
|
+
return (
|
|
47
|
+
buildTreeFromLinked(roots[0], nodesById) || {
|
|
48
|
+
id: roots[0],
|
|
49
|
+
children: [],
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const children = roots
|
|
55
|
+
.map((r) => buildTreeFromLinked(r, nodesById))
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
return children.length > 0
|
|
59
|
+
? { id: "__root__", label: "Start", children }
|
|
60
|
+
: null;
|
|
61
|
+
}, [nodesById, roots]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<FlowNode
|
|
65
|
+
node={treeData}
|
|
66
|
+
variant={variant}
|
|
67
|
+
style={style}
|
|
68
|
+
plugin={plugin}
|
|
69
|
+
isRoot={true}
|
|
70
|
+
nodesById={allNodesById}
|
|
71
|
+
onPaste={editable ? handlePaste : undefined}
|
|
72
|
+
onCut={editable ? handleCut : undefined}
|
|
73
|
+
onConnect={editable ? handleConnect : undefined}
|
|
74
|
+
floatingNodes={floatingNodes}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default Flow;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import FlowNodeView from "../nodes/FlowNodeView";
|
|
2
|
+
import FlowViewport from "./FlowViewport";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { SelectionProvider } from "../selection/SelectionContext";
|
|
5
|
+
import { getBaseStyleForVariant } from "../styles";
|
|
6
|
+
|
|
7
|
+
const FlowNode = ({
|
|
8
|
+
isRoot = false,
|
|
9
|
+
onAddNode,
|
|
10
|
+
variant,
|
|
11
|
+
nodesById,
|
|
12
|
+
onPaste,
|
|
13
|
+
onCut,
|
|
14
|
+
onConnect,
|
|
15
|
+
floatingNodes,
|
|
16
|
+
style,
|
|
17
|
+
plugin,
|
|
18
|
+
node,
|
|
19
|
+
...props
|
|
20
|
+
}) => {
|
|
21
|
+
if (!isRoot) {
|
|
22
|
+
if (!node) return null;
|
|
23
|
+
return (
|
|
24
|
+
<FlowNodeView
|
|
25
|
+
node={node}
|
|
26
|
+
onAddNode={onAddNode}
|
|
27
|
+
variant={variant}
|
|
28
|
+
style={style}
|
|
29
|
+
plugin={plugin}
|
|
30
|
+
onConnect={onConnect}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseStyle = getBaseStyleForVariant(variant);
|
|
37
|
+
const selectionColor = baseStyle.selectionColor ?? "#64748b";
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<SelectionProvider>
|
|
41
|
+
<FlowViewport
|
|
42
|
+
selectionColor={selectionColor}
|
|
43
|
+
nodesById={nodesById}
|
|
44
|
+
onPaste={onPaste}
|
|
45
|
+
onCut={onCut}
|
|
46
|
+
onConnect={onConnect}
|
|
47
|
+
floatingNodes={floatingNodes}
|
|
48
|
+
variant={variant}
|
|
49
|
+
style={style}
|
|
50
|
+
plugin={plugin}
|
|
51
|
+
>
|
|
52
|
+
{node && (
|
|
53
|
+
<FlowNodeView
|
|
54
|
+
node={node}
|
|
55
|
+
onAddNode={onAddNode}
|
|
56
|
+
variant={variant}
|
|
57
|
+
style={style}
|
|
58
|
+
plugin={plugin}
|
|
59
|
+
onConnect={onConnect}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</FlowViewport>
|
|
64
|
+
</SelectionProvider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default FlowNode;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import FloatingGraph from "../graph/FloatingGraph";
|
|
3
|
+
import SelectionOverlay from "../selection/SelectionOverlay";
|
|
4
|
+
import { useSelection } from "../selection/SelectionContext";
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
7
|
+
|
|
8
|
+
const FlowViewport = ({
|
|
9
|
+
children,
|
|
10
|
+
selectionColor = "#64748b",
|
|
11
|
+
nodesById,
|
|
12
|
+
onPaste,
|
|
13
|
+
onCut,
|
|
14
|
+
onConnect,
|
|
15
|
+
floatingNodes = [],
|
|
16
|
+
variant,
|
|
17
|
+
style,
|
|
18
|
+
plugin,
|
|
19
|
+
}) => {
|
|
20
|
+
const clampZoom = (zoom) => Math.min(2.5, Math.max(0.25, zoom));
|
|
21
|
+
|
|
22
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
23
|
+
const [zoom, setZoom] = useState(1);
|
|
24
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
25
|
+
const [selectionBox, setSelectionBox] = useState(null);
|
|
26
|
+
|
|
27
|
+
const containerRef = useRef(null);
|
|
28
|
+
const selectionBoxRef = useRef(null);
|
|
29
|
+
const mousePositionRef = useRef({ x: 0, y: 0 });
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
clearSelection,
|
|
33
|
+
selectMultiple,
|
|
34
|
+
addToSelection,
|
|
35
|
+
cutSelectedNodes,
|
|
36
|
+
pasteNodes,
|
|
37
|
+
selectedIds,
|
|
38
|
+
} = useSelection();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handleMouseMove = (e) => {
|
|
42
|
+
mousePositionRef.current = { x: e.clientX, y: e.clientY };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const onWheel = (e) => {
|
|
54
|
+
const wantsZoom = e.ctrlKey || e.metaKey;
|
|
55
|
+
if (!wantsZoom) return;
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
const direction = e.deltaY > 0 ? -1 : 1;
|
|
58
|
+
const factor = direction > 0 ? 1.1 : 1 / 1.1;
|
|
59
|
+
setZoom((z) => clampZoom(z * factor));
|
|
60
|
+
};
|
|
61
|
+
window.addEventListener("wheel", onWheel, { passive: false });
|
|
62
|
+
return () => window.removeEventListener("wheel", onWheel);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handleKeyDown = (e) => {
|
|
67
|
+
const isMod = e.ctrlKey || e.metaKey;
|
|
68
|
+
if (!isMod) return;
|
|
69
|
+
|
|
70
|
+
if (e.key === "x" && selectedIds.size > 0 && nodesById) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
cutSelectedNodes(nodesById, onCut);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (e.key === "v" && onPaste) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
78
|
+
const mousePos = mousePositionRef.current;
|
|
79
|
+
|
|
80
|
+
const canvasX = containerRect
|
|
81
|
+
? (mousePos.x -
|
|
82
|
+
containerRect.left -
|
|
83
|
+
containerRect.width / 2 -
|
|
84
|
+
offset.x) /
|
|
85
|
+
zoom
|
|
86
|
+
: 0;
|
|
87
|
+
const canvasY = containerRect
|
|
88
|
+
? (mousePos.y -
|
|
89
|
+
containerRect.top -
|
|
90
|
+
containerRect.height / 2 -
|
|
91
|
+
offset.y) /
|
|
92
|
+
zoom
|
|
93
|
+
: 0;
|
|
94
|
+
|
|
95
|
+
pasteNodes(onPaste, canvasX, canvasY);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
100
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
101
|
+
}, [
|
|
102
|
+
cutSelectedNodes,
|
|
103
|
+
pasteNodes,
|
|
104
|
+
selectedIds,
|
|
105
|
+
nodesById,
|
|
106
|
+
onPaste,
|
|
107
|
+
onCut,
|
|
108
|
+
offset,
|
|
109
|
+
zoom,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const handleViewportMouseDown = (e) => {
|
|
113
|
+
if (e.target?.closest?.(".MuiCard-root") || e.target?.closest?.("button"))
|
|
114
|
+
return;
|
|
115
|
+
|
|
116
|
+
if (e.button !== 0) return;
|
|
117
|
+
|
|
118
|
+
const startX = e.clientX;
|
|
119
|
+
const startY = e.clientY;
|
|
120
|
+
|
|
121
|
+
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
|
122
|
+
setSelectionBox({ startX, startY, currentX: startX, currentY: startY });
|
|
123
|
+
selectionBoxRef.current = {
|
|
124
|
+
startX,
|
|
125
|
+
startY,
|
|
126
|
+
currentX: startX,
|
|
127
|
+
currentY: startY,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const onMove = (ev) => {
|
|
131
|
+
const newBox = {
|
|
132
|
+
startX,
|
|
133
|
+
startY,
|
|
134
|
+
currentX: ev.clientX,
|
|
135
|
+
currentY: ev.clientY,
|
|
136
|
+
};
|
|
137
|
+
setSelectionBox(newBox);
|
|
138
|
+
selectionBoxRef.current = newBox;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const onUp = () => {
|
|
142
|
+
if (containerRef.current && selectionBoxRef.current) {
|
|
143
|
+
const box = selectionBoxRef.current;
|
|
144
|
+
const nodes = containerRef.current.querySelectorAll("[data-node-id]");
|
|
145
|
+
const selectedNodeIds = [];
|
|
146
|
+
|
|
147
|
+
const boxLeft = Math.min(box.startX, box.currentX);
|
|
148
|
+
const boxRight = Math.max(box.startX, box.currentX);
|
|
149
|
+
const boxTop = Math.min(box.startY, box.currentY);
|
|
150
|
+
const boxBottom = Math.max(box.startY, box.currentY);
|
|
151
|
+
|
|
152
|
+
nodes.forEach((node) => {
|
|
153
|
+
const rect = node.getBoundingClientRect();
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
rect.left < boxRight &&
|
|
157
|
+
rect.right > boxLeft &&
|
|
158
|
+
rect.top < boxBottom &&
|
|
159
|
+
rect.bottom > boxTop
|
|
160
|
+
) {
|
|
161
|
+
const nodeId = node.getAttribute("data-node-id");
|
|
162
|
+
if (nodeId) selectedNodeIds.push(nodeId);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (selectedNodeIds.length > 0) {
|
|
167
|
+
if (e.shiftKey) {
|
|
168
|
+
addToSelection(selectedNodeIds);
|
|
169
|
+
} else {
|
|
170
|
+
selectMultiple(selectedNodeIds);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setSelectionBox(null);
|
|
176
|
+
selectionBoxRef.current = null;
|
|
177
|
+
window.removeEventListener("mousemove", onMove);
|
|
178
|
+
window.removeEventListener("mouseup", onUp);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
window.addEventListener("mousemove", onMove);
|
|
182
|
+
window.addEventListener("mouseup", onUp);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
clearSelection();
|
|
187
|
+
|
|
188
|
+
setIsDragging(true);
|
|
189
|
+
const startOffset = { ...offset };
|
|
190
|
+
|
|
191
|
+
const onMove = (ev) => {
|
|
192
|
+
setOffset({
|
|
193
|
+
x: startOffset.x + (ev.clientX - startX),
|
|
194
|
+
y: startOffset.y + (ev.clientY - startY),
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const onUp = () => {
|
|
199
|
+
setIsDragging(false);
|
|
200
|
+
window.removeEventListener("mousemove", onMove);
|
|
201
|
+
window.removeEventListener("mouseup", onUp);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
window.addEventListener("mousemove", onMove);
|
|
205
|
+
window.addEventListener("mouseup", onUp);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Box
|
|
210
|
+
ref={containerRef}
|
|
211
|
+
onMouseDown={handleViewportMouseDown}
|
|
212
|
+
sx={{
|
|
213
|
+
width: "100vw",
|
|
214
|
+
height: "100vh",
|
|
215
|
+
overflow: "hidden",
|
|
216
|
+
bgcolor: "none",
|
|
217
|
+
cursor: isDragging ? "grabbing" : "default",
|
|
218
|
+
userSelect: "none",
|
|
219
|
+
position: "relative",
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<SelectionOverlay box={selectionBox} selectionColor={selectionColor} />
|
|
223
|
+
<Box
|
|
224
|
+
sx={{
|
|
225
|
+
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
|
|
226
|
+
transformOrigin: "center center",
|
|
227
|
+
width: "100%",
|
|
228
|
+
height: "100%",
|
|
229
|
+
display: "flex",
|
|
230
|
+
alignItems: "center",
|
|
231
|
+
justifyContent: "center",
|
|
232
|
+
transition: isDragging ? "none" : "transform 0.1s ease-out",
|
|
233
|
+
pointerEvents: "auto",
|
|
234
|
+
position: "relative",
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
{children}
|
|
238
|
+
{floatingNodes.map((structure, index) => {
|
|
239
|
+
const structureKey =
|
|
240
|
+
(structure && (structure.id || structure.key)) ??
|
|
241
|
+
`floating-${index}`;
|
|
242
|
+
return (
|
|
243
|
+
<FloatingGraph
|
|
244
|
+
key={structureKey}
|
|
245
|
+
structure={structure}
|
|
246
|
+
variant={variant}
|
|
247
|
+
style={style}
|
|
248
|
+
plugin={plugin}
|
|
249
|
+
selectionColor={selectionColor}
|
|
250
|
+
onConnect={onConnect}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</Box>
|
|
255
|
+
</Box>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export default FlowViewport;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import FlowNodeView from "../nodes/FlowNodeView";
|
|
3
|
+
import { buildDetachedTree } from "../utils/flowUtils";
|
|
4
|
+
|
|
5
|
+
import React, { useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
const FloatingGraph = ({ structure, variant, style, plugin, onConnect }) => {
|
|
8
|
+
const position = structure?._pastePosition || { x: 0, y: 0 };
|
|
9
|
+
|
|
10
|
+
const treesData = useMemo(() => {
|
|
11
|
+
if (!structure?.roots?.length || !structure?.nodes) return [];
|
|
12
|
+
return structure.roots
|
|
13
|
+
.map((rootId) => buildDetachedTree(rootId, structure.nodes))
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}, [structure]);
|
|
16
|
+
|
|
17
|
+
if (!treesData.length) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box
|
|
21
|
+
sx={{
|
|
22
|
+
position: "absolute",
|
|
23
|
+
left: "50%",
|
|
24
|
+
top: "50%",
|
|
25
|
+
transform: `translate(${position.x}px, ${position.y}px)`,
|
|
26
|
+
display: "flex",
|
|
27
|
+
gap: 2,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{treesData.map((tree, idx) => (
|
|
31
|
+
<FlowNodeView
|
|
32
|
+
key={tree.id || `floating-${idx}`}
|
|
33
|
+
node={tree}
|
|
34
|
+
variant={variant}
|
|
35
|
+
style={style}
|
|
36
|
+
plugin={plugin}
|
|
37
|
+
onConnect={onConnect}
|
|
38
|
+
/>
|
|
39
|
+
))}
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default FloatingGraph;
|