@arronqzy/react-view 1.0.37 → 1.0.39
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 +6 -6
- package/readme.md +16 -5
- package/src/index.ts +51 -1
- package/src/panel/ReactViewPanel.tsx +66 -19
- package/src/panel/hooks/useWorkspaceProjects.ts +228 -52
- package/src/panel/library/abuilder-storage.ts +168 -0
- package/src/panel/library/abuilder-view-handle.ts +100 -0
- package/src/panel/library/blueprint-library-sync.ts +22 -0
- package/src/panel/library/event-subscription.ts +79 -5
- package/src/panel/library/workspace-project-sync.ts +7 -1
- package/src/panel/library/workspace-storage.ts +70 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/react-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"dist",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"@codemirror/lang-json": "^6.0.1",
|
|
31
31
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
32
32
|
"@codemirror/view": "^6.36.2",
|
|
33
|
+
"@arronqzy/i18n": "0.1.17",
|
|
33
34
|
"@arronqzy/ui": "1.0.10",
|
|
34
35
|
"@arronqzy/blueprint-dsl": "1.0.17",
|
|
35
|
-
"@arronqzy/i18n": "0.1.17",
|
|
36
36
|
"@arronqzy/rx-store": "1.0.8",
|
|
37
37
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
38
38
|
"@arronqzy/view-table": "0.1.8",
|
|
39
|
-
"@arronqzy/
|
|
40
|
-
"@arronqzy/
|
|
39
|
+
"@arronqzy/webllm-assistant": "0.1.16",
|
|
40
|
+
"@arronqzy/react-blueprint": "1.0.24"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/react": "^19.0.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"eslint": "^8.57.0",
|
|
47
47
|
"typescript": "^5.5.4",
|
|
48
48
|
"@arronqzy/eslint-config": "0.0.0",
|
|
49
|
-
"@arronqzy/
|
|
50
|
-
"@arronqzy/
|
|
49
|
+
"@arronqzy/tailwind": "1.0.2",
|
|
50
|
+
"@arronqzy/typescript-config": "1.0.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
package/readme.md
CHANGED
|
@@ -57,7 +57,8 @@ export function Editor({ workspace }: { workspace?: WorkspaceProjectRecord | nul
|
|
|
57
57
|
|------|------|------|
|
|
58
58
|
| `className` | 根节点 class | — |
|
|
59
59
|
| `initialZoom` | 初始缩放 | `1` |
|
|
60
|
-
| `initialWorkspace` |
|
|
60
|
+
| `initialWorkspace` | 挂载后加载的完整工作区。空则空白画布。带 `id` 时默认同步进 IndexedDB | — |
|
|
61
|
+
| `workspaceId` | 按 IndexedDB id 打开(不 remount) | — |
|
|
61
62
|
|
|
62
63
|
## 在线预览
|
|
63
64
|
|
|
@@ -89,7 +90,7 @@ parseOnlinePreviewSearchParams("?preview=online&projectId=xxx");
|
|
|
89
90
|
|
|
90
91
|
## 工作区事件
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
回调带完整工作区,用来写自己的后端。`initialWorkspace` 会默认同步进 IndexedDB;本地副本可用导出的 list / get / delete / clear 清理。
|
|
93
94
|
|
|
94
95
|
```ts
|
|
95
96
|
import {
|
|
@@ -111,9 +112,12 @@ syncSub.unsubscribe();
|
|
|
111
112
|
| 事件 | 常量 | 何时触发 |
|
|
112
113
|
|------|------|----------|
|
|
113
114
|
| `workspace:add` | `AbuilderEvents.workspaceAdd` | 创建成功 |
|
|
114
|
-
| `workspace:sync` | `AbuilderEvents.workspaceSync` |
|
|
115
|
+
| `workspace:sync` | `AbuilderEvents.workspaceSync` | 同步成功(载荷含 `nameSpace`) |
|
|
116
|
+
| `workspace:delete` | `AbuilderEvents.workspaceDelete` | 删除工作区,参数 `{ ids, nameSpace }` |
|
|
117
|
+
| `blueprint-library:changed` | `AbuilderEvents.blueprintLibraryChanged` | 蓝图库变更 |
|
|
118
|
+
| `api-collection:changed` | `AbuilderEvents.apiCollectionChanged` | 接口集合变更 |
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
`workspace:add` / `workspace:sync` 的参数字段:上述工作区字段 + `nameSpace`。可用 `addEventSubscription(event, cb, { nameSpace })` 过滤。
|
|
117
121
|
|
|
118
122
|
校验:
|
|
119
123
|
|
|
@@ -154,7 +158,14 @@ const dataUrl = await getPreviewSnapshot({
|
|
|
154
158
|
| `addEventSubscription` / `AbuilderEvents` | 工作区事件 |
|
|
155
159
|
| `getPreviewSnapshot` | 预览截图 data URL |
|
|
156
160
|
| `parseWorkspaceData` 等 | 校验 / 解析 / 空工作区 |
|
|
157
|
-
| `
|
|
161
|
+
| `putWorkspaceProject` / `upsertWorkspaceProject` / `list` / `get` / `delete` / `clear` | 工作区完整 CRUD |
|
|
162
|
+
| `putBlueprintLibraryRecord` / `list` / `get` / `delete` / `clear` | 蓝图库完整 CRUD |
|
|
163
|
+
| `putExecutionRunRecord` / `list` / `get` / `delete` / `clear` / `purge*` / `trim*` | 执行日志 |
|
|
164
|
+
| `putApiCollection` / `upsertApiCollectionFromJson` / `list` / `delete` / `clear` | 接口集合 |
|
|
165
|
+
| `getCurrentWorkspace` / `openWorkspace` / `createWorkspace` / `saveWorkspace` / `syncWorkspace` | 命令式操作已挂载面板 |
|
|
166
|
+
| `clearAbuilderStorage` | 一次清空四类数据 |
|
|
167
|
+
|
|
168
|
+
本地数据函数的 `nameSpace` 要和面板一致。面板 `ref` 类型为 `AbuilderViewHandle`。
|
|
158
169
|
| `createView` / `createAction` | 视图 DSL 扩展点(`src/core`) |
|
|
159
170
|
|
|
160
171
|
## 样式
|
package/src/index.ts
CHANGED
|
@@ -12,18 +12,68 @@ export {
|
|
|
12
12
|
export type {
|
|
13
13
|
AbuilderEventName,
|
|
14
14
|
AbuilderEventPayloadMap,
|
|
15
|
+
AddEventSubscriptionOptions,
|
|
15
16
|
WorkspaceAddEventPayload,
|
|
16
17
|
WorkspaceSyncEventPayload,
|
|
18
|
+
WorkspaceDeleteEventPayload,
|
|
19
|
+
BlueprintLibraryChangedEventPayload,
|
|
17
20
|
ApiCollectionUploadStartEventPayload,
|
|
18
21
|
ApiCollectionUploadSuccessEventPayload,
|
|
22
|
+
ApiCollectionChangedEventPayload,
|
|
19
23
|
WorkspaceData,
|
|
20
24
|
} from "./panel/library/event-subscription";
|
|
21
25
|
export type { WorkspaceProjectRecord } from "./panel/library/workspace-project-db";
|
|
22
26
|
export {
|
|
23
27
|
listWorkspaceProjects,
|
|
28
|
+
getWorkspaceProject,
|
|
29
|
+
putWorkspaceProject,
|
|
30
|
+
upsertWorkspaceProject,
|
|
24
31
|
deleteWorkspaceProject,
|
|
25
32
|
clearWorkspaceProjects,
|
|
26
33
|
} from "./panel/library/workspace-storage";
|
|
27
|
-
export type {
|
|
34
|
+
export type {
|
|
35
|
+
WorkspaceProjectListItem,
|
|
36
|
+
PutWorkspaceProjectOptions,
|
|
37
|
+
} from "./panel/library/workspace-storage";
|
|
38
|
+
export {
|
|
39
|
+
listBlueprintLibrary,
|
|
40
|
+
getBlueprintLibraryRecord,
|
|
41
|
+
putBlueprintLibraryRecord,
|
|
42
|
+
deleteBlueprintLibraryRecord,
|
|
43
|
+
clearBlueprintLibrary,
|
|
44
|
+
listExecutionRunRecords,
|
|
45
|
+
getExecutionRunRecord,
|
|
46
|
+
putExecutionRunRecord,
|
|
47
|
+
deleteExecutionRunRecord,
|
|
48
|
+
clearAllExecutionRunRecords,
|
|
49
|
+
purgeExecutionRunsOlderThan,
|
|
50
|
+
trimExecutionRunRecordsToMax,
|
|
51
|
+
readExecutionLogSettings,
|
|
52
|
+
writeExecutionLogSettings,
|
|
53
|
+
listApiCollectionRecords,
|
|
54
|
+
putApiCollection,
|
|
55
|
+
upsertApiCollectionFromJson,
|
|
56
|
+
deleteApiCollection,
|
|
57
|
+
clearApiCollections,
|
|
58
|
+
clearAbuilderStorage,
|
|
59
|
+
} from "./panel/library/abuilder-storage";
|
|
60
|
+
export type {
|
|
61
|
+
AbuilderStorageClearResult,
|
|
62
|
+
BlueprintLibraryListItem,
|
|
63
|
+
BlueprintLibraryRecord,
|
|
64
|
+
ExecutionRunRecord,
|
|
65
|
+
ExecutionLogSettings,
|
|
66
|
+
ApiCollectionRecord,
|
|
67
|
+
UpsertApiCollectionInput,
|
|
68
|
+
UpsertApiCollectionResult,
|
|
69
|
+
} from "./panel/library/abuilder-storage";
|
|
70
|
+
export {
|
|
71
|
+
getCurrentWorkspace,
|
|
72
|
+
openWorkspace,
|
|
73
|
+
createWorkspace,
|
|
74
|
+
saveWorkspace,
|
|
75
|
+
syncWorkspace,
|
|
76
|
+
} from "./panel/library/abuilder-view-handle";
|
|
77
|
+
export type { AbuilderViewHandle } from "./panel/library/abuilder-view-handle";
|
|
28
78
|
export { getPreviewSnapshot } from "./panel/library/preview-snapshot";
|
|
29
79
|
export type { GetPreviewSnapshotOptions } from "./panel/library/preview-snapshot";
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
registerAbuilderViewHandle,
|
|
12
|
+
type AbuilderViewHandle,
|
|
13
|
+
} from "./library/abuilder-view-handle";
|
|
2
14
|
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
15
|
import { relocalizeMessage, type Locale } from "@arronqzy/i18n";
|
|
4
16
|
import type { State } from "@arronqzy/rx-store";
|
|
@@ -7,6 +19,7 @@ import type { PanelElement } from "./types";
|
|
|
7
19
|
import { usePanelElements } from "./hooks/usePanelElements";
|
|
8
20
|
import { useWorkspaceProjects } from "./hooks/useWorkspaceProjects";
|
|
9
21
|
import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
|
|
22
|
+
import { subscribeBlueprintLibraryChanged } from "./library/blueprint-library-sync";
|
|
10
23
|
import { WorkspaceProjectNav } from "./components/WorkspaceProjectNav";
|
|
11
24
|
import { BusyOverlay } from "./components/BusyOverlay";
|
|
12
25
|
import { PanelCanvas } from "./components/PanelCanvas";
|
|
@@ -140,6 +153,11 @@ export type ReactViewPanelProps = {
|
|
|
140
153
|
className?: string;
|
|
141
154
|
/** 外部传入的完整工作区数据,挂载后按这份数据完整渲染;空则显示空白,不自动打开 IndexedDB 记录 */
|
|
142
155
|
initialWorkspace?: WorkspaceProjectRecord | null;
|
|
156
|
+
/**
|
|
157
|
+
* 按 IndexedDB id 打开工作区(不 remount)。
|
|
158
|
+
* 与 `initialWorkspace` 同时存在时:首次优先完整记录;之后 id 变化从 IndexedDB 打开。
|
|
159
|
+
*/
|
|
160
|
+
workspaceId?: string | null;
|
|
143
161
|
/** 界面语言;省略时按 localStorage / 浏览器语言解析 */
|
|
144
162
|
locale?: Locale | null;
|
|
145
163
|
onLocaleChange?: (locale: Locale) => void;
|
|
@@ -155,14 +173,18 @@ export type ReactViewPanelProps = {
|
|
|
155
173
|
packageVersion?: string | null;
|
|
156
174
|
};
|
|
157
175
|
|
|
158
|
-
function ReactViewPanelInner(
|
|
176
|
+
const ReactViewPanelInner = forwardRef<AbuilderViewHandle, ReactViewPanelProps>(function ReactViewPanelInner(
|
|
177
|
+
{
|
|
159
178
|
initialZoom = 1,
|
|
160
179
|
className,
|
|
161
180
|
initialWorkspace = null,
|
|
181
|
+
workspaceId = null,
|
|
162
182
|
nameSpace = null,
|
|
163
183
|
packageName = null,
|
|
164
184
|
packageVersion = null,
|
|
165
|
-
}: ReactViewPanelProps
|
|
185
|
+
}: ReactViewPanelProps,
|
|
186
|
+
ref
|
|
187
|
+
) {
|
|
166
188
|
const { t, locale, setLocale } = useI18n();
|
|
167
189
|
const messages = useMemo(() => getPanelMessages(t), [t]);
|
|
168
190
|
const THEME_STORAGE_KEY = appStorageKey("panel:theme", nameSpace);
|
|
@@ -803,6 +825,11 @@ function ReactViewPanelInner({
|
|
|
803
825
|
void refreshBlueprintLibrary();
|
|
804
826
|
}, [refreshBlueprintLibrary]);
|
|
805
827
|
|
|
828
|
+
useEffect(
|
|
829
|
+
() => subscribeBlueprintLibraryChanged(() => void refreshBlueprintLibrary(), nameSpace),
|
|
830
|
+
[nameSpace, refreshBlueprintLibrary]
|
|
831
|
+
);
|
|
832
|
+
|
|
806
833
|
const loadBlueprintFromLibrary = useCallback(
|
|
807
834
|
async (id: string) => {
|
|
808
835
|
const record = await getBlueprintLibraryRecord(id, nameSpace);
|
|
@@ -1070,9 +1097,26 @@ function ReactViewPanelInner({
|
|
|
1070
1097
|
panelRevision: `${historyCursor}|${allElements.length}`,
|
|
1071
1098
|
onProjectApplied: handleWorkspaceProjectApplied,
|
|
1072
1099
|
initialWorkspace,
|
|
1100
|
+
workspaceId,
|
|
1073
1101
|
nameSpace,
|
|
1074
1102
|
});
|
|
1075
1103
|
|
|
1104
|
+
const viewHandle = useMemo<AbuilderViewHandle>(
|
|
1105
|
+
() => ({
|
|
1106
|
+
getNameSpace: () => nameSpace ?? null,
|
|
1107
|
+
getCurrentWorkspace: () => workspaceProjects.getCurrentWorkspaceRecord(),
|
|
1108
|
+
openWorkspace: (idOrRecord) => workspaceProjects.handleOpenProject(idOrRecord),
|
|
1109
|
+
createWorkspace: () => workspaceProjects.handleCreateProject(),
|
|
1110
|
+
saveWorkspace: () => workspaceProjects.handleSaveProject(),
|
|
1111
|
+
syncWorkspace: () => workspaceProjects.handleSyncProject(),
|
|
1112
|
+
}),
|
|
1113
|
+
[nameSpace, workspaceProjects]
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
useImperativeHandle(ref, () => viewHandle, [viewHandle]);
|
|
1117
|
+
|
|
1118
|
+
useEffect(() => registerAbuilderViewHandle(viewHandle), [viewHandle]);
|
|
1119
|
+
|
|
1076
1120
|
useEffect(() => {
|
|
1077
1121
|
registerPreviewSnapshotProvider(async () => {
|
|
1078
1122
|
return captureEditorPreviewSnapshot({
|
|
@@ -2792,10 +2836,16 @@ function ReactViewPanelInner({
|
|
|
2792
2836
|
open={apiCollectionManagerOpen}
|
|
2793
2837
|
onOpenChange={setApiCollectionManagerOpen}
|
|
2794
2838
|
onUploadStart={(info) => {
|
|
2795
|
-
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart,
|
|
2839
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, {
|
|
2840
|
+
...info,
|
|
2841
|
+
nameSpace: nameSpace ?? null,
|
|
2842
|
+
});
|
|
2796
2843
|
}}
|
|
2797
2844
|
onUploadSuccess={(info) => {
|
|
2798
|
-
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess,
|
|
2845
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, {
|
|
2846
|
+
...info,
|
|
2847
|
+
nameSpace: nameSpace ?? null,
|
|
2848
|
+
});
|
|
2799
2849
|
}}
|
|
2800
2850
|
onSyncCollection={(record) => {
|
|
2801
2851
|
const patches = buildApiCollectionSyncPatches({
|
|
@@ -2840,17 +2890,14 @@ function ReactViewPanelInner({
|
|
|
2840
2890
|
<Toaster />
|
|
2841
2891
|
</div>
|
|
2842
2892
|
);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
export
|
|
2846
|
-
locale,
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
</I18nRoot>
|
|
2855
|
-
);
|
|
2856
|
-
}
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2895
|
+
export const ReactViewPanel = forwardRef<AbuilderViewHandle, ReactViewPanelProps>(
|
|
2896
|
+
function ReactViewPanel({ locale, onLocaleChange, nameSpace, ...rest }, ref) {
|
|
2897
|
+
return (
|
|
2898
|
+
<I18nRoot locale={locale} onLocaleChange={onLocaleChange} nameSpace={nameSpace}>
|
|
2899
|
+
<ReactViewPanelInner ref={ref} nameSpace={nameSpace} {...rest} />
|
|
2900
|
+
</I18nRoot>
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
);
|
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
createWorkspaceProjectId,
|
|
8
8
|
getWorkspaceProject,
|
|
9
9
|
listWorkspaceProjects,
|
|
10
|
-
putWorkspaceProject,
|
|
11
10
|
type WorkspaceProjectListItem,
|
|
12
11
|
type WorkspaceProjectRecord,
|
|
13
12
|
} from "../library/workspace-project-db";
|
|
14
|
-
import { deleteWorkspaceProject } from "../library/workspace-storage";
|
|
15
13
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "../library/workspace-
|
|
14
|
+
deleteWorkspaceProject,
|
|
15
|
+
putWorkspaceProject,
|
|
16
|
+
} from "../library/workspace-storage";
|
|
17
|
+
import { subscribeWorkspaceProjectsChanged } from "../library/workspace-project-sync";
|
|
19
18
|
import { writeWorkspacePreviewCache, readWorkspacePreviewCache } from "../library/workspace-project-cache";
|
|
19
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
20
20
|
import {
|
|
21
21
|
cloneWorkspaceSnapshot,
|
|
22
22
|
workspaceSnapshotsEqual,
|
|
@@ -45,6 +45,11 @@ export type UseWorkspaceProjectsOptions = {
|
|
|
45
45
|
onProjectApplied?: (record: WorkspaceProjectRecord) => void;
|
|
46
46
|
/** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
|
|
47
47
|
initialWorkspace?: WorkspaceProjectRecord | null;
|
|
48
|
+
/**
|
|
49
|
+
* 按 IndexedDB id 打开工作区。与 `initialWorkspace` 同时存在时:
|
|
50
|
+
* 首次优先 `initialWorkspace`;之后 `workspaceId` 变化会从 IndexedDB 打开。
|
|
51
|
+
*/
|
|
52
|
+
workspaceId?: string | null;
|
|
48
53
|
/** 隔离 IndexedDB / 预览缓存 / 同步频道 */
|
|
49
54
|
nameSpace?: string | null;
|
|
50
55
|
};
|
|
@@ -103,17 +108,20 @@ export function useWorkspaceProjects({
|
|
|
103
108
|
panelRevision,
|
|
104
109
|
onProjectApplied,
|
|
105
110
|
initialWorkspace,
|
|
111
|
+
workspaceId = null,
|
|
106
112
|
nameSpace = null,
|
|
107
113
|
}: UseWorkspaceProjectsOptions) {
|
|
108
114
|
const { t } = useI18nOptional();
|
|
109
115
|
const [projects, setProjects] = useState<WorkspaceProjectListItem[]>([]);
|
|
110
116
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
|
111
117
|
const [activeProjectName, setActiveProjectName] = useState<string | null>(null);
|
|
118
|
+
const activeCreatedAtRef = useRef<number | null>(null);
|
|
112
119
|
const syncedSnapshotRef = useRef<WorkspaceSnapshot | null>(null);
|
|
113
120
|
const syncedPanelRevisionRef = useRef<unknown>(null);
|
|
114
121
|
const pendingRevisionSyncRef = useRef(false);
|
|
115
122
|
const previewWindowsRef = useRef(new Map<string, Window>());
|
|
116
123
|
const [previewingProjectIds, setPreviewingProjectIds] = useState<string[]>([]);
|
|
124
|
+
const eventNameSpace = normalizeAppNameSpace(nameSpace) || null;
|
|
117
125
|
|
|
118
126
|
const refreshPreviewing = useCallback(() => {
|
|
119
127
|
const ids: string[] = [];
|
|
@@ -166,7 +174,9 @@ export function useWorkspaceProjects({
|
|
|
166
174
|
return subscribeWorkspaceProjectsChanged((message) => {
|
|
167
175
|
const removedActive =
|
|
168
176
|
message.type === "cleared" ||
|
|
169
|
-
(
|
|
177
|
+
(message.type === "deleted" &&
|
|
178
|
+
activeProjectId !== null &&
|
|
179
|
+
message.projectIds.includes(activeProjectId));
|
|
170
180
|
if (removedActive) {
|
|
171
181
|
setActiveProjectId(null);
|
|
172
182
|
setActiveProjectName(null);
|
|
@@ -197,8 +207,67 @@ export function useWorkspaceProjects({
|
|
|
197
207
|
buildCurrentSnapshot,
|
|
198
208
|
]);
|
|
199
209
|
|
|
210
|
+
const ensureWorkspaceInIndexedDb = useCallback(
|
|
211
|
+
async (record: WorkspaceProjectRecord) => {
|
|
212
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
213
|
+
if (!id) return;
|
|
214
|
+
try {
|
|
215
|
+
const existing = await getWorkspaceProject(id, nameSpace);
|
|
216
|
+
const incomingUpdatedAt =
|
|
217
|
+
typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt)
|
|
218
|
+
? record.updatedAt
|
|
219
|
+
: 0;
|
|
220
|
+
if (existing && existing.updatedAt >= incomingUpdatedAt) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const name =
|
|
225
|
+
(typeof record.name === "string" && record.name.trim()) ||
|
|
226
|
+
(typeof record.productName === "string" && record.productName.trim()) ||
|
|
227
|
+
t("panel.defaults.unnamedProduct");
|
|
228
|
+
const toStore: WorkspaceProjectRecord = {
|
|
229
|
+
...record,
|
|
230
|
+
id,
|
|
231
|
+
name,
|
|
232
|
+
createdAt: existing?.createdAt ?? record.createdAt ?? now,
|
|
233
|
+
updatedAt: incomingUpdatedAt || now,
|
|
234
|
+
panelState:
|
|
235
|
+
normalizeImportedPanelState(record.panelState) ??
|
|
236
|
+
existing?.panelState ??
|
|
237
|
+
createEmptyPanelState(),
|
|
238
|
+
blueprintDocument:
|
|
239
|
+
parseBlueprintData(record.blueprintDocument).value ??
|
|
240
|
+
existing?.blueprintDocument ??
|
|
241
|
+
BlueprintGraph.empty().document,
|
|
242
|
+
blueprintMeta: {
|
|
243
|
+
name:
|
|
244
|
+
record.blueprintMeta?.name ??
|
|
245
|
+
existing?.blueprintMeta?.name ??
|
|
246
|
+
t("panel.defaults.unnamedBlueprint"),
|
|
247
|
+
remark:
|
|
248
|
+
record.blueprintMeta?.remark ?? existing?.blueprintMeta?.remark ?? "",
|
|
249
|
+
},
|
|
250
|
+
productName:
|
|
251
|
+
typeof record.productName === "string" && record.productName.trim()
|
|
252
|
+
? record.productName
|
|
253
|
+
: existing?.productName ?? t("panel.defaults.unnamedProduct"),
|
|
254
|
+
titleIconDataUrl:
|
|
255
|
+
record.titleIconDataUrl ?? existing?.titleIconDataUrl,
|
|
256
|
+
};
|
|
257
|
+
await putWorkspaceProject(toStore, nameSpace);
|
|
258
|
+
await refreshProjects();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn("[abuilder] ensureWorkspaceInIndexedDb failed", error);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
[nameSpace, refreshProjects, t]
|
|
264
|
+
);
|
|
265
|
+
|
|
200
266
|
const applyProjectRecord = useCallback(
|
|
201
|
-
(
|
|
267
|
+
(
|
|
268
|
+
record: WorkspaceProjectRecord,
|
|
269
|
+
options?: { notifyApplied?: boolean; persistToIndexedDb?: boolean }
|
|
270
|
+
) => {
|
|
202
271
|
const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
|
|
203
272
|
const blueprintDocument =
|
|
204
273
|
parsedBlueprint.value ?? BlueprintGraph.empty().document;
|
|
@@ -221,6 +290,10 @@ export function useWorkspaceProjects({
|
|
|
221
290
|
setTitleIconDataUrl(titleIconDataUrl);
|
|
222
291
|
setActiveProjectId(record.id || null);
|
|
223
292
|
setActiveProjectName(record.name ?? null);
|
|
293
|
+
activeCreatedAtRef.current =
|
|
294
|
+
typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
|
|
295
|
+
? record.createdAt
|
|
296
|
+
: Date.now();
|
|
224
297
|
syncedSnapshotRef.current = cloneWorkspaceSnapshot({
|
|
225
298
|
blueprintDocument,
|
|
226
299
|
blueprintMeta,
|
|
@@ -229,18 +302,27 @@ export function useWorkspaceProjects({
|
|
|
229
302
|
});
|
|
230
303
|
syncedPanelRevisionRef.current = panelRevision;
|
|
231
304
|
pendingRevisionSyncRef.current = true;
|
|
305
|
+
const normalized: WorkspaceProjectRecord = {
|
|
306
|
+
...record,
|
|
307
|
+
id: record.id,
|
|
308
|
+
name: record.name ?? productName,
|
|
309
|
+
createdAt: activeCreatedAtRef.current,
|
|
310
|
+
updatedAt: record.updatedAt ?? Date.now(),
|
|
311
|
+
blueprintDocument,
|
|
312
|
+
blueprintMeta,
|
|
313
|
+
panelState,
|
|
314
|
+
productName,
|
|
315
|
+
titleIconDataUrl: titleIconDataUrl || undefined,
|
|
316
|
+
};
|
|
232
317
|
if (options?.notifyApplied !== false) {
|
|
233
|
-
onProjectApplied?.(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
panelState,
|
|
238
|
-
productName,
|
|
239
|
-
titleIconDataUrl: titleIconDataUrl || undefined,
|
|
240
|
-
});
|
|
318
|
+
onProjectApplied?.(normalized);
|
|
319
|
+
}
|
|
320
|
+
if (options?.persistToIndexedDb) {
|
|
321
|
+
void ensureWorkspaceInIndexedDb(normalized);
|
|
241
322
|
}
|
|
242
323
|
},
|
|
243
324
|
[
|
|
325
|
+
ensureWorkspaceInIndexedDb,
|
|
244
326
|
importPanelData,
|
|
245
327
|
onProjectApplied,
|
|
246
328
|
setBlueprintGraph,
|
|
@@ -263,6 +345,7 @@ export function useWorkspaceProjects({
|
|
|
263
345
|
setTitleIconDataUrl("");
|
|
264
346
|
setActiveProjectId(null);
|
|
265
347
|
setActiveProjectName(null);
|
|
348
|
+
activeCreatedAtRef.current = null;
|
|
266
349
|
syncedSnapshotRef.current = null;
|
|
267
350
|
}, [
|
|
268
351
|
importPanelData,
|
|
@@ -275,23 +358,7 @@ export function useWorkspaceProjects({
|
|
|
275
358
|
|
|
276
359
|
const initialWorkspaceAppliedKeyRef = useRef<string | null>(null);
|
|
277
360
|
const bootstrappedRef = useRef(false);
|
|
278
|
-
|
|
279
|
-
if (!bootstrappedRef.current) {
|
|
280
|
-
bootstrappedRef.current = true;
|
|
281
|
-
if (hasWorkspacePayload(initialWorkspace)) {
|
|
282
|
-
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
283
|
-
initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
|
|
284
|
-
} else {
|
|
285
|
-
applyEmptyWorkspace();
|
|
286
|
-
}
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (!hasWorkspacePayload(initialWorkspace)) return;
|
|
290
|
-
const key = workspaceApplyKey(initialWorkspace);
|
|
291
|
-
if (initialWorkspaceAppliedKeyRef.current === key) return;
|
|
292
|
-
initialWorkspaceAppliedKeyRef.current = key;
|
|
293
|
-
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
294
|
-
}, [applyEmptyWorkspace, applyProjectRecord, initialWorkspace]);
|
|
361
|
+
const workspaceIdAppliedRef = useRef<string | null>(null);
|
|
295
362
|
|
|
296
363
|
const persistProject = useCallback(
|
|
297
364
|
async (options: { id?: string; name: string; createdAt?: number }) => {
|
|
@@ -299,7 +366,7 @@ export function useWorkspaceProjects({
|
|
|
299
366
|
const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
|
|
300
367
|
const now = Date.now();
|
|
301
368
|
const id = options.id ?? createWorkspaceProjectId();
|
|
302
|
-
const createdAt = options.createdAt ?? now;
|
|
369
|
+
const createdAt = options.createdAt ?? activeCreatedAtRef.current ?? now;
|
|
303
370
|
const name = (options.name ?? "").trim() || resolveProjectName();
|
|
304
371
|
|
|
305
372
|
let panelState: State;
|
|
@@ -325,10 +392,9 @@ export function useWorkspaceProjects({
|
|
|
325
392
|
};
|
|
326
393
|
|
|
327
394
|
await putWorkspaceProject(record, nameSpace);
|
|
328
|
-
writeWorkspacePreviewCache(record, nameSpace);
|
|
329
|
-
broadcastWorkspaceProjectUpdate(id, now, nameSpace);
|
|
330
395
|
setActiveProjectId(id);
|
|
331
396
|
setActiveProjectName(name);
|
|
397
|
+
activeCreatedAtRef.current = createdAt;
|
|
332
398
|
syncedSnapshotRef.current = snapshot;
|
|
333
399
|
syncedPanelRevisionRef.current = panelRevision;
|
|
334
400
|
pendingRevisionSyncRef.current = false;
|
|
@@ -349,14 +415,49 @@ export function useWorkspaceProjects({
|
|
|
349
415
|
]
|
|
350
416
|
);
|
|
351
417
|
|
|
418
|
+
const getCurrentWorkspaceRecord = useCallback((): WorkspaceProjectRecord | null => {
|
|
419
|
+
if (!activeProjectId) return null;
|
|
420
|
+
try {
|
|
421
|
+
const snapshot = buildCurrentSnapshot();
|
|
422
|
+
const panelState = exportPanelData();
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
return {
|
|
425
|
+
id: activeProjectId,
|
|
426
|
+
name: activeProjectName || resolveProjectName(),
|
|
427
|
+
createdAt: activeCreatedAtRef.current ?? now,
|
|
428
|
+
updatedAt: now,
|
|
429
|
+
panelState,
|
|
430
|
+
blueprintDocument: snapshot.blueprintDocument,
|
|
431
|
+
blueprintMeta: snapshot.blueprintMeta,
|
|
432
|
+
productName:
|
|
433
|
+
typeof snapshot.productName === "string"
|
|
434
|
+
? snapshot.productName
|
|
435
|
+
: resolveProjectName(),
|
|
436
|
+
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.warn("[abuilder] getCurrentWorkspace failed", error);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}, [
|
|
443
|
+
activeProjectId,
|
|
444
|
+
activeProjectName,
|
|
445
|
+
buildCurrentSnapshot,
|
|
446
|
+
exportPanelData,
|
|
447
|
+
resolveProjectName,
|
|
448
|
+
]);
|
|
449
|
+
|
|
352
450
|
const handleCreateProject = useCallback(async () => {
|
|
353
451
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
354
452
|
const name = resolveProjectName();
|
|
355
453
|
const record = await persistProject({ name });
|
|
356
|
-
emitAbuilderEvent(AbuilderEvents.workspaceAdd,
|
|
454
|
+
emitAbuilderEvent(AbuilderEvents.workspaceAdd, {
|
|
455
|
+
...record,
|
|
456
|
+
nameSpace: eventNameSpace,
|
|
457
|
+
});
|
|
357
458
|
return { name, id: record.id };
|
|
358
459
|
});
|
|
359
|
-
}, [persistProject, resolveProjectName, t]);
|
|
460
|
+
}, [eventNameSpace, persistProject, resolveProjectName, t]);
|
|
360
461
|
|
|
361
462
|
const handleSaveProject = useCallback(async () => {
|
|
362
463
|
if (!activeProjectId) {
|
|
@@ -368,17 +469,33 @@ export function useWorkspaceProjects({
|
|
|
368
469
|
const record = await persistProject({
|
|
369
470
|
id: activeProjectId,
|
|
370
471
|
name,
|
|
371
|
-
createdAt: existing?.createdAt,
|
|
472
|
+
createdAt: existing?.createdAt ?? activeCreatedAtRef.current ?? undefined,
|
|
473
|
+
});
|
|
474
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, {
|
|
475
|
+
...record,
|
|
476
|
+
nameSpace: eventNameSpace,
|
|
372
477
|
});
|
|
373
478
|
return { name, id: record.id };
|
|
374
479
|
});
|
|
375
|
-
}, [
|
|
480
|
+
}, [
|
|
481
|
+
activeProjectId,
|
|
482
|
+
eventNameSpace,
|
|
483
|
+
handleCreateProject,
|
|
484
|
+
nameSpace,
|
|
485
|
+
persistProject,
|
|
486
|
+
resolveProjectName,
|
|
487
|
+
t,
|
|
488
|
+
]);
|
|
376
489
|
|
|
377
490
|
const handleOpenProject = useCallback(
|
|
378
|
-
async (
|
|
491
|
+
async (idOrRecord: string | WorkspaceProjectRecord) => {
|
|
379
492
|
try {
|
|
380
493
|
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
381
|
-
|
|
494
|
+
if (typeof idOrRecord !== "string") {
|
|
495
|
+
applyProjectRecord(idOrRecord, { persistToIndexedDb: true });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const record = await getWorkspaceProject(idOrRecord, nameSpace);
|
|
382
499
|
if (!record) {
|
|
383
500
|
await refreshProjects();
|
|
384
501
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
@@ -397,24 +514,29 @@ export function useWorkspaceProjects({
|
|
|
397
514
|
if (!activeProjectId) {
|
|
398
515
|
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
399
516
|
}
|
|
517
|
+
// 本地无记录时 upsert,避免宿主 initialWorkspace 未落盘后同步报错
|
|
400
518
|
const existing = await getWorkspaceProject(activeProjectId, nameSpace);
|
|
401
|
-
|
|
402
|
-
setActiveProjectId(null);
|
|
403
|
-
setActiveProjectName(null);
|
|
404
|
-
syncedSnapshotRef.current = null;
|
|
405
|
-
await refreshProjects();
|
|
406
|
-
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
407
|
-
}
|
|
408
|
-
const name = resolveProjectName() || existing.name;
|
|
519
|
+
const name = resolveProjectName() || existing?.name || activeProjectName || "";
|
|
409
520
|
const record = await persistProject({
|
|
410
521
|
id: activeProjectId,
|
|
411
522
|
name,
|
|
412
|
-
createdAt: existing.
|
|
523
|
+
createdAt: existing?.createdAt ?? activeCreatedAtRef.current ?? undefined,
|
|
524
|
+
});
|
|
525
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, {
|
|
526
|
+
...record,
|
|
527
|
+
nameSpace: eventNameSpace,
|
|
413
528
|
});
|
|
414
|
-
emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
|
|
415
529
|
return name;
|
|
416
530
|
});
|
|
417
|
-
}, [
|
|
531
|
+
}, [
|
|
532
|
+
activeProjectId,
|
|
533
|
+
activeProjectName,
|
|
534
|
+
eventNameSpace,
|
|
535
|
+
nameSpace,
|
|
536
|
+
persistProject,
|
|
537
|
+
resolveProjectName,
|
|
538
|
+
t,
|
|
539
|
+
]);
|
|
418
540
|
|
|
419
541
|
const handleDeleteProject = useCallback(
|
|
420
542
|
async (id: string) => {
|
|
@@ -424,6 +546,7 @@ export function useWorkspaceProjects({
|
|
|
424
546
|
if (activeProjectId === id) {
|
|
425
547
|
setActiveProjectId(null);
|
|
426
548
|
setActiveProjectName(null);
|
|
549
|
+
activeCreatedAtRef.current = null;
|
|
427
550
|
syncedSnapshotRef.current = null;
|
|
428
551
|
}
|
|
429
552
|
previewWindowsRef.current.delete(id);
|
|
@@ -478,6 +601,58 @@ export function useWorkspaceProjects({
|
|
|
478
601
|
[activeProjectId, dirty, handleSyncProject, nameSpace, refreshPreviewing, t]
|
|
479
602
|
);
|
|
480
603
|
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (!bootstrappedRef.current) {
|
|
606
|
+
bootstrappedRef.current = true;
|
|
607
|
+
if (hasWorkspacePayload(initialWorkspace)) {
|
|
608
|
+
applyProjectRecord(initialWorkspace, {
|
|
609
|
+
notifyApplied: false,
|
|
610
|
+
persistToIndexedDb: true,
|
|
611
|
+
});
|
|
612
|
+
initialWorkspaceAppliedKeyRef.current = workspaceApplyKey(initialWorkspace);
|
|
613
|
+
if (initialWorkspace.id) {
|
|
614
|
+
workspaceIdAppliedRef.current = initialWorkspace.id;
|
|
615
|
+
}
|
|
616
|
+
} else if (typeof workspaceId === "string" && workspaceId.trim()) {
|
|
617
|
+
workspaceIdAppliedRef.current = workspaceId.trim();
|
|
618
|
+
void handleOpenProject(workspaceId.trim());
|
|
619
|
+
} else {
|
|
620
|
+
applyEmptyWorkspace();
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (!hasWorkspacePayload(initialWorkspace)) return;
|
|
625
|
+
const key = workspaceApplyKey(initialWorkspace);
|
|
626
|
+
if (initialWorkspaceAppliedKeyRef.current === key) return;
|
|
627
|
+
initialWorkspaceAppliedKeyRef.current = key;
|
|
628
|
+
if (initialWorkspace.id) {
|
|
629
|
+
workspaceIdAppliedRef.current = initialWorkspace.id;
|
|
630
|
+
}
|
|
631
|
+
applyProjectRecord(initialWorkspace, {
|
|
632
|
+
notifyApplied: false,
|
|
633
|
+
persistToIndexedDb: true,
|
|
634
|
+
});
|
|
635
|
+
}, [
|
|
636
|
+
applyEmptyWorkspace,
|
|
637
|
+
applyProjectRecord,
|
|
638
|
+
handleOpenProject,
|
|
639
|
+
initialWorkspace,
|
|
640
|
+
workspaceId,
|
|
641
|
+
]);
|
|
642
|
+
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
if (!bootstrappedRef.current) return;
|
|
645
|
+
const id = typeof workspaceId === "string" ? workspaceId.trim() : "";
|
|
646
|
+
if (!id) return;
|
|
647
|
+
if (workspaceIdAppliedRef.current === id) return;
|
|
648
|
+
if (activeProjectId === id) {
|
|
649
|
+
workspaceIdAppliedRef.current = id;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
workspaceIdAppliedRef.current = id;
|
|
653
|
+
void handleOpenProject(id);
|
|
654
|
+
}, [activeProjectId, handleOpenProject, workspaceId]);
|
|
655
|
+
|
|
481
656
|
return {
|
|
482
657
|
projects,
|
|
483
658
|
activeProjectId,
|
|
@@ -490,6 +665,7 @@ export function useWorkspaceProjects({
|
|
|
490
665
|
handleSyncProject,
|
|
491
666
|
handleDeleteProject,
|
|
492
667
|
handlePreviewProject,
|
|
668
|
+
getCurrentWorkspaceRecord,
|
|
493
669
|
refreshProjects,
|
|
494
670
|
};
|
|
495
671
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
import {
|
|
3
|
+
clearAllExecutionRunRecords,
|
|
4
|
+
clearApiCollections as clearApiCollectionsRaw,
|
|
5
|
+
clearBlueprintLibrary as clearBlueprintLibraryRecords,
|
|
6
|
+
deleteApiCollection as deleteApiCollectionRaw,
|
|
7
|
+
deleteBlueprintLibraryRecord as deleteBlueprintLibraryRecordRaw,
|
|
8
|
+
putApiCollection as putApiCollectionRaw,
|
|
9
|
+
putBlueprintLibraryRecord as putBlueprintLibraryRecordRaw,
|
|
10
|
+
putExecutionRunRecord as putExecutionRunRecordRaw,
|
|
11
|
+
upsertApiCollectionFromJson as upsertApiCollectionFromJsonRaw,
|
|
12
|
+
type BlueprintLibraryRecord,
|
|
13
|
+
type ExecutionRunRecord,
|
|
14
|
+
type UpsertApiCollectionInput,
|
|
15
|
+
type UpsertApiCollectionResult,
|
|
16
|
+
} from "@arronqzy/react-blueprint";
|
|
17
|
+
import type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
|
|
18
|
+
import { broadcastBlueprintLibraryChanged } from "./blueprint-library-sync";
|
|
19
|
+
import { AbuilderEvents, emitAbuilderEvent } from "./event-subscription";
|
|
20
|
+
import { clearWorkspaceProjects } from "./workspace-storage";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
listBlueprintLibrary,
|
|
24
|
+
getBlueprintLibraryRecord,
|
|
25
|
+
listExecutionRunRecords,
|
|
26
|
+
getExecutionRunRecord,
|
|
27
|
+
deleteExecutionRunRecord,
|
|
28
|
+
clearAllExecutionRunRecords,
|
|
29
|
+
purgeExecutionRunsOlderThan,
|
|
30
|
+
trimExecutionRunRecordsToMax,
|
|
31
|
+
readExecutionLogSettings,
|
|
32
|
+
writeExecutionLogSettings,
|
|
33
|
+
listApiCollectionRecords,
|
|
34
|
+
} from "@arronqzy/react-blueprint";
|
|
35
|
+
export type {
|
|
36
|
+
BlueprintLibraryListItem,
|
|
37
|
+
BlueprintLibraryRecord,
|
|
38
|
+
ExecutionRunRecord,
|
|
39
|
+
ExecutionLogSettings,
|
|
40
|
+
UpsertApiCollectionInput,
|
|
41
|
+
UpsertApiCollectionResult,
|
|
42
|
+
} from "@arronqzy/react-blueprint";
|
|
43
|
+
export type { ApiCollectionRecord } from "@arronqzy/blueprint-dsl";
|
|
44
|
+
|
|
45
|
+
function resolvedNameSpace(nameSpace?: string | null): string | null {
|
|
46
|
+
return normalizeAppNameSpace(nameSpace) || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 写入 / 覆盖蓝图库记录;已挂载的编辑器会刷新蓝图库列表 */
|
|
50
|
+
export async function putBlueprintLibraryRecord(
|
|
51
|
+
record: BlueprintLibraryRecord,
|
|
52
|
+
nameSpace?: string | null
|
|
53
|
+
): Promise<BlueprintLibraryRecord> {
|
|
54
|
+
const saved = await putBlueprintLibraryRecordRaw(record, nameSpace);
|
|
55
|
+
broadcastBlueprintLibraryChanged(nameSpace);
|
|
56
|
+
emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
|
|
57
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
58
|
+
action: "put",
|
|
59
|
+
ids: [saved.id],
|
|
60
|
+
});
|
|
61
|
+
return saved;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 删除蓝图库中的一条蓝图;已挂载的编辑器会刷新蓝图库列表 */
|
|
65
|
+
export async function deleteBlueprintLibraryRecord(
|
|
66
|
+
id: string,
|
|
67
|
+
nameSpace?: string | null
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await deleteBlueprintLibraryRecordRaw(id, nameSpace);
|
|
70
|
+
broadcastBlueprintLibraryChanged(nameSpace);
|
|
71
|
+
emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
|
|
72
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
73
|
+
action: "delete",
|
|
74
|
+
ids: [id],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 清空该 nameSpace 下的蓝图库,返回删除数量;已挂载的编辑器会刷新蓝图库列表 */
|
|
79
|
+
export async function clearBlueprintLibrary(nameSpace?: string | null): Promise<number> {
|
|
80
|
+
const count = await clearBlueprintLibraryRecords(nameSpace);
|
|
81
|
+
broadcastBlueprintLibraryChanged(nameSpace);
|
|
82
|
+
emitAbuilderEvent(AbuilderEvents.blueprintLibraryChanged, {
|
|
83
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
84
|
+
action: "clear",
|
|
85
|
+
});
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 写入一条蓝图执行日志 */
|
|
90
|
+
export async function putExecutionRunRecord(
|
|
91
|
+
record: ExecutionRunRecord,
|
|
92
|
+
nameSpace?: string | null
|
|
93
|
+
): Promise<ExecutionRunRecord> {
|
|
94
|
+
return putExecutionRunRecordRaw(record, nameSpace);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 写入接口集合;persistLocal 时落 IndexedDB,并同步已挂载编辑器的内存列表 */
|
|
98
|
+
export async function putApiCollection(
|
|
99
|
+
record: ApiCollectionRecord,
|
|
100
|
+
nameSpace?: string | null
|
|
101
|
+
): Promise<ApiCollectionRecord> {
|
|
102
|
+
const saved = await putApiCollectionRaw(record, nameSpace);
|
|
103
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
|
|
104
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
105
|
+
action: "put",
|
|
106
|
+
ids: [saved.id],
|
|
107
|
+
});
|
|
108
|
+
return saved;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 从 JSON 文本 upsert 接口集合;可传 nameSpace */
|
|
112
|
+
export async function upsertApiCollectionFromJson(
|
|
113
|
+
input: UpsertApiCollectionInput
|
|
114
|
+
): Promise<UpsertApiCollectionResult> {
|
|
115
|
+
const result = await upsertApiCollectionFromJsonRaw(input);
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
|
|
118
|
+
nameSpace: resolvedNameSpace(input.nameSpace),
|
|
119
|
+
action: "put",
|
|
120
|
+
ids: [result.record.id],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function deleteApiCollection(
|
|
127
|
+
id: string,
|
|
128
|
+
nameSpace?: string | null
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
await deleteApiCollectionRaw(id, nameSpace);
|
|
131
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
|
|
132
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
133
|
+
action: "delete",
|
|
134
|
+
ids: [id],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function clearApiCollections(nameSpace?: string | null): Promise<number> {
|
|
139
|
+
const count = await clearApiCollectionsRaw(nameSpace);
|
|
140
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionChanged, {
|
|
141
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
142
|
+
action: "clear",
|
|
143
|
+
});
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type AbuilderStorageClearResult = {
|
|
148
|
+
workspaces: number;
|
|
149
|
+
blueprints: number;
|
|
150
|
+
executionRuns: number;
|
|
151
|
+
apiCollections: number;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 清空该 nameSpace 下编辑器写入浏览器的全部数据:工作区、蓝图库、执行日志、接口集合。
|
|
156
|
+
* 主题、字号、日志保留设置等偏好不受影响。
|
|
157
|
+
*/
|
|
158
|
+
export async function clearAbuilderStorage(
|
|
159
|
+
nameSpace?: string | null
|
|
160
|
+
): Promise<AbuilderStorageClearResult> {
|
|
161
|
+
const [workspaces, blueprints, executionRuns, apiCollections] = await Promise.all([
|
|
162
|
+
clearWorkspaceProjects(nameSpace),
|
|
163
|
+
clearBlueprintLibrary(nameSpace),
|
|
164
|
+
clearAllExecutionRunRecords(nameSpace),
|
|
165
|
+
clearApiCollections(nameSpace),
|
|
166
|
+
]);
|
|
167
|
+
return { workspaces, blueprints, executionRuns, apiCollections };
|
|
168
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
3
|
+
|
|
4
|
+
/** 已挂载编辑面板的命令式句柄(也可通过 `ref` 拿到) */
|
|
5
|
+
export type AbuilderViewHandle = {
|
|
6
|
+
/** 当前 nameSpace */
|
|
7
|
+
getNameSpace: () => string | null;
|
|
8
|
+
/** 导出当前编辑器中的完整工作区;尚无关联 id 时返回 null */
|
|
9
|
+
getCurrentWorkspace: () => WorkspaceProjectRecord | null;
|
|
10
|
+
/** 按 id 从 IndexedDB 打开,或直接应用完整记录 */
|
|
11
|
+
openWorkspace: (idOrRecord: string | WorkspaceProjectRecord) => Promise<void>;
|
|
12
|
+
/** 创建新工作区并写入 IndexedDB,发出 workspace:add */
|
|
13
|
+
createWorkspace: () => Promise<{ name: string; id: string }>;
|
|
14
|
+
/** 保存当前工作区到 IndexedDB,发出 workspace:sync;无 active 时等同创建 */
|
|
15
|
+
saveWorkspace: () => Promise<{ name: string; id: string }>;
|
|
16
|
+
/** 同步当前工作区到 IndexedDB,发出 workspace:sync */
|
|
17
|
+
syncWorkspace: () => Promise<string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handles = new Set<AbuilderViewHandle>();
|
|
21
|
+
|
|
22
|
+
export function registerAbuilderViewHandle(handle: AbuilderViewHandle): () => void {
|
|
23
|
+
handles.add(handle);
|
|
24
|
+
return () => {
|
|
25
|
+
handles.delete(handle);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findHandle(nameSpace?: string | null): AbuilderViewHandle | null {
|
|
30
|
+
if (handles.size === 0) return null;
|
|
31
|
+
if (nameSpace === undefined) {
|
|
32
|
+
return handles.values().next().value ?? null;
|
|
33
|
+
}
|
|
34
|
+
const wanted = normalizeAppNameSpace(nameSpace);
|
|
35
|
+
for (const handle of handles) {
|
|
36
|
+
if (normalizeAppNameSpace(handle.getNameSpace()) === wanted) return handle;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 读取当前已挂载编辑器中的工作区快照。
|
|
43
|
+
* 同页多个实例时请传入 `nameSpace`;未挂载或尚无 active 工作区时返回 null。
|
|
44
|
+
*/
|
|
45
|
+
export function getCurrentWorkspace(
|
|
46
|
+
nameSpace?: string | null
|
|
47
|
+
): WorkspaceProjectRecord | null {
|
|
48
|
+
return findHandle(nameSpace)?.getCurrentWorkspace() ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 命令式打开工作区(不 remount)。
|
|
53
|
+
* 同页多个实例时请传入 `nameSpace`。
|
|
54
|
+
*/
|
|
55
|
+
export async function openWorkspace(
|
|
56
|
+
idOrRecord: string | WorkspaceProjectRecord,
|
|
57
|
+
nameSpace?: string | null
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const handle = findHandle(nameSpace);
|
|
60
|
+
if (!handle) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
await handle.openWorkspace(idOrRecord);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function createWorkspace(
|
|
69
|
+
nameSpace?: string | null
|
|
70
|
+
): Promise<{ name: string; id: string }> {
|
|
71
|
+
const handle = findHandle(nameSpace);
|
|
72
|
+
if (!handle) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return handle.createWorkspace();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function saveWorkspace(
|
|
81
|
+
nameSpace?: string | null
|
|
82
|
+
): Promise<{ name: string; id: string }> {
|
|
83
|
+
const handle = findHandle(nameSpace);
|
|
84
|
+
if (!handle) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return handle.saveWorkspace();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function syncWorkspace(nameSpace?: string | null): Promise<string> {
|
|
93
|
+
const handle = findHandle(nameSpace);
|
|
94
|
+
if (!handle) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"No mounted Abuilder panel. Pass initialWorkspace or mount <App> first."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return handle.syncWorkspace();
|
|
100
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
2
|
+
|
|
3
|
+
const CHANNEL = "arronqzy-blueprint-library-changed";
|
|
4
|
+
|
|
5
|
+
export function broadcastBlueprintLibraryChanged(nameSpace?: string | null): void {
|
|
6
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
7
|
+
const channel = new BroadcastChannel(appStorageKey(CHANNEL, nameSpace));
|
|
8
|
+
channel.postMessage({ type: "changed" });
|
|
9
|
+
channel.close();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function subscribeBlueprintLibraryChanged(
|
|
13
|
+
onChange: () => void,
|
|
14
|
+
nameSpace?: string | null
|
|
15
|
+
): () => void {
|
|
16
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
17
|
+
return () => {};
|
|
18
|
+
}
|
|
19
|
+
const channel = new BroadcastChannel(appStorageKey(CHANNEL, nameSpace));
|
|
20
|
+
channel.onmessage = () => onChange();
|
|
21
|
+
return () => channel.close();
|
|
22
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
1
2
|
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
3
|
|
|
3
4
|
export const AbuilderEvents = {
|
|
@@ -5,10 +6,16 @@ export const AbuilderEvents = {
|
|
|
5
6
|
workspaceAdd: "workspace:add",
|
|
6
7
|
/** 同步工作区 */
|
|
7
8
|
workspaceSync: "workspace:sync",
|
|
9
|
+
/** 删除工作区(界面删除,或调用 deleteWorkspaceProject / clearWorkspaceProjects) */
|
|
10
|
+
workspaceDelete: "workspace:delete",
|
|
11
|
+
/** 蓝图库写入 / 删除 / 清空 */
|
|
12
|
+
blueprintLibraryChanged: "blueprint-library:changed",
|
|
8
13
|
/** 开始上传接口集合 */
|
|
9
14
|
apiCollectionUploadStart: "api-collection:upload-start",
|
|
10
15
|
/** 上传接口集合成功 */
|
|
11
16
|
apiCollectionUploadSuccess: "api-collection:upload-success",
|
|
17
|
+
/** 接口集合写入 / 删除 / 清空 */
|
|
18
|
+
apiCollectionChanged: "api-collection:changed",
|
|
12
19
|
} as const;
|
|
13
20
|
|
|
14
21
|
export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
|
|
@@ -16,12 +23,32 @@ export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEve
|
|
|
16
23
|
/** 工作区完整数据(面板 + 蓝图),可用于外部持久化与再次加载 */
|
|
17
24
|
export type WorkspaceData = WorkspaceProjectRecord;
|
|
18
25
|
|
|
19
|
-
export type WorkspaceAddEventPayload = WorkspaceData
|
|
20
|
-
|
|
26
|
+
export type WorkspaceAddEventPayload = WorkspaceData & {
|
|
27
|
+
/** 所属 nameSpace,默认库为 null */
|
|
28
|
+
nameSpace: string | null;
|
|
29
|
+
};
|
|
30
|
+
export type WorkspaceSyncEventPayload = WorkspaceData & {
|
|
31
|
+
/** 所属 nameSpace,默认库为 null */
|
|
32
|
+
nameSpace: string | null;
|
|
33
|
+
};
|
|
34
|
+
export type WorkspaceDeleteEventPayload = {
|
|
35
|
+
/** 被删除的工作区 id */
|
|
36
|
+
ids: string[];
|
|
37
|
+
/** 所属 nameSpace,默认库为 null */
|
|
38
|
+
nameSpace: string | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type BlueprintLibraryChangedEventPayload = {
|
|
42
|
+
nameSpace: string | null;
|
|
43
|
+
action: "put" | "delete" | "clear";
|
|
44
|
+
/** 涉及的蓝图 id;clear 时可能为空 */
|
|
45
|
+
ids?: string[];
|
|
46
|
+
};
|
|
21
47
|
|
|
22
48
|
export type ApiCollectionUploadStartEventPayload = {
|
|
23
49
|
fileNames: string[];
|
|
24
50
|
count: number;
|
|
51
|
+
nameSpace: string | null;
|
|
25
52
|
};
|
|
26
53
|
|
|
27
54
|
export type ApiCollectionUploadSuccessEventPayload = {
|
|
@@ -31,19 +58,57 @@ export type ApiCollectionUploadSuccessEventPayload = {
|
|
|
31
58
|
apiCount: number;
|
|
32
59
|
persistLocal: boolean;
|
|
33
60
|
}>;
|
|
61
|
+
nameSpace: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ApiCollectionChangedEventPayload = {
|
|
65
|
+
nameSpace: string | null;
|
|
66
|
+
action: "put" | "delete" | "clear";
|
|
67
|
+
ids?: string[];
|
|
34
68
|
};
|
|
35
69
|
|
|
36
70
|
export type AbuilderEventPayloadMap = {
|
|
37
71
|
[AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
|
|
38
72
|
[AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
|
|
73
|
+
[AbuilderEvents.workspaceDelete]: WorkspaceDeleteEventPayload;
|
|
74
|
+
[AbuilderEvents.blueprintLibraryChanged]: BlueprintLibraryChangedEventPayload;
|
|
39
75
|
[AbuilderEvents.apiCollectionUploadStart]: ApiCollectionUploadStartEventPayload;
|
|
40
76
|
[AbuilderEvents.apiCollectionUploadSuccess]: ApiCollectionUploadSuccessEventPayload;
|
|
77
|
+
[AbuilderEvents.apiCollectionChanged]: ApiCollectionChangedEventPayload;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type AddEventSubscriptionOptions = {
|
|
81
|
+
/**
|
|
82
|
+
* 仅接收该 nameSpace 的事件。
|
|
83
|
+
* 省略则接收全部;传入空字符串 / null 表示默认库。
|
|
84
|
+
*/
|
|
85
|
+
nameSpace?: string | null;
|
|
41
86
|
};
|
|
42
87
|
|
|
43
88
|
type EventCallback = (payload: unknown) => void;
|
|
44
89
|
|
|
45
90
|
const listeners = new Map<string, Set<EventCallback>>();
|
|
46
91
|
|
|
92
|
+
function resolvePayloadNameSpace(payload: unknown): string | null | undefined {
|
|
93
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
94
|
+
if (!("nameSpace" in payload)) return undefined;
|
|
95
|
+
const value = (payload as { nameSpace?: unknown }).nameSpace;
|
|
96
|
+
if (value === null || value === undefined) return null;
|
|
97
|
+
if (typeof value === "string") return value;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchesNameSpaceFilter(
|
|
102
|
+
payload: unknown,
|
|
103
|
+
filter: string | null | undefined
|
|
104
|
+
): boolean {
|
|
105
|
+
if (filter === undefined) return true;
|
|
106
|
+
const wanted = normalizeAppNameSpace(filter);
|
|
107
|
+
const actual = resolvePayloadNameSpace(payload);
|
|
108
|
+
if (actual === undefined) return true;
|
|
109
|
+
return normalizeAppNameSpace(actual) === wanted;
|
|
110
|
+
}
|
|
111
|
+
|
|
47
112
|
export function emitAbuilderEvent<T extends AbuilderEventName>(
|
|
48
113
|
eventName: T,
|
|
49
114
|
payload: AbuilderEventPayloadMap[T]
|
|
@@ -61,17 +126,26 @@ export function emitAbuilderEvent<T extends AbuilderEventName>(
|
|
|
61
126
|
|
|
62
127
|
export function addEventSubscription<T extends AbuilderEventName>(
|
|
63
128
|
eventName: T,
|
|
64
|
-
callback: (payload: AbuilderEventPayloadMap[T]) => void
|
|
129
|
+
callback: (payload: AbuilderEventPayloadMap[T]) => void,
|
|
130
|
+
options?: AddEventSubscriptionOptions
|
|
65
131
|
): { unsubscribe: () => void } {
|
|
66
132
|
let set = listeners.get(eventName);
|
|
67
133
|
if (!set) {
|
|
68
134
|
set = new Set();
|
|
69
135
|
listeners.set(eventName, set);
|
|
70
136
|
}
|
|
71
|
-
|
|
137
|
+
const filter =
|
|
138
|
+
options && Object.prototype.hasOwnProperty.call(options, "nameSpace")
|
|
139
|
+
? options.nameSpace
|
|
140
|
+
: undefined;
|
|
141
|
+
const wrapped: EventCallback = (payload) => {
|
|
142
|
+
if (!matchesNameSpaceFilter(payload, filter)) return;
|
|
143
|
+
callback(payload as AbuilderEventPayloadMap[T]);
|
|
144
|
+
};
|
|
145
|
+
set.add(wrapped);
|
|
72
146
|
return {
|
|
73
147
|
unsubscribe: () => {
|
|
74
|
-
set.delete(
|
|
148
|
+
set.delete(wrapped);
|
|
75
149
|
if (set.size === 0) {
|
|
76
150
|
listeners.delete(eventName);
|
|
77
151
|
}
|
|
@@ -11,6 +11,7 @@ export type WorkspaceProjectSyncMessage = {
|
|
|
11
11
|
|
|
12
12
|
export type WorkspaceProjectsChangedMessage =
|
|
13
13
|
| { type: "deleted"; projectIds: string[] }
|
|
14
|
+
| { type: "upserted"; projectIds: string[] }
|
|
14
15
|
| { type: "cleared" };
|
|
15
16
|
|
|
16
17
|
export function broadcastWorkspaceProjectsChanged(
|
|
@@ -33,7 +34,12 @@ export function subscribeWorkspaceProjectsChanged(
|
|
|
33
34
|
const channel = new BroadcastChannel(appStorageKey(LIST_CHANNEL, nameSpace));
|
|
34
35
|
channel.onmessage = (event: MessageEvent<WorkspaceProjectsChangedMessage>) => {
|
|
35
36
|
const data = event.data;
|
|
36
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
!data ||
|
|
39
|
+
(data.type !== "deleted" && data.type !== "cleared" && data.type !== "upserted")
|
|
40
|
+
) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
37
43
|
onChange(data);
|
|
38
44
|
};
|
|
39
45
|
return () => channel.close();
|
|
@@ -1,16 +1,73 @@
|
|
|
1
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
1
2
|
import {
|
|
2
3
|
clearWorkspaceProjectStore,
|
|
3
4
|
deleteWorkspaceProject as deleteWorkspaceProjectRecord,
|
|
5
|
+
getWorkspaceProject,
|
|
4
6
|
listWorkspaceProjects,
|
|
7
|
+
putWorkspaceProject as putWorkspaceProjectRecord,
|
|
8
|
+
type WorkspaceProjectRecord,
|
|
5
9
|
} from "./workspace-project-db";
|
|
6
10
|
import {
|
|
7
11
|
clearWorkspacePreviewCaches,
|
|
8
12
|
removeWorkspacePreviewCache,
|
|
13
|
+
writeWorkspacePreviewCache,
|
|
9
14
|
} from "./workspace-project-cache";
|
|
10
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
broadcastWorkspaceProjectUpdate,
|
|
17
|
+
broadcastWorkspaceProjectsChanged,
|
|
18
|
+
} from "./workspace-project-sync";
|
|
19
|
+
import { AbuilderEvents, emitAbuilderEvent } from "./event-subscription";
|
|
20
|
+
|
|
21
|
+
export { listWorkspaceProjects, getWorkspaceProject };
|
|
22
|
+
export type { WorkspaceProjectListItem, WorkspaceProjectRecord } from "./workspace-project-db";
|
|
23
|
+
|
|
24
|
+
export type PutWorkspaceProjectOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* 写入后是否发出工作区事件。
|
|
27
|
+
* - `false` / 省略:只写 IndexedDB(宿主自己管后端时常用)
|
|
28
|
+
* - `"add"`:发出 `workspace:add`
|
|
29
|
+
* - `"sync"`:发出 `workspace:sync`
|
|
30
|
+
*/
|
|
31
|
+
emitEvent?: false | "add" | "sync";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolvedNameSpace(nameSpace?: string | null): string | null {
|
|
35
|
+
return normalizeAppNameSpace(nameSpace) || null;
|
|
36
|
+
}
|
|
11
37
|
|
|
12
|
-
|
|
13
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 写入 / 覆盖 IndexedDB 中的工作区(upsert),并更新预览缓存、通知其他标签页。
|
|
40
|
+
* `nameSpace` 需与面板的 `nameSpace` 一致。
|
|
41
|
+
*/
|
|
42
|
+
export async function putWorkspaceProject(
|
|
43
|
+
record: WorkspaceProjectRecord,
|
|
44
|
+
nameSpace?: string | null,
|
|
45
|
+
options?: PutWorkspaceProjectOptions
|
|
46
|
+
): Promise<WorkspaceProjectRecord> {
|
|
47
|
+
const saved = await putWorkspaceProjectRecord(record, nameSpace);
|
|
48
|
+
writeWorkspacePreviewCache(saved, nameSpace);
|
|
49
|
+
broadcastWorkspaceProjectUpdate(saved.id, saved.updatedAt, nameSpace);
|
|
50
|
+
broadcastWorkspaceProjectsChanged(
|
|
51
|
+
{ type: "upserted", projectIds: [saved.id] },
|
|
52
|
+
nameSpace
|
|
53
|
+
);
|
|
54
|
+
const ns = resolvedNameSpace(nameSpace);
|
|
55
|
+
if (options?.emitEvent === "add") {
|
|
56
|
+
emitAbuilderEvent(AbuilderEvents.workspaceAdd, { ...saved, nameSpace: ns });
|
|
57
|
+
} else if (options?.emitEvent === "sync") {
|
|
58
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, { ...saved, nameSpace: ns });
|
|
59
|
+
}
|
|
60
|
+
return saved;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `putWorkspaceProject` 的别名 */
|
|
64
|
+
export async function upsertWorkspaceProject(
|
|
65
|
+
record: WorkspaceProjectRecord,
|
|
66
|
+
nameSpace?: string | null,
|
|
67
|
+
options?: PutWorkspaceProjectOptions
|
|
68
|
+
): Promise<WorkspaceProjectRecord> {
|
|
69
|
+
return putWorkspaceProject(record, nameSpace, options);
|
|
70
|
+
}
|
|
14
71
|
|
|
15
72
|
/**
|
|
16
73
|
* 删除 IndexedDB 中保存的单个工作区(及其 localStorage 预览标记)。
|
|
@@ -23,6 +80,10 @@ export async function deleteWorkspaceProject(
|
|
|
23
80
|
await deleteWorkspaceProjectRecord(id, nameSpace);
|
|
24
81
|
removeWorkspacePreviewCache(id, nameSpace);
|
|
25
82
|
broadcastWorkspaceProjectsChanged({ type: "deleted", projectIds: [id] }, nameSpace);
|
|
83
|
+
emitAbuilderEvent(AbuilderEvents.workspaceDelete, {
|
|
84
|
+
ids: [id],
|
|
85
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
86
|
+
});
|
|
26
87
|
}
|
|
27
88
|
|
|
28
89
|
/**
|
|
@@ -34,5 +95,11 @@ export async function clearWorkspaceProjects(nameSpace?: string | null): Promise
|
|
|
34
95
|
await clearWorkspaceProjectStore(nameSpace);
|
|
35
96
|
clearWorkspacePreviewCaches(nameSpace);
|
|
36
97
|
broadcastWorkspaceProjectsChanged({ type: "cleared" }, nameSpace);
|
|
98
|
+
if (items.length > 0) {
|
|
99
|
+
emitAbuilderEvent(AbuilderEvents.workspaceDelete, {
|
|
100
|
+
ids: items.map((item) => item.id),
|
|
101
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
37
104
|
return items.length;
|
|
38
105
|
}
|