@arronqzy/react-blueprint 1.0.6 → 1.0.8

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.
@@ -4,6 +4,8 @@ import type {
4
4
  JsonNodeConfig,
5
5
  LogicNodeConfig,
6
6
  PageLifecyclePhase,
7
+ ViewEventNodeConfig,
8
+ ViewEventType,
7
9
  } from "@arronqzy/blueprint-dsl";
8
10
  import {
9
11
  BLUEPRINT_NODE_TYPE,
@@ -14,10 +16,13 @@ import {
14
16
  DEFAULT_JSON_NODE_CONFIG,
15
17
  DEFAULT_LOGIC_NODE_CONFIG,
16
18
  DEFAULT_LOGIC_NODE_TYPE,
19
+ DEFAULT_VIEW_EVENT_NODE_CONFIG,
20
+ EVENT_NODE_TYPE,
17
21
  FETCH_NODE_TYPE,
18
22
  JSON_NODE_TYPE,
19
23
  LIFECYCLE_NODE_TYPE,
20
24
  normalizeClockConfig,
25
+ normalizeViewEventConfig,
21
26
  VIEW_NODE_TYPE,
22
27
  } from "@arronqzy/blueprint-dsl";
23
28
 
@@ -25,8 +30,9 @@ export type { FetchRequestConfig as BlueprintFetchConfig } from "@arronqzy/bluep
25
30
  export type { JsonNodeConfig as BlueprintJsonConfig } from "@arronqzy/blueprint-dsl";
26
31
  export type { LogicNodeConfig as BlueprintLogicConfig } from "@arronqzy/blueprint-dsl";
27
32
  export type { ClockNodeConfig as BlueprintClockConfig } from "@arronqzy/blueprint-dsl";
33
+ export type { ViewEventNodeConfig as BlueprintEventConfig } from "@arronqzy/blueprint-dsl";
28
34
 
29
- export type BlueprintNodeRole = "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock";
35
+ export type BlueprintNodeRole = "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock" | "event";
30
36
 
31
37
  /** 决定右侧配置面板展示哪类配置 */
32
38
  export type BlueprintConfigSource =
@@ -37,7 +43,8 @@ export type BlueprintConfigSource =
37
43
  | "view"
38
44
  | "fetch"
39
45
  | "json"
40
- | "clock";
46
+ | "clock"
47
+ | "event";
41
48
 
42
49
  export type BlueprintGraphNode = {
43
50
  id: string;
@@ -68,6 +75,8 @@ export type BlueprintGraphNode = {
68
75
  logicConfig?: LogicNodeConfig;
69
76
  /** 时钟节点的间隔与时间格式配置 */
70
77
  clockConfig?: ClockNodeConfig;
78
+ /** 视图事件节点监听的事件类型 */
79
+ eventConfig?: ViewEventNodeConfig;
71
80
  };
72
81
 
73
82
  export function resolveViewElementIds(
@@ -94,6 +103,7 @@ export function resolveBlueprintConfigSource(
94
103
  ): BlueprintConfigSource {
95
104
  if (node.configSource) return node.configSource;
96
105
  if (node.role === "lifecycle") return "lifecycle";
106
+ if (node.role === "event") return "event";
97
107
  if (node.role === "clock") return "clock";
98
108
  if (node.role === "and") return "and";
99
109
  if (node.role === "fetch") return "fetch";
@@ -101,11 +111,60 @@ export function resolveBlueprintConfigSource(
101
111
  if (node.nodeType === FETCH_NODE_TYPE) return "fetch";
102
112
  if (node.nodeType === JSON_NODE_TYPE) return "json";
103
113
  if (node.nodeType === CLOCK_NODE_TYPE) return "clock";
114
+ if (node.nodeType === EVENT_NODE_TYPE) return "event";
104
115
  if (node.nodeType === VIEW_NODE_TYPE) return "view";
105
116
  if (resolveViewElementIds(node).length > 0) return "view";
106
117
  return node.role === "logic" ? "logic" : "blueprint";
107
118
  }
108
119
 
120
+ export type BlueprintConfigSourceNode = Pick<
121
+ BlueprintGraphNode,
122
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
123
+ >;
124
+
125
+ export type BlueprintConnectionIssue =
126
+ | "lifecycle-no-input"
127
+ | "event-requires-lifecycle";
128
+
129
+ /** 连入校验:生命周期无输入;事件节点输入必须全部来自生命周期节点 */
130
+ export function resolveBlueprintConnectionIssue(
131
+ source: BlueprintConfigSourceNode | undefined,
132
+ target: BlueprintConfigSourceNode | undefined
133
+ ): BlueprintConnectionIssue | null {
134
+ if (!source || !target) return null;
135
+ const targetSource = resolveBlueprintConfigSource(target);
136
+ if (targetSource === "lifecycle") return "lifecycle-no-input";
137
+ if (
138
+ targetSource === "event" &&
139
+ resolveBlueprintConfigSource(source) !== "lifecycle"
140
+ ) {
141
+ return "event-requires-lifecycle";
142
+ }
143
+ return null;
144
+ }
145
+
146
+ export function inspectEventNodeInputs(
147
+ nodeId: string,
148
+ nodes: Array<BlueprintConfigSourceNode & { id: string }>,
149
+ edges: Array<{ source: string; target: string }>
150
+ ) {
151
+ const byId = new Map(nodes.map((node) => [node.id, node]));
152
+ let incomingCount = 0;
153
+ let lifecycleCount = 0;
154
+ let nonLifecycleCount = 0;
155
+ for (const edge of edges) {
156
+ if (edge.target !== nodeId) continue;
157
+ incomingCount += 1;
158
+ const source = byId.get(edge.source);
159
+ if (source && resolveRunnableNodeType(source) === LIFECYCLE_NODE_TYPE) {
160
+ lifecycleCount += 1;
161
+ } else {
162
+ nonLifecycleCount += 1;
163
+ }
164
+ }
165
+ return { incomingCount, lifecycleCount, nonLifecycleCount };
166
+ }
167
+
109
168
  /** 可执行图节点类型:与配置类型对齐,避免文档里残留旧 nodeType */
110
169
  export function resolveRunnableNodeType(
111
170
  node: Pick<
@@ -116,6 +175,8 @@ export function resolveRunnableNodeType(
116
175
  switch (resolveBlueprintConfigSource(node)) {
117
176
  case "view":
118
177
  return VIEW_NODE_TYPE;
178
+ case "event":
179
+ return EVENT_NODE_TYPE;
119
180
  case "lifecycle":
120
181
  return LIFECYCLE_NODE_TYPE;
121
182
  case "clock":
@@ -146,6 +207,7 @@ export const BLUEPRINT_CONFIG_TYPE_LABEL_KEYS: Record<
146
207
  and: "blueprint.node.typeAnd",
147
208
  lifecycle: "blueprint.node.typeLifecycle",
148
209
  view: "blueprint.node.typeView",
210
+ event: "blueprint.node.typeEvent",
149
211
  fetch: "blueprint.node.typeFetch",
150
212
  json: "blueprint.node.typeJson",
151
213
  clock: "blueprint.node.typeClock",
@@ -164,6 +226,14 @@ export const BLUEPRINT_LIFECYCLE_PHASE_KEYS: Record<PageLifecyclePhase, string>
164
226
  blueprintActivated: "blueprint.node.phaseBlueprintActivated",
165
227
  };
166
228
 
229
+ export const BLUEPRINT_VIEW_EVENT_TYPE_KEYS: Record<ViewEventType, string> = {
230
+ click: "blueprint.node.eventClick",
231
+ dblclick: "blueprint.node.eventDblclick",
232
+ contextmenu: "blueprint.node.eventContextmenu",
233
+ mouseenter: "blueprint.node.eventMouseenter",
234
+ mouseleave: "blueprint.node.eventMouseleave",
235
+ };
236
+
167
237
  function defaultTranslate(): TranslateFn {
168
238
  return tForLocale(resolveLocale());
169
239
  }
@@ -183,6 +253,11 @@ export function getLifecyclePhaseLabel(
183
253
  return key ? t(key) : phase;
184
254
  }
185
255
 
256
+ export function getViewEventTypeLabel(t: TranslateFn, eventType: ViewEventType): string {
257
+ const key = BLUEPRINT_VIEW_EVENT_TYPE_KEYS[eventType];
258
+ return key ? t(key) : eventType;
259
+ }
260
+
186
261
  export function resolveBlueprintNodeTypeLabel(
187
262
  node: Pick<
188
263
  BlueprintGraphNode,
@@ -211,6 +286,7 @@ export function resolveNodeFetchConfig(
211
286
  ...DEFAULT_FETCH_REQUEST_CONFIG.headers,
212
287
  ...node.fetchConfig?.headers,
213
288
  },
289
+ headersJson: node.fetchConfig?.headersJson,
214
290
  swaggerEndpoints: node.fetchConfig?.swaggerEndpoints,
215
291
  };
216
292
  }
@@ -242,6 +318,15 @@ export function resolveNodeClockConfig(
242
318
  });
243
319
  }
244
320
 
321
+ export function resolveNodeEventConfig(
322
+ node: Pick<BlueprintGraphNode, "eventConfig">
323
+ ): ViewEventNodeConfig {
324
+ return normalizeViewEventConfig({
325
+ ...DEFAULT_VIEW_EVENT_NODE_CONFIG,
326
+ ...node.eventConfig,
327
+ });
328
+ }
329
+
245
330
  export type BlueprintGraphEdge = {
246
331
  id: string;
247
332
  source: string;
@@ -274,12 +359,18 @@ const noInputTargetNodeIds = (document: BlueprintDocument) =>
274
359
  .map((node) => node.id)
275
360
  );
276
361
 
277
- /** 生命周期节点无输入口:移除连入边,并修正节点配置 */
362
+ function isLifecycleSourceNode(node: BlueprintGraphNode | undefined): boolean {
363
+ return Boolean(node) && resolveRunnableNodeType(node!) === LIFECYCLE_NODE_TYPE;
364
+ }
365
+
366
+ function isEventTargetNode(node: BlueprintGraphNode | undefined): boolean {
367
+ return Boolean(node) && resolveBlueprintConfigSource(node!) === "event";
368
+ }
369
+
370
+ /** 生命周期节点无输入口;事件节点输入必须来自生命周期节点 */
278
371
  export function sanitizeBlueprintDocument(
279
372
  document: BlueprintDocument
280
373
  ): BlueprintDocument {
281
- const blockedTargets = noInputTargetNodeIds(document);
282
-
283
374
  const nodes = document.nodes.map((node) => {
284
375
  let next = node;
285
376
  if (node.viewElementId && !node.viewElementIds?.length) {
@@ -296,6 +387,9 @@ export function sanitizeBlueprintDocument(
296
387
  if (next.role === "lifecycle") {
297
388
  return { ...next, configSource: "lifecycle" as const };
298
389
  }
390
+ if (next.role === "event") {
391
+ return { ...next, configSource: "event" as const };
392
+ }
299
393
  if (next.role === "clock") {
300
394
  return { ...next, configSource: "clock" as const };
301
395
  }
@@ -308,13 +402,16 @@ export function sanitizeBlueprintDocument(
308
402
  if (next.role === "and") {
309
403
  return { ...next, configSource: "and" as const };
310
404
  }
311
- if (next.configSource === "lifecycle" || next.configSource === "clock") {
405
+ if (next.configSource === "lifecycle" || next.configSource === "clock" || next.configSource === "event") {
312
406
  return { ...next, configSource: undefined };
313
407
  }
314
408
  return next;
315
409
  });
316
410
 
317
- const edges = document.edges.filter((edge) => !blockedTargets.has(edge.target));
411
+ const edges = filterInvalidBlueprintEdges(
412
+ { ...document, nodes },
413
+ document.edges
414
+ );
318
415
 
319
416
  return { ...document, nodes, edges };
320
417
  }
@@ -324,7 +421,15 @@ export function filterInvalidBlueprintEdges(
324
421
  edges: BlueprintGraphEdge[]
325
422
  ): BlueprintGraphEdge[] {
326
423
  const blockedTargets = noInputTargetNodeIds(document);
327
- return edges.filter((edge) => !blockedTargets.has(edge.target));
424
+ const byId = new Map(document.nodes.map((node) => [node.id, node]));
425
+ return edges.filter((edge) => {
426
+ if (blockedTargets.has(edge.target)) return false;
427
+ const target = byId.get(edge.target);
428
+ if (isEventTargetNode(target)) {
429
+ return isLifecycleSourceNode(byId.get(edge.source));
430
+ }
431
+ return true;
432
+ });
328
433
  }
329
434
 
330
435
  /** 切换配置类型时同步节点 role / nodeType,确保生命周期节点渲染为无输入口 */
@@ -346,6 +451,7 @@ export function patchNodeConfigSource(
346
451
  jsonConfig: undefined,
347
452
  logicConfig: undefined,
348
453
  clockConfig: undefined,
454
+ eventConfig: undefined,
349
455
  };
350
456
  }
351
457
 
@@ -362,6 +468,7 @@ export function patchNodeConfigSource(
362
468
  fetchConfig: undefined,
363
469
  jsonConfig: undefined,
364
470
  logicConfig: undefined,
471
+ eventConfig: undefined,
365
472
  };
366
473
  }
367
474
 
@@ -378,6 +485,7 @@ export function patchNodeConfigSource(
378
485
  jsonConfig: undefined,
379
486
  logicConfig: undefined,
380
487
  clockConfig: undefined,
488
+ eventConfig: undefined,
381
489
  };
382
490
  }
383
491
 
@@ -394,6 +502,7 @@ export function patchNodeConfigSource(
394
502
  jsonConfig: resolveNodeJsonConfig(node),
395
503
  logicConfig: undefined,
396
504
  clockConfig: undefined,
505
+ eventConfig: undefined,
397
506
  };
398
507
  }
399
508
 
@@ -410,6 +519,7 @@ export function patchNodeConfigSource(
410
519
  jsonConfig: undefined,
411
520
  logicConfig: undefined,
412
521
  clockConfig: undefined,
522
+ eventConfig: undefined,
413
523
  };
414
524
  }
415
525
 
@@ -426,6 +536,7 @@ export function patchNodeConfigSource(
426
536
  jsonConfig: undefined,
427
537
  logicConfig: resolveNodeLogicConfig(node),
428
538
  clockConfig: undefined,
539
+ eventConfig: undefined,
429
540
  };
430
541
  }
431
542
 
@@ -442,6 +553,22 @@ export function patchNodeConfigSource(
442
553
  jsonConfig: undefined,
443
554
  logicConfig: undefined,
444
555
  clockConfig: undefined,
556
+ eventConfig: undefined,
557
+ };
558
+ }
559
+
560
+ if (configSource === "event") {
561
+ return {
562
+ role: "event",
563
+ nodeType: EVENT_NODE_TYPE,
564
+ configSource: "event",
565
+ eventConfig: resolveNodeEventConfig(node),
566
+ lifecyclePhase: undefined,
567
+ libraryBlueprintId: undefined,
568
+ fetchConfig: undefined,
569
+ jsonConfig: undefined,
570
+ logicConfig: undefined,
571
+ clockConfig: undefined,
445
572
  };
446
573
  }
447
574
 
@@ -455,5 +582,6 @@ export function patchNodeConfigSource(
455
582
  jsonConfig: undefined,
456
583
  logicConfig: undefined,
457
584
  clockConfig: undefined,
585
+ eventConfig: undefined,
458
586
  };
459
587
  }
@@ -10,6 +10,8 @@ import {
10
10
  resolveNodeJsonConfig,
11
11
  resolveNodeLogicConfig,
12
12
  resolveViewElementIds,
13
+ resolveNodeEventConfig,
14
+ getViewEventTypeLabel,
13
15
  } from "./document";
14
16
  import type { BlueprintFlowNodeData } from "./react-flow-adapter";
15
17
 
@@ -94,6 +96,24 @@ export function resolveBlueprintNodeSummary(
94
96
  }
95
97
  return t("blueprint.node.summaryViewMany", { count: ids.length });
96
98
  }
99
+ case "event": {
100
+ const ids = resolveViewElementIds(data);
101
+ const types = resolveNodeEventConfig(data).eventTypes;
102
+ const typeLabel = types.map((type) => getViewEventTypeLabel(t, type)).join(" / ");
103
+ if (ids.length === 0) {
104
+ return truncate(
105
+ t("blueprint.node.summaryEventNoView", { events: typeLabel }),
106
+ 44
107
+ );
108
+ }
109
+ return truncate(
110
+ t("blueprint.node.summaryEvent", {
111
+ events: typeLabel,
112
+ count: ids.length,
113
+ }),
114
+ 44
115
+ );
116
+ }
97
117
  case "and":
98
118
  return t("blueprint.node.summaryAnd");
99
119
  default:
@@ -6,4 +6,5 @@ export {
6
6
  JSON_NODE_TYPE,
7
7
  CLOCK_NODE_TYPE,
8
8
  AND_NODE_TYPE,
9
+ EVENT_NODE_TYPE,
9
10
  } from "@arronqzy/blueprint-dsl";
@@ -20,6 +20,7 @@ export type BlueprintFlowNodeData = {
20
20
  fetchConfig?: BlueprintGraphNode["fetchConfig"];
21
21
  jsonConfig?: BlueprintGraphNode["jsonConfig"];
22
22
  clockConfig?: BlueprintGraphNode["clockConfig"];
23
+ eventConfig?: BlueprintGraphNode["eventConfig"];
23
24
  logicConfig?: BlueprintGraphNode["logicConfig"];
24
25
  /** 调试执行时时钟节点已发送次数 */
25
26
  clockEmitProgress?: { current: number; total: number } | null;
@@ -70,6 +71,8 @@ export function toReactFlowNodes(document: BlueprintDocument): ReactFlowNodeView
70
71
  ? "fetch"
71
72
  : n.role === "json"
72
73
  ? "json"
74
+ : n.role === "event"
75
+ ? "event"
73
76
  : "logic",
74
77
  position: { ...n.position },
75
78
  width: BP_FLOW_NODE_WIDTH,
@@ -91,6 +94,7 @@ export function toReactFlowNodes(document: BlueprintDocument): ReactFlowNodeView
91
94
  fetchConfig: n.fetchConfig,
92
95
  jsonConfig: n.jsonConfig,
93
96
  clockConfig: n.clockConfig,
97
+ eventConfig: n.eventConfig,
94
98
  logicConfig: n.logicConfig,
95
99
  },
96
100
  }));
@@ -48,12 +48,13 @@ export function nodeStructureSignature(
48
48
  jsonConfig?: { jsonString?: string };
49
49
  logicConfig?: { sourceCode?: string };
50
50
  clockConfig?: { intervalSeconds?: number; timeFormat?: string; outputCount?: number; emitImmediately?: boolean };
51
+ eventConfig?: { eventTypes?: string[] };
51
52
  }>
52
53
  ): string {
53
54
  return nodes
54
55
  .map(
55
56
  (n) =>
56
- `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${(n.viewElementIds ?? (n.viewElementId ? [n.viewElementId] : [])).join(",")}:${n.fetchConfig?.url ?? ""}:${n.fetchConfig?.method ?? ""}:${n.fetchConfig?.apiBaseUrl ?? ""}:${n.fetchConfig?.swaggerDocsUrl ?? ""}:${n.jsonConfig?.jsonString ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}`
57
+ `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${(n.viewElementIds ?? (n.viewElementId ? [n.viewElementId] : [])).join(",")}:${n.fetchConfig?.url ?? ""}:${n.fetchConfig?.method ?? ""}:${n.fetchConfig?.apiBaseUrl ?? ""}:${n.fetchConfig?.swaggerDocsUrl ?? ""}:${n.jsonConfig?.jsonString ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}:${(n.eventConfig?.eventTypes ?? []).join(",")}`
57
58
  )
58
59
  .sort()
59
60
  .join("|");
@@ -1,4 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import { toast } from "@arronqzy/ui";
2
4
  import {
3
5
  addEdge,
4
6
  useEdgesState,
@@ -7,12 +9,16 @@ import {
7
9
  type Connection,
8
10
  type Edge,
9
11
  type EdgeChange,
12
+ type FinalConnectionState,
10
13
  type Node,
11
14
  type NodeChange,
12
15
  } from "@xyflow/react";
13
16
 
14
17
  import type { BlueprintGraph } from "../graph/blueprint-graph";
15
- import { sanitizeBlueprintDocument } from "../graph/document";
18
+ import {
19
+ resolveBlueprintConnectionIssue,
20
+ sanitizeBlueprintDocument,
21
+ } from "../graph/document";
16
22
  import {
17
23
  applyFlowNodePositions,
18
24
  BP_EDGE_STYLE,
@@ -452,6 +458,8 @@ export function useBlueprintFlowState({
452
458
  [getNodes, onGraphChange]
453
459
  );
454
460
 
461
+ const { t } = useI18n();
462
+
455
463
  const isValidConnection = useCallback(
456
464
  (connection: Edge | Connection) => {
457
465
  if (!connection.source || !connection.target) return false;
@@ -464,16 +472,40 @@ export function useBlueprintFlowState({
464
472
  }
465
473
 
466
474
  const targetNode = rfNodes.find((n) => n.id === connection.target);
467
- // 生命周期节点无输入口,禁止连入
468
- if (targetNode?.data.role === "lifecycle") {
469
- return false;
470
- }
471
-
472
- return true;
475
+ const sourceNode = rfNodes.find((n) => n.id === connection.source);
476
+ return !resolveBlueprintConnectionIssue(sourceNode?.data, targetNode?.data);
473
477
  },
474
478
  [getNodes]
475
479
  );
476
480
 
481
+ const onConnectEnd = useCallback(
482
+ (_event: MouseEvent | TouchEvent, state: FinalConnectionState) => {
483
+ if (state.isValid) return;
484
+ const fromNode = state.fromNode as Node<BlueprintFlowNodeData> | null;
485
+ const toNode = state.toNode as Node<BlueprintFlowNodeData> | null;
486
+ if (!fromNode?.data || !toNode?.data) return;
487
+
488
+ const fromIsSource = state.fromHandle?.type !== "target";
489
+ const sourceData = fromIsSource ? fromNode.data : toNode.data;
490
+ const targetData = fromIsSource ? toNode.data : fromNode.data;
491
+ const issue = resolveBlueprintConnectionIssue(sourceData, targetData);
492
+ if (issue === "event-requires-lifecycle") {
493
+ toast({
494
+ title: t("blueprint.config.eventConnectNeedLifecycle"),
495
+ variant: "destructive",
496
+ });
497
+ return;
498
+ }
499
+ if (issue === "lifecycle-no-input") {
500
+ toast({
501
+ title: t("blueprint.config.lifecycleNoInput"),
502
+ variant: "destructive",
503
+ });
504
+ }
505
+ },
506
+ [t]
507
+ );
508
+
477
509
  return {
478
510
  nodes,
479
511
  edges,
@@ -481,6 +513,7 @@ export function useBlueprintFlowState({
481
513
  onEdgesChange,
482
514
  onNodeDragStop,
483
515
  onConnect,
516
+ onConnectEnd,
484
517
  isValidConnection,
485
518
  };
486
519
  }