@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.
- package/.eslintrc.cjs +5 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/assets/index-CneNgzWb.css +1 -0
- package/dist/assets/index-DsVhHV0-.js +226 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/index.ts +70 -0
- package/package.json +43 -0
- package/postcss.config.cjs +6 -0
- package/src/BlueprintCanvasContext.tsx +61 -0
- package/src/BlueprintContextMenu.tsx +108 -0
- package/src/BlueprintNodeConfigSidebar.tsx +358 -0
- package/src/blueprint.css +321 -0
- package/src/blueprintNodeTypes.ts +20 -0
- package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
- package/src/components/BlueprintLibrarySelect.tsx +68 -0
- package/src/components/BlueprintMetaDialog.tsx +96 -0
- package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
- package/src/components/BlueprintPanelToolbar.tsx +664 -0
- package/src/components/ClockNodeConfigPanel.tsx +139 -0
- package/src/components/FetchNodeConfigPanel.tsx +716 -0
- package/src/components/FetchUrlAutocomplete.tsx +190 -0
- package/src/components/JsonNodeConfigPanel.tsx +79 -0
- package/src/components/LogicNodeConfigPanel.tsx +80 -0
- package/src/components/ViewElementMultiSelect.tsx +140 -0
- package/src/createBlueprintEdgeTypes.tsx +10 -0
- package/src/dev-seed.ts +34 -0
- package/src/dev.css +38 -0
- package/src/edges/BlueprintSmoothEdge.tsx +35 -0
- package/src/fetch-config-task-store.ts +197 -0
- package/src/flowCoordinates.ts +19 -0
- package/src/flowDefaults.ts +9 -0
- package/src/graph/blueprint-graph.ts +261 -0
- package/src/graph/document.ts +384 -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/react-flow-adapter.ts +127 -0
- package/src/graph/sync-edges.ts +69 -0
- package/src/graph/sync-nodes.ts +104 -0
- package/src/hooks/useBlueprintDebugSession.ts +515 -0
- package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
- package/src/hooks/useBlueprintFlowState.ts +486 -0
- package/src/hooks/useBlueprintFlowViewport.ts +54 -0
- package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
- package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
- package/src/index.tsx +271 -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/main.tsx +43 -0
- package/src/nodes/AndFlowNode.tsx +59 -0
- package/src/nodes/BlueprintFlowNode.tsx +26 -0
- package/src/nodes/BlueprintNodeCard.tsx +169 -0
- package/src/nodes/BlueprintNodeShell.tsx +45 -0
- package/src/nodes/ClockFlowNode.tsx +67 -0
- package/src/nodes/FetchFlowNode.tsx +26 -0
- package/src/nodes/JsonFlowNode.tsx +26 -0
- package/src/nodes/LifecycleFlowNode.tsx +44 -0
- package/src/nodes/LogicFlowNode.tsx +26 -0
- package/src/runtime/document-to-runnable-graph.ts +48 -0
- package/src/runtime/execution-overlay.ts +169 -0
- package/src/types.ts +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +6 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BlueprintGraphRunner,
|
|
4
|
+
detectBlueprintReferenceCycle,
|
|
5
|
+
type LibraryBlueprintResolver,
|
|
6
|
+
type PageLifecyclePhase,
|
|
7
|
+
} from "@arronqzy/blueprint-dsl";
|
|
8
|
+
|
|
9
|
+
import type { BlueprintGraph } from "../graph/blueprint-graph";
|
|
10
|
+
import { documentToRunnableGraph } from "../runtime/document-to-runnable-graph";
|
|
11
|
+
|
|
12
|
+
const PAGE_BOOT_PHASES: PageLifecyclePhase[] = [
|
|
13
|
+
"created",
|
|
14
|
+
"beforeMount",
|
|
15
|
+
"mounted",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const PAGE_TEARDOWN_PHASES: PageLifecyclePhase[] = [
|
|
19
|
+
"beforeDestroy",
|
|
20
|
+
"destroy",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function waitForPagePaint() {
|
|
24
|
+
if (document.readyState !== "complete") {
|
|
25
|
+
await new Promise<void>((resolve) => {
|
|
26
|
+
window.addEventListener("load", () => resolve(), { once: true });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
await new Promise<void>((resolve) => {
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
requestAnimationFrame(() => resolve());
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type UseBlueprintPageLifecycleOptions = {
|
|
37
|
+
graph: BlueprintGraph;
|
|
38
|
+
/** 页面(工作区)是否处于激活状态,用于 activated / deactivated */
|
|
39
|
+
active?: boolean;
|
|
40
|
+
/** 为 false 时不触发任何生命周期阶段 */
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
/** 挂载时要执行的阶段;默认 created → beforeMount → mounted */
|
|
43
|
+
bootPhases?: PageLifecyclePhase[];
|
|
44
|
+
/**
|
|
45
|
+
* 挂载流程的触发键。提供后会在键变化时重新执行 bootPhases;
|
|
46
|
+
* 未提供时仅在首次满足 enabled 时执行一次。
|
|
47
|
+
*/
|
|
48
|
+
bootKey?: unknown;
|
|
49
|
+
/** 为 true 时在执行 bootPhases 前等待 window load 与双 rAF(页面绘制完成) */
|
|
50
|
+
waitForPageReady?: boolean;
|
|
51
|
+
/** 元素或图层变更时是否触发 updated */
|
|
52
|
+
onUpdated?: unknown;
|
|
53
|
+
/** 解析蓝图库记录为可执行子图 */
|
|
54
|
+
resolveLibraryBlueprint?: LibraryBlueprintResolver;
|
|
55
|
+
/** 蓝图库 id -> 名称,用于 signal 中的 blueprintName */
|
|
56
|
+
libraryNameById?: ReadonlyMap<string, string>;
|
|
57
|
+
/** 当前蓝图在蓝图库中的 id;未入库时为 null */
|
|
58
|
+
rootLibraryBlueprintId?: string | null;
|
|
59
|
+
/** 检测到蓝图相互引用死循环时回调 */
|
|
60
|
+
onExecutionBlocked?: (message: string) => void;
|
|
61
|
+
/** 视图绑定节点收到真信号时,为关联视图元素写入 scope */
|
|
62
|
+
onViewScopeUpdate?: (
|
|
63
|
+
viewElementIds: string[],
|
|
64
|
+
scope: unknown
|
|
65
|
+
) => void;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type RunnerOptions = Pick<
|
|
69
|
+
UseBlueprintPageLifecycleOptions,
|
|
70
|
+
| "resolveLibraryBlueprint"
|
|
71
|
+
| "libraryNameById"
|
|
72
|
+
| "rootLibraryBlueprintId"
|
|
73
|
+
| "onExecutionBlocked"
|
|
74
|
+
| "onViewScopeUpdate"
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
function resolveBlueprintName(
|
|
78
|
+
libraryNameById: ReadonlyMap<string, string> | undefined,
|
|
79
|
+
id: string
|
|
80
|
+
) {
|
|
81
|
+
return libraryNameById?.get(id) ?? id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createRunner(graph: BlueprintGraph, options: RunnerOptions) {
|
|
85
|
+
return new BlueprintGraphRunner(
|
|
86
|
+
documentToRunnableGraph(graph.document, {
|
|
87
|
+
libraryNameById: options.libraryNameById,
|
|
88
|
+
}),
|
|
89
|
+
{
|
|
90
|
+
resolveLibraryBlueprint: options.resolveLibraryBlueprint,
|
|
91
|
+
rootLibraryBlueprintId: options.rootLibraryBlueprintId,
|
|
92
|
+
resolveBlueprintName: (id) =>
|
|
93
|
+
resolveBlueprintName(options.libraryNameById, id),
|
|
94
|
+
onViewScopeUpdate: options.onViewScopeUpdate,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function emitPhases(
|
|
100
|
+
graph: BlueprintGraph,
|
|
101
|
+
phases: PageLifecyclePhase[],
|
|
102
|
+
options: RunnerOptions
|
|
103
|
+
) {
|
|
104
|
+
if (!phases.length) return;
|
|
105
|
+
|
|
106
|
+
if (options.resolveLibraryBlueprint) {
|
|
107
|
+
const result = await detectBlueprintReferenceCycle({
|
|
108
|
+
rootGraph: documentToRunnableGraph(graph.document, {
|
|
109
|
+
libraryNameById: options.libraryNameById,
|
|
110
|
+
}),
|
|
111
|
+
rootLibraryBlueprintId: options.rootLibraryBlueprintId,
|
|
112
|
+
resolveLibraryBlueprint: options.resolveLibraryBlueprint,
|
|
113
|
+
resolveBlueprintName: (id) =>
|
|
114
|
+
resolveBlueprintName(options.libraryNameById, id),
|
|
115
|
+
});
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
options.onExecutionBlocked?.(result.message);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const runner = createRunner(graph, options);
|
|
123
|
+
for (const phase of phases) {
|
|
124
|
+
await runner.emitLifecycle(phase);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 将蓝图中的生命周期节点与当前页面生命周期对齐。
|
|
130
|
+
* 在编辑器工作区挂载/卸载、图层切换时自动触发对应钩子。
|
|
131
|
+
*/
|
|
132
|
+
export function useBlueprintPageLifecycle({
|
|
133
|
+
graph,
|
|
134
|
+
active = true,
|
|
135
|
+
enabled = true,
|
|
136
|
+
bootPhases = PAGE_BOOT_PHASES,
|
|
137
|
+
bootKey,
|
|
138
|
+
waitForPageReady = false,
|
|
139
|
+
onUpdated,
|
|
140
|
+
resolveLibraryBlueprint,
|
|
141
|
+
libraryNameById,
|
|
142
|
+
rootLibraryBlueprintId = null,
|
|
143
|
+
onExecutionBlocked,
|
|
144
|
+
onViewScopeUpdate,
|
|
145
|
+
}: UseBlueprintPageLifecycleOptions) {
|
|
146
|
+
const graphRef = useRef(graph);
|
|
147
|
+
graphRef.current = graph;
|
|
148
|
+
|
|
149
|
+
const runnerOptionsRef = useRef<RunnerOptions>({
|
|
150
|
+
resolveLibraryBlueprint,
|
|
151
|
+
libraryNameById,
|
|
152
|
+
rootLibraryBlueprintId,
|
|
153
|
+
onExecutionBlocked,
|
|
154
|
+
onViewScopeUpdate,
|
|
155
|
+
});
|
|
156
|
+
runnerOptionsRef.current = {
|
|
157
|
+
resolveLibraryBlueprint,
|
|
158
|
+
libraryNameById,
|
|
159
|
+
rootLibraryBlueprintId,
|
|
160
|
+
onExecutionBlocked,
|
|
161
|
+
onViewScopeUpdate,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const bootPhasesKey = bootPhases.join("|");
|
|
165
|
+
const bootTrigger = bootKey ?? "__initial__";
|
|
166
|
+
const [bootCompleted, setBootCompleted] = useState(false);
|
|
167
|
+
const hadBootRef = useRef(false);
|
|
168
|
+
const activeRef = useRef(active);
|
|
169
|
+
activeRef.current = active;
|
|
170
|
+
const prevActiveRef = useRef<boolean | undefined>(undefined);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!enabled) {
|
|
174
|
+
setBootCompleted(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let cancelled = false;
|
|
179
|
+
|
|
180
|
+
const runBoot = async () => {
|
|
181
|
+
if (waitForPageReady) {
|
|
182
|
+
await waitForPagePaint();
|
|
183
|
+
}
|
|
184
|
+
if (cancelled) return;
|
|
185
|
+
|
|
186
|
+
await emitPhases(
|
|
187
|
+
graphRef.current,
|
|
188
|
+
bootPhases,
|
|
189
|
+
runnerOptionsRef.current
|
|
190
|
+
);
|
|
191
|
+
if (cancelled) return;
|
|
192
|
+
|
|
193
|
+
if (activeRef.current) {
|
|
194
|
+
await emitPhases(
|
|
195
|
+
graphRef.current,
|
|
196
|
+
["activated"],
|
|
197
|
+
runnerOptionsRef.current
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (cancelled) return;
|
|
201
|
+
|
|
202
|
+
hadBootRef.current = true;
|
|
203
|
+
prevActiveRef.current = activeRef.current;
|
|
204
|
+
setBootCompleted(true);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
void runBoot();
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
cancelled = true;
|
|
211
|
+
setBootCompleted(false);
|
|
212
|
+
prevActiveRef.current = undefined;
|
|
213
|
+
if (hadBootRef.current) {
|
|
214
|
+
hadBootRef.current = false;
|
|
215
|
+
void emitPhases(
|
|
216
|
+
graphRef.current,
|
|
217
|
+
PAGE_TEARDOWN_PHASES,
|
|
218
|
+
runnerOptionsRef.current
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}, [bootPhasesKey, bootTrigger, enabled, waitForPageReady]);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (!enabled || !bootCompleted) return;
|
|
226
|
+
|
|
227
|
+
const prev = prevActiveRef.current;
|
|
228
|
+
if (prev === undefined || prev === active) {
|
|
229
|
+
prevActiveRef.current = active;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
prevActiveRef.current = active;
|
|
233
|
+
|
|
234
|
+
void emitPhases(
|
|
235
|
+
graphRef.current,
|
|
236
|
+
[active ? "activated" : "deactivated"],
|
|
237
|
+
runnerOptionsRef.current
|
|
238
|
+
);
|
|
239
|
+
}, [active, bootCompleted, enabled]);
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (!enabled || !bootCompleted) return;
|
|
243
|
+
void emitPhases(graphRef.current, ["updated"], runnerOptionsRef.current);
|
|
244
|
+
}, [bootCompleted, enabled, onUpdated]);
|
|
245
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FC,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type Dispatch,
|
|
7
|
+
type SetStateAction,
|
|
8
|
+
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
ReactFlow,
|
|
11
|
+
ReactFlowProvider,
|
|
12
|
+
ConnectionMode,
|
|
13
|
+
useReactFlow,
|
|
14
|
+
type Node,
|
|
15
|
+
type Edge,
|
|
16
|
+
} from "@xyflow/react";
|
|
17
|
+
import "@xyflow/react/dist/style.css";
|
|
18
|
+
import "./blueprint.css";
|
|
19
|
+
|
|
20
|
+
import { BlueprintGraph, type BlueprintFlowNodeData } from "./graph";
|
|
21
|
+
import {
|
|
22
|
+
BlueprintContextMenu,
|
|
23
|
+
type BlueprintContextMenuState,
|
|
24
|
+
} from "./BlueprintContextMenu";
|
|
25
|
+
import { BlueprintCanvasProvider } from "./BlueprintCanvasContext";
|
|
26
|
+
import { blueprintNodeTypes } from "./blueprintNodeTypes";
|
|
27
|
+
import { createBlueprintEdgeTypes } from "./createBlueprintEdgeTypes";
|
|
28
|
+
import { BLUEPRINT_DEFAULT_EDGE_OPTIONS } from "./flowDefaults";
|
|
29
|
+
import { useBlueprintFlowColorMode } from "./hooks/useBlueprintFlowColorMode";
|
|
30
|
+
import { useBlueprintFlowState } from "./hooks/useBlueprintFlowState";
|
|
31
|
+
import { useBlueprintFlowViewport } from "./hooks/useBlueprintFlowViewport";
|
|
32
|
+
import { clientToFlowNodePosition } from "./flowCoordinates";
|
|
33
|
+
import type { BlueprintExecutionOverlay } from "./runtime/execution-overlay";
|
|
34
|
+
|
|
35
|
+
export type { BlueprintExecutionOverlay } from "./runtime/execution-overlay";
|
|
36
|
+
|
|
37
|
+
export type { BlueprintNodeConfigSidebarProps, BlueprintViewElementOption, BlueprintLibraryOption } from "./BlueprintNodeConfigSidebar";
|
|
38
|
+
export { BlueprintNodeConfigSidebar } from "./BlueprintNodeConfigSidebar";
|
|
39
|
+
|
|
40
|
+
export type BluePrintReactRootProps = {
|
|
41
|
+
style?: React.CSSProperties;
|
|
42
|
+
graph: BlueprintGraph;
|
|
43
|
+
onGraphChange: Dispatch<SetStateAction<BlueprintGraph>>;
|
|
44
|
+
selectedNodeId?: string | null;
|
|
45
|
+
onSelectNode?: (nodeId: string | null) => void;
|
|
46
|
+
/** 中止正在运行的时钟节点 */
|
|
47
|
+
onAbortClock?: (nodeId: string) => void;
|
|
48
|
+
/** 调试执行时的画布高亮(节点 + 边线信号色) */
|
|
49
|
+
executionOverlay?: BlueprintExecutionOverlay | null;
|
|
50
|
+
/** 蓝图库 id -> 名称,用于画布节点展示引用蓝图名 */
|
|
51
|
+
libraryNameById?: ReadonlyMap<string, string>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type BlueprintCanvasProps = Omit<BluePrintReactRootProps, "style"> & {
|
|
55
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const blueprintEdgeTypes = createBlueprintEdgeTypes();
|
|
59
|
+
|
|
60
|
+
function BlueprintCanvas({
|
|
61
|
+
graph,
|
|
62
|
+
onGraphChange,
|
|
63
|
+
selectedNodeId = null,
|
|
64
|
+
executionOverlay = null,
|
|
65
|
+
onSelectNode,
|
|
66
|
+
onAbortClock,
|
|
67
|
+
libraryNameById,
|
|
68
|
+
containerRef,
|
|
69
|
+
}: BlueprintCanvasProps) {
|
|
70
|
+
const { screenToFlowPosition } = useReactFlow();
|
|
71
|
+
const [menu, setMenu] = useState<BlueprintContextMenuState | null>(null);
|
|
72
|
+
|
|
73
|
+
const applyGraphChange = useCallback(
|
|
74
|
+
(updater: (prev: BlueprintGraph) => BlueprintGraph) => {
|
|
75
|
+
onGraphChange((prev) => updater(prev));
|
|
76
|
+
},
|
|
77
|
+
[onGraphChange]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
nodes,
|
|
82
|
+
edges,
|
|
83
|
+
onNodesChange,
|
|
84
|
+
onEdgesChange,
|
|
85
|
+
onNodeDragStop,
|
|
86
|
+
onConnect,
|
|
87
|
+
isValidConnection,
|
|
88
|
+
} = useBlueprintFlowState({
|
|
89
|
+
graph,
|
|
90
|
+
selectedNodeId,
|
|
91
|
+
executionOverlay,
|
|
92
|
+
libraryNameById,
|
|
93
|
+
onGraphChange: applyGraphChange,
|
|
94
|
+
onSelectNode,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const flowColorMode = useBlueprintFlowColorMode();
|
|
98
|
+
|
|
99
|
+
useBlueprintFlowViewport(containerRef, true);
|
|
100
|
+
|
|
101
|
+
const onPaneContextMenu = useCallback(
|
|
102
|
+
(event: MouseEvent | React.MouseEvent) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
setMenu({
|
|
105
|
+
kind: "pane",
|
|
106
|
+
clientX: event.clientX,
|
|
107
|
+
clientY: event.clientY,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
[]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const onNodeContextMenu = useCallback(
|
|
114
|
+
(
|
|
115
|
+
event: MouseEvent | React.MouseEvent,
|
|
116
|
+
node: Node<BlueprintFlowNodeData>
|
|
117
|
+
) => {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
onSelectNode?.(node.id);
|
|
120
|
+
setMenu({
|
|
121
|
+
kind: "node",
|
|
122
|
+
clientX: event.clientX,
|
|
123
|
+
clientY: event.clientY,
|
|
124
|
+
nodeId: node.id,
|
|
125
|
+
role: node.data.role,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
[onSelectNode]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const onEdgeContextMenu = useCallback(
|
|
132
|
+
(event: MouseEvent | React.MouseEvent, edge: Edge) => {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
setMenu({
|
|
135
|
+
kind: "edge",
|
|
136
|
+
clientX: event.clientX,
|
|
137
|
+
clientY: event.clientY,
|
|
138
|
+
edgeId: edge.id,
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
[]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const onEdgeClick = useCallback(() => {
|
|
145
|
+
setMenu(null);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
const onPaneClick = useCallback(() => {
|
|
149
|
+
setMenu(null);
|
|
150
|
+
onSelectNode?.(null);
|
|
151
|
+
}, [onSelectNode]);
|
|
152
|
+
|
|
153
|
+
const setGraph = useCallback(
|
|
154
|
+
(updater: (prev: BlueprintGraph) => BlueprintGraph) => {
|
|
155
|
+
applyGraphChange(updater);
|
|
156
|
+
},
|
|
157
|
+
[applyGraphChange]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleAddBlueprintNode = useCallback(
|
|
161
|
+
(clientX: number, clientY: number) => {
|
|
162
|
+
const position = clientToFlowNodePosition(
|
|
163
|
+
(point) => screenToFlowPosition(point, { snapToGrid: false }),
|
|
164
|
+
clientX,
|
|
165
|
+
clientY
|
|
166
|
+
);
|
|
167
|
+
setGraph((prev) => prev.addBlueprintNode(position));
|
|
168
|
+
},
|
|
169
|
+
[screenToFlowPosition, setGraph]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const handleDeleteNode = useCallback(
|
|
173
|
+
(nodeId: string) => {
|
|
174
|
+
if (nodeId === selectedNodeId) {
|
|
175
|
+
onSelectNode?.(null);
|
|
176
|
+
}
|
|
177
|
+
setGraph((prev) => prev.removeNode(nodeId));
|
|
178
|
+
},
|
|
179
|
+
[onSelectNode, selectedNodeId, setGraph]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const handleDeleteEdge = useCallback(
|
|
183
|
+
(edgeId: string) => {
|
|
184
|
+
setGraph((prev) => prev.removeEdge(edgeId));
|
|
185
|
+
},
|
|
186
|
+
[setGraph]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<ReactFlow
|
|
192
|
+
className="bp-flow h-full w-full"
|
|
193
|
+
colorMode={flowColorMode}
|
|
194
|
+
nodes={nodes}
|
|
195
|
+
edges={edges}
|
|
196
|
+
nodeTypes={blueprintNodeTypes}
|
|
197
|
+
edgeTypes={blueprintEdgeTypes}
|
|
198
|
+
onNodesChange={onNodesChange}
|
|
199
|
+
onEdgesChange={onEdgesChange}
|
|
200
|
+
onConnect={onConnect}
|
|
201
|
+
isValidConnection={isValidConnection}
|
|
202
|
+
onPaneContextMenu={onPaneContextMenu}
|
|
203
|
+
onNodeContextMenu={onNodeContextMenu}
|
|
204
|
+
onEdgeContextMenu={onEdgeContextMenu}
|
|
205
|
+
onEdgeClick={onEdgeClick}
|
|
206
|
+
onNodeDragStop={onNodeDragStop}
|
|
207
|
+
onPaneClick={onPaneClick}
|
|
208
|
+
nodesConnectable
|
|
209
|
+
elementsSelectable
|
|
210
|
+
edgesFocusable
|
|
211
|
+
connectionMode={ConnectionMode.Strict}
|
|
212
|
+
defaultEdgeOptions={BLUEPRINT_DEFAULT_EDGE_OPTIONS}
|
|
213
|
+
selectNodesOnDrag={false}
|
|
214
|
+
elevateNodesOnSelect={false}
|
|
215
|
+
nodeClickDistance={8}
|
|
216
|
+
proOptions={{ hideAttribution: true }}
|
|
217
|
+
/>
|
|
218
|
+
<BlueprintContextMenu
|
|
219
|
+
menu={menu}
|
|
220
|
+
onClose={() => setMenu(null)}
|
|
221
|
+
onAddBlueprintNode={handleAddBlueprintNode}
|
|
222
|
+
onDeleteNode={handleDeleteNode}
|
|
223
|
+
onDeleteEdge={handleDeleteEdge}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const BluePrintReactRoot: FC<BluePrintReactRootProps> = ({
|
|
230
|
+
style,
|
|
231
|
+
graph,
|
|
232
|
+
onGraphChange,
|
|
233
|
+
selectedNodeId,
|
|
234
|
+
executionOverlay,
|
|
235
|
+
onSelectNode,
|
|
236
|
+
onAbortClock,
|
|
237
|
+
libraryNameById,
|
|
238
|
+
}) => {
|
|
239
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
ref={containerRef}
|
|
244
|
+
data-workspace-region="blueprint"
|
|
245
|
+
className="bp-canvas h-full w-full bg-background text-foreground"
|
|
246
|
+
style={{
|
|
247
|
+
width: "100%",
|
|
248
|
+
height: "100%",
|
|
249
|
+
minHeight: 0,
|
|
250
|
+
...style,
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<BlueprintCanvasProvider
|
|
254
|
+
onSelectNode={onSelectNode}
|
|
255
|
+
onAbortClock={onAbortClock}
|
|
256
|
+
>
|
|
257
|
+
<ReactFlowProvider>
|
|
258
|
+
<BlueprintCanvas
|
|
259
|
+
containerRef={containerRef}
|
|
260
|
+
graph={graph}
|
|
261
|
+
onGraphChange={onGraphChange}
|
|
262
|
+
selectedNodeId={selectedNodeId}
|
|
263
|
+
executionOverlay={executionOverlay}
|
|
264
|
+
onSelectNode={onSelectNode}
|
|
265
|
+
libraryNameById={libraryNameById}
|
|
266
|
+
/>
|
|
267
|
+
</ReactFlowProvider>
|
|
268
|
+
</BlueprintCanvasProvider>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { BlueprintDocument } from "../graph/document";
|
|
2
|
+
import { createNodeId } from "../graph/document";
|
|
3
|
+
import {
|
|
4
|
+
BLUEPRINT_EXPORT_KIND,
|
|
5
|
+
BLUEPRINT_EXPORT_VERSION,
|
|
6
|
+
type BlueprintExportPayload,
|
|
7
|
+
type BlueprintLibraryRecord,
|
|
8
|
+
type BlueprintLibrarySource,
|
|
9
|
+
type BlueprintMetaDraft,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export function createLibraryBlueprintId() {
|
|
13
|
+
return createNodeId("lib_bp");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function blueprintDocumentsEqual(
|
|
17
|
+
a: BlueprintDocument,
|
|
18
|
+
b: BlueprintDocument
|
|
19
|
+
): boolean {
|
|
20
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildBlueprintExportPayload(
|
|
24
|
+
document: BlueprintDocument,
|
|
25
|
+
meta: BlueprintMetaDraft
|
|
26
|
+
): BlueprintExportPayload {
|
|
27
|
+
return {
|
|
28
|
+
kind: BLUEPRINT_EXPORT_KIND,
|
|
29
|
+
version: BLUEPRINT_EXPORT_VERSION,
|
|
30
|
+
name: meta.name.trim() || "未命名蓝图",
|
|
31
|
+
remark: meta.remark.trim() || undefined,
|
|
32
|
+
exportedAt: Date.now(),
|
|
33
|
+
document: {
|
|
34
|
+
...document,
|
|
35
|
+
name: meta.name.trim() || document.name,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseBlueprintImportFile(raw: unknown): BlueprintExportPayload {
|
|
41
|
+
if (!raw || typeof raw !== "object") {
|
|
42
|
+
throw new Error("invalid-format");
|
|
43
|
+
}
|
|
44
|
+
const data = raw as Partial<BlueprintExportPayload>;
|
|
45
|
+
if (data.kind !== BLUEPRINT_EXPORT_KIND || data.version !== BLUEPRINT_EXPORT_VERSION) {
|
|
46
|
+
throw new Error("unsupported-version");
|
|
47
|
+
}
|
|
48
|
+
if (!data.document || !Array.isArray(data.document.nodes) || !Array.isArray(data.document.edges)) {
|
|
49
|
+
throw new Error("invalid-document");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
kind: BLUEPRINT_EXPORT_KIND,
|
|
53
|
+
version: BLUEPRINT_EXPORT_VERSION,
|
|
54
|
+
name: typeof data.name === "string" ? data.name : "导入的蓝图",
|
|
55
|
+
remark: typeof data.remark === "string" ? data.remark : undefined,
|
|
56
|
+
exportedAt: typeof data.exportedAt === "number" ? data.exportedAt : Date.now(),
|
|
57
|
+
document: data.document,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function downloadBlueprintExport(payload: BlueprintExportPayload) {
|
|
62
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
|
63
|
+
type: "application/json",
|
|
64
|
+
});
|
|
65
|
+
const url = URL.createObjectURL(blob);
|
|
66
|
+
const anchor = document.createElement("a");
|
|
67
|
+
anchor.href = url;
|
|
68
|
+
const safeName = payload.name
|
|
69
|
+
.replace(/[\\/:*?"<>|]/g, "-")
|
|
70
|
+
.replace(/\s+/g, "-");
|
|
71
|
+
anchor.download = `${safeName || "blueprint"}-${Date.now()}.json`;
|
|
72
|
+
anchor.click();
|
|
73
|
+
URL.revokeObjectURL(url);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildLibraryRecord(args: {
|
|
77
|
+
document: BlueprintDocument;
|
|
78
|
+
meta: BlueprintMetaDraft;
|
|
79
|
+
source: BlueprintLibrarySource;
|
|
80
|
+
id?: string;
|
|
81
|
+
createdAt?: number;
|
|
82
|
+
}): BlueprintLibraryRecord {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
const name = args.meta.name.trim() || "未命名蓝图";
|
|
85
|
+
return {
|
|
86
|
+
id: args.id ?? createLibraryBlueprintId(),
|
|
87
|
+
name,
|
|
88
|
+
remark: args.meta.remark.trim() || undefined,
|
|
89
|
+
source: args.source,
|
|
90
|
+
createdAt: args.createdAt ?? now,
|
|
91
|
+
updatedAt: now,
|
|
92
|
+
document: {
|
|
93
|
+
...args.document,
|
|
94
|
+
name,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function libraryRecordFromImport(payload: BlueprintExportPayload): BlueprintLibraryRecord {
|
|
100
|
+
return buildLibraryRecord({
|
|
101
|
+
document: payload.document,
|
|
102
|
+
meta: {
|
|
103
|
+
name: payload.name,
|
|
104
|
+
remark: payload.remark ?? "",
|
|
105
|
+
},
|
|
106
|
+
source: "imported",
|
|
107
|
+
});
|
|
108
|
+
}
|