@arronqzy/react-blueprint 1.0.19 → 1.0.22

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/src/index.tsx CHANGED
@@ -10,11 +10,14 @@ 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";
17
19
  import { EnsureI18nProvider } from "@arronqzy/i18n/react";
20
+ import { BlueprintDslLocaleSync } from "./BlueprintDslLocaleSync";
18
21
  import "@xyflow/react/dist/style.css";
19
22
  import "./blueprint.css";
20
23
 
@@ -37,6 +40,7 @@ export type { BlueprintExecutionOverlay } from "./runtime/execution-overlay";
37
40
 
38
41
  export type { BlueprintNodeConfigSidebarProps, BlueprintViewElementOption, BlueprintLibraryOption } from "./BlueprintNodeConfigSidebar";
39
42
  export { BlueprintNodeConfigSidebar } from "./BlueprintNodeConfigSidebar";
43
+ export { BlueprintDslLocaleSync } from "./BlueprintDslLocaleSync";
40
44
 
41
45
  export type BluePrintReactRootProps = {
42
46
  style?: React.CSSProperties;
@@ -64,7 +68,6 @@ function BlueprintCanvas({
64
68
  selectedNodeId = null,
65
69
  executionOverlay = null,
66
70
  onSelectNode,
67
- onAbortClock,
68
71
  libraryNameById,
69
72
  containerRef,
70
73
  }: BlueprintCanvasProps) {
@@ -217,7 +220,15 @@ function BlueprintCanvas({
217
220
  elevateNodesOnSelect={false}
218
221
  nodeClickDistance={8}
219
222
  proOptions={{ hideAttribution: true }}
220
- />
223
+ >
224
+ <Background
225
+ id="bp-dots"
226
+ variant={BackgroundVariant.Dots}
227
+ gap={18}
228
+ size={1.25}
229
+ color="rgba(148, 163, 184, 0.45)"
230
+ />
231
+ </ReactFlow>
221
232
  <BlueprintContextMenu
222
233
  menu={menu}
223
234
  onClose={() => setMenu(null)}
@@ -243,34 +254,36 @@ export const BluePrintReactRoot: FC<BluePrintReactRootProps> = ({
243
254
 
244
255
  return (
245
256
  <EnsureI18nProvider>
246
- <div
247
- ref={containerRef}
248
- data-workspace-region="blueprint"
249
- className="bp-canvas h-full w-full bg-background text-foreground"
250
- style={{
251
- width: "100%",
252
- height: "100%",
253
- minHeight: 0,
254
- ...style,
255
- }}
256
- >
257
- <BlueprintCanvasProvider
258
- onSelectNode={onSelectNode}
259
- onAbortClock={onAbortClock}
257
+ <BlueprintDslLocaleSync>
258
+ <div
259
+ ref={containerRef}
260
+ data-workspace-region="blueprint"
261
+ className="bp-canvas h-full w-full bg-background text-foreground"
262
+ style={{
263
+ width: "100%",
264
+ height: "100%",
265
+ minHeight: 0,
266
+ ...style,
267
+ }}
260
268
  >
261
- <ReactFlowProvider>
262
- <BlueprintCanvas
263
- containerRef={containerRef}
264
- graph={graph}
265
- onGraphChange={onGraphChange}
266
- selectedNodeId={selectedNodeId}
267
- executionOverlay={executionOverlay}
268
- onSelectNode={onSelectNode}
269
- libraryNameById={libraryNameById}
270
- />
271
- </ReactFlowProvider>
272
- </BlueprintCanvasProvider>
273
- </div>
269
+ <BlueprintCanvasProvider
270
+ onSelectNode={onSelectNode}
271
+ onAbortClock={onAbortClock}
272
+ >
273
+ <ReactFlowProvider>
274
+ <BlueprintCanvas
275
+ containerRef={containerRef}
276
+ graph={graph}
277
+ onGraphChange={onGraphChange}
278
+ selectedNodeId={selectedNodeId}
279
+ executionOverlay={executionOverlay}
280
+ onSelectNode={onSelectNode}
281
+ libraryNameById={libraryNameById}
282
+ />
283
+ </ReactFlowProvider>
284
+ </BlueprintCanvasProvider>
285
+ </div>
286
+ </BlueprintDslLocaleSync>
274
287
  </EnsureI18nProvider>
275
288
  );
276
289
  };
@@ -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
+ }
@@ -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: "35%" }}
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: "65%" }}
41
+ style={{ top: "68%" }}
41
42
  title={t("blueprint.node.andInB")}
42
43
  />
43
44
  <BlueprintNodeCard
@@ -1,7 +1,10 @@
1
1
  import { useI18nOptional as useI18n } from "@arronqzy/i18n/react";
2
2
  import type { NodeProps } from "@xyflow/react";
3
3
 
4
- import { resolveBlueprintNodeTypeLabel } from "../graph/document";
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="blueprint"
27
+ variant={variant}
23
28
  selected={Boolean(nodeData.isSelected)}
24
29
  executionTone={resolveBlueprintNodeExecutionTone(nodeData)}
25
30
  onSelect={onSelect}