@arronqzy/react-blueprint 1.0.0

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.
Files changed (124) hide show
  1. package/.eslintrc.cjs +5 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/assets/index-CneNgzWb.css +1 -0
  58. package/dist/assets/index-DsVhHV0-.js +226 -0
  59. package/dist/index.html +14 -0
  60. package/index.html +13 -0
  61. package/index.ts +70 -0
  62. package/package.json +43 -0
  63. package/postcss.config.cjs +6 -0
  64. package/src/BlueprintCanvasContext.tsx +61 -0
  65. package/src/BlueprintContextMenu.tsx +108 -0
  66. package/src/BlueprintNodeConfigSidebar.tsx +358 -0
  67. package/src/blueprint.css +321 -0
  68. package/src/blueprintNodeTypes.ts +20 -0
  69. package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
  70. package/src/components/BlueprintLibrarySelect.tsx +68 -0
  71. package/src/components/BlueprintMetaDialog.tsx +96 -0
  72. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
  73. package/src/components/BlueprintPanelToolbar.tsx +664 -0
  74. package/src/components/ClockNodeConfigPanel.tsx +139 -0
  75. package/src/components/FetchNodeConfigPanel.tsx +716 -0
  76. package/src/components/FetchUrlAutocomplete.tsx +190 -0
  77. package/src/components/JsonNodeConfigPanel.tsx +79 -0
  78. package/src/components/LogicNodeConfigPanel.tsx +80 -0
  79. package/src/components/ViewElementMultiSelect.tsx +140 -0
  80. package/src/createBlueprintEdgeTypes.tsx +10 -0
  81. package/src/dev-seed.ts +34 -0
  82. package/src/dev.css +38 -0
  83. package/src/edges/BlueprintSmoothEdge.tsx +35 -0
  84. package/src/fetch-config-task-store.ts +197 -0
  85. package/src/flowCoordinates.ts +19 -0
  86. package/src/flowDefaults.ts +9 -0
  87. package/src/graph/blueprint-graph.ts +261 -0
  88. package/src/graph/document.ts +384 -0
  89. package/src/graph/index.ts +7 -0
  90. package/src/graph/node-summary.ts +88 -0
  91. package/src/graph/node-types.ts +9 -0
  92. package/src/graph/react-flow-adapter.ts +127 -0
  93. package/src/graph/sync-edges.ts +69 -0
  94. package/src/graph/sync-nodes.ts +104 -0
  95. package/src/hooks/useBlueprintDebugSession.ts +515 -0
  96. package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
  97. package/src/hooks/useBlueprintFlowState.ts +486 -0
  98. package/src/hooks/useBlueprintFlowViewport.ts +54 -0
  99. package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
  100. package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
  101. package/src/index.tsx +271 -0
  102. package/src/library/blueprint-io.ts +108 -0
  103. package/src/library/blueprint-library-db.ts +112 -0
  104. package/src/library/execution-log-db.ts +171 -0
  105. package/src/library/execution-log-settings.ts +50 -0
  106. package/src/library/swagger-docs.ts +56 -0
  107. package/src/library/types.ts +35 -0
  108. package/src/main.tsx +43 -0
  109. package/src/nodes/AndFlowNode.tsx +59 -0
  110. package/src/nodes/BlueprintFlowNode.tsx +26 -0
  111. package/src/nodes/BlueprintNodeCard.tsx +169 -0
  112. package/src/nodes/BlueprintNodeShell.tsx +45 -0
  113. package/src/nodes/ClockFlowNode.tsx +67 -0
  114. package/src/nodes/FetchFlowNode.tsx +26 -0
  115. package/src/nodes/JsonFlowNode.tsx +26 -0
  116. package/src/nodes/LifecycleFlowNode.tsx +44 -0
  117. package/src/nodes/LogicFlowNode.tsx +26 -0
  118. package/src/runtime/document-to-runnable-graph.ts +48 -0
  119. package/src/runtime/execution-overlay.ts +169 -0
  120. package/src/types.ts +1 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/tailwind.config.ts +6 -0
  123. package/tsconfig.json +12 -0
  124. package/vite.config.ts +16 -0
@@ -0,0 +1,486 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import {
3
+ addEdge,
4
+ useEdgesState,
5
+ useNodesState,
6
+ useReactFlow,
7
+ type Connection,
8
+ type Edge,
9
+ type EdgeChange,
10
+ type Node,
11
+ type NodeChange,
12
+ } from "@xyflow/react";
13
+
14
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
15
+ import { sanitizeBlueprintDocument } from "../graph/document";
16
+ import {
17
+ applyFlowNodePositions,
18
+ BP_EDGE_STYLE,
19
+ BP_FLOW_EDGE_TYPE,
20
+ edgeListSignature,
21
+ flowEdgesToGraphEdges,
22
+ graphToFlowEdges,
23
+ graphToFlowNodes,
24
+ mergeMeasuredFlowNodes,
25
+ nodeStructureSignature,
26
+ normalizeFlowEdges,
27
+ resolveConnection,
28
+ type BlueprintFlowNodeData,
29
+ } from "../graph";
30
+ import type { BlueprintExecutionOverlay, BlueprintEdgeSignalKind } from "../runtime/execution-overlay";
31
+
32
+ type UseBlueprintFlowStateOptions = {
33
+ graph: BlueprintGraph;
34
+ selectedNodeId: string | null;
35
+ executionOverlay?: BlueprintExecutionOverlay | null;
36
+ libraryNameById?: ReadonlyMap<string, string>;
37
+ onGraphChange: (updater: (prev: BlueprintGraph) => BlueprintGraph) => void;
38
+ onSelectNode?: (nodeId: string | null) => void;
39
+ };
40
+
41
+ const SIGNAL_EDGE_STROKE = {
42
+ true: "#16a34a",
43
+ false: "#dc2626",
44
+ "clock-green": "#16a34a",
45
+ "clock-blue": "#2563eb",
46
+ } as const;
47
+
48
+ const SIGNAL_EDGE_RESET_MS = 320;
49
+
50
+ type ActiveEdgePulse = {
51
+ edgeColors: Record<string, BlueprintEdgeSignalKind>;
52
+ timerId: ReturnType<typeof setTimeout>;
53
+ };
54
+
55
+ function withoutEdgeSignals(
56
+ overlay: BlueprintExecutionOverlay,
57
+ edgeIds: Set<string>
58
+ ): BlueprintExecutionOverlay {
59
+ return {
60
+ ...overlay,
61
+ edgeSignals: Object.fromEntries(
62
+ Object.entries(overlay.edgeSignals).filter(([id]) => !edgeIds.has(id))
63
+ ),
64
+ };
65
+ }
66
+
67
+ function signalEdgeClass(
68
+ kind: BlueprintExecutionOverlay["edgeSignals"][string] | undefined
69
+ ) {
70
+ if (kind === "true" || kind === "clock-green") return "bp-edge--signal-true";
71
+ if (kind === "clock-blue") return "bp-edge--signal-clock-blue";
72
+ if (kind === "false") return "bp-edge--signal-false";
73
+ return undefined;
74
+ }
75
+
76
+ function signalEdgeStyle(
77
+ kind: BlueprintExecutionOverlay["edgeSignals"][string] | undefined
78
+ ) {
79
+ if (kind === "true") {
80
+ return { ...BP_EDGE_STYLE, stroke: SIGNAL_EDGE_STROKE.true };
81
+ }
82
+ if (kind === "clock-green") {
83
+ return { ...BP_EDGE_STYLE, stroke: SIGNAL_EDGE_STROKE["clock-green"] };
84
+ }
85
+ if (kind === "clock-blue") {
86
+ return { ...BP_EDGE_STYLE, stroke: SIGNAL_EDGE_STROKE["clock-blue"] };
87
+ }
88
+ if (kind === "false") {
89
+ return { ...BP_EDGE_STYLE, stroke: SIGNAL_EDGE_STROKE.false };
90
+ }
91
+ return { ...BP_EDGE_STYLE };
92
+ }
93
+
94
+ function applyExecutionOverlayToEdges(
95
+ edges: Edge[],
96
+ executionOverlay: BlueprintExecutionOverlay | null
97
+ ): Edge[] {
98
+ return edges.map((edge) => {
99
+ const signal = executionOverlay?.edgeSignals[edge.id];
100
+ return {
101
+ ...edge,
102
+ className: signalEdgeClass(signal),
103
+ style: signalEdgeStyle(signal),
104
+ };
105
+ });
106
+ }
107
+
108
+ export function useBlueprintFlowState({
109
+ graph,
110
+ selectedNodeId,
111
+ executionOverlay = null,
112
+ libraryNameById,
113
+ onGraphChange,
114
+ onSelectNode,
115
+ }: UseBlueprintFlowStateOptions) {
116
+ const { getNodes } = useReactFlow();
117
+ const nodeSigRef = useRef(nodeStructureSignature(graph.document.nodes));
118
+ const edgeSigRef = useRef(edgeListSignature(graph.document.edges));
119
+ const selectedIdRef = useRef(selectedNodeId);
120
+ const executionActiveIdRef = useRef(executionOverlay?.activeNodeId ?? null);
121
+ const executionSignalKindRef = useRef(
122
+ executionOverlay?.activeNodeSignalKind ?? null
123
+ );
124
+ const prevEdgeSignalsRef = useRef<
125
+ BlueprintExecutionOverlay["edgeSignals"]
126
+ >({});
127
+ const prevClockEdgeTicksRef = useRef<Record<string, number>>({});
128
+ const activePulseRef = useRef<ActiveEdgePulse | null>(null);
129
+ const latestOverlayRef = useRef<BlueprintExecutionOverlay | null>(null);
130
+
131
+ const [nodes, setNodes, onNodesChangeRf] = useNodesState(
132
+ graphToFlowNodes(graph, selectedNodeId, libraryNameById)
133
+ );
134
+ const [edges, setEdges, onEdgesChangeRf] = useEdgesState(
135
+ graphToFlowEdges(graph)
136
+ );
137
+
138
+ useEffect(() => {
139
+ onGraphChange((prev) => {
140
+ const sanitized = sanitizeBlueprintDocument(prev.document);
141
+ const edgesChanged = sanitized.edges.length !== prev.document.edges.length;
142
+ const nodesChanged = sanitized.nodes.some((node, index) => {
143
+ const before = prev.document.nodes[index];
144
+ return before && node.configSource !== before.configSource;
145
+ });
146
+ if (!edgesChanged && !nodesChanged) return prev;
147
+ return prev.withDocument(sanitized);
148
+ });
149
+ }, [onGraphChange]);
150
+
151
+ const nodeStructureSig = useMemo(
152
+ () => nodeStructureSignature(graph.document.nodes),
153
+ [graph.document.nodes]
154
+ );
155
+ const edgeIdSig = useMemo(
156
+ () => edgeListSignature(graph.document.edges),
157
+ [graph.document.edges]
158
+ );
159
+
160
+ useEffect(() => {
161
+ if (nodeStructureSig === nodeSigRef.current) return;
162
+ nodeSigRef.current = nodeStructureSig;
163
+ setNodes((prev) => {
164
+ const activeId = executionActiveIdRef.current;
165
+ const merged = mergeMeasuredFlowNodes(
166
+ graphToFlowNodes(graph, selectedIdRef.current, libraryNameById),
167
+ prev as Node<BlueprintFlowNodeData>[]
168
+ );
169
+ return merged.map((n) => ({
170
+ ...n,
171
+ data: {
172
+ ...(n.data as BlueprintFlowNodeData),
173
+ isExecutionActive: n.id === activeId,
174
+ executionSignalKind:
175
+ n.id === activeId ? executionSignalKindRef.current : null,
176
+ },
177
+ }));
178
+ });
179
+ }, [nodeStructureSig, graph, libraryNameById, setNodes]);
180
+
181
+ useEffect(() => {
182
+ if (edgeIdSig === edgeSigRef.current) return;
183
+ edgeSigRef.current = edgeIdSig;
184
+ setEdges(applyExecutionOverlayToEdges(graphToFlowEdges(graph), executionOverlay));
185
+ }, [edgeIdSig, executionOverlay, graph, setEdges]);
186
+
187
+ useEffect(() => {
188
+ const prevId = selectedIdRef.current;
189
+ if (prevId === selectedNodeId) return;
190
+ selectedIdRef.current = selectedNodeId;
191
+
192
+ setNodes((nds) => {
193
+ let changed = false;
194
+ const next = nds.map((n) => {
195
+ const isSelected = n.id === selectedNodeId;
196
+ const wasSelected = Boolean(
197
+ (n.data as BlueprintFlowNodeData).isSelected
198
+ );
199
+ if (wasSelected === isSelected) return n;
200
+ changed = true;
201
+ return {
202
+ ...n,
203
+ data: {
204
+ ...(n.data as BlueprintFlowNodeData),
205
+ isSelected,
206
+ },
207
+ };
208
+ });
209
+ return changed ? next : nds;
210
+ });
211
+ }, [selectedNodeId, setNodes]);
212
+
213
+ useEffect(() => {
214
+ const activeId = executionOverlay?.activeNodeId ?? null;
215
+ const activeSignalKind = executionOverlay?.activeNodeSignalKind ?? null;
216
+ const clockProgressByNodeId = executionOverlay?.clockNodeProgress ?? {};
217
+ executionActiveIdRef.current = activeId;
218
+ executionSignalKindRef.current = activeSignalKind;
219
+
220
+ setNodes((nds) => {
221
+ let changed = false;
222
+ const next = nds.map((n) => {
223
+ const isExecutionActive = n.id === activeId;
224
+ const executionSignalKind = isExecutionActive ? activeSignalKind : null;
225
+ const clockEmitProgress = clockProgressByNodeId[n.id] ?? null;
226
+ const data = n.data as BlueprintFlowNodeData;
227
+ if (
228
+ Boolean(data.isExecutionActive) === isExecutionActive &&
229
+ data.executionSignalKind === executionSignalKind &&
230
+ data.clockEmitProgress?.current === clockEmitProgress?.current &&
231
+ data.clockEmitProgress?.total === clockEmitProgress?.total
232
+ ) {
233
+ return n;
234
+ }
235
+ changed = true;
236
+ return {
237
+ ...n,
238
+ data: {
239
+ ...data,
240
+ isExecutionActive,
241
+ executionSignalKind,
242
+ clockEmitProgress,
243
+ },
244
+ };
245
+ });
246
+ return changed ? next : nds;
247
+ });
248
+ }, [
249
+ executionOverlay?.activeNodeId,
250
+ executionOverlay?.activeNodeSignalKind,
251
+ executionOverlay?.clockNodeProgress,
252
+ setNodes,
253
+ ]);
254
+
255
+ useEffect(() => {
256
+ return () => {
257
+ if (activePulseRef.current) {
258
+ clearTimeout(activePulseRef.current.timerId);
259
+ }
260
+ };
261
+ }, []);
262
+
263
+ useEffect(() => {
264
+ latestOverlayRef.current = executionOverlay ?? null;
265
+ const nextSignals = executionOverlay?.edgeSignals ?? {};
266
+ const prevSignals = prevEdgeSignalsRef.current;
267
+ const nextClockTicks = executionOverlay?.clockEdgeTicks ?? {};
268
+ const prevClockTicks = prevClockEdgeTicksRef.current;
269
+
270
+ const pulseEdgeIds = Object.keys(nextSignals).filter((edgeId) => {
271
+ const prevKind = prevSignals[edgeId];
272
+ if (prevKind === undefined) return false;
273
+
274
+ const prevTick = prevClockTicks[edgeId];
275
+ const nextTick = nextClockTicks[edgeId];
276
+ if (
277
+ nextTick !== undefined &&
278
+ prevTick !== undefined &&
279
+ nextTick > prevTick
280
+ ) {
281
+ return true;
282
+ }
283
+
284
+ return prevKind !== nextSignals[edgeId];
285
+ });
286
+
287
+ prevEdgeSignalsRef.current = { ...nextSignals };
288
+ prevClockEdgeTicksRef.current = { ...nextClockTicks };
289
+
290
+ const applyOverlay = (
291
+ overlay: BlueprintExecutionOverlay | null,
292
+ hiddenEdgeIds?: Set<string>
293
+ ) => {
294
+ const effective =
295
+ overlay && hiddenEdgeIds && hiddenEdgeIds.size > 0
296
+ ? withoutEdgeSignals(overlay, hiddenEdgeIds)
297
+ : overlay;
298
+ setEdges((prev) => applyExecutionOverlayToEdges(prev, effective));
299
+ };
300
+
301
+ if (!executionOverlay || pulseEdgeIds.length === 0) {
302
+ if (activePulseRef.current) {
303
+ applyOverlay(
304
+ executionOverlay,
305
+ new Set(Object.keys(activePulseRef.current.edgeColors))
306
+ );
307
+ return;
308
+ }
309
+ applyOverlay(executionOverlay);
310
+ return;
311
+ }
312
+
313
+ const pulseColors = Object.fromEntries(
314
+ pulseEdgeIds.map((id) => [id, nextSignals[id]!])
315
+ ) as Record<string, BlueprintEdgeSignalKind>;
316
+
317
+ if (activePulseRef.current) {
318
+ clearTimeout(activePulseRef.current.timerId);
319
+ }
320
+
321
+ applyOverlay(executionOverlay, new Set(pulseEdgeIds));
322
+
323
+ const timerId = setTimeout(() => {
324
+ const pending = activePulseRef.current;
325
+ activePulseRef.current = null;
326
+ if (!pending) return;
327
+
328
+ setEdges((prev) => {
329
+ const base = latestOverlayRef.current;
330
+ const merged = base
331
+ ? {
332
+ ...base,
333
+ edgeSignals: {
334
+ ...base.edgeSignals,
335
+ ...pending.edgeColors,
336
+ },
337
+ }
338
+ : {
339
+ activeNodeId: null,
340
+ activeNodeSignalKind: null,
341
+ edgeSignals: pending.edgeColors,
342
+ };
343
+ return applyExecutionOverlayToEdges(prev, merged);
344
+ });
345
+ }, SIGNAL_EDGE_RESET_MS);
346
+
347
+ activePulseRef.current = { edgeColors: pulseColors, timerId };
348
+ }, [executionOverlay, setEdges]);
349
+
350
+ useEffect(() => {
351
+ if (!libraryNameById) return;
352
+ setNodes((nds) => {
353
+ let changed = false;
354
+ const next = nds.map((n) => {
355
+ const data = n.data as BlueprintFlowNodeData;
356
+ const libraryBlueprintLabel = data.libraryBlueprintId
357
+ ? libraryNameById.get(data.libraryBlueprintId)
358
+ : undefined;
359
+ if (data.libraryBlueprintLabel === libraryBlueprintLabel) return n;
360
+ changed = true;
361
+ return {
362
+ ...n,
363
+ data: { ...data, libraryBlueprintLabel },
364
+ };
365
+ });
366
+ return changed ? next : nds;
367
+ });
368
+ }, [libraryNameById, setNodes]);
369
+
370
+ const onNodesChange = useCallback(
371
+ (changes: NodeChange<Node<BlueprintFlowNodeData>>[]) => {
372
+ onNodesChangeRf(changes);
373
+
374
+ const removals = changes.filter(
375
+ (c): c is NodeChange & { type: "remove"; id: string } =>
376
+ c.type === "remove"
377
+ );
378
+ if (removals.length === 0) return;
379
+
380
+ onGraphChange((prev) => {
381
+ let doc = prev;
382
+ for (const { id } of removals) {
383
+ if (id === selectedIdRef.current) {
384
+ onSelectNode?.(null);
385
+ }
386
+ doc = doc.removeNode(id);
387
+ }
388
+ return doc;
389
+ });
390
+ },
391
+ [onNodesChangeRf, onGraphChange, onSelectNode]
392
+ );
393
+
394
+ const onNodeDragStop = useCallback(() => {
395
+ onGraphChange((prev) => applyFlowNodePositions(prev, getNodes()));
396
+ }, [getNodes, onGraphChange]);
397
+
398
+ const onEdgesChange = useCallback(
399
+ (changes: EdgeChange[]) => {
400
+ if (changes.length === 0) return;
401
+ onEdgesChangeRf(changes);
402
+
403
+ const removals = changes.filter(
404
+ (c): c is EdgeChange & { type: "remove"; id: string } => c.type === "remove"
405
+ );
406
+ if (removals.length === 0) return;
407
+
408
+ onGraphChange((prev) => {
409
+ let next = prev;
410
+ for (const { id } of removals) {
411
+ next = next.removeEdge(id);
412
+ }
413
+ return next;
414
+ });
415
+ },
416
+ [onEdgesChangeRf, onGraphChange]
417
+ );
418
+
419
+ const onConnect = useCallback(
420
+ (connection: Connection) => {
421
+ const resolved = resolveConnection(
422
+ connection,
423
+ getNodes() as Node<BlueprintFlowNodeData>[]
424
+ );
425
+ if (!resolved?.source || !resolved.target) return;
426
+
427
+ onGraphChange((prev) => {
428
+ const current = graphToFlowEdges(prev);
429
+ const exists = current.some(
430
+ (e) =>
431
+ e.source === resolved.source &&
432
+ e.target === resolved.target &&
433
+ (e.sourceHandle ?? "out") === (resolved.sourceHandle ?? "out") &&
434
+ (e.targetHandle ?? "in") === (resolved.targetHandle ?? "in")
435
+ );
436
+ if (exists) return prev;
437
+
438
+ const next = normalizeFlowEdges(
439
+ addEdge(
440
+ {
441
+ ...resolved,
442
+ type: BP_FLOW_EDGE_TYPE,
443
+ zIndex: 1000,
444
+ style: { ...BP_EDGE_STYLE },
445
+ },
446
+ current
447
+ )
448
+ );
449
+ return prev.replaceEdges(flowEdgesToGraphEdges(next));
450
+ });
451
+ },
452
+ [getNodes, onGraphChange]
453
+ );
454
+
455
+ const isValidConnection = useCallback(
456
+ (connection: Edge | Connection) => {
457
+ if (!connection.source || !connection.target) return false;
458
+ if (connection.source === connection.target) return false;
459
+
460
+ const rfNodes = getNodes() as Node<BlueprintFlowNodeData>[];
461
+ const nodeIds = new Set(rfNodes.map((n) => n.id));
462
+ if (!nodeIds.has(connection.source) || !nodeIds.has(connection.target)) {
463
+ return false;
464
+ }
465
+
466
+ const targetNode = rfNodes.find((n) => n.id === connection.target);
467
+ // 生命周期节点无输入口,禁止连入
468
+ if (targetNode?.data.role === "lifecycle") {
469
+ return false;
470
+ }
471
+
472
+ return true;
473
+ },
474
+ [getNodes]
475
+ );
476
+
477
+ return {
478
+ nodes,
479
+ edges,
480
+ onNodesChange,
481
+ onEdgesChange,
482
+ onNodeDragStop,
483
+ onConnect,
484
+ isValidConnection,
485
+ };
486
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef, type RefObject } from "react";
2
+ import { useReactFlow } from "@xyflow/react";
3
+
4
+ /** 仅在蓝图面板尺寸变化时 fitView;不批量 updateNodeInternals,避免测量风暴卡死 */
5
+ export function useBlueprintFlowViewport(
6
+ containerRef: RefObject<HTMLElement | null>,
7
+ enabled: boolean
8
+ ) {
9
+ const { fitView, getNodes } = useReactFlow();
10
+ const lastSizeRef = useRef({ w: 0, h: 0 });
11
+ const fitRafRef = useRef<number | null>(null);
12
+ const hasFittedRef = useRef(false);
13
+
14
+ useEffect(() => {
15
+ if (!enabled) {
16
+ hasFittedRef.current = false;
17
+ return;
18
+ }
19
+
20
+ const el = containerRef.current;
21
+ if (!el) return;
22
+
23
+ const scheduleFit = () => {
24
+ if (getNodes().length === 0) return;
25
+ if (fitRafRef.current != null) cancelAnimationFrame(fitRafRef.current);
26
+ fitRafRef.current = requestAnimationFrame(() => {
27
+ fitRafRef.current = null;
28
+ fitView({ padding: 0.2, duration: 0, includeHiddenNodes: true });
29
+ hasFittedRef.current = true;
30
+ });
31
+ };
32
+
33
+ const syncOnResize = () => {
34
+ const { clientWidth: w, clientHeight: h } = el;
35
+ if (w < 8 || h < 8) return;
36
+
37
+ const sizeChanged =
38
+ Math.abs(w - lastSizeRef.current.w) > 2 ||
39
+ Math.abs(h - lastSizeRef.current.h) > 2;
40
+ if (!sizeChanged && hasFittedRef.current) return;
41
+
42
+ lastSizeRef.current = { w, h };
43
+ scheduleFit();
44
+ };
45
+
46
+ syncOnResize();
47
+ const ro = new ResizeObserver(syncOnResize);
48
+ ro.observe(el);
49
+ return () => {
50
+ ro.disconnect();
51
+ if (fitRafRef.current != null) cancelAnimationFrame(fitRafRef.current);
52
+ };
53
+ }, [containerRef, fitView, getNodes, enabled]);
54
+ }
@@ -0,0 +1,63 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+
3
+ import {
4
+ cancelFetchDebugTask,
5
+ cancelSwaggerLoadTask,
6
+ findActiveFetchConfigTaskNodeId,
7
+ } from "../fetch-config-task-store";
8
+
9
+ export type PendingBlueprintNodeSwitch = {
10
+ fromNodeId: string;
11
+ toNodeId: string | null;
12
+ };
13
+
14
+ export function useBlueprintNodeSelectionGuard(
15
+ selectedNodeId: string | null,
16
+ onSelectNode: (nodeId: string | null) => void
17
+ ) {
18
+ const [pendingSwitch, setPendingSwitch] =
19
+ useState<PendingBlueprintNodeSwitch | null>(null);
20
+ const selectedNodeIdRef = useRef(selectedNodeId);
21
+ const onSelectNodeRef = useRef(onSelectNode);
22
+ selectedNodeIdRef.current = selectedNodeId;
23
+ onSelectNodeRef.current = onSelectNode;
24
+
25
+ const requestSelectNode = useCallback((nodeId: string | null) => {
26
+ const activeTaskNodeId = findActiveFetchConfigTaskNodeId();
27
+ if (
28
+ activeTaskNodeId &&
29
+ selectedNodeIdRef.current === activeTaskNodeId &&
30
+ nodeId !== activeTaskNodeId
31
+ ) {
32
+ setPendingSwitch({ fromNodeId: activeTaskNodeId, toNodeId: nodeId });
33
+ return;
34
+ }
35
+ onSelectNodeRef.current(nodeId);
36
+ }, []);
37
+
38
+ const keepTaskAndSwitch = useCallback(() => {
39
+ if (!pendingSwitch) return;
40
+ onSelectNodeRef.current(pendingSwitch.toNodeId);
41
+ setPendingSwitch(null);
42
+ }, [pendingSwitch]);
43
+
44
+ const cancelTaskAndSwitch = useCallback(() => {
45
+ if (!pendingSwitch) return;
46
+ cancelSwaggerLoadTask(pendingSwitch.fromNodeId);
47
+ cancelFetchDebugTask(pendingSwitch.fromNodeId);
48
+ onSelectNodeRef.current(pendingSwitch.toNodeId);
49
+ setPendingSwitch(null);
50
+ }, [pendingSwitch]);
51
+
52
+ const stayOnCurrentNode = useCallback(() => {
53
+ setPendingSwitch(null);
54
+ }, []);
55
+
56
+ return {
57
+ requestSelectNode,
58
+ pendingSwitch,
59
+ keepTaskAndSwitch,
60
+ cancelTaskAndSwitch,
61
+ stayOnCurrentNode,
62
+ };
63
+ }