@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.10",
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.8",
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"
@@ -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 { data } = await oauth.post("/oauth", {
95
- refreshToken: storage.get("link", "refreshToken"),
96
- appId,
97
- projectId,
98
- identityProvider,
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.accessToken;
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 { name, base } = config();
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: 480,
20
- px: { xs: 2, md: 8 },
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={200}
29
+ maxSize={140}
28
30
  sx={{
29
- mt: { xs: 2, md: 8 },
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
- py: { xs: 5, md: 0 },
37
- px: { xs: 3, md: 0 },
38
- boxShadow: { md: "none" },
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: "background.default" },
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 flexGrow={1} sx={{ position: "relative" }}>
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
- oauth
98
- .post("/oauth", { appId, refreshToken, projectId, identityProvider })
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;