@arronqzy/react-blueprint 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
4
4
  "description": "",
5
5
  "files": [
6
6
  "index.ts",
@@ -18,10 +18,10 @@
18
18
  "dayjs": "^1.11.13",
19
19
  "react-dom": "^19",
20
20
  "@xyflow/react": "^12.9.3",
21
- "@arronqzy/i18n": "0.1.11",
22
- "@arronqzy/react-rx-store": "1.1.8",
23
- "@arronqzy/blueprint-dsl": "1.0.14",
24
- "@arronqzy/ui": "1.0.9"
21
+ "@arronqzy/i18n": "0.1.13",
22
+ "@arronqzy/ui": "1.0.9",
23
+ "@arronqzy/blueprint-dsl": "1.0.15",
24
+ "@arronqzy/react-rx-store": "1.1.8"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/react": "^19.0.1",
@@ -34,9 +34,9 @@
34
34
  "eslint": "^8.57.0",
35
35
  "typescript": "^5.5.4",
36
36
  "vite": "^5.1.4",
37
+ "@arronqzy/eslint-config": "0.0.0",
37
38
  "@arronqzy/tailwind": "1.0.2",
38
- "@arronqzy/typescript-config": "1.0.0",
39
- "@arronqzy/eslint-config": "0.0.0"
39
+ "@arronqzy/typescript-config": "1.0.0"
40
40
  },
41
41
  "keywords": [],
42
42
  "author": "",
@@ -400,7 +400,13 @@ export function BlueprintNodeConfigSidebar({
400
400
  ) : null}
401
401
 
402
402
  {configSource === "json" ? (
403
- <JsonNodeConfigPanel node={node} onUpdateNode={onUpdateNode} />
403
+ <JsonNodeConfigPanel
404
+ node={node}
405
+ graphNodes={graphNodes}
406
+ graphEdges={graphEdges}
407
+ traceEntries={traceEntries}
408
+ onUpdateNode={onUpdateNode}
409
+ />
404
410
  ) : null}
405
411
 
406
412
  {configSource === "storage" ? (
@@ -1,14 +1,26 @@
1
1
  import { useCallback, useMemo, useState, type ChangeEvent } from "react";
2
- import { validateJsonString } from "@arronqzy/blueprint-dsl";
3
- import type { JsonNodeConfig } from "@arronqzy/blueprint-dsl";
2
+ import type {
3
+ ExecutionTraceEntry,
4
+ JsonNodeConfig,
5
+ } from "@arronqzy/blueprint-dsl";
6
+ import {
7
+ latestTraceOutputsByNode,
8
+ resolveFetchIncomingScope,
9
+ resolveFetchScopeAutocompleteRoot,
10
+ validateJsonStringAllowingScope,
11
+ } from "@arronqzy/blueprint-dsl";
4
12
  import { useI18n } from "@arronqzy/i18n/react";
5
13
 
6
- import type { BlueprintGraphNode } from "../graph/document";
14
+ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
7
15
  import { resolveNodeJsonConfig } from "../graph/document";
8
- import { ConfigSectionTitle } from "./ConfigHintIcon";
16
+ import { ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon";
17
+ import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost";
9
18
 
10
19
  export type JsonNodeConfigPanelProps = {
11
20
  node: BlueprintGraphNode;
21
+ graphNodes?: BlueprintGraphNode[];
22
+ graphEdges?: BlueprintGraphEdge[];
23
+ traceEntries?: ExecutionTraceEntry[];
12
24
  onUpdateNode: (
13
25
  nodeId: string,
14
26
  patch: Partial<Pick<BlueprintGraphNode, "jsonConfig" | "configSource">>
@@ -27,25 +39,44 @@ function patchJsonConfig(
27
39
 
28
40
  export function JsonNodeConfigPanel({
29
41
  node,
42
+ graphNodes = [],
43
+ graphEdges = [],
44
+ traceEntries = [],
30
45
  onUpdateNode,
31
46
  }: JsonNodeConfigPanelProps) {
32
47
  const { t } = useI18n();
48
+ const [formEl, setFormEl] = useState<HTMLDivElement | null>(null);
33
49
  const jsonConfig = resolveNodeJsonConfig(node);
34
50
  const [draftError, setDraftError] = useState<string | null>(null);
35
51
 
52
+ const incomingScope = useMemo(() => {
53
+ const outputs = latestTraceOutputsByNode(traceEntries);
54
+ return resolveFetchIncomingScope({
55
+ fetchNodeId: node.id,
56
+ nodes: graphNodes,
57
+ edges: graphEdges,
58
+ getOutput: (sourceId, port) => outputs[sourceId]?.[port],
59
+ });
60
+ }, [graphEdges, graphNodes, node.id, traceEntries]);
61
+ const autocompleteScope = resolveFetchScopeAutocompleteRoot(incomingScope);
62
+
36
63
  const storedValidation = useMemo(
37
- () => validateJsonString(jsonConfig.jsonString),
64
+ () => validateJsonStringAllowingScope(jsonConfig.jsonString),
38
65
  [jsonConfig.jsonString]
39
66
  );
40
67
 
41
68
  const parseError =
42
69
  draftError ??
43
70
  (storedValidation.ok ? null : storedValidation.error);
71
+ const deferredScope =
72
+ storedValidation.ok && "deferred" in storedValidation
73
+ ? storedValidation.deferred === true
74
+ : false;
44
75
 
45
76
  const handleChange = useCallback(
46
77
  (e: ChangeEvent<HTMLTextAreaElement>) => {
47
78
  const jsonString = e.target.value;
48
- const result = validateJsonString(jsonString);
79
+ const result = validateJsonStringAllowingScope(jsonString);
49
80
  setDraftError(result.ok ? null : result.error);
50
81
  onUpdateNode(node.id, patchJsonConfig(node, { jsonString }));
51
82
  },
@@ -53,15 +84,25 @@ export function JsonNodeConfigPanel({
53
84
  );
54
85
 
55
86
  return (
56
- <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
87
+ <div
88
+ ref={setFormEl}
89
+ className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
90
+ >
91
+ <ScopeTemplateAutocompleteHost
92
+ scope={autocompleteScope}
93
+ container={formEl}
94
+ />
57
95
  <ConfigSectionTitle
58
96
  title={t("blueprint.config.jsonTitle")}
59
97
  hint={t("blueprint.config.jsonHint")}
60
98
  />
61
99
 
62
100
  <label className="block space-y-1">
63
- <span className="text-muted-foreground">
101
+ <span className="inline-flex items-center gap-1 text-muted-foreground">
64
102
  {t("blueprint.config.jsonContent")}
103
+ <ConfigHintIcon label={t("blueprint.config.jsonContent")}>
104
+ {t("blueprint.config.jsonScopeHint")}
105
+ </ConfigHintIcon>
65
106
  </span>
66
107
  <textarea
67
108
  value={jsonConfig.jsonString}
@@ -69,12 +110,16 @@ export function JsonNodeConfigPanel({
69
110
  rows={12}
70
111
  spellCheck={false}
71
112
  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"
72
- placeholder={'{\n "key": "value"\n}'}
113
+ placeholder={'{\n "key": "{scope?.value}"\n}'}
73
114
  />
74
115
  {parseError ? (
75
116
  <p className="text-[11px] text-destructive">
76
117
  {t("blueprint.config.jsonFormatError", { error: parseError })}
77
118
  </p>
119
+ ) : deferredScope ? (
120
+ <p className="text-[11px] text-muted-foreground">
121
+ {t("blueprint.config.jsonScopeDeferred")}
122
+ </p>
78
123
  ) : (
79
124
  <p className="text-[11px] text-muted-foreground">
80
125
  {t("blueprint.config.jsonFormatOk")}
@@ -0,0 +1,142 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { cn } from "@arronqzy/ui";
3
+ import { useI18n } from "@arronqzy/i18n/react";
4
+ import {
5
+ filterStorageKeySuggestions,
6
+ listBrowserStorageKeys,
7
+ type StorageKind,
8
+ } from "@arronqzy/blueprint-dsl";
9
+
10
+ export type StorageKeyAutocompleteProps = {
11
+ value: string;
12
+ /** One or more storage targets whose keys should be suggested. */
13
+ storages: StorageKind | readonly StorageKind[];
14
+ placeholder?: string;
15
+ onChange: (value: string) => void;
16
+ };
17
+
18
+ export function StorageKeyAutocomplete({
19
+ value,
20
+ storages,
21
+ placeholder,
22
+ onChange,
23
+ }: StorageKeyAutocompleteProps) {
24
+ const { t } = useI18n();
25
+ const [open, setOpen] = useState(false);
26
+ const [activeIndex, setActiveIndex] = useState(0);
27
+ const [keys, setKeys] = useState<string[]>([]);
28
+ const containerRef = useRef<HTMLDivElement>(null);
29
+
30
+ const storageSignature = useMemo(() => {
31
+ const list = Array.isArray(storages) ? [...storages] : [storages];
32
+ return list.join(",");
33
+ }, [storages]);
34
+
35
+ const refreshKeys = useCallback(() => {
36
+ const list = Array.isArray(storages) ? storages : [storages];
37
+ setKeys(listBrowserStorageKeys(list));
38
+ }, [storages]);
39
+
40
+ useEffect(() => {
41
+ refreshKeys();
42
+ }, [refreshKeys, storageSignature]);
43
+
44
+ const filtered = useMemo(
45
+ () => filterStorageKeySuggestions(keys, value),
46
+ [keys, value]
47
+ );
48
+
49
+ useEffect(() => {
50
+ setActiveIndex(0);
51
+ }, [value, filtered.length]);
52
+
53
+ useEffect(() => {
54
+ if (!open) return;
55
+ const onPointerDown = (event: MouseEvent) => {
56
+ if (!containerRef.current?.contains(event.target as globalThis.Node)) {
57
+ setOpen(false);
58
+ }
59
+ };
60
+ window.addEventListener("pointerdown", onPointerDown);
61
+ return () => window.removeEventListener("pointerdown", onPointerDown);
62
+ }, [open]);
63
+
64
+ const showSuggestions = open && keys.length > 0;
65
+
66
+ const selectSuggestion = (index: number) => {
67
+ const item = filtered[index];
68
+ if (!item) return;
69
+ onChange(item);
70
+ setOpen(false);
71
+ };
72
+
73
+ const openDropdown = () => {
74
+ refreshKeys();
75
+ setOpen(true);
76
+ };
77
+
78
+ return (
79
+ <div ref={containerRef} className="relative">
80
+ <input
81
+ value={value}
82
+ placeholder={placeholder}
83
+ spellCheck={false}
84
+ onChange={(e) => {
85
+ onChange(e.target.value);
86
+ setOpen(true);
87
+ }}
88
+ onFocus={openDropdown}
89
+ onClick={openDropdown}
90
+ onKeyDown={(e) => {
91
+ if (!showSuggestions || filtered.length === 0) {
92
+ if (e.key === "Escape") setOpen(false);
93
+ return;
94
+ }
95
+ if (e.key === "ArrowDown") {
96
+ e.preventDefault();
97
+ setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));
98
+ } else if (e.key === "ArrowUp") {
99
+ e.preventDefault();
100
+ setActiveIndex((prev) => Math.max(prev - 1, 0));
101
+ } else if (e.key === "Enter") {
102
+ e.preventDefault();
103
+ selectSuggestion(activeIndex);
104
+ } else if (e.key === "Escape") {
105
+ setOpen(false);
106
+ }
107
+ }}
108
+ className={cn(
109
+ "flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 font-mono text-[11px] text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
110
+ )}
111
+ />
112
+ {showSuggestions ? (
113
+ <div className="absolute z-[10150] mt-1 max-h-52 w-full overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md">
114
+ {filtered.length === 0 ? (
115
+ <div className="px-2 py-1.5 text-[11px] text-muted-foreground">
116
+ {t("blueprint.config.noMatchingStorageKey")}
117
+ </div>
118
+ ) : (
119
+ filtered.map((key, index) => (
120
+ <button
121
+ key={key}
122
+ type="button"
123
+ className={cn(
124
+ "flex w-full items-center px-2 py-1.5 text-left font-mono text-[11px] hover:bg-accent",
125
+ index === activeIndex && "bg-accent"
126
+ )}
127
+ onMouseDown={(e) => e.preventDefault()}
128
+ onClick={() => selectSuggestion(index)}
129
+ >
130
+ {key}
131
+ </button>
132
+ ))
133
+ )}
134
+ </div>
135
+ ) : open && keys.length === 0 ? (
136
+ <div className="absolute z-[10150] mt-1 w-full rounded-md border border-border bg-popover px-2 py-1.5 text-[11px] text-muted-foreground shadow-md">
137
+ {t("blueprint.config.noStorageKeys")}
138
+ </div>
139
+ ) : null}
140
+ </div>
141
+ );
142
+ }
@@ -26,6 +26,7 @@ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
26
26
  import { resolveNodeStorageConfig } from "../graph/document";
27
27
  import { ConfigFieldLabel, ConfigSectionTitle } from "./ConfigHintIcon";
28
28
  import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost";
29
+ import { StorageKeyAutocomplete } from "./StorageKeyAutocomplete";
29
30
 
30
31
  export type StorageNodeConfigPanelProps = {
31
32
  node: BlueprintGraphNode;
@@ -90,10 +91,10 @@ export function StorageNodeConfigPanel({
90
91
  );
91
92
 
92
93
  const handleReadKeyChange = useCallback(
93
- (e: ChangeEvent<HTMLInputElement>) => {
94
+ (key: string) => {
94
95
  onUpdateNode(
95
96
  node.id,
96
- patchStorageConfig(node, { read: { key: e.target.value } })
97
+ patchStorageConfig(node, { read: { key } })
97
98
  );
98
99
  },
99
100
  [node, onUpdateNode]
@@ -115,10 +116,10 @@ export function StorageNodeConfigPanel({
115
116
  );
116
117
 
117
118
  const handleSetKeyChange = useCallback(
118
- (e: ChangeEvent<HTMLInputElement>) => {
119
+ (key: string) => {
119
120
  onUpdateNode(
120
121
  node.id,
121
- patchStorageConfig(node, { set: { key: e.target.value } })
122
+ patchStorageConfig(node, { set: { key } })
122
123
  );
123
124
  },
124
125
  [node, onUpdateNode]
@@ -134,6 +135,14 @@ export function StorageNodeConfigPanel({
134
135
  [node, onUpdateNode]
135
136
  );
136
137
 
138
+ const setKeySuggestionStorages = useMemo(
139
+ () =>
140
+ storageConfig.set.storages.length > 0
141
+ ? storageConfig.set.storages
142
+ : (["session", "local"] as const),
143
+ [storageConfig.set.storages]
144
+ );
145
+
137
146
  return (
138
147
  <div
139
148
  ref={setFormEl}
@@ -174,12 +183,11 @@ export function StorageNodeConfigPanel({
174
183
  </label>
175
184
  <label className="block space-y-1">
176
185
  <ConfigFieldLabel label={t("blueprint.config.storageKey")} />
177
- <Input
186
+ <StorageKeyAutocomplete
178
187
  value={storageConfig.read.key}
179
- onChange={handleReadKeyChange}
180
- spellCheck={false}
181
- className="h-8 font-mono text-[11px]"
188
+ storages={storageConfig.read.storage}
182
189
  placeholder="key"
190
+ onChange={handleReadKeyChange}
183
191
  />
184
192
  </label>
185
193
  </div>
@@ -213,12 +221,11 @@ export function StorageNodeConfigPanel({
213
221
  </div>
214
222
  <label className="block space-y-1">
215
223
  <ConfigFieldLabel label={t("blueprint.config.storageKey")} />
216
- <Input
224
+ <StorageKeyAutocomplete
217
225
  value={storageConfig.set.key}
218
- onChange={handleSetKeyChange}
219
- spellCheck={false}
220
- className="h-8 font-mono text-[11px]"
226
+ storages={setKeySuggestionStorages}
221
227
  placeholder="key"
228
+ onChange={handleSetKeyChange}
222
229
  />
223
230
  </label>
224
231
  <label className="block space-y-1">
@@ -302,12 +302,15 @@ export function useBlueprintPageLifecycle({
302
302
 
303
303
  useEffect(() => {
304
304
  if (!enabled || !bootCompleted) return;
305
- void emitPhases(
306
- graphRef.current,
307
- ["updated"],
308
- runnerOptionsRef.current,
309
- markPhaseFired
310
- );
305
+ const timer = window.setTimeout(() => {
306
+ void emitPhases(
307
+ graphRef.current,
308
+ ["updated"],
309
+ runnerOptionsRef.current,
310
+ markPhaseFired
311
+ );
312
+ }, 160);
313
+ return () => window.clearTimeout(timer);
311
314
  }, [bootCompleted, enabled, onUpdated, markPhaseFired]);
312
315
 
313
316
  const triggerBlueprintNode = useCallback(