@arronqzy/react-blueprint 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/BlueprintContextMenu.tsx +1 -1
- package/src/BlueprintNodeConfigSidebar.tsx +73 -6
- package/src/blueprintNodeTypes.ts +2 -0
- package/src/components/FetchNodeConfigPanel.tsx +71 -8
- package/src/components/FetchUrlAutocomplete.tsx +1 -0
- package/src/components/ScopeTemplateAutocompleteHost.tsx +245 -0
- package/src/graph/blueprint-graph.ts +8 -0
- package/src/graph/document.ts +135 -8
- package/src/graph/node-summary.ts +20 -0
- package/src/graph/node-types.ts +1 -0
- package/src/graph/react-flow-adapter.ts +4 -0
- package/src/graph/sync-nodes.ts +2 -1
- package/src/hooks/useBlueprintFlowState.ts +40 -7
- package/src/hooks/useBlueprintPageLifecycle.ts +106 -14
- package/src/index.tsx +2 -0
- package/src/nodes/BlueprintNodeCard.tsx +12 -2
- package/src/nodes/BlueprintNodeShell.tsx +6 -2
- package/src/nodes/EventFlowNode.tsx +31 -0
- package/src/runtime/document-to-runnable-graph.ts +1 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
BlueprintGraphRunner,
|
|
4
|
+
collectArmedViewEventNodeIds,
|
|
4
5
|
detectBlueprintReferenceCycle,
|
|
6
|
+
EVENT_NODE_TYPE,
|
|
7
|
+
LIFECYCLE_NODE_TYPE,
|
|
5
8
|
type LibraryBlueprintResolver,
|
|
6
9
|
type PageLifecyclePhase,
|
|
10
|
+
type ViewEventSignal,
|
|
7
11
|
} from "@arronqzy/blueprint-dsl";
|
|
8
12
|
|
|
9
13
|
import type { BlueprintGraph } from "../graph/blueprint-graph";
|
|
14
|
+
import { resolveRunnableNodeType } from "../graph/document";
|
|
10
15
|
import { documentToRunnableGraph } from "../runtime/document-to-runnable-graph";
|
|
11
16
|
|
|
12
17
|
const PAGE_BOOT_PHASES: PageLifecyclePhase[] = [
|
|
@@ -96,12 +101,30 @@ function createRunner(graph: BlueprintGraph, options: RunnerOptions) {
|
|
|
96
101
|
);
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
function collectArmedIds(
|
|
105
|
+
graph: BlueprintGraph,
|
|
106
|
+
firedPhases: ReadonlySet<string>
|
|
107
|
+
) {
|
|
108
|
+
return collectArmedViewEventNodeIds(
|
|
109
|
+
graph.document.nodes.map((node) => ({
|
|
110
|
+
id: node.id,
|
|
111
|
+
nodeType: resolveRunnableNodeType(node),
|
|
112
|
+
lifecyclePhase: node.lifecyclePhase,
|
|
113
|
+
})),
|
|
114
|
+
graph.document.edges,
|
|
115
|
+
EVENT_NODE_TYPE,
|
|
116
|
+
LIFECYCLE_NODE_TYPE,
|
|
117
|
+
firedPhases
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
async function emitPhases(
|
|
100
122
|
graph: BlueprintGraph,
|
|
101
123
|
phases: PageLifecyclePhase[],
|
|
102
|
-
options: RunnerOptions
|
|
103
|
-
)
|
|
104
|
-
|
|
124
|
+
options: RunnerOptions,
|
|
125
|
+
onPhaseFired?: (phase: PageLifecyclePhase) => void
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
if (!phases.length) return true;
|
|
105
128
|
|
|
106
129
|
if (options.resolveLibraryBlueprint) {
|
|
107
130
|
const result = await detectBlueprintReferenceCycle({
|
|
@@ -115,14 +138,16 @@ async function emitPhases(
|
|
|
115
138
|
});
|
|
116
139
|
if (!result.ok) {
|
|
117
140
|
options.onExecutionBlocked?.(result.message);
|
|
118
|
-
return;
|
|
141
|
+
return false;
|
|
119
142
|
}
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
const runner = createRunner(graph, options);
|
|
123
146
|
for (const phase of phases) {
|
|
124
147
|
await runner.emitLifecycle(phase);
|
|
148
|
+
onPhaseFired?.(phase);
|
|
125
149
|
}
|
|
150
|
+
return true;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
/**
|
|
@@ -164,37 +189,64 @@ export function useBlueprintPageLifecycle({
|
|
|
164
189
|
const bootPhasesKey = bootPhases.join("|");
|
|
165
190
|
const bootTrigger = bootKey ?? "__initial__";
|
|
166
191
|
const [bootCompleted, setBootCompleted] = useState(false);
|
|
192
|
+
const [firedLifecyclePhases, setFiredLifecyclePhases] = useState<
|
|
193
|
+
ReadonlySet<PageLifecyclePhase>
|
|
194
|
+
>(() => new Set());
|
|
195
|
+
const firedLifecyclePhasesRef = useRef(firedLifecyclePhases);
|
|
196
|
+
firedLifecyclePhasesRef.current = firedLifecyclePhases;
|
|
167
197
|
const hadBootRef = useRef(false);
|
|
168
198
|
const activeRef = useRef(active);
|
|
169
199
|
activeRef.current = active;
|
|
170
200
|
const prevActiveRef = useRef<boolean | undefined>(undefined);
|
|
171
201
|
|
|
202
|
+
const resetFiredPhases = useCallback(() => {
|
|
203
|
+
setFiredLifecyclePhases(new Set());
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
const markPhaseFired = useCallback((phase: PageLifecyclePhase) => {
|
|
207
|
+
setFiredLifecyclePhases((prev) => {
|
|
208
|
+
if (prev.has(phase)) return prev;
|
|
209
|
+
const next = new Set(prev);
|
|
210
|
+
next.add(phase);
|
|
211
|
+
return next;
|
|
212
|
+
});
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
172
215
|
useEffect(() => {
|
|
173
216
|
if (!enabled) {
|
|
174
217
|
setBootCompleted(false);
|
|
218
|
+
resetFiredPhases();
|
|
175
219
|
return;
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
let cancelled = false;
|
|
179
223
|
|
|
180
224
|
const runBoot = async () => {
|
|
225
|
+
resetFiredPhases();
|
|
181
226
|
if (waitForPageReady) {
|
|
182
227
|
await waitForPagePaint();
|
|
183
228
|
}
|
|
184
229
|
if (cancelled) return;
|
|
185
230
|
|
|
186
|
-
await emitPhases(
|
|
231
|
+
const bootOk = await emitPhases(
|
|
187
232
|
graphRef.current,
|
|
188
233
|
bootPhases,
|
|
189
|
-
runnerOptionsRef.current
|
|
234
|
+
runnerOptionsRef.current,
|
|
235
|
+
(phase) => {
|
|
236
|
+
if (!cancelled) markPhaseFired(phase);
|
|
237
|
+
}
|
|
190
238
|
);
|
|
191
239
|
if (cancelled) return;
|
|
240
|
+
if (!bootOk) return;
|
|
192
241
|
|
|
193
242
|
if (activeRef.current) {
|
|
194
243
|
await emitPhases(
|
|
195
244
|
graphRef.current,
|
|
196
245
|
["activated"],
|
|
197
|
-
runnerOptionsRef.current
|
|
246
|
+
runnerOptionsRef.current,
|
|
247
|
+
(phase) => {
|
|
248
|
+
if (!cancelled) markPhaseFired(phase);
|
|
249
|
+
}
|
|
198
250
|
);
|
|
199
251
|
}
|
|
200
252
|
if (cancelled) return;
|
|
@@ -209,6 +261,7 @@ export function useBlueprintPageLifecycle({
|
|
|
209
261
|
return () => {
|
|
210
262
|
cancelled = true;
|
|
211
263
|
setBootCompleted(false);
|
|
264
|
+
resetFiredPhases();
|
|
212
265
|
prevActiveRef.current = undefined;
|
|
213
266
|
if (hadBootRef.current) {
|
|
214
267
|
hadBootRef.current = false;
|
|
@@ -219,7 +272,14 @@ export function useBlueprintPageLifecycle({
|
|
|
219
272
|
);
|
|
220
273
|
}
|
|
221
274
|
};
|
|
222
|
-
}, [
|
|
275
|
+
}, [
|
|
276
|
+
bootPhasesKey,
|
|
277
|
+
bootTrigger,
|
|
278
|
+
enabled,
|
|
279
|
+
waitForPageReady,
|
|
280
|
+
markPhaseFired,
|
|
281
|
+
resetFiredPhases,
|
|
282
|
+
]);
|
|
223
283
|
|
|
224
284
|
useEffect(() => {
|
|
225
285
|
if (!enabled || !bootCompleted) return;
|
|
@@ -231,17 +291,24 @@ export function useBlueprintPageLifecycle({
|
|
|
231
291
|
}
|
|
232
292
|
prevActiveRef.current = active;
|
|
233
293
|
|
|
294
|
+
const phase: PageLifecyclePhase = active ? "activated" : "deactivated";
|
|
234
295
|
void emitPhases(
|
|
235
296
|
graphRef.current,
|
|
236
|
-
[
|
|
237
|
-
runnerOptionsRef.current
|
|
297
|
+
[phase],
|
|
298
|
+
runnerOptionsRef.current,
|
|
299
|
+
markPhaseFired
|
|
238
300
|
);
|
|
239
|
-
}, [active, bootCompleted, enabled]);
|
|
301
|
+
}, [active, bootCompleted, enabled, markPhaseFired]);
|
|
240
302
|
|
|
241
303
|
useEffect(() => {
|
|
242
304
|
if (!enabled || !bootCompleted) return;
|
|
243
|
-
void emitPhases(
|
|
244
|
-
|
|
305
|
+
void emitPhases(
|
|
306
|
+
graphRef.current,
|
|
307
|
+
["updated"],
|
|
308
|
+
runnerOptionsRef.current,
|
|
309
|
+
markPhaseFired
|
|
310
|
+
);
|
|
311
|
+
}, [bootCompleted, enabled, onUpdated, markPhaseFired]);
|
|
245
312
|
|
|
246
313
|
const triggerBlueprintNode = useCallback(
|
|
247
314
|
async (nodeId: string, inputValue?: unknown) => {
|
|
@@ -269,5 +336,30 @@ export function useBlueprintPageLifecycle({
|
|
|
269
336
|
[]
|
|
270
337
|
);
|
|
271
338
|
|
|
272
|
-
|
|
339
|
+
const emitViewEvent = useCallback(async (payload: ViewEventSignal) => {
|
|
340
|
+
if (!payload?.node?.id || !payload.eventType) return;
|
|
341
|
+
const opts = runnerOptionsRef.current;
|
|
342
|
+
if (opts.resolveLibraryBlueprint) {
|
|
343
|
+
const result = await detectBlueprintReferenceCycle({
|
|
344
|
+
rootGraph: documentToRunnableGraph(graphRef.current.document, {
|
|
345
|
+
libraryNameById: opts.libraryNameById,
|
|
346
|
+
}),
|
|
347
|
+
rootLibraryBlueprintId: opts.rootLibraryBlueprintId,
|
|
348
|
+
resolveLibraryBlueprint: opts.resolveLibraryBlueprint,
|
|
349
|
+
resolveBlueprintName: (libId) =>
|
|
350
|
+
resolveBlueprintName(opts.libraryNameById, libId),
|
|
351
|
+
});
|
|
352
|
+
if (!result.ok) {
|
|
353
|
+
opts.onExecutionBlocked?.(result.message);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const runner = createRunner(graphRef.current, opts);
|
|
358
|
+
await runner.emitViewEvent(
|
|
359
|
+
payload,
|
|
360
|
+
collectArmedIds(graphRef.current, firedLifecyclePhasesRef.current)
|
|
361
|
+
);
|
|
362
|
+
}, []);
|
|
363
|
+
|
|
364
|
+
return { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases };
|
|
273
365
|
}
|
package/src/index.tsx
CHANGED
|
@@ -84,6 +84,7 @@ function BlueprintCanvas({
|
|
|
84
84
|
onEdgesChange,
|
|
85
85
|
onNodeDragStop,
|
|
86
86
|
onConnect,
|
|
87
|
+
onConnectEnd,
|
|
87
88
|
isValidConnection,
|
|
88
89
|
} = useBlueprintFlowState({
|
|
89
90
|
graph,
|
|
@@ -198,6 +199,7 @@ function BlueprintCanvas({
|
|
|
198
199
|
onNodesChange={onNodesChange}
|
|
199
200
|
onEdgesChange={onEdgesChange}
|
|
200
201
|
onConnect={onConnect}
|
|
202
|
+
onConnectEnd={onConnectEnd}
|
|
201
203
|
isValidConnection={isValidConnection}
|
|
202
204
|
onPaneContextMenu={onPaneContextMenu}
|
|
203
205
|
onNodeContextMenu={onNodeContextMenu}
|
|
@@ -9,7 +9,7 @@ export type BlueprintNodeCardProps = {
|
|
|
9
9
|
subtitle?: string;
|
|
10
10
|
/** 调试执行进度(如时钟 2/3) */
|
|
11
11
|
progressLabel?: string;
|
|
12
|
-
variant?: "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
|
|
12
|
+
variant?: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "clock";
|
|
13
13
|
selected?: boolean;
|
|
14
14
|
hideLeadingDot?: boolean;
|
|
15
15
|
onSelect?: (nodeId: string) => void;
|
|
@@ -36,6 +36,11 @@ const variantStyle = {
|
|
|
36
36
|
badge: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
37
37
|
dot: "bg-amber-500",
|
|
38
38
|
},
|
|
39
|
+
event: {
|
|
40
|
+
accent: "border-l-fuchsia-500 dark:border-l-fuchsia-400",
|
|
41
|
+
badge: "bg-fuchsia-500/10 text-fuchsia-700 dark:text-fuchsia-300",
|
|
42
|
+
dot: "bg-fuchsia-500",
|
|
43
|
+
},
|
|
39
44
|
fetch: {
|
|
40
45
|
accent: "border-l-violet-500 dark:border-l-violet-400",
|
|
41
46
|
badge: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
|
|
@@ -92,7 +97,12 @@ export function BlueprintNodeCard({
|
|
|
92
97
|
"bp-node-card w-[168px] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
|
93
98
|
"transition-[box-shadow,border-color] duration-150",
|
|
94
99
|
hideLeadingDot
|
|
95
|
-
?
|
|
100
|
+
? cn(
|
|
101
|
+
"border-t-[3px]",
|
|
102
|
+
variant === "event"
|
|
103
|
+
? "border-t-fuchsia-500 dark:border-t-fuchsia-400"
|
|
104
|
+
: "border-t-amber-500 dark:border-t-amber-400"
|
|
105
|
+
)
|
|
96
106
|
: cn("border-l-[3px]", v.accent),
|
|
97
107
|
selected &&
|
|
98
108
|
"border-primary/50 shadow-[0_0_0_1px_hsl(var(--primary)/0.35),0_4px_12px_hsl(var(--primary)/0.12)]"
|
|
@@ -10,12 +10,16 @@ export type BlueprintNodeShellProps = BlueprintNodeCardProps & {
|
|
|
10
10
|
selected?: boolean;
|
|
11
11
|
/** 调试执行中当前节点的信号高亮(与用户选中独立) */
|
|
12
12
|
executionTone?: BlueprintNodeExecutionTone | null;
|
|
13
|
+
targetTitle?: string;
|
|
14
|
+
sourceTitle?: string;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
/** 左进右出的标准节点连线布局 */
|
|
16
18
|
export function BlueprintNodeShell({
|
|
17
19
|
selected = false,
|
|
18
20
|
executionTone = null,
|
|
21
|
+
targetTitle,
|
|
22
|
+
sourceTitle,
|
|
19
23
|
...cardProps
|
|
20
24
|
}: BlueprintNodeShellProps) {
|
|
21
25
|
const { t } = useI18n();
|
|
@@ -32,7 +36,7 @@ export function BlueprintNodeShell({
|
|
|
32
36
|
position={Position.Left}
|
|
33
37
|
id="in"
|
|
34
38
|
className="bp-flow-handle bp-flow-handle--target"
|
|
35
|
-
title={t("blueprint.node.signalIn")}
|
|
39
|
+
title={targetTitle ?? t("blueprint.node.signalIn")}
|
|
36
40
|
/>
|
|
37
41
|
<BlueprintNodeCard {...cardProps} selected={selected} />
|
|
38
42
|
<Handle
|
|
@@ -40,7 +44,7 @@ export function BlueprintNodeShell({
|
|
|
40
44
|
position={Position.Right}
|
|
41
45
|
id="out"
|
|
42
46
|
className="bp-flow-handle bp-flow-handle--source"
|
|
43
|
-
title={t("blueprint.node.signalOut")}
|
|
47
|
+
title={sourceTitle ?? t("blueprint.node.signalOut")}
|
|
44
48
|
/>
|
|
45
49
|
</div>
|
|
46
50
|
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
|
+
import type { NodeProps } from "@xyflow/react";
|
|
3
|
+
|
|
4
|
+
import { resolveBlueprintNodeTypeLabel } from "../graph/document";
|
|
5
|
+
import { resolveBlueprintNodeSummary } from "../graph/node-summary";
|
|
6
|
+
import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
|
|
7
|
+
import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
|
|
8
|
+
import type { BlueprintFlowNodeData } from "../types";
|
|
9
|
+
import { BlueprintNodeShell } from "./BlueprintNodeShell";
|
|
10
|
+
|
|
11
|
+
/** 视图事件节点:输入必须来自生命周期节点,对应阶段触发后注册事件 */
|
|
12
|
+
export function EventFlowNode({ id, data }: NodeProps) {
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
const nodeData = data as BlueprintFlowNodeData;
|
|
15
|
+
const onSelect = useBlueprintNodeSelect();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<BlueprintNodeShell
|
|
19
|
+
nodeId={id}
|
|
20
|
+
label={nodeData.label}
|
|
21
|
+
meta={resolveBlueprintNodeTypeLabel(nodeData, t)}
|
|
22
|
+
subtitle={resolveBlueprintNodeSummary(nodeData, t)}
|
|
23
|
+
variant="event"
|
|
24
|
+
selected={Boolean(nodeData.isSelected)}
|
|
25
|
+
executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
|
|
26
|
+
targetTitle={t("blueprint.node.eventIn")}
|
|
27
|
+
sourceTitle={t("blueprint.node.eventOut")}
|
|
28
|
+
onSelect={onSelect}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -36,6 +36,7 @@ export function documentToRunnableGraph(
|
|
|
36
36
|
jsonConfig: node.jsonConfig,
|
|
37
37
|
logicConfig: node.logicConfig,
|
|
38
38
|
clockConfig: node.clockConfig,
|
|
39
|
+
eventConfig: node.eventConfig,
|
|
39
40
|
blueprintName: node.libraryBlueprintId
|
|
40
41
|
? (options?.libraryNameById?.get(node.libraryBlueprintId) ??
|
|
41
42
|
node.label)
|