@arronqzy/vue-view 0.1.16 → 0.1.17
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 +4 -4
- package/src/panel/VueViewPanel.vue +16 -4
- package/src/panel/components/PanelConfigSidebar.vue +46 -1
- package/src/panel/constants/messages.ts +2 -0
- package/src/panel/hooks/usePanelElements.ts +8 -1
- package/src/panel/hooks/useWorkspaceProjects.ts +74 -48
- package/src/panel/library/workspace-project-db.ts +161 -16
- package/src/panel/library/workspace-storage-error.ts +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"uuid": "^13.0.0",
|
|
25
25
|
"vue": "^3.5.13",
|
|
26
26
|
"@arronqzy/blueprint-dsl": "1.0.14",
|
|
27
|
+
"@arronqzy/i18n": "0.1.12",
|
|
27
28
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
28
|
-
"@arronqzy/i18n": "0.1.11",
|
|
29
29
|
"@arronqzy/view-table": "0.1.8",
|
|
30
|
+
"@arronqzy/rx-store": "1.0.8",
|
|
30
31
|
"@arronqzy/vue-blueprint": "0.1.12",
|
|
31
|
-
"@arronqzy/vue-rx-store": "0.1.6"
|
|
32
|
-
"@arronqzy/rx-store": "1.0.8"
|
|
32
|
+
"@arronqzy/vue-rx-store": "0.1.6"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"vue": "^3.4.0"
|
|
@@ -583,8 +583,14 @@ async function handleBlueprintImportFile(file: File) {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
async function handleWorkspaceCreateProject() {
|
|
586
|
-
|
|
587
|
-
|
|
586
|
+
try {
|
|
587
|
+
const result = await workspaceProjects.handleCreateProject();
|
|
588
|
+
if (result?.name) message.success(panelMessages().workspaceCreated(result.name));
|
|
589
|
+
} catch (error) {
|
|
590
|
+
message.error(
|
|
591
|
+
error instanceof Error ? error.message : panelMessages().createWorkspaceFailed
|
|
592
|
+
);
|
|
593
|
+
}
|
|
588
594
|
}
|
|
589
595
|
|
|
590
596
|
async function handleWorkspaceOpenProject(id: string) {
|
|
@@ -609,8 +615,14 @@ async function handleWorkspaceSyncProject() {
|
|
|
609
615
|
}
|
|
610
616
|
|
|
611
617
|
async function handleWorkspaceDeleteProject(id: string) {
|
|
612
|
-
|
|
613
|
-
|
|
618
|
+
try {
|
|
619
|
+
await workspaceProjects.handleDeleteProject(id);
|
|
620
|
+
message.success(panelMessages().workspaceDeleted);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
message.error(
|
|
623
|
+
error instanceof Error ? error.message : panelMessages().deleteWorkspaceFailed
|
|
624
|
+
);
|
|
625
|
+
}
|
|
614
626
|
}
|
|
615
627
|
|
|
616
628
|
async function openOnlinePreviewForProject(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
|
-
import { computed, onMounted, provide, ref, watch } from "vue";
|
|
3
|
+
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from "vue";
|
|
4
4
|
import { Empty, Input } from "ant-design-vue";
|
|
5
5
|
import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../types";
|
|
6
6
|
import { CHART_TYPES } from "../utils/chartOptionBuilder";
|
|
@@ -60,6 +60,8 @@ const props = withDefaults(
|
|
|
60
60
|
const configSearch = ref("");
|
|
61
61
|
const isSearchCollapsed = ref(false);
|
|
62
62
|
const sidebarScrollRef = ref<HTMLElement | null>(null);
|
|
63
|
+
const configScrollPosById = new Map<string, number>();
|
|
64
|
+
const activeConfigScrollKey = ref<string | null>(null);
|
|
63
65
|
|
|
64
66
|
const expandedSections = ref<Record<string, boolean>>({
|
|
65
67
|
nodeInfo: true,
|
|
@@ -88,6 +90,49 @@ const effectiveSelectedElements = computed(() => {
|
|
|
88
90
|
|
|
89
91
|
const isMultiSelectMode = computed(() => effectiveSelectedElements.value.length > 1);
|
|
90
92
|
|
|
93
|
+
const configScrollKey = computed(() => {
|
|
94
|
+
if (isMultiSelectMode.value || !props.selectedElement) return null;
|
|
95
|
+
return props.selectedElement.id;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function onConfigSidebarScroll() {
|
|
99
|
+
const el = sidebarScrollRef.value;
|
|
100
|
+
const key = activeConfigScrollKey.value;
|
|
101
|
+
if (!el || !key) return;
|
|
102
|
+
configScrollPosById.set(key, el.scrollTop);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restoreConfigScroll(key: string | null) {
|
|
106
|
+
const el = sidebarScrollRef.value;
|
|
107
|
+
if (!el) return;
|
|
108
|
+
const top = key != null ? (configScrollPosById.get(key) ?? 0) : 0;
|
|
109
|
+
el.scrollTop = top;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
watch(
|
|
113
|
+
configScrollKey,
|
|
114
|
+
(nextKey, prevKey) => {
|
|
115
|
+
const el = sidebarScrollRef.value;
|
|
116
|
+
if (el && prevKey && prevKey !== nextKey) {
|
|
117
|
+
configScrollPosById.set(prevKey, el.scrollTop);
|
|
118
|
+
}
|
|
119
|
+
activeConfigScrollKey.value = nextKey;
|
|
120
|
+
void nextTick(() => restoreConfigScroll(nextKey));
|
|
121
|
+
},
|
|
122
|
+
{ flush: "post" }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
onMounted(() => {
|
|
126
|
+
activeConfigScrollKey.value = configScrollKey.value;
|
|
127
|
+
const el = sidebarScrollRef.value;
|
|
128
|
+
el?.addEventListener("scroll", onConfigSidebarScroll, { passive: true });
|
|
129
|
+
restoreConfigScroll(configScrollKey.value);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
onUnmounted(() => {
|
|
133
|
+
sidebarScrollRef.value?.removeEventListener("scroll", onConfigSidebarScroll);
|
|
134
|
+
});
|
|
135
|
+
|
|
91
136
|
const selectedLayer = computed(() => {
|
|
92
137
|
if (!props.selectedElement) return null;
|
|
93
138
|
return props.layers.find((l) => l.id === props.selectedElement?.layerId) ?? null;
|
|
@@ -52,7 +52,9 @@ export function getPanelMessages(t: TranslateFn) {
|
|
|
52
52
|
workspaceLoaded: t("panel.messages.workspaceLoaded"),
|
|
53
53
|
workspaceLoadedDesc: t("panel.messages.workspaceLoadedDesc"),
|
|
54
54
|
openWorkspaceFailed: t("panel.messages.openWorkspaceFailed"),
|
|
55
|
+
createWorkspaceFailed: t("panel.messages.createWorkspaceFailed"),
|
|
55
56
|
syncFailed: t("panel.messages.syncFailed"),
|
|
57
|
+
deleteWorkspaceFailed: t("panel.messages.deleteWorkspaceFailed"),
|
|
56
58
|
workspaceDeleted: t("panel.messages.workspaceDeleted"),
|
|
57
59
|
openPreviewFailed: t("panel.messages.openPreviewFailed"),
|
|
58
60
|
clearedBlueprintLogs: (removed: number) =>
|
|
@@ -1047,7 +1047,14 @@ export function usePanelElements() {
|
|
|
1047
1047
|
/* fall through */
|
|
1048
1048
|
}
|
|
1049
1049
|
}
|
|
1050
|
-
|
|
1050
|
+
try {
|
|
1051
|
+
return JSON.parse(JSON.stringify(current)) as State;
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw Object.assign(new Error("indexeddb-serialize-failed"), {
|
|
1054
|
+
name: "DataCloneError",
|
|
1055
|
+
cause: error,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1051
1058
|
}
|
|
1052
1059
|
|
|
1053
1060
|
function importPanelData(nextState: State) {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
workspaceSnapshotsEqual,
|
|
24
24
|
type WorkspaceSnapshot,
|
|
25
25
|
} from "../library/workspace-snapshot";
|
|
26
|
+
import { mapWorkspaceStorageError } from "../library/workspace-storage-error";
|
|
26
27
|
import { runBusyTask } from "../utils/async-work";
|
|
27
28
|
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
28
29
|
import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
|
|
@@ -125,7 +126,11 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
async function refreshProjects() {
|
|
128
|
-
|
|
129
|
+
try {
|
|
130
|
+
projects.value = await listWorkspaceProjects(nameSpace());
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn("[abuilder] listWorkspaceProjects failed", error);
|
|
133
|
+
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
watch(
|
|
@@ -262,35 +267,48 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
262
267
|
name: string;
|
|
263
268
|
createdAt?: number;
|
|
264
269
|
}) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const record: WorkspaceProjectRecord = {
|
|
272
|
-
id,
|
|
273
|
-
name,
|
|
274
|
-
createdAt,
|
|
275
|
-
updatedAt: now,
|
|
276
|
-
panelState: options.exportPanelData(),
|
|
277
|
-
blueprintDocument: snapshot.blueprintDocument,
|
|
278
|
-
blueprintMeta: snapshot.blueprintMeta,
|
|
279
|
-
productName:
|
|
280
|
-
typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
|
|
281
|
-
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
282
|
-
};
|
|
270
|
+
try {
|
|
271
|
+
const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const id = persistOptions.id ?? createWorkspaceProjectId();
|
|
274
|
+
const createdAt = persistOptions.createdAt ?? now;
|
|
275
|
+
const name = (persistOptions.name ?? "").trim() || resolveProjectName();
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
277
|
+
let panelState: State;
|
|
278
|
+
try {
|
|
279
|
+
panelState = options.exportPanelData();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw mapWorkspaceStorageError(error, t);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const record: WorkspaceProjectRecord = {
|
|
285
|
+
id,
|
|
286
|
+
name,
|
|
287
|
+
createdAt,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
panelState,
|
|
290
|
+
blueprintDocument: snapshot.blueprintDocument,
|
|
291
|
+
blueprintMeta: snapshot.blueprintMeta,
|
|
292
|
+
productName:
|
|
293
|
+
typeof snapshot.productName === "string"
|
|
294
|
+
? snapshot.productName
|
|
295
|
+
: resolveProjectName(),
|
|
296
|
+
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
await putWorkspaceProject(record, nameSpace());
|
|
300
|
+
writeWorkspacePreviewCache(record, nameSpace());
|
|
301
|
+
broadcastWorkspaceProjectUpdate(id, now, nameSpace());
|
|
302
|
+
activeProjectId.value = id;
|
|
303
|
+
activeProjectName.value = name;
|
|
304
|
+
syncedSnapshotRef.value = snapshot;
|
|
305
|
+
syncedPanelRevision = toValue(options.panelRevision);
|
|
306
|
+
pendingRevisionSync = false;
|
|
307
|
+
await refreshProjects();
|
|
308
|
+
return record;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw mapWorkspaceStorageError(error, t);
|
|
311
|
+
}
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
async function handleCreateProject() {
|
|
@@ -320,14 +338,18 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
320
338
|
}
|
|
321
339
|
|
|
322
340
|
async function handleOpenProject(id: string) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
341
|
+
try {
|
|
342
|
+
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
343
|
+
const record = await getWorkspaceProject(id, nameSpace());
|
|
344
|
+
if (!record) {
|
|
345
|
+
await refreshProjects();
|
|
346
|
+
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
347
|
+
}
|
|
348
|
+
applyProjectRecord(record);
|
|
349
|
+
});
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw mapWorkspaceStorageError(error, t);
|
|
352
|
+
}
|
|
331
353
|
}
|
|
332
354
|
|
|
333
355
|
async function handleSyncProject() {
|
|
@@ -355,17 +377,21 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
355
377
|
}
|
|
356
378
|
|
|
357
379
|
async function handleDeleteProject(id: string) {
|
|
358
|
-
|
|
359
|
-
await
|
|
360
|
-
|
|
361
|
-
activeProjectId.value
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
380
|
+
try {
|
|
381
|
+
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
382
|
+
await deleteWorkspaceProject(id, nameSpace());
|
|
383
|
+
if (activeProjectId.value === id) {
|
|
384
|
+
activeProjectId.value = null;
|
|
385
|
+
activeProjectName.value = null;
|
|
386
|
+
syncedSnapshotRef.value = null;
|
|
387
|
+
}
|
|
388
|
+
previewWindows.delete(id);
|
|
389
|
+
refreshPreviewing();
|
|
390
|
+
await refreshProjects();
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw mapWorkspaceStorageError(error, t);
|
|
394
|
+
}
|
|
369
395
|
}
|
|
370
396
|
|
|
371
397
|
async function handlePreviewProject(id: string, previewOptions?: { syncFirst?: boolean }) {
|
|
@@ -5,6 +5,7 @@ import { appStorageKey } from "@arronqzy/blueprint-dsl";
|
|
|
5
5
|
const DB_NAME = "arronqzy-workspace-projects";
|
|
6
6
|
const DB_VERSION = 1;
|
|
7
7
|
const STORE_NAME = "projects";
|
|
8
|
+
const OPEN_TIMEOUT_MS = 10_000;
|
|
8
9
|
|
|
9
10
|
export type WorkspaceProjectListItem = {
|
|
10
11
|
id: string;
|
|
@@ -25,10 +26,77 @@ export type WorkspaceProjectRecord = {
|
|
|
25
26
|
titleIconDataUrl?: string;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
const dbPromises = new Map<string, Promise<IDBDatabase>>();
|
|
30
|
+
|
|
31
|
+
function getIndexedDb(): IDBFactory {
|
|
32
|
+
if (typeof indexedDB === "undefined" || !indexedDB) {
|
|
33
|
+
throw Object.assign(new Error("indexeddb-unavailable"), {
|
|
34
|
+
name: "IndexedDbUnavailable",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return indexedDB;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Make a value safe for IndexedDB structured clone (drops non-cloneable fields). */
|
|
41
|
+
export function cloneForIndexedDb<T>(value: T): T {
|
|
42
|
+
if (typeof structuredClone === "function") {
|
|
43
|
+
try {
|
|
44
|
+
return structuredClone(value);
|
|
45
|
+
} catch {
|
|
46
|
+
/* fall through to JSON */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw Object.assign(new Error("indexeddb-serialize-failed"), {
|
|
53
|
+
name: "DataCloneError",
|
|
54
|
+
cause: error,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
const key = appStorageKey(DB_NAME, nameSpace);
|
|
61
|
+
const cached = dbPromises.get(key);
|
|
62
|
+
if (cached) return cached;
|
|
63
|
+
|
|
64
|
+
const promise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
65
|
+
let settled = false;
|
|
66
|
+
const fail = (error: unknown) => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
dbPromises.delete(key);
|
|
70
|
+
reject(
|
|
71
|
+
error instanceof Error
|
|
72
|
+
? error
|
|
73
|
+
: Object.assign(new Error("indexeddb-open-failed"), { cause: error })
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let request: IDBOpenDBRequest;
|
|
78
|
+
try {
|
|
79
|
+
request = getIndexedDb().open(key, DB_VERSION);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
fail(error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
fail(
|
|
87
|
+
Object.assign(new Error("indexeddb-open-timeout"), {
|
|
88
|
+
name: "TimeoutError",
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}, OPEN_TIMEOUT_MS);
|
|
92
|
+
|
|
93
|
+
request.onerror = () => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
fail(request.error ?? new Error("indexeddb-open-failed"));
|
|
96
|
+
};
|
|
97
|
+
request.onblocked = () => {
|
|
98
|
+
// Wait for onsuccess / timeout; another tab may be holding an older connection.
|
|
99
|
+
};
|
|
32
100
|
request.onupgradeneeded = () => {
|
|
33
101
|
const db = request.result;
|
|
34
102
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
@@ -36,8 +104,35 @@ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
|
|
|
36
104
|
store.createIndex("updatedAt", "updatedAt", { unique: false });
|
|
37
105
|
}
|
|
38
106
|
};
|
|
39
|
-
request.onsuccess = () =>
|
|
107
|
+
request.onsuccess = () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
if (settled) {
|
|
110
|
+
try {
|
|
111
|
+
request.result.close();
|
|
112
|
+
} catch {
|
|
113
|
+
/* ignore */
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
settled = true;
|
|
118
|
+
const db = request.result;
|
|
119
|
+
db.onversionchange = () => {
|
|
120
|
+
try {
|
|
121
|
+
db.close();
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
dbPromises.delete(key);
|
|
126
|
+
};
|
|
127
|
+
db.onclose = () => {
|
|
128
|
+
dbPromises.delete(key);
|
|
129
|
+
};
|
|
130
|
+
resolve(db);
|
|
131
|
+
};
|
|
40
132
|
});
|
|
133
|
+
|
|
134
|
+
dbPromises.set(key, promise);
|
|
135
|
+
return promise;
|
|
41
136
|
}
|
|
42
137
|
|
|
43
138
|
function runTransaction<T>(
|
|
@@ -48,27 +143,74 @@ function runTransaction<T>(
|
|
|
48
143
|
return openDb(nameSpace).then(
|
|
49
144
|
(db) =>
|
|
50
145
|
new Promise<T>((resolve, reject) => {
|
|
51
|
-
|
|
146
|
+
let tx: IDBTransaction;
|
|
147
|
+
try {
|
|
148
|
+
tx = db.transaction(STORE_NAME, mode);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
dbPromises.delete(appStorageKey(DB_NAME, nameSpace));
|
|
151
|
+
reject(error);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
52
154
|
const store = tx.objectStore(STORE_NAME);
|
|
53
155
|
const request = runner(store);
|
|
54
156
|
let result!: T;
|
|
157
|
+
let settled = false;
|
|
158
|
+
|
|
159
|
+
const fail = (error: unknown) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
reject(
|
|
163
|
+
error instanceof Error
|
|
164
|
+
? error
|
|
165
|
+
: Object.assign(new Error("indexeddb-transaction-failed"), {
|
|
166
|
+
cause: error,
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
};
|
|
55
170
|
|
|
56
171
|
request.onerror = () =>
|
|
57
|
-
|
|
172
|
+
fail(request.error ?? new Error("indexeddb-request-failed"));
|
|
58
173
|
request.onsuccess = () => {
|
|
59
174
|
result = request.result as T;
|
|
60
175
|
};
|
|
61
176
|
|
|
62
177
|
tx.oncomplete = () => {
|
|
63
|
-
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
64
180
|
resolve(result);
|
|
65
181
|
};
|
|
66
|
-
tx.onerror = () =>
|
|
67
|
-
|
|
68
|
-
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
69
|
-
};
|
|
182
|
+
tx.onerror = () => fail(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
183
|
+
tx.onabort = () => fail(tx.error ?? new Error("indexeddb-transaction-aborted"));
|
|
70
184
|
})
|
|
71
|
-
)
|
|
185
|
+
).catch(async (error) => {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
const name = error instanceof DOMException ? error.name : "";
|
|
188
|
+
if (
|
|
189
|
+
name === "InvalidStateError" ||
|
|
190
|
+
message.includes("InvalidStateError") ||
|
|
191
|
+
message.includes("closing")
|
|
192
|
+
) {
|
|
193
|
+
dbPromises.delete(appStorageKey(DB_NAME, nameSpace));
|
|
194
|
+
const db = await openDb(nameSpace);
|
|
195
|
+
return new Promise<T>((resolve, reject) => {
|
|
196
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
197
|
+
const store = tx.objectStore(STORE_NAME);
|
|
198
|
+
const request = runner(store);
|
|
199
|
+
let result!: T;
|
|
200
|
+
request.onerror = () =>
|
|
201
|
+
reject(request.error ?? new Error("indexeddb-request-failed"));
|
|
202
|
+
request.onsuccess = () => {
|
|
203
|
+
result = request.result as T;
|
|
204
|
+
};
|
|
205
|
+
tx.oncomplete = () => resolve(result);
|
|
206
|
+
tx.onerror = () =>
|
|
207
|
+
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
208
|
+
tx.onabort = () =>
|
|
209
|
+
reject(tx.error ?? new Error("indexeddb-transaction-aborted"));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
});
|
|
72
214
|
}
|
|
73
215
|
|
|
74
216
|
export function createWorkspaceProjectId(): string {
|
|
@@ -102,11 +244,9 @@ export async function listWorkspaceProjects(
|
|
|
102
244
|
cursor.continue();
|
|
103
245
|
};
|
|
104
246
|
tx.oncomplete = () => {
|
|
105
|
-
db.close();
|
|
106
247
|
resolve(items.sort((a, b) => b.updatedAt - a.updatedAt));
|
|
107
248
|
};
|
|
108
249
|
tx.onerror = () => {
|
|
109
|
-
db.close();
|
|
110
250
|
reject(tx.error ?? new Error("indexeddb-transaction-failed"));
|
|
111
251
|
};
|
|
112
252
|
});
|
|
@@ -128,8 +268,13 @@ export async function putWorkspaceProject(
|
|
|
128
268
|
record: WorkspaceProjectRecord,
|
|
129
269
|
nameSpace?: string | null
|
|
130
270
|
): Promise<WorkspaceProjectRecord> {
|
|
131
|
-
|
|
132
|
-
|
|
271
|
+
const safeRecord = cloneForIndexedDb(record);
|
|
272
|
+
await runTransaction<IDBValidKey>(
|
|
273
|
+
"readwrite",
|
|
274
|
+
(store) => store.put(safeRecord),
|
|
275
|
+
nameSpace
|
|
276
|
+
);
|
|
277
|
+
return safeRecord;
|
|
133
278
|
}
|
|
134
279
|
|
|
135
280
|
export async function deleteWorkspaceProject(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map IndexedDB / clone failures to localized workspace messages.
|
|
3
|
+
* Used by create / sync / open / delete so device-specific DOMExceptions
|
|
4
|
+
* show actionable copy instead of opaque browser English.
|
|
5
|
+
*/
|
|
6
|
+
export function mapWorkspaceStorageError(
|
|
7
|
+
error: unknown,
|
|
8
|
+
t: (key: string) => string
|
|
9
|
+
): Error {
|
|
10
|
+
const raw = error instanceof Error ? error : new Error(String(error));
|
|
11
|
+
const name =
|
|
12
|
+
(error as { name?: string } | null)?.name ||
|
|
13
|
+
(raw as { name?: string }).name ||
|
|
14
|
+
"";
|
|
15
|
+
const message = raw.message || "";
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
name === "IndexedDbUnavailable" ||
|
|
19
|
+
name === "SecurityError" ||
|
|
20
|
+
message.includes("indexeddb-unavailable") ||
|
|
21
|
+
/indexedDB is not available|Access is denied|The operation is insecure/i.test(
|
|
22
|
+
message
|
|
23
|
+
)
|
|
24
|
+
) {
|
|
25
|
+
return new Error(t("panel.messages.workspaceStorageUnavailable"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
name === "TimeoutError" ||
|
|
30
|
+
message.includes("indexeddb-open-timeout")
|
|
31
|
+
) {
|
|
32
|
+
return new Error(t("panel.messages.workspaceStorageTimeout"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
name === "QuotaExceededError" ||
|
|
37
|
+
/QuotaExceeded|quota|storage.*full|exceeded the quota/i.test(message)
|
|
38
|
+
) {
|
|
39
|
+
return new Error(t("panel.messages.workspaceStorageQuotaExceeded"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
name === "DataCloneError" ||
|
|
44
|
+
message.includes("indexeddb-serialize-failed") ||
|
|
45
|
+
/could not be cloned|DataCloneError|circular/i.test(message)
|
|
46
|
+
) {
|
|
47
|
+
return new Error(t("panel.messages.workspaceStorageSerializeFailed"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
message.includes("indexeddb-open-failed") ||
|
|
52
|
+
message.includes("indexeddb-transaction-failed") ||
|
|
53
|
+
message.includes("indexeddb-request-failed") ||
|
|
54
|
+
message.includes("indexeddb-transaction-aborted")
|
|
55
|
+
) {
|
|
56
|
+
return new Error(t("panel.messages.workspaceStorageFailed"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return raw;
|
|
60
|
+
}
|