@arronqzy/vue-view 0.1.15 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.15",
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,10 +24,10 @@
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.11",
28
- "@arronqzy/rx-store": "1.0.8",
27
+ "@arronqzy/i18n": "0.1.12",
29
28
  "@arronqzy/view-scene3d": "0.1.5",
30
29
  "@arronqzy/view-table": "0.1.8",
30
+ "@arronqzy/rx-store": "1.0.8",
31
31
  "@arronqzy/vue-blueprint": "0.1.12",
32
32
  "@arronqzy/vue-rx-store": "0.1.6"
33
33
  },
@@ -583,8 +583,14 @@ async function handleBlueprintImportFile(file: File) {
583
583
  }
584
584
 
585
585
  async function handleWorkspaceCreateProject() {
586
- const result = await workspaceProjects.handleCreateProject();
587
- if (result?.name) message.success(panelMessages().workspaceCreated(result.name));
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
- await workspaceProjects.handleDeleteProject(id);
613
- message.success(panelMessages().workspaceDeleted);
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
- return JSON.parse(JSON.stringify(current)) as State;
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
- projects.value = await listWorkspaceProjects(nameSpace());
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
- const snapshot = cloneWorkspaceSnapshot(buildCurrentSnapshot());
266
- const now = Date.now();
267
- const id = persistOptions.id ?? createWorkspaceProjectId();
268
- const createdAt = persistOptions.createdAt ?? now;
269
- const name = (persistOptions.name ?? "").trim() || resolveProjectName();
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
- await putWorkspaceProject(record, nameSpace());
285
- writeWorkspacePreviewCache(record, nameSpace());
286
- broadcastWorkspaceProjectUpdate(id, now, nameSpace());
287
- activeProjectId.value = id;
288
- activeProjectName.value = name;
289
- syncedSnapshotRef.value = snapshot;
290
- syncedPanelRevision = toValue(options.panelRevision);
291
- pendingRevisionSync = false;
292
- await refreshProjects();
293
- return record;
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
- await runBusyTask(t("common.loadingWorkspace"), async () => {
324
- const record = await getWorkspaceProject(id, nameSpace());
325
- if (!record) {
326
- await refreshProjects();
327
- throw new Error(t("panel.messages.workspaceNotFound"));
328
- }
329
- applyProjectRecord(record);
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
- await runBusyTask(t("common.deletingWorkspace"), async () => {
359
- await deleteWorkspaceProject(id, nameSpace());
360
- if (activeProjectId.value === id) {
361
- activeProjectId.value = null;
362
- activeProjectName.value = null;
363
- syncedSnapshotRef.value = null;
364
- }
365
- previewWindows.delete(id);
366
- refreshPreviewing();
367
- await refreshProjects();
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
- return new Promise((resolve, reject) => {
30
- const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
31
- request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
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 = () => resolve(request.result);
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
- const tx = db.transaction(STORE_NAME, mode);
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
- reject(request.error ?? new Error("indexeddb-request-failed"));
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
- db.close();
178
+ if (settled) return;
179
+ settled = true;
64
180
  resolve(result);
65
181
  };
66
- tx.onerror = () => {
67
- db.close();
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
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
132
- return record;
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
+ }
@@ -2,6 +2,7 @@ import type { EChartsOption } from "echarts";
2
2
  import type { PanelChartConfig, PanelElement } from "../types";
3
3
 
4
4
  const DEFAULT_CHART_VALUES = [12, 18, 9, 24];
5
+ const DEFAULT_CHART_LABELS = ["A", "B", "C", "D"];
5
6
 
6
7
  export function getChartValuesDisplayText(chart?: PanelChartConfig): string {
7
8
  if (chart?.valuesText !== undefined) return chart.valuesText;
@@ -13,10 +14,26 @@ export function getChartLabelsDisplayText(chart?: PanelChartConfig): string {
13
14
  return (chart?.labels ?? []).join(",");
14
15
  }
15
16
 
17
+ export function parseChartLabelsFromText(
18
+ text: string | undefined,
19
+ fallback: string[] = DEFAULT_CHART_LABELS
20
+ ): string[] {
21
+ if (text === undefined) return fallback;
22
+ const trimmed = text.trim();
23
+ if (!trimmed) return fallback;
24
+ const parts = trimmed.split(",").map((s) => s.trim()).filter(Boolean);
25
+ if (!parts.length) return fallback;
26
+ return parts;
27
+ }
28
+
16
29
  export function resolveChartLabels(element: PanelElement): string[] {
17
30
  const chart = element.chart;
31
+ if (chart?.labelsText !== undefined) {
32
+ const parsed = parseChartLabelsFromText(chart.labelsText, []);
33
+ return parsed.length ? parsed : DEFAULT_CHART_LABELS;
34
+ }
18
35
  if (chart?.labels?.length) return chart.labels;
19
- return ["A", "B", "C", "D"];
36
+ return DEFAULT_CHART_LABELS;
20
37
  }
21
38
 
22
39
  export function parseChartValuesFromText(