@arronqzy/react-blueprint 1.0.18 → 1.0.21
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/README.md +36 -26
- package/dist/blueprint.css +336 -54
- package/index.ts +23 -0
- package/package.json +5 -5
- package/src/BlueprintContextMenu.tsx +1 -1
- package/src/BlueprintNodeConfigSidebar.tsx +10 -2
- package/src/blueprint.css +336 -54
- package/src/components/ApiCollectionManagerDialog.tsx +448 -0
- package/src/components/BlueprintExecutionLogPanel.tsx +10 -2
- package/src/components/BlueprintLibrarySelect.tsx +1 -1
- package/src/components/BlueprintMetaDialog.tsx +1 -1
- package/src/components/BlueprintNodeSwitchTaskDialog.tsx +1 -1
- package/src/components/BlueprintPanelToolbar.tsx +10 -2
- package/src/components/ClockNodeConfigPanel.tsx +1 -1
- package/src/components/ConfigHintIcon.tsx +1 -1
- package/src/components/FetchNodeConfigPanel.tsx +314 -3
- package/src/components/FetchUrlAutocomplete.tsx +1 -1
- package/src/components/JsonLintTextarea.tsx +97 -0
- package/src/components/JsonNodeConfigPanel.tsx +14 -7
- package/src/components/LogicNodeConfigPanel.tsx +1 -1
- package/src/components/ScopeTemplateAutocompleteHost.tsx +4 -8
- package/src/components/StorageKeyAutocomplete.tsx +1 -1
- package/src/components/StorageNodeConfigPanel.tsx +1 -1
- package/src/components/ViewElementMultiSelect.tsx +1 -1
- package/src/edges/BlueprintSmoothEdge.tsx +2 -2
- package/src/graph/blueprint-graph.ts +9 -0
- package/src/graph/document.ts +12 -0
- package/src/graph/sync-edges.ts +2 -2
- package/src/hooks/useBlueprintFlowState.ts +1 -1
- package/src/index.tsx +41 -28
- package/src/library/api-collection-db.ts +98 -0
- package/src/library/api-collection-store.ts +139 -0
- package/src/nodes/AndFlowNode.tsx +4 -3
- package/src/nodes/BlueprintFlowNode.tsx +8 -3
- package/src/nodes/BlueprintNodeCard.tsx +137 -121
- package/src/nodes/BlueprintNodeShell.tsx +2 -1
- package/src/nodes/ClockFlowNode.tsx +1 -1
- package/src/nodes/EventFlowNode.tsx +1 -1
- package/src/nodes/FetchFlowNode.tsx +1 -1
- package/src/nodes/JsonFlowNode.tsx +1 -1
- package/src/nodes/LifecycleFlowNode.tsx +2 -1
- package/src/nodes/LogicFlowNode.tsx +1 -1
- package/src/nodes/StorageFlowNode.tsx +1 -1
package/src/graph/sync-edges.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { BlueprintGraph } from "./blueprint-graph";
|
|
|
6
6
|
|
|
7
7
|
/** 边线描边(写入 edge.style;使用 hex 避免 SVG style 解析 hsl 异常) */
|
|
8
8
|
export const BP_EDGE_STYLE = {
|
|
9
|
-
stroke: "#
|
|
10
|
-
strokeWidth:
|
|
9
|
+
stroke: "#94a3b8",
|
|
10
|
+
strokeWidth: 1.75,
|
|
11
11
|
} as const;
|
|
12
12
|
|
|
13
13
|
export const BP_FLOW_EDGE_TYPE = "blueprintSmooth" as const;
|
package/src/index.tsx
CHANGED
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
ReactFlow,
|
|
11
11
|
ReactFlowProvider,
|
|
12
12
|
ConnectionMode,
|
|
13
|
+
Background,
|
|
14
|
+
BackgroundVariant,
|
|
13
15
|
useReactFlow,
|
|
14
16
|
type Node,
|
|
15
17
|
type Edge,
|
|
16
18
|
} from "@xyflow/react";
|
|
19
|
+
import { EnsureI18nProvider } from "@arronqzy/i18n/react";
|
|
17
20
|
import "@xyflow/react/dist/style.css";
|
|
18
21
|
import "./blueprint.css";
|
|
19
22
|
|
|
@@ -216,7 +219,15 @@ function BlueprintCanvas({
|
|
|
216
219
|
elevateNodesOnSelect={false}
|
|
217
220
|
nodeClickDistance={8}
|
|
218
221
|
proOptions={{ hideAttribution: true }}
|
|
219
|
-
|
|
222
|
+
>
|
|
223
|
+
<Background
|
|
224
|
+
id="bp-dots"
|
|
225
|
+
variant={BackgroundVariant.Dots}
|
|
226
|
+
gap={18}
|
|
227
|
+
size={1.25}
|
|
228
|
+
color="rgba(148, 163, 184, 0.45)"
|
|
229
|
+
/>
|
|
230
|
+
</ReactFlow>
|
|
220
231
|
<BlueprintContextMenu
|
|
221
232
|
menu={menu}
|
|
222
233
|
onClose={() => setMenu(null)}
|
|
@@ -241,33 +252,35 @@ export const BluePrintReactRoot: FC<BluePrintReactRootProps> = ({
|
|
|
241
252
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
242
253
|
|
|
243
254
|
return (
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<BlueprintCanvasProvider
|
|
256
|
-
onSelectNode={onSelectNode}
|
|
257
|
-
onAbortClock={onAbortClock}
|
|
255
|
+
<EnsureI18nProvider>
|
|
256
|
+
<div
|
|
257
|
+
ref={containerRef}
|
|
258
|
+
data-workspace-region="blueprint"
|
|
259
|
+
className="bp-canvas h-full w-full bg-background text-foreground"
|
|
260
|
+
style={{
|
|
261
|
+
width: "100%",
|
|
262
|
+
height: "100%",
|
|
263
|
+
minHeight: 0,
|
|
264
|
+
...style,
|
|
265
|
+
}}
|
|
258
266
|
>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
<BlueprintCanvasProvider
|
|
268
|
+
onSelectNode={onSelectNode}
|
|
269
|
+
onAbortClock={onAbortClock}
|
|
270
|
+
>
|
|
271
|
+
<ReactFlowProvider>
|
|
272
|
+
<BlueprintCanvas
|
|
273
|
+
containerRef={containerRef}
|
|
274
|
+
graph={graph}
|
|
275
|
+
onGraphChange={onGraphChange}
|
|
276
|
+
selectedNodeId={selectedNodeId}
|
|
277
|
+
executionOverlay={executionOverlay}
|
|
278
|
+
onSelectNode={onSelectNode}
|
|
279
|
+
libraryNameById={libraryNameById}
|
|
280
|
+
/>
|
|
281
|
+
</ReactFlowProvider>
|
|
282
|
+
</BlueprintCanvasProvider>
|
|
283
|
+
</div>
|
|
284
|
+
</EnsureI18nProvider>
|
|
272
285
|
);
|
|
273
286
|
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
|
|
3
|
+
|
|
4
|
+
const DB_NAME = "arronqzy-api-collections";
|
|
5
|
+
const DB_VERSION = 1;
|
|
6
|
+
const STORE_NAME = "collections";
|
|
7
|
+
|
|
8
|
+
function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
|
|
11
|
+
request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
|
|
12
|
+
request.onupgradeneeded = () => {
|
|
13
|
+
const db = request.result;
|
|
14
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
15
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
16
|
+
store.createIndex("updatedAt", "updatedAt", { unique: false });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
request.onsuccess = () => resolve(request.result);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runTransaction<T>(
|
|
24
|
+
mode: IDBTransactionMode,
|
|
25
|
+
runner: (store: IDBObjectStore) => IDBRequest<T>,
|
|
26
|
+
nameSpace?: string | null
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
return openDb(nameSpace).then(
|
|
29
|
+
(db) =>
|
|
30
|
+
new Promise<T>((resolve, reject) => {
|
|
31
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
32
|
+
const store = tx.objectStore(STORE_NAME);
|
|
33
|
+
const request = runner(store);
|
|
34
|
+
let result!: T;
|
|
35
|
+
|
|
36
|
+
request.onerror = () =>
|
|
37
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
38
|
+
request.onsuccess = () => {
|
|
39
|
+
result = request.result as T;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
tx.oncomplete = () => {
|
|
43
|
+
db.close();
|
|
44
|
+
resolve(result);
|
|
45
|
+
};
|
|
46
|
+
tx.onerror = () => {
|
|
47
|
+
db.close();
|
|
48
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
49
|
+
};
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function listApiCollectionRecords(
|
|
55
|
+
nameSpace?: string | null
|
|
56
|
+
): Promise<ApiCollectionRecord[]> {
|
|
57
|
+
const records = await runTransaction<ApiCollectionRecord[]>(
|
|
58
|
+
"readonly",
|
|
59
|
+
(store) => store.getAll(),
|
|
60
|
+
nameSpace
|
|
61
|
+
);
|
|
62
|
+
return records.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getApiCollectionRecord(
|
|
66
|
+
id: string,
|
|
67
|
+
nameSpace?: string | null
|
|
68
|
+
): Promise<ApiCollectionRecord | null> {
|
|
69
|
+
const record = await runTransaction<ApiCollectionRecord | undefined>(
|
|
70
|
+
"readonly",
|
|
71
|
+
(store) => store.get(id),
|
|
72
|
+
nameSpace
|
|
73
|
+
);
|
|
74
|
+
return record ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function putApiCollectionRecord(
|
|
78
|
+
record: ApiCollectionRecord,
|
|
79
|
+
nameSpace?: string | null
|
|
80
|
+
): Promise<ApiCollectionRecord> {
|
|
81
|
+
await runTransaction<IDBValidKey>(
|
|
82
|
+
"readwrite",
|
|
83
|
+
(store) => store.put(record),
|
|
84
|
+
nameSpace
|
|
85
|
+
);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function deleteApiCollectionRecord(
|
|
90
|
+
id: string,
|
|
91
|
+
nameSpace?: string | null
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
await runTransaction<undefined>(
|
|
94
|
+
"readwrite",
|
|
95
|
+
(store) => store.delete(id),
|
|
96
|
+
nameSpace
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
apiCollectionToListItem,
|
|
4
|
+
createApiCollectionId,
|
|
5
|
+
parseApiCollectionJson,
|
|
6
|
+
type ApiCollectionDocument,
|
|
7
|
+
type ApiCollectionListItem,
|
|
8
|
+
type ApiCollectionRecord,
|
|
9
|
+
} from "@arronqzy/blueprint-dsl";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
deleteApiCollectionRecord,
|
|
13
|
+
listApiCollectionRecords,
|
|
14
|
+
putApiCollectionRecord,
|
|
15
|
+
} from "./api-collection-db";
|
|
16
|
+
|
|
17
|
+
type Listener = () => void;
|
|
18
|
+
|
|
19
|
+
let nameSpaceRef: string | null | undefined = null;
|
|
20
|
+
let records: ApiCollectionRecord[] = [];
|
|
21
|
+
let hydrated = false;
|
|
22
|
+
const listeners = new Set<Listener>();
|
|
23
|
+
|
|
24
|
+
function emit() {
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sortRecords(list: ApiCollectionRecord[]): ApiCollectionRecord[] {
|
|
29
|
+
return [...list].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getApiCollectionRecords(): ApiCollectionRecord[] {
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getApiCollectionListItems(): ApiCollectionListItem[] {
|
|
37
|
+
return records.map(apiCollectionToListItem);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getApiCollectionById(id: string): ApiCollectionRecord | null {
|
|
41
|
+
return records.find((item) => item.id === id) ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subscribeApiCollections(listener: Listener): () => void {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
return () => listeners.delete(listener);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function hydrateApiCollections(
|
|
50
|
+
nameSpace?: string | null
|
|
51
|
+
): Promise<ApiCollectionRecord[]> {
|
|
52
|
+
nameSpaceRef = nameSpace;
|
|
53
|
+
const persisted = await listApiCollectionRecords(nameSpace);
|
|
54
|
+
const memoryOnly = records.filter((item) => !item.persistLocal);
|
|
55
|
+
const persistedIds = new Set(persisted.map((item) => item.id));
|
|
56
|
+
records = sortRecords([
|
|
57
|
+
...persisted,
|
|
58
|
+
...memoryOnly.filter((item) => !persistedIds.has(item.id)),
|
|
59
|
+
]);
|
|
60
|
+
hydrated = true;
|
|
61
|
+
emit();
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type UpsertApiCollectionInput = {
|
|
66
|
+
id?: string;
|
|
67
|
+
rawJson: string;
|
|
68
|
+
persistLocal: boolean;
|
|
69
|
+
/** 未写 document.name 时用文件名 */
|
|
70
|
+
fallbackName?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type UpsertApiCollectionResult =
|
|
74
|
+
| { ok: true; record: ApiCollectionRecord }
|
|
75
|
+
| { ok: false; error: string };
|
|
76
|
+
|
|
77
|
+
export async function upsertApiCollectionFromJson(
|
|
78
|
+
input: UpsertApiCollectionInput
|
|
79
|
+
): Promise<UpsertApiCollectionResult> {
|
|
80
|
+
const parsed = parseApiCollectionJson(input.rawJson);
|
|
81
|
+
if (!parsed.ok) return parsed;
|
|
82
|
+
|
|
83
|
+
const document: ApiCollectionDocument = {
|
|
84
|
+
...parsed.document,
|
|
85
|
+
name:
|
|
86
|
+
parsed.document.name.trim() ||
|
|
87
|
+
input.fallbackName?.replace(/\.json$/i, "").trim() ||
|
|
88
|
+
parsed.document.name,
|
|
89
|
+
};
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const existing = input.id
|
|
92
|
+
? records.find((item) => item.id === input.id) ?? null
|
|
93
|
+
: null;
|
|
94
|
+
const record: ApiCollectionRecord = {
|
|
95
|
+
id: existing?.id ?? createApiCollectionId(),
|
|
96
|
+
name: document.name,
|
|
97
|
+
persistLocal: input.persistLocal,
|
|
98
|
+
document,
|
|
99
|
+
rawJson: JSON.stringify(document, null, 2),
|
|
100
|
+
createdAt: existing?.createdAt ?? now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (record.persistLocal) {
|
|
105
|
+
await putApiCollectionRecord(record, nameSpaceRef);
|
|
106
|
+
} else if (existing?.persistLocal) {
|
|
107
|
+
await deleteApiCollectionRecord(record.id, nameSpaceRef);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const next = records.filter((item) => item.id !== record.id);
|
|
111
|
+
next.push(record);
|
|
112
|
+
records = sortRecords(next);
|
|
113
|
+
emit();
|
|
114
|
+
return { ok: true, record };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function removeApiCollection(id: string): Promise<void> {
|
|
118
|
+
const existing = records.find((item) => item.id === id);
|
|
119
|
+
if (existing?.persistLocal || hydrated) {
|
|
120
|
+
try {
|
|
121
|
+
await deleteApiCollectionRecord(id, nameSpaceRef);
|
|
122
|
+
} catch {
|
|
123
|
+
// 内存-only 记录在库里可能不存在
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
records = records.filter((item) => item.id !== id);
|
|
127
|
+
emit();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useApiCollections(): ApiCollectionRecord[] {
|
|
131
|
+
const [list, setList] = useState<ApiCollectionRecord[]>(() => records);
|
|
132
|
+
useEffect(() => subscribeApiCollections(() => setList([...records])), []);
|
|
133
|
+
return list;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useApiCollectionListItems(): ApiCollectionListItem[] {
|
|
137
|
+
const list = useApiCollections();
|
|
138
|
+
return list.map(apiCollectionToListItem);
|
|
139
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useI18n } from "@arronqzy/i18n/react";
|
|
1
|
+
import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
|
|
2
2
|
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
|
3
3
|
import { cn } from "@arronqzy/ui";
|
|
4
4
|
|
|
@@ -20,6 +20,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
|
|
|
20
20
|
<div
|
|
21
21
|
className={cn(
|
|
22
22
|
"bp-node bp-node--and",
|
|
23
|
+
nodeData.isSelected && "bp-node--selected",
|
|
23
24
|
executionTone === "success" && "bp-node--execution-true",
|
|
24
25
|
executionTone === "error" && "bp-node--execution-false"
|
|
25
26
|
)}
|
|
@@ -29,7 +30,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
|
|
|
29
30
|
position={Position.Left}
|
|
30
31
|
id="inA"
|
|
31
32
|
className="bp-flow-handle bp-flow-handle--target"
|
|
32
|
-
style={{ top: "
|
|
33
|
+
style={{ top: "32%" }}
|
|
33
34
|
title={t("blueprint.node.andInA")}
|
|
34
35
|
/>
|
|
35
36
|
<Handle
|
|
@@ -37,7 +38,7 @@ export function AndFlowNode({ id, data }: NodeProps) {
|
|
|
37
38
|
position={Position.Left}
|
|
38
39
|
id="inB"
|
|
39
40
|
className="bp-flow-handle bp-flow-handle--target"
|
|
40
|
-
style={{ top: "
|
|
41
|
+
style={{ top: "68%" }}
|
|
41
42
|
title={t("blueprint.node.andInB")}
|
|
42
43
|
/>
|
|
43
44
|
<BlueprintNodeCard
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { useI18n } from "@arronqzy/i18n/react";
|
|
1
|
+
import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
|
|
2
2
|
import type { NodeProps } from "@xyflow/react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveBlueprintConfigSource,
|
|
6
|
+
resolveBlueprintNodeTypeLabel,
|
|
7
|
+
} from "../graph/document";
|
|
5
8
|
import { resolveBlueprintNodeSummary } from "../graph/node-summary";
|
|
6
9
|
import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
|
|
7
10
|
import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
|
|
@@ -12,6 +15,8 @@ export function BlueprintFlowNode({ id, data }: NodeProps) {
|
|
|
12
15
|
const { t } = useI18n();
|
|
13
16
|
const nodeData = data as BlueprintFlowNodeData;
|
|
14
17
|
const onSelect = useBlueprintNodeSelect();
|
|
18
|
+
const configSource = resolveBlueprintConfigSource(nodeData);
|
|
19
|
+
const variant = configSource === "view" ? "view" : "blueprint";
|
|
15
20
|
|
|
16
21
|
return (
|
|
17
22
|
<BlueprintNodeShell
|
|
@@ -19,7 +24,7 @@ export function BlueprintFlowNode({ id, data }: NodeProps) {
|
|
|
19
24
|
label={nodeData.label}
|
|
20
25
|
meta={resolveBlueprintNodeTypeLabel(nodeData, t)}
|
|
21
26
|
subtitle={resolveBlueprintNodeSummary(nodeData, t)}
|
|
22
|
-
variant=
|
|
27
|
+
variant={variant}
|
|
23
28
|
selected={Boolean(nodeData.isSelected)}
|
|
24
29
|
executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
|
|
25
30
|
onSelect={onSelect}
|