@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.
Files changed (57) hide show
  1. package/README.md +50 -0
  2. package/package.json +44 -0
  3. package/src/BlueprintCanvasContext.ts +71 -0
  4. package/src/BlueprintNodeConfigSidebar.vue +338 -0
  5. package/src/blueprint.css +327 -0
  6. package/src/blueprintNodeTypes.ts +20 -0
  7. package/src/components/BluePrintVueRoot.vue +73 -0
  8. package/src/components/BlueprintCanvas.vue +220 -0
  9. package/src/components/BlueprintContextMenu.vue +114 -0
  10. package/src/components/BlueprintExecutionLogPanel.vue +294 -0
  11. package/src/components/BlueprintMetaDialog.vue +80 -0
  12. package/src/components/BlueprintNodeSwitchTaskDialog.vue +41 -0
  13. package/src/components/ClockNodeConfigPanel.vue +124 -0
  14. package/src/components/FetchNodeConfigPanel.vue +559 -0
  15. package/src/components/FetchUrlAutocomplete.vue +174 -0
  16. package/src/components/JsonNodeConfigPanel.vue +73 -0
  17. package/src/components/LogicNodeConfigPanel.vue +73 -0
  18. package/src/components/ViewElementMultiSelect.vue +50 -0
  19. package/src/composables/useBlueprintDebugSession.ts +441 -0
  20. package/src/composables/useBlueprintFlowState.ts +486 -0
  21. package/src/composables/useBlueprintFlowViewport.ts +65 -0
  22. package/src/composables/useBlueprintNodeSelectionGuard.ts +41 -0
  23. package/src/composables/useBlueprintPageLifecycle.ts +244 -0
  24. package/src/createBlueprintEdgeTypes.ts +10 -0
  25. package/src/edges/BlueprintSmoothEdge.vue +31 -0
  26. package/src/env.d.ts +7 -0
  27. package/src/fetch-config-task-store.ts +206 -0
  28. package/src/flowCoordinates.ts +19 -0
  29. package/src/flowDefaults.ts +9 -0
  30. package/src/graph/blueprint-graph.ts +265 -0
  31. package/src/graph/document.ts +422 -0
  32. package/src/graph/index.ts +7 -0
  33. package/src/graph/node-summary.ts +88 -0
  34. package/src/graph/node-types.ts +9 -0
  35. package/src/graph/sync-edges.ts +69 -0
  36. package/src/graph/sync-nodes.ts +110 -0
  37. package/src/graph/vue-flow-adapter.ts +127 -0
  38. package/src/index.ts +37 -0
  39. package/src/library/blueprint-io.ts +108 -0
  40. package/src/library/blueprint-library-db.ts +112 -0
  41. package/src/library/execution-log-db.ts +171 -0
  42. package/src/library/execution-log-settings.ts +50 -0
  43. package/src/library/swagger-docs.ts +56 -0
  44. package/src/library/types.ts +35 -0
  45. package/src/nodes/AndFlowNode.vue +60 -0
  46. package/src/nodes/BlueprintFlowNode.vue +26 -0
  47. package/src/nodes/BlueprintNodeCard.vue +155 -0
  48. package/src/nodes/BlueprintNodeShell.vue +70 -0
  49. package/src/nodes/ClockFlowNode.vue +60 -0
  50. package/src/nodes/FetchFlowNode.vue +26 -0
  51. package/src/nodes/JsonFlowNode.vue +26 -0
  52. package/src/nodes/LifecycleFlowNode.vue +45 -0
  53. package/src/nodes/LogicFlowNode.vue +26 -0
  54. package/src/runtime/document-to-runnable-graph.ts +51 -0
  55. package/src/runtime/execution-overlay.ts +169 -0
  56. package/src/types.ts +1 -0
  57. package/src/utils/cn.ts +3 -0
@@ -0,0 +1,327 @@
1
+ /* 画布:与 panel / next-themes 主题一致 */
2
+ .bp-canvas,
3
+ [data-workspace-region="blueprint"] .bp-canvas {
4
+ background-color: hsl(var(--background));
5
+ color: hsl(var(--foreground));
6
+ }
7
+
8
+ .bp-canvas .vue-flow__pane,
9
+ .bp-flow.vue-flow__pane {
10
+ cursor: default;
11
+ }
12
+
13
+ .bp-canvas .vue-flow,
14
+ .bp-flow.vue-flow,
15
+ [data-workspace-region="blueprint"] .vue-flow {
16
+ --xy-background-color: transparent;
17
+ --xy-edge-stroke: #2563eb;
18
+ --xy-edge-stroke-width: 2.5;
19
+ --xy-edge-stroke-selected: #1d4ed8;
20
+ --xy-connectionline-stroke: #2563eb;
21
+ --xy-connectionline-stroke-width: 2.5;
22
+ --xy-handle-background-color: hsl(var(--primary));
23
+ --xy-handle-border-color: hsl(var(--background));
24
+ background-color: transparent;
25
+ }
26
+
27
+ /*
28
+ * 全局 reset(apps/web style.css、Tailwind preflight)会给 svg 设 max-width:100%,
29
+ * 导致 Vue Flow 边线 SVG 坐标系被压扁;连线预览单独有 z-index:1001 所以能看见。
30
+ */
31
+ .bp-canvas svg.vue-flow__connectionline,
32
+ .bp-canvas .vue-flow__edges svg,
33
+ .bp-flow svg.vue-flow__connectionline,
34
+ .bp-flow .vue-flow__edges svg,
35
+ [data-workspace-region="blueprint"] svg.vue-flow__connectionline,
36
+ [data-workspace-region="blueprint"] .vue-flow__edges svg {
37
+ display: block;
38
+ max-width: none !important;
39
+ max-height: none !important;
40
+ width: auto !important;
41
+ height: auto !important;
42
+ overflow: visible !important;
43
+ }
44
+
45
+ /*
46
+ * 边层必须在节点之上:.vue-flow__edges 与 .vue-flow__nodes 为兄弟节点,
47
+ * 默认 DOM 顺序下节点后渲染会盖住边;连接线因有 z-index:1001 不受影响。
48
+ */
49
+ .bp-canvas .vue-flow__edges,
50
+ .bp-flow .vue-flow__edges,
51
+ [data-workspace-region="blueprint"] .vue-flow__edges {
52
+ z-index: 1000 !important;
53
+ pointer-events: none;
54
+ }
55
+
56
+ .bp-canvas .vue-flow__nodes,
57
+ .bp-flow .vue-flow__nodes,
58
+ [data-workspace-region="blueprint"] .vue-flow__nodes {
59
+ z-index: 0 !important;
60
+ }
61
+
62
+ .bp-canvas .vue-flow__edge,
63
+ .bp-flow .vue-flow__edge,
64
+ [data-workspace-region="blueprint"] .vue-flow__edge {
65
+ pointer-events: visibleStroke;
66
+ }
67
+
68
+ /* 持久边 path:强制描边(覆盖 currentColor / 主题色) */
69
+ .bp-canvas .vue-flow__edges path.vue-flow__edge-path,
70
+ .bp-canvas .vue-flow__edges path.bp-edge-visible,
71
+ .bp-flow .vue-flow__edges path.vue-flow__edge-path,
72
+ .bp-flow .vue-flow__edges path.bp-edge-visible,
73
+ [data-workspace-region="blueprint"] .vue-flow__edges path.vue-flow__edge-path,
74
+ [data-workspace-region="blueprint"] .vue-flow__edges path.bp-edge-visible {
75
+ fill: none !important;
76
+ stroke: #2563eb !important;
77
+ stroke-width: 2.5px !important;
78
+ opacity: 1 !important;
79
+ }
80
+
81
+ .bp-canvas .vue-flow__edge.selected path.vue-flow__edge-path,
82
+ .bp-flow .vue-flow__edge.selected path.vue-flow__edge-path,
83
+ [data-workspace-region="blueprint"] .vue-flow__edge.selected path.vue-flow__edge-path {
84
+ stroke: #1d4ed8 !important;
85
+ stroke-width: 3px !important;
86
+ }
87
+
88
+ .bp-canvas .vue-flow__edge.bp-edge--signal-true path.vue-flow__edge-path,
89
+ .bp-flow .vue-flow__edge.bp-edge--signal-true path.vue-flow__edge-path,
90
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-true path.vue-flow__edge-path {
91
+ stroke: #16a34a !important;
92
+ stroke-width: 3px !important;
93
+ }
94
+
95
+ .bp-canvas .vue-flow__edge.bp-edge--signal-false path.vue-flow__edge-path,
96
+ .bp-flow .vue-flow__edge.bp-edge--signal-false path.vue-flow__edge-path,
97
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-false path.vue-flow__edge-path {
98
+ stroke: #dc2626 !important;
99
+ stroke-width: 3px !important;
100
+ }
101
+
102
+ .bp-canvas .vue-flow__edge.bp-edge--signal-true .vue-flow__arrowhead polyline,
103
+ .bp-flow .vue-flow__edge.bp-edge--signal-true .vue-flow__arrowhead polyline,
104
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-true .vue-flow__arrowhead polyline {
105
+ stroke: #16a34a;
106
+ fill: #16a34a;
107
+ }
108
+
109
+ .bp-canvas .vue-flow__edge.bp-edge--signal-false .vue-flow__arrowhead polyline,
110
+ .bp-flow .vue-flow__edge.bp-edge--signal-false .vue-flow__arrowhead polyline,
111
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-false .vue-flow__arrowhead polyline {
112
+ stroke: #dc2626;
113
+ fill: #dc2626;
114
+ }
115
+
116
+ .bp-canvas .vue-flow__edge.bp-edge--signal-clock-blue path.vue-flow__edge-path,
117
+ .bp-flow .vue-flow__edge.bp-edge--signal-clock-blue path.vue-flow__edge-path,
118
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-clock-blue path.vue-flow__edge-path {
119
+ stroke: #2563eb !important;
120
+ stroke-width: 3px !important;
121
+ }
122
+
123
+ .bp-canvas .vue-flow__edge.bp-edge--signal-clock-blue .vue-flow__arrowhead polyline,
124
+ .bp-flow .vue-flow__edge.bp-edge--signal-clock-blue .vue-flow__arrowhead polyline,
125
+ [data-workspace-region="blueprint"] .vue-flow__edge.bp-edge--signal-clock-blue .vue-flow__arrowhead polyline {
126
+ stroke: #2563eb;
127
+ fill: #2563eb;
128
+ }
129
+
130
+ .bp-canvas .vue-flow__arrowhead polyline,
131
+ .bp-flow .vue-flow__arrowhead polyline,
132
+ [data-workspace-region="blueprint"] .vue-flow__arrowhead polyline {
133
+ stroke: #2563eb;
134
+ fill: #2563eb;
135
+ }
136
+
137
+ .bp-canvas .vue-flow__connection-path,
138
+ .bp-flow .vue-flow__connection-path,
139
+ [data-workspace-region="blueprint"] .vue-flow__connection-path {
140
+ fill: none !important;
141
+ stroke: #2563eb !important;
142
+ stroke-width: 2.5px !important;
143
+ }
144
+
145
+ .bp-canvas .vue-flow__edge-interaction,
146
+ .bp-flow .vue-flow__edge-interaction,
147
+ [data-workspace-region="blueprint"] .vue-flow__edge-interaction {
148
+ pointer-events: stroke;
149
+ stroke: transparent !important;
150
+ stroke-opacity: 0 !important;
151
+ stroke-width: 18px !important;
152
+ }
153
+
154
+ .bp-canvas .vue-flow__edge.selected .vue-flow__edge-interaction,
155
+ .bp-flow .vue-flow__edge.selected .vue-flow__edge-interaction,
156
+ [data-workspace-region="blueprint"] .vue-flow__edge.selected .vue-flow__edge-interaction {
157
+ pointer-events: stroke;
158
+ }
159
+
160
+ /* 节点容器:去掉默认包裹样式 */
161
+ .bp-canvas .vue-flow__node,
162
+ .bp-flow .vue-flow__node,
163
+ [data-workspace-region="blueprint"] .vue-flow__node {
164
+ border: none;
165
+ background: transparent;
166
+ box-shadow: none;
167
+ padding: 0;
168
+ }
169
+
170
+ /* 连接点:禁止 transform,避免覆盖 Vue Flow 的 translate(-50%, -50%) 定位 */
171
+ .bp-canvas .vue-flow__handle.bp-flow-handle,
172
+ .bp-flow .vue-flow__handle.bp-flow-handle,
173
+ [data-workspace-region="blueprint"] .vue-flow__handle.bp-flow-handle {
174
+ z-index: 3;
175
+ width: 10px;
176
+ height: 10px;
177
+ min-width: 10px;
178
+ min-height: 10px;
179
+ border: 2px solid hsl(var(--background)) !important;
180
+ border-radius: 9999px;
181
+ background-color: hsl(var(--foreground) / 0.42) !important;
182
+ transition:
183
+ background-color 0.15s ease,
184
+ border-color 0.15s ease,
185
+ box-shadow 0.15s ease;
186
+ }
187
+
188
+ .bp-canvas .vue-flow__handle.bp-flow-handle:hover,
189
+ .bp-flow .vue-flow__handle.bp-flow-handle:hover,
190
+ [data-workspace-region="blueprint"] .vue-flow__handle.bp-flow-handle:hover {
191
+ background-color: hsl(var(--primary)) !important;
192
+ border-color: hsl(var(--background)) !important;
193
+ box-shadow: 0 0 0 3px hsl(var(--primary) / 0.28);
194
+ }
195
+
196
+ .bp-canvas .vue-flow__handle.bp-flow-handle--target,
197
+ .bp-flow .vue-flow__handle.bp-flow-handle--target,
198
+ [data-workspace-region="blueprint"] .vue-flow__handle.bp-flow-handle--target {
199
+ background-color: hsl(var(--foreground) / 0.48) !important;
200
+ }
201
+
202
+ .bp-canvas .vue-flow__handle.bp-flow-handle--source,
203
+ .bp-flow .vue-flow__handle.bp-flow-handle--source,
204
+ [data-workspace-region="blueprint"] .vue-flow__handle.bp-flow-handle--source {
205
+ background-color: hsl(var(--primary) / 0.72) !important;
206
+ }
207
+
208
+ .bp-node--selected .bp-flow-handle {
209
+ border-color: hsl(var(--card)) !important;
210
+ box-shadow: 0 0 0 2px hsl(var(--primary) / 0.35);
211
+ }
212
+
213
+ .bp-node--selected .bp-flow-handle--target {
214
+ background-color: hsl(var(--foreground) / 0.55) !important;
215
+ }
216
+
217
+ .bp-node--selected .bp-flow-handle--source {
218
+ background-color: hsl(var(--primary)) !important;
219
+ }
220
+
221
+ .bp-node--execution-true .bp-node-card {
222
+ border-color: rgb(22 163 74 / 0.65);
223
+ box-shadow:
224
+ 0 0 0 1px rgb(22 163 74 / 0.35),
225
+ 0 4px 14px rgb(22 163 74 / 0.18);
226
+ }
227
+
228
+ .bp-node--execution-true .bp-flow-handle {
229
+ border-color: hsl(var(--card));
230
+ box-shadow: 0 0 0 2px rgb(22 163 74 / 0.45);
231
+ }
232
+
233
+ .bp-node--execution-true .bp-flow-handle--source {
234
+ background-color: #16a34a !important;
235
+ }
236
+
237
+ .bp-node--execution-true .bp-flow-handle--target {
238
+ background-color: rgb(22 163 74 / 0.55) !important;
239
+ }
240
+
241
+ .bp-node--execution-false .bp-node-card {
242
+ border-color: hsl(var(--destructive) / 0.65);
243
+ box-shadow:
244
+ 0 0 0 1px hsl(var(--destructive) / 0.4),
245
+ 0 4px 14px hsl(var(--destructive) / 0.16);
246
+ }
247
+
248
+ .bp-node--execution-false .bp-flow-handle {
249
+ border-color: hsl(var(--card));
250
+ box-shadow: 0 0 0 2px hsl(var(--destructive) / 0.4);
251
+ }
252
+
253
+ .bp-node--execution-false .bp-flow-handle--source {
254
+ background-color: hsl(var(--destructive)) !important;
255
+ }
256
+
257
+ .bp-node--execution-false .bp-flow-handle--target {
258
+ background-color: hsl(var(--destructive) / 0.55) !important;
259
+ }
260
+
261
+ /* 生命周期节点无输入口:隐藏任何左侧/目标连接点(含旧数据产生的幽灵 handle) */
262
+ .bp-node--lifecycle .vue-flow__handle.bp-flow-handle--target,
263
+ .bp-node--lifecycle .vue-flow__handle-left,
264
+ .bp-node--lifecycle .vue-flow__handle[data-handlepos="left"] {
265
+ display: none !important;
266
+ visibility: hidden !important;
267
+ pointer-events: none !important;
268
+ width: 0 !important;
269
+ height: 0 !important;
270
+ opacity: 0 !important;
271
+ }
272
+
273
+ .bp-canvas .vue-flow__handle.bp-flow-handle--signal,
274
+ .bp-flow .vue-flow__handle.bp-flow-handle--signal,
275
+ [data-workspace-region="blueprint"] .vue-flow__handle.bp-flow-handle--signal {
276
+ background-color: hsl(38 92% 50% / 0.75) !important;
277
+ width: 8px;
278
+ height: 8px;
279
+ }
280
+
281
+ .bp-node--selected .bp-flow-handle--signal {
282
+ background-color: hsl(38 92% 50%) !important;
283
+ }
284
+
285
+ /* 右键菜单 */
286
+ .bp-context-menu {
287
+ position: fixed;
288
+ z-index: 10050;
289
+ min-width: 148px;
290
+ padding: 4px;
291
+ border-radius: 8px;
292
+ border: 1px solid hsl(var(--border));
293
+ background: hsl(var(--card));
294
+ color: hsl(var(--card-foreground));
295
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
296
+ }
297
+
298
+ .bp-context-menu__item {
299
+ display: block;
300
+ width: 100%;
301
+ padding: 6px 10px;
302
+ border: none;
303
+ border-radius: 6px;
304
+ background: transparent;
305
+ text-align: left;
306
+ font-size: 12px;
307
+ color: inherit;
308
+ cursor: pointer;
309
+ }
310
+
311
+ .bp-context-menu__item:hover {
312
+ background: hsl(var(--accent));
313
+ }
314
+
315
+ .bp-context-menu__item--danger {
316
+ color: hsl(var(--destructive));
317
+ }
318
+
319
+ .bp-context-menu__item--danger:hover {
320
+ background: hsl(var(--destructive) / 0.1);
321
+ }
322
+
323
+ .bp-context-menu__separator {
324
+ height: 1px;
325
+ margin: 4px 0;
326
+ background: hsl(var(--border));
327
+ }
@@ -0,0 +1,20 @@
1
+ import { markRaw, type Component } from "vue";
2
+
3
+ import AndFlowNode from "./nodes/AndFlowNode.vue";
4
+ import BlueprintFlowNode from "./nodes/BlueprintFlowNode.vue";
5
+ import ClockFlowNode from "./nodes/ClockFlowNode.vue";
6
+ import FetchFlowNode from "./nodes/FetchFlowNode.vue";
7
+ import JsonFlowNode from "./nodes/JsonFlowNode.vue";
8
+ import LifecycleFlowNode from "./nodes/LifecycleFlowNode.vue";
9
+ import LogicFlowNode from "./nodes/LogicFlowNode.vue";
10
+
11
+ /** 稳定引用,避免 Vue Flow 因 nodeTypes 变化反复卸载节点 */
12
+ export const blueprintNodeTypes: Record<string, Component> = {
13
+ and: markRaw(AndFlowNode),
14
+ blueprint: markRaw(BlueprintFlowNode),
15
+ clock: markRaw(ClockFlowNode),
16
+ fetch: markRaw(FetchFlowNode),
17
+ json: markRaw(JsonFlowNode),
18
+ logic: markRaw(LogicFlowNode),
19
+ lifecycle: markRaw(LifecycleFlowNode),
20
+ };
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
3
+ import type { BlueprintExecutionOverlay } from "../runtime/execution-overlay";
4
+ import BlueprintCanvas from "./BlueprintCanvas.vue";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ style?: Record<string, string | number>;
9
+ graph: BlueprintGraph;
10
+ selectedNodeId?: string | null;
11
+ executionOverlay?: BlueprintExecutionOverlay | null;
12
+ libraryNameById?: ReadonlyMap<string, string>;
13
+ onSelectNode?: (nodeId: string | null) => void;
14
+ onAbortClock?: (nodeId: string) => void;
15
+ }>(),
16
+ {
17
+ selectedNodeId: null,
18
+ executionOverlay: null,
19
+ }
20
+ );
21
+
22
+ const emit = defineEmits<{
23
+ graphChange: [graph: BlueprintGraph];
24
+ selectNode: [nodeId: string | null];
25
+ abortClock: [nodeId: string];
26
+ }>();
27
+
28
+ function onGraphChange(next: BlueprintGraph) {
29
+ emit("graphChange", next);
30
+ }
31
+
32
+ function onSelectNode(nodeId: string | null) {
33
+ props.onSelectNode?.(nodeId);
34
+ emit("selectNode", nodeId);
35
+ }
36
+
37
+ function onAbortClock(nodeId: string) {
38
+ props.onAbortClock?.(nodeId);
39
+ emit("abortClock", nodeId);
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <div
45
+ data-workspace-region="blueprint"
46
+ class="blueprint-vue-root bp-canvas h-full w-full bg-background text-foreground"
47
+ :style="{
48
+ width: '100%',
49
+ height: '100%',
50
+ minHeight: 0,
51
+ ...style,
52
+ }"
53
+ >
54
+ <BlueprintCanvas
55
+ :graph="graph"
56
+ :selected-node-id="selectedNodeId"
57
+ :execution-overlay="executionOverlay"
58
+ :library-name-by-id="libraryNameById"
59
+ :on-select-node="onSelectNode"
60
+ :on-abort-clock="onAbortClock"
61
+ @graph-change="onGraphChange"
62
+ @select-node="onSelectNode"
63
+ />
64
+ </div>
65
+ </template>
66
+
67
+ <style scoped>
68
+ .blueprint-vue-root {
69
+ display: flex;
70
+ flex-direction: column;
71
+ min-height: 240px;
72
+ }
73
+ </style>
@@ -0,0 +1,220 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ConnectionMode,
4
+ VueFlow,
5
+ useVueFlow,
6
+ type Edge,
7
+ type Node,
8
+ type NodeMouseEvent,
9
+ type EdgeMouseEvent,
10
+ } from "@vue-flow/core";
11
+ import { computed, ref, toRef, watch } from "vue";
12
+ import "@vue-flow/core/dist/style.css";
13
+ import "@vue-flow/core/dist/theme-default.css";
14
+ import "../blueprint.css";
15
+
16
+ import {
17
+ provideBlueprintCanvasContext,
18
+ syncBlueprintCanvasContext,
19
+ } from "../BlueprintCanvasContext";
20
+ import { blueprintNodeTypes } from "../blueprintNodeTypes";
21
+ import { createBlueprintEdgeTypes } from "../createBlueprintEdgeTypes";
22
+ import BlueprintContextMenu, {
23
+ type BlueprintContextMenuState,
24
+ } from "./BlueprintContextMenu.vue";
25
+ import { BLUEPRINT_DEFAULT_EDGE_OPTIONS } from "../flowDefaults";
26
+ import { clientToFlowNodePosition } from "../flowCoordinates";
27
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
28
+ import type { BlueprintFlowNodeData } from "../graph/vue-flow-adapter";
29
+ import { useBlueprintFlowState } from "../composables/useBlueprintFlowState";
30
+ import { useBlueprintFlowViewport } from "../composables/useBlueprintFlowViewport";
31
+ import type { BlueprintExecutionOverlay } from "../runtime/execution-overlay";
32
+
33
+ const props = withDefaults(
34
+ defineProps<{
35
+ graph: BlueprintGraph;
36
+ selectedNodeId?: string | null;
37
+ executionOverlay?: BlueprintExecutionOverlay | null;
38
+ libraryNameById?: ReadonlyMap<string, string>;
39
+ onSelectNode?: (nodeId: string | null) => void;
40
+ onAbortClock?: (nodeId: string) => void;
41
+ }>(),
42
+ {
43
+ selectedNodeId: null,
44
+ executionOverlay: null,
45
+ }
46
+ );
47
+
48
+ const emit = defineEmits<{
49
+ graphChange: [graph: BlueprintGraph];
50
+ selectNode: [nodeId: string | null];
51
+ }>();
52
+
53
+ const containerRef = ref<HTMLElement | null>(null);
54
+ const menu = ref<BlueprintContextMenuState | null>(null);
55
+ const blueprintEdgeTypes = createBlueprintEdgeTypes();
56
+
57
+ const canvasContext = provideBlueprintCanvasContext({
58
+ onSelectNode: (nodeId) => {
59
+ props.onSelectNode?.(nodeId);
60
+ emit("selectNode", nodeId);
61
+ },
62
+ onAbortClock: props.onAbortClock,
63
+ });
64
+
65
+ watch(
66
+ () => [props.onSelectNode, props.onAbortClock] as const,
67
+ () => {
68
+ syncBlueprintCanvasContext(canvasContext, {
69
+ onSelectNode: (nodeId) => {
70
+ props.onSelectNode?.(nodeId);
71
+ emit("selectNode", nodeId);
72
+ },
73
+ onAbortClock: props.onAbortClock,
74
+ });
75
+ }
76
+ );
77
+
78
+ const graphRef = toRef(props, "graph");
79
+ const selectedNodeIdRef = toRef(props, "selectedNodeId");
80
+ const executionOverlayRef = toRef(props, "executionOverlay");
81
+ const libraryNameByIdRef = toRef(props, "libraryNameById");
82
+
83
+ const { screenToFlowCoordinate } = useVueFlow();
84
+
85
+ function applyGraphChange(updater: (prev: BlueprintGraph) => BlueprintGraph) {
86
+ emit("graphChange", updater(props.graph));
87
+ }
88
+
89
+ const flowState = useBlueprintFlowState({
90
+ graph: graphRef,
91
+ selectedNodeId: computed(() => selectedNodeIdRef.value ?? null),
92
+ executionOverlay: executionOverlayRef,
93
+ libraryNameById: libraryNameByIdRef,
94
+ onGraphChange: applyGraphChange,
95
+ onSelectNode: (nodeId) => {
96
+ props.onSelectNode?.(nodeId);
97
+ emit("selectNode", nodeId);
98
+ },
99
+ });
100
+
101
+ const {
102
+ nodes,
103
+ edges,
104
+ onNodesChange,
105
+ onEdgesChange,
106
+ onNodeDragStop,
107
+ onConnect,
108
+ isValidConnection,
109
+ } = flowState;
110
+
111
+ useBlueprintFlowViewport(containerRef, true);
112
+
113
+ function onPaneContextMenu(event: MouseEvent) {
114
+ event.preventDefault();
115
+ menu.value = {
116
+ kind: "pane",
117
+ clientX: event.clientX,
118
+ clientY: event.clientY,
119
+ };
120
+ }
121
+
122
+ function onNodeContextMenu({ event, node }: NodeMouseEvent) {
123
+ event.preventDefault();
124
+ const mouse = event as MouseEvent;
125
+ props.onSelectNode?.(node.id);
126
+ emit("selectNode", node.id);
127
+ const data = node.data as BlueprintFlowNodeData;
128
+ menu.value = {
129
+ kind: "node",
130
+ clientX: mouse.clientX,
131
+ clientY: mouse.clientY,
132
+ nodeId: node.id,
133
+ role: data.role,
134
+ };
135
+ }
136
+
137
+ function onEdgeContextMenu({ event, edge }: EdgeMouseEvent) {
138
+ event.preventDefault();
139
+ const mouse = event as MouseEvent;
140
+ menu.value = {
141
+ kind: "edge",
142
+ clientX: mouse.clientX,
143
+ clientY: mouse.clientY,
144
+ edgeId: edge.id,
145
+ };
146
+ }
147
+
148
+ function onEdgeClick() {
149
+ menu.value = null;
150
+ }
151
+
152
+ function onPaneClick() {
153
+ menu.value = null;
154
+ props.onSelectNode?.(null);
155
+ emit("selectNode", null);
156
+ }
157
+
158
+ function handleAddBlueprintNode(clientX: number, clientY: number) {
159
+ const position = clientToFlowNodePosition(
160
+ (point) => screenToFlowCoordinate(point),
161
+ clientX,
162
+ clientY
163
+ );
164
+ applyGraphChange((prev) => prev.addBlueprintNode(position));
165
+ }
166
+
167
+ function handleDeleteNode(nodeId: string) {
168
+ if (nodeId === props.selectedNodeId) {
169
+ props.onSelectNode?.(null);
170
+ emit("selectNode", null);
171
+ }
172
+ applyGraphChange((prev) => prev.removeNode(nodeId));
173
+ }
174
+
175
+ function handleDeleteEdge(edgeId: string) {
176
+ applyGraphChange((prev) => prev.removeEdge(edgeId));
177
+ }
178
+ </script>
179
+
180
+ <template>
181
+ <div
182
+ ref="containerRef"
183
+ data-workspace-region="blueprint"
184
+ class="bp-canvas vue-blueprint-canvas h-full w-full"
185
+ >
186
+ <VueFlow
187
+ v-model:nodes="nodes"
188
+ v-model:edges="edges"
189
+ class="bp-flow h-full w-full"
190
+ :node-types="blueprintNodeTypes"
191
+ :edge-types="blueprintEdgeTypes"
192
+ :default-edge-options="BLUEPRINT_DEFAULT_EDGE_OPTIONS"
193
+ :connection-mode="ConnectionMode.Strict"
194
+ :nodes-connectable="true"
195
+ :elements-selectable="true"
196
+ :edges-focusable="true"
197
+ :select-nodes-on-drag="false"
198
+ :elevate-nodes-on-select="false"
199
+ :node-click-distance="8"
200
+ fit-view-on-init
201
+ @nodes-change="onNodesChange"
202
+ @edges-change="onEdgesChange"
203
+ @connect="onConnect"
204
+ :is-valid-connection="isValidConnection"
205
+ @node-drag-stop="onNodeDragStop"
206
+ @pane-context-menu="onPaneContextMenu"
207
+ @node-context-menu="onNodeContextMenu"
208
+ @edge-context-menu="onEdgeContextMenu"
209
+ @edge-click="onEdgeClick"
210
+ @pane-click="onPaneClick"
211
+ />
212
+ <BlueprintContextMenu
213
+ :menu="menu"
214
+ @close="menu = null"
215
+ @add-blueprint-node="handleAddBlueprintNode"
216
+ @delete-node="handleDeleteNode"
217
+ @delete-edge="handleDeleteEdge"
218
+ />
219
+ </div>
220
+ </template>