@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.
@@ -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
- if (!phases.length) return;
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
- }, [bootPhasesKey, bootTrigger, enabled, waitForPageReady]);
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
- [active ? "activated" : "deactivated"],
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(graphRef.current, ["updated"], runnerOptionsRef.current);
244
- }, [bootCompleted, enabled, onUpdated]);
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
- return { triggerBlueprintNode };
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
- ? "border-t-[3px] border-t-amber-500 dark:border-t-amber-400"
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)