@arronqzy/react-blueprint 1.0.6 → 1.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-blueprint",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.ts",
6
6
  "exports": {
@@ -12,9 +12,9 @@
12
12
  "dayjs": "^1.11.13",
13
13
  "react-dom": "^19",
14
14
  "@xyflow/react": "^12.9.3",
15
- "@arronqzy/blueprint-dsl": "1.0.5",
16
- "@arronqzy/i18n": "0.1.3",
15
+ "@arronqzy/blueprint-dsl": "1.0.6",
17
16
  "@arronqzy/react-rx-store": "1.1.4",
17
+ "@arronqzy/i18n": "0.1.5",
18
18
  "@arronqzy/ui": "1.0.5"
19
19
  },
20
20
  "devDependencies": {
@@ -29,8 +29,8 @@
29
29
  "typescript": "^5.5.4",
30
30
  "vite": "^5.1.4",
31
31
  "@arronqzy/tailwind": "1.0.2",
32
- "@arronqzy/typescript-config": "1.0.0",
33
- "@arronqzy/eslint-config": "0.0.0"
32
+ "@arronqzy/eslint-config": "0.0.0",
33
+ "@arronqzy/typescript-config": "1.0.0"
34
34
  },
35
35
  "keywords": [],
36
36
  "author": "",
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  LIFECYCLE_NODE_TYPE,
12
12
  PAGE_LIFECYCLE_PHASES,
13
+ type ExecutionTraceEntry,
13
14
  type PageLifecyclePhase,
14
15
  } from "@arronqzy/blueprint-dsl";
15
16
  import { useI18n } from "@arronqzy/i18n/react";
@@ -19,6 +20,7 @@ import { ClockNodeConfigPanel } from "./components/ClockNodeConfigPanel";
19
20
  import { JsonNodeConfigPanel } from "./components/JsonNodeConfigPanel";
20
21
  import { LogicNodeConfigPanel } from "./components/LogicNodeConfigPanel";
21
22
  import { ViewElementMultiSelect } from "./components/ViewElementMultiSelect";
23
+ import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./components/ConfigHintIcon";
22
24
  import {
23
25
  getLifecyclePhaseLabel,
24
26
  patchNodeConfigSource,
@@ -26,6 +28,7 @@ import {
26
28
  resolveBlueprintConfigSource,
27
29
  resolveViewElementIds,
28
30
  type BlueprintConfigSource,
31
+ type BlueprintGraphEdge,
29
32
  type BlueprintGraphNode,
30
33
  type BlueprintNodeRole,
31
34
  } from "./graph/document";
@@ -42,6 +45,9 @@ export type BlueprintLibraryOption = {
42
45
 
43
46
  export type BlueprintNodeConfigSidebarProps = {
44
47
  node: BlueprintGraphNode;
48
+ graphNodes?: BlueprintGraphNode[];
49
+ graphEdges?: BlueprintGraphEdge[];
50
+ traceEntries?: ExecutionTraceEntry[];
45
51
  viewElementOptions?: BlueprintViewElementOption[];
46
52
  blueprintLibraryOptions?: BlueprintLibraryOption[];
47
53
  allowFalseSignalPropagation?: boolean;
@@ -81,6 +87,9 @@ const ROLE_LABEL_KEYS: Record<BlueprintNodeRole, string> = {
81
87
 
82
88
  export function BlueprintNodeConfigSidebar({
83
89
  node,
90
+ graphNodes = [],
91
+ graphEdges = [],
92
+ traceEntries = [],
84
93
  viewElementOptions = [],
85
94
  blueprintLibraryOptions = [],
86
95
  allowFalseSignalPropagation = false,
@@ -181,7 +190,10 @@ export function BlueprintNodeConfigSidebar({
181
190
 
182
191
  {configSource === "view" ? (
183
192
  <div className="block space-y-1">
184
- <span className="text-muted-foreground">{t("blueprint.config.linkedViewNodes")}</span>
193
+ <ConfigFieldLabel
194
+ label={t("blueprint.config.linkedViewNodes")}
195
+ hint={t("blueprint.config.viewMultiHint")}
196
+ />
185
197
  <ViewElementMultiSelect
186
198
  options={viewElementOptions}
187
199
  value={linkedViewElementIds}
@@ -194,11 +206,7 @@ export function BlueprintNodeConfigSidebar({
194
206
  })
195
207
  }
196
208
  />
197
- {linkedViewElementIds.length === 0 ? (
198
- <p className="text-[11px] text-muted-foreground">
199
- {t("blueprint.config.viewMultiHint")}
200
- </p>
201
- ) : (
209
+ {linkedViewElementIds.length > 0 ? (
202
210
  <p className="text-[11px] text-muted-foreground">
203
211
  {t("blueprint.config.linkedViewCount", {
204
212
  count: linkedViewElementIds.length,
@@ -207,16 +215,16 @@ export function BlueprintNodeConfigSidebar({
207
215
  .join("、"),
208
216
  })}
209
217
  </p>
210
- )}
218
+ ) : null}
211
219
  </div>
212
220
  ) : null}
213
221
 
214
222
  {configSource === "blueprint" ? (
215
223
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
216
- <div className="font-medium text-foreground">{t("blueprint.config.blueprintAttrs")}</div>
217
- <p className="text-[11px] text-muted-foreground">
218
- {t("blueprint.config.blueprintAttrsHint")}
219
- </p>
224
+ <ConfigSectionTitle
225
+ title={t("blueprint.config.blueprintAttrs")}
226
+ hint={t("blueprint.config.blueprintAttrsHint")}
227
+ />
220
228
  <label className="block space-y-1">
221
229
  <span className="text-muted-foreground">{t("blueprint.config.refLibrary")}</span>
222
230
  <Select
@@ -264,13 +272,19 @@ export function BlueprintNodeConfigSidebar({
264
272
 
265
273
  {configSource === "lifecycle" ? (
266
274
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
267
- <div className="font-medium text-foreground">{t("blueprint.config.lifecycleHook")}</div>
268
- <p className="text-[11px] text-muted-foreground">
269
- {t("blueprint.config.lifecycleNoInput")}
270
- {node.lifecyclePhase === "blueprintActivated"
271
- ? t("blueprint.config.lifecycleBlueprintActivatedHint")
272
- : t("blueprint.config.lifecyclePageHint")}
273
- </p>
275
+ <div className="flex items-center gap-1.5">
276
+ <div className="font-medium text-foreground">
277
+ {t("blueprint.config.lifecycleHook")}
278
+ </div>
279
+ <ConfigHintIcon label={t("blueprint.config.lifecycleHook")}>
280
+ <p>{t("blueprint.config.lifecycleNoInput")}</p>
281
+ <p>
282
+ {node.lifecyclePhase === "blueprintActivated"
283
+ ? t("blueprint.config.lifecycleBlueprintActivatedHint")
284
+ : t("blueprint.config.lifecyclePageHint")}
285
+ </p>
286
+ </ConfigHintIcon>
287
+ </div>
274
288
  <label className="block space-y-1">
275
289
  <span className="text-muted-foreground">{t("blueprint.config.listenPhase")}</span>
276
290
  <Select
@@ -305,7 +319,13 @@ export function BlueprintNodeConfigSidebar({
305
319
  ) : null}
306
320
 
307
321
  {configSource === "fetch" ? (
308
- <FetchNodeConfigPanel node={node} onUpdateNode={onUpdateNode} />
322
+ <FetchNodeConfigPanel
323
+ node={node}
324
+ graphNodes={graphNodes}
325
+ graphEdges={graphEdges}
326
+ traceEntries={traceEntries}
327
+ onUpdateNode={onUpdateNode}
328
+ />
309
329
  ) : null}
310
330
 
311
331
  {configSource === "json" ? (
@@ -318,10 +338,10 @@ export function BlueprintNodeConfigSidebar({
318
338
 
319
339
  {configSource === "and" ? (
320
340
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
321
- <div className="font-medium text-foreground">{t("blueprint.config.andTitle")}</div>
322
- <p className="text-[11px] text-muted-foreground">
323
- {t("blueprint.config.andHint")}
324
- </p>
341
+ <ConfigSectionTitle
342
+ title={t("blueprint.config.andTitle")}
343
+ hint={t("blueprint.config.andHint")}
344
+ />
325
345
  </div>
326
346
  ) : null}
327
347
 
@@ -5,6 +5,7 @@ import { useI18n } from "@arronqzy/i18n/react";
5
5
 
6
6
  import type { BlueprintGraphNode } from "../graph/document";
7
7
  import { resolveNodeClockConfig } from "../graph/document";
8
+ import { ConfigFieldLabel, ConfigSectionTitle } from "./ConfigHintIcon";
8
9
 
9
10
  export type ClockNodeConfigPanelProps = {
10
11
  node: BlueprintGraphNode;
@@ -73,17 +74,16 @@ export function ClockNodeConfigPanel({
73
74
 
74
75
  return (
75
76
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
76
- <div className="font-medium text-foreground">
77
- {t("blueprint.config.clockTitle")}
78
- </div>
79
- <p className="text-[11px] text-muted-foreground">
80
- {t("blueprint.config.clockHint")}
81
- </p>
77
+ <ConfigSectionTitle
78
+ title={t("blueprint.config.clockTitle")}
79
+ hint={t("blueprint.config.clockHint")}
80
+ />
82
81
 
83
82
  <label className="block space-y-1">
84
- <span className="text-muted-foreground">
85
- {t("blueprint.config.clockIntervalSeconds")}
86
- </span>
83
+ <ConfigFieldLabel
84
+ label={t("blueprint.config.clockIntervalSeconds")}
85
+ hint={t("blueprint.config.clockIntervalHint")}
86
+ />
87
87
  <Input
88
88
  type="number"
89
89
  min={0}
@@ -92,15 +92,13 @@ export function ClockNodeConfigPanel({
92
92
  onChange={handleIntervalChange}
93
93
  className="h-8"
94
94
  />
95
- <p className="text-[11px] text-muted-foreground">
96
- {t("blueprint.config.clockIntervalHint")}
97
- </p>
98
95
  </label>
99
96
 
100
97
  <label className="block space-y-1">
101
- <span className="text-muted-foreground">
102
- {t("blueprint.config.outputCount")}
103
- </span>
98
+ <ConfigFieldLabel
99
+ label={t("blueprint.config.outputCount")}
100
+ hint={t("blueprint.config.outputCountHint")}
101
+ />
104
102
  <Input
105
103
  type="number"
106
104
  min={1}
@@ -109,9 +107,6 @@ export function ClockNodeConfigPanel({
109
107
  onChange={handleOutputCountChange}
110
108
  className="h-8"
111
109
  />
112
- <p className="text-[11px] text-muted-foreground">
113
- {t("blueprint.config.outputCountHint")}
114
- </p>
115
110
  </label>
116
111
 
117
112
  <label className="flex items-start gap-2">
@@ -127,9 +122,10 @@ export function ClockNodeConfigPanel({
127
122
  </label>
128
123
 
129
124
  <label className="block space-y-1">
130
- <span className="text-muted-foreground">
131
- {t("blueprint.config.timeFormat")}
132
- </span>
125
+ <ConfigFieldLabel
126
+ label={t("blueprint.config.timeFormat")}
127
+ hint={t("blueprint.config.timeFormatHint")}
128
+ />
133
129
  <Input
134
130
  value={clockConfig.timeFormat}
135
131
  onChange={handleFormatChange}
@@ -137,9 +133,6 @@ export function ClockNodeConfigPanel({
137
133
  className="h-8 font-mono text-[11px]"
138
134
  placeholder="YYYY-MM-DD HH:mm:ss"
139
135
  />
140
- <p className="text-[11px] text-muted-foreground">
141
- {t("blueprint.config.timeFormatHint")}
142
- </p>
143
136
  </label>
144
137
  </div>
145
138
  );
@@ -0,0 +1,83 @@
1
+ import type { ReactNode } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipProvider,
7
+ TooltipTrigger,
8
+ } from "@arronqzy/ui";
9
+
10
+ export type ConfigHintIconProps = {
11
+ label?: string;
12
+ children: ReactNode;
13
+ className?: string;
14
+ contentClassName?: string;
15
+ };
16
+
17
+ export function ConfigHintIcon({
18
+ label,
19
+ children,
20
+ className,
21
+ contentClassName,
22
+ }: ConfigHintIconProps) {
23
+ const { t } = useI18n();
24
+ const resolvedLabel = label ?? t("common.hint");
25
+
26
+ return (
27
+ <TooltipProvider delayDuration={120}>
28
+ <Tooltip>
29
+ <TooltipTrigger asChild>
30
+ <button
31
+ type="button"
32
+ className={
33
+ className ??
34
+ "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
35
+ }
36
+ aria-label={t("common.hintAria", { label: resolvedLabel })}
37
+ >
38
+ ?
39
+ </button>
40
+ </TooltipTrigger>
41
+ <TooltipContent
42
+ side="top"
43
+ className={`z-[10120] max-w-[360px] space-y-1.5 text-[11px] leading-5 ${contentClassName ?? ""}`}
44
+ >
45
+ {children}
46
+ </TooltipContent>
47
+ </Tooltip>
48
+ </TooltipProvider>
49
+ );
50
+ }
51
+
52
+ export function ConfigSectionTitle({
53
+ title,
54
+ hint,
55
+ }: {
56
+ title: string;
57
+ hint: ReactNode;
58
+ }) {
59
+ return (
60
+ <div className="flex items-center gap-1.5">
61
+ <div className="font-medium text-foreground">{title}</div>
62
+ <ConfigHintIcon label={title}>{hint}</ConfigHintIcon>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export function ConfigFieldLabel({
68
+ label,
69
+ hint,
70
+ }: {
71
+ label: string;
72
+ hint?: ReactNode;
73
+ }) {
74
+ if (!hint) {
75
+ return <span className="text-muted-foreground">{label}</span>;
76
+ }
77
+ return (
78
+ <span className="inline-flex items-center gap-1 text-muted-foreground">
79
+ {label}
80
+ <ConfigHintIcon label={label}>{hint}</ConfigHintIcon>
81
+ </span>
82
+ );
83
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useState, type ChangeEvent } from "react";
1
+ import { useCallback, useMemo, useState, type ChangeEvent } from "react";
2
2
  import {
3
3
  Button,
4
4
  Input,
@@ -10,11 +10,14 @@ import {
10
10
  cn,
11
11
  } from "@arronqzy/ui";
12
12
  import type {
13
+ ExecutionTraceEntry,
13
14
  FetchHttpMethod,
14
15
  FetchRequestConfig,
15
16
  FetchResponseType,
16
17
  } from "@arronqzy/blueprint-dsl";
17
18
  import {
19
+ applyFetchConfigScope,
20
+ draftFetchHeadersText,
18
21
  FETCH_CACHES,
19
22
  FETCH_CREDENTIALS,
20
23
  FETCH_HTTP_METHODS,
@@ -22,9 +25,14 @@ import {
22
25
  FETCH_NODE_TYPE,
23
26
  FETCH_REDIRECTS,
24
27
  FETCH_RESPONSE_TYPES,
28
+ fetchConfigHasScopeTemplate,
29
+ latestTraceOutputsByNode,
30
+ parseFetchHeadersJson,
31
+ resolveFetchIncomingScope,
32
+ resolveFetchRequestUrl,
25
33
  } from "@arronqzy/blueprint-dsl";
26
34
 
27
- import type { BlueprintGraphNode } from "../graph/document";
35
+ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
28
36
  import { resolveNodeFetchConfig } from "../graph/document";
29
37
  import {
30
38
  cancelSwaggerLoadTask,
@@ -36,6 +44,7 @@ import {
36
44
  } from "../fetch-config-task-store";
37
45
  import { useI18n } from "@arronqzy/i18n/react";
38
46
 
47
+ import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon";
39
48
  import { FetchUrlAutocomplete } from "./FetchUrlAutocomplete";
40
49
 
41
50
  function SendIcon({ className }: { className?: string }) {
@@ -231,6 +240,9 @@ function LoaderIcon({ className }: { className?: string }) {
231
240
 
232
241
  export type FetchNodeConfigPanelProps = {
233
242
  node: BlueprintGraphNode;
243
+ graphNodes?: BlueprintGraphNode[];
244
+ graphEdges?: BlueprintGraphEdge[];
245
+ traceEntries?: ExecutionTraceEntry[];
234
246
  onUpdateNode: (
235
247
  nodeId: string,
236
248
  patch: Partial<Pick<BlueprintGraphNode, "fetchConfig" | "configSource">>
@@ -251,6 +263,9 @@ function patchFetchConfig(
251
263
 
252
264
  export function FetchNodeConfigPanel({
253
265
  node,
266
+ graphNodes = [],
267
+ graphEdges = [],
268
+ traceEntries = [],
254
269
  onUpdateNode,
255
270
  }: FetchNodeConfigPanelProps) {
256
271
  const { t } = useI18n();
@@ -268,6 +283,55 @@ export function FetchNodeConfigPanel({
268
283
  const [validationError, setValidationError] = useState<string | null>(null);
269
284
  const [fetchValidationError, setFetchValidationError] = useState<string | null>(null);
270
285
  const swaggerError = validationError ?? swaggerTaskError;
286
+ const incomingScope = useMemo(() => {
287
+ const outputs = latestTraceOutputsByNode(traceEntries);
288
+ return resolveFetchIncomingScope({
289
+ fetchNodeId: node.id,
290
+ nodes: graphNodes,
291
+ edges: graphEdges,
292
+ getOutput: (sourceId, port) => outputs[sourceId]?.[port],
293
+ });
294
+ }, [graphEdges, graphNodes, node.id, traceEntries]);
295
+ const usesScopeTemplate = fetchConfigHasScopeTemplate(fetchConfig);
296
+ const resolvedFetchConfig = useMemo(
297
+ () => applyFetchConfigScope(fetchConfig, incomingScope),
298
+ [fetchConfig, incomingScope]
299
+ );
300
+ const resolvedUrlPreview = useMemo(() => {
301
+ if (!usesScopeTemplate) return "";
302
+ try {
303
+ return resolveFetchRequestUrl(resolvedFetchConfig);
304
+ } catch {
305
+ return resolvedFetchConfig.url?.trim() ?? "";
306
+ }
307
+ }, [resolvedFetchConfig, usesScopeTemplate]);
308
+ const incomingScopeJson = useMemo(() => {
309
+ if (incomingScope === undefined) return "";
310
+ try {
311
+ return JSON.stringify(incomingScope, null, 2);
312
+ } catch {
313
+ return String(incomingScope);
314
+ }
315
+ }, [incomingScope]);
316
+ const headersDraft = draftFetchHeadersText(fetchConfig);
317
+ const resolvedHeadersPreview = useMemo(() => {
318
+ if (!usesScopeTemplate) return "";
319
+ const headers = resolvedFetchConfig.headers;
320
+ if (!headers || Object.keys(headers).length === 0) return "";
321
+ try {
322
+ return JSON.stringify(headers, null, 2);
323
+ } catch {
324
+ return "";
325
+ }
326
+ }, [resolvedFetchConfig.headers, usesScopeTemplate]);
327
+ const incomingHasPendingValue = useMemo(() => {
328
+ if (!incomingScope || typeof incomingScope !== "object") return false;
329
+ const entries =
330
+ "kind" in incomingScope
331
+ ? [incomingScope]
332
+ : Object.values(incomingScope as Record<string, { kind?: string }>);
333
+ return entries.some((entry) => entry && entry.kind === "pending");
334
+ }, [incomingScope]);
271
335
 
272
336
  const setUrlInputMode = useCallback(
273
337
  (mode: "swagger" | "manual") => {
@@ -307,30 +371,72 @@ export function FetchNodeConfigPanel({
307
371
  }, [node.id]);
308
372
 
309
373
  const handleSendFetchDebug = useCallback(() => {
310
- const url = fetchConfig.url?.trim();
374
+ const resolved = applyFetchConfigScope(fetchConfig, incomingScope);
375
+ const url = resolved.url?.trim();
311
376
  if (!url) {
312
- setFetchValidationError(t("blueprint.config.fillRequestUrlFirst"));
377
+ setFetchValidationError(
378
+ usesScopeTemplate && fetchConfig.url?.trim()
379
+ ? t("blueprint.config.fetchScopeUnresolved")
380
+ : t("blueprint.config.fillRequestUrlFirst")
381
+ );
313
382
  return;
314
383
  }
315
384
 
316
385
  setFetchValidationError(null);
317
386
  startFetchDebugTask({
318
387
  nodeId: node.id,
319
- config: fetchConfig,
388
+ config: resolved,
320
389
  });
321
- }, [fetchConfig, node.id]);
390
+ }, [fetchConfig, incomingScope, node.id, t, usesScopeTemplate]);
322
391
 
323
392
  const handleAbortFetchDebug = useCallback(() => {
324
393
  cancelFetchDebugTask(node.id);
325
394
  setFetchValidationError(null);
326
395
  }, [node.id]);
327
396
 
397
+ const handleHeadersBlur = useCallback(
398
+ (e: ChangeEvent<HTMLTextAreaElement>) => {
399
+ const headersJson = e.target.value;
400
+ const parsed = parseFetchHeadersJson(headersJson);
401
+ onUpdateNode(
402
+ node.id,
403
+ patchFetchConfig(node, {
404
+ headersJson,
405
+ ...(parsed ? { headers: parsed } : {}),
406
+ })
407
+ );
408
+ },
409
+ [node, onUpdateNode]
410
+ );
411
+
328
412
  return (
329
413
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
330
- <div className="font-medium text-foreground">{t("blueprint.config.fetchTitle")}</div>
331
- <p className="text-[11px] text-muted-foreground">
332
- {t("blueprint.config.fetchHint")}
333
- </p>
414
+ <ConfigSectionTitle
415
+ title={t("blueprint.config.fetchTitle")}
416
+ hint={t("blueprint.config.fetchHint")}
417
+ />
418
+ <div className="space-y-1 rounded-md border border-border/60 bg-background/70 p-2">
419
+ <div className="flex items-center gap-1.5 text-[11px] font-medium text-foreground">
420
+ {t("blueprint.config.fetchScopeTitle")}
421
+ <ConfigHintIcon label={t("blueprint.config.fetchScopeTitle")}>
422
+ <p>{t("blueprint.config.fetchScopeHint")}</p>
423
+ <p>{t("blueprint.config.fetchScopeEmpty")}</p>
424
+ <p>{t("blueprint.config.fetchScopePending")}</p>
425
+ </ConfigHintIcon>
426
+ </div>
427
+ {incomingScope === undefined ? null : (
428
+ <>
429
+ {incomingHasPendingValue ? (
430
+ <p className="text-[11px] text-amber-700 dark:text-amber-300">
431
+ {t("blueprint.config.fetchScopePending")}
432
+ </p>
433
+ ) : null}
434
+ <pre className="max-h-[180px] overflow-auto rounded border border-border/60 bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground">
435
+ {incomingScopeJson}
436
+ </pre>
437
+ </>
438
+ )}
439
+ </div>
334
440
 
335
441
  <label className="block space-y-1">
336
442
  <span className="text-muted-foreground">{t("blueprint.config.swaggerUrlOptional")}</span>
@@ -440,7 +546,7 @@ export function FetchNodeConfigPanel({
440
546
  patchFetchConfig(node, { apiBaseUrl: e.target.value })
441
547
  )
442
548
  }
443
- placeholder="https://api.example.com/v1"
549
+ placeholder="https://api.example.com/v1/{scope?.value?.tenant}"
444
550
  className="h-8 font-mono text-[11px]"
445
551
  />
446
552
  </label>
@@ -489,7 +595,7 @@ export function FetchNodeConfigPanel({
489
595
  onChange={(e: ChangeEvent<HTMLInputElement>) =>
490
596
  onUpdateNode(node.id, patchFetchConfig(node, { url: e.target.value }))
491
597
  }
492
- placeholder="https://api.example.com/data"
598
+ placeholder="https://api.example.com/users/{scope?.value?.id}"
493
599
  className="h-8 flex-1 font-mono text-[11px]"
494
600
  />
495
601
  <AsyncSendButton
@@ -504,6 +610,11 @@ export function FetchNodeConfigPanel({
504
610
  />
505
611
  </div>
506
612
  )}
613
+ {usesScopeTemplate && resolvedUrlPreview ? (
614
+ <p className="break-all font-mono text-[10px] text-muted-foreground">
615
+ {t("blueprint.config.fetchScopeResolvedUrl")}: {resolvedUrlPreview}
616
+ </p>
617
+ ) : null}
507
618
  {loadingFetchDebug ? (
508
619
  <p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
509
620
  <LoaderIcon className="h-3.5 w-3.5 shrink-0" />
@@ -541,25 +652,23 @@ export function FetchNodeConfigPanel({
541
652
  </label>
542
653
 
543
654
  <label className="block space-y-1">
544
- <span className="text-muted-foreground">{t("blueprint.config.requestHeadersJson")}</span>
655
+ <ConfigFieldLabel
656
+ label={t("blueprint.config.requestHeadersJson")}
657
+ hint={t("blueprint.config.fetchHeadersHint")}
658
+ />
545
659
  <textarea
546
- defaultValue={JSON.stringify(fetchConfig.headers ?? {}, null, 2)}
547
- key={`${node.id}-headers-${JSON.stringify(fetchConfig.headers)}`}
548
- onBlur={(e) => {
549
- try {
550
- const headers = JSON.parse(e.target.value || "{}") as Record<
551
- string,
552
- string
553
- >;
554
- onUpdateNode(node.id, patchFetchConfig(node, { headers }));
555
- } catch {
556
- /* 保留上次有效值 */
557
- }
558
- }}
660
+ defaultValue={headersDraft}
661
+ key={`${node.id}-headers-${headersDraft}`}
662
+ onBlur={handleHeadersBlur}
559
663
  rows={4}
560
664
  className="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
561
- placeholder='{"Content-Type":"application/json"}'
665
+ placeholder='{"Authorization":"Bearer {scope?.value?.token}"}'
562
666
  />
667
+ {usesScopeTemplate && resolvedHeadersPreview ? (
668
+ <p className="whitespace-pre-wrap break-all font-mono text-[10px] text-muted-foreground">
669
+ {t("blueprint.config.fetchScopeResolvedHeaders")}: {resolvedHeadersPreview}
670
+ </p>
671
+ ) : null}
563
672
  </label>
564
673
 
565
674
  <label className="block space-y-1">
@@ -571,7 +680,7 @@ export function FetchNodeConfigPanel({
571
680
  }
572
681
  rows={4}
573
682
  className="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
574
- placeholder='{"key":"value"}'
683
+ placeholder='{"id":"{scope?.value?.id}"}'
575
684
  />
576
685
  </label>
577
686
 
@@ -5,6 +5,7 @@ import { useI18n } from "@arronqzy/i18n/react";
5
5
 
6
6
  import type { BlueprintGraphNode } from "../graph/document";
7
7
  import { resolveNodeJsonConfig } from "../graph/document";
8
+ import { ConfigSectionTitle } from "./ConfigHintIcon";
8
9
 
9
10
  export type JsonNodeConfigPanelProps = {
10
11
  node: BlueprintGraphNode;
@@ -53,12 +54,10 @@ export function JsonNodeConfigPanel({
53
54
 
54
55
  return (
55
56
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
56
- <div className="font-medium text-foreground">
57
- {t("blueprint.config.jsonTitle")}
58
- </div>
59
- <p className="text-[11px] text-muted-foreground">
60
- {t("blueprint.config.jsonHint")}
61
- </p>
57
+ <ConfigSectionTitle
58
+ title={t("blueprint.config.jsonTitle")}
59
+ hint={t("blueprint.config.jsonHint")}
60
+ />
62
61
 
63
62
  <label className="block space-y-1">
64
63
  <span className="text-muted-foreground">
@@ -5,6 +5,7 @@ import { useI18n } from "@arronqzy/i18n/react";
5
5
 
6
6
  import type { BlueprintGraphNode } from "../graph/document";
7
7
  import { resolveNodeLogicConfig } from "../graph/document";
8
+ import { ConfigSectionTitle } from "./ConfigHintIcon";
8
9
 
9
10
  export type LogicNodeConfigPanelProps = {
10
11
  node: BlueprintGraphNode;
@@ -52,12 +53,10 @@ export function LogicNodeConfigPanel({
52
53
 
53
54
  return (
54
55
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
55
- <div className="font-medium text-foreground">
56
- {t("blueprint.config.logicTitle")}
57
- </div>
58
- <p className="text-[11px] text-muted-foreground">
59
- {t("blueprint.config.logicHint")}
60
- </p>
56
+ <ConfigSectionTitle
57
+ title={t("blueprint.config.logicTitle")}
58
+ hint={t("blueprint.config.logicHint")}
59
+ />
61
60
 
62
61
  <label className="block space-y-1">
63
62
  <span className="text-muted-foreground">
@@ -211,6 +211,7 @@ export function resolveNodeFetchConfig(
211
211
  ...DEFAULT_FETCH_REQUEST_CONFIG.headers,
212
212
  ...node.fetchConfig?.headers,
213
213
  },
214
+ headersJson: node.fetchConfig?.headersJson,
214
215
  swaggerEndpoints: node.fetchConfig?.swaggerEndpoints,
215
216
  };
216
217
  }
@@ -23,6 +23,8 @@ export function documentToRunnableGraph(
23
23
  return {
24
24
  id: node.id,
25
25
  nodeType,
26
+ label: node.label,
27
+ role: node.role,
26
28
  lifecyclePhase: node.lifecyclePhase,
27
29
  libraryBlueprintId: node.libraryBlueprintId,
28
30
  viewElementIds: