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