@arronqzy/react-blueprint 1.0.17 → 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.
|
|
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/
|
|
22
|
-
"@arronqzy/
|
|
23
|
-
"@arronqzy/
|
|
24
|
-
"@arronqzy/
|
|
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,8 +34,8 @@
|
|
|
34
34
|
"eslint": "^8.57.0",
|
|
35
35
|
"typescript": "^5.5.4",
|
|
36
36
|
"vite": "^5.1.4",
|
|
37
|
-
"@arronqzy/tailwind": "1.0.2",
|
|
38
37
|
"@arronqzy/eslint-config": "0.0.0",
|
|
38
|
+
"@arronqzy/tailwind": "1.0.2",
|
|
39
39
|
"@arronqzy/typescript-config": "1.0.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [],
|
|
@@ -400,7 +400,13 @@ export function BlueprintNodeConfigSidebar({
|
|
|
400
400
|
) : null}
|
|
401
401
|
|
|
402
402
|
{configSource === "json" ? (
|
|
403
|
-
<JsonNodeConfigPanel
|
|
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 {
|
|
3
|
-
|
|
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
|
-
() =>
|
|
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 =
|
|
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
|
|
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
|
-
(
|
|
94
|
+
(key: string) => {
|
|
94
95
|
onUpdateNode(
|
|
95
96
|
node.id,
|
|
96
|
-
patchStorageConfig(node, { read: { key
|
|
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
|
-
(
|
|
119
|
+
(key: string) => {
|
|
119
120
|
onUpdateNode(
|
|
120
121
|
node.id,
|
|
121
|
-
patchStorageConfig(node, { set: { key
|
|
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
|
-
<
|
|
186
|
+
<StorageKeyAutocomplete
|
|
178
187
|
value={storageConfig.read.key}
|
|
179
|
-
|
|
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
|
-
<
|
|
224
|
+
<StorageKeyAutocomplete
|
|
217
225
|
value={storageConfig.set.key}
|
|
218
|
-
|
|
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">
|