@canmingir/link 1.2.51 → 1.2.53

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.51",
3
+ "version": "1.2.53",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-server": "./server/server.ts"
package/src/http/index.js CHANGED
@@ -40,9 +40,8 @@ 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
43
 
45
- if (statusCode === 500 && identityProvider === "DEMO") {
44
+ if (statusCode === 500) {
46
45
  const token = storage.get("link", "accessToken");
47
46
  try {
48
47
  const decodedToken = jwtDecode(token);
@@ -155,22 +154,18 @@ refreshInterceptor(instance, refreshAuthLogic, {
155
154
  return false;
156
155
  }
157
156
 
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
- }
157
+ if (identityProvider === "DEMO") {
158
+ if (statusCode !== 500) {
159
+ return true;
160
+ }
171
161
 
172
- if (statusCode === 500) {
173
- return false;
162
+ try {
163
+ const decodedToken = jwtDecode(token);
164
+ return decodedToken.exp * 1000 < Date.now();
165
+ } catch (_) {
166
+ // If we can't decode, attempt refresh once.
167
+ return true;
168
+ }
174
169
  }
175
170
 
176
171
  try {
@@ -46,11 +46,7 @@ export default function Header({ onOpenNav }) {
46
46
  </IconButton>
47
47
  )}
48
48
  {projectBar && <ProjectBar />}
49
- {TopBar && (
50
- <Box sx={{ flexGrow: 1, mx: 2 }}>
51
- <TopBar />
52
- </Box>
53
- )}
49
+ {TopBar && <TopBar />}
54
50
 
55
51
  <Stack
56
52
  direction="row"
@@ -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: 0,
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(1);
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: "100vh",
295
+ height: height,
295
296
  overflow: "hidden",
296
297
  bgcolor: "none",
297
298
  cursor: isDragging ? "grabbing" : "default",
@@ -1 +1,2 @@
1
1
  export { default as Flow } from "./core/Flow";
2
+ export { default as FlowBoard } from "./core/FlowBoard";
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,