@arronqzy/vue-view 0.1.16 → 0.1.18
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 +5 -5
- package/src/panel/VueViewPanel.vue +31 -5
- package/src/panel/components/ElementsLayer.vue +3 -0
- package/src/panel/components/MoveableLayer.vue +170 -132
- package/src/panel/components/PanelConfigSidebar.vue +46 -1
- package/src/panel/constants/messages.ts +2 -0
- package/src/panel/hooks/usePanelElements.ts +28 -12
- 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/src/panel/utils/canvas-interaction-busy.ts +26 -0
- package/src/panel/utils/materializePanelElements.ts +96 -0
- package/src/panel/utils/panelStateIO.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"selecto": "^1.26.3",
|
|
24
24
|
"uuid": "^13.0.0",
|
|
25
25
|
"vue": "^3.5.13",
|
|
26
|
-
"@arronqzy/
|
|
26
|
+
"@arronqzy/rx-store": "1.0.8",
|
|
27
|
+
"@arronqzy/i18n": "0.1.12",
|
|
27
28
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
28
|
-
"@arronqzy/
|
|
29
|
+
"@arronqzy/vue-blueprint": "0.1.13",
|
|
29
30
|
"@arronqzy/view-table": "0.1.8",
|
|
30
|
-
"@arronqzy/vue-blueprint": "0.1.12",
|
|
31
31
|
"@arronqzy/vue-rx-store": "0.1.6",
|
|
32
|
-
"@arronqzy/
|
|
32
|
+
"@arronqzy/blueprint-dsl": "1.0.14"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"vue": "^3.4.0"
|
|
@@ -409,11 +409,25 @@ const workspaceProjects = useWorkspaceProjects({
|
|
|
409
409
|
nameSpace: () => props.nameSpace,
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
+
const blueprintLifecycleToken = computed(() => {
|
|
413
|
+
const doc = blueprintGraph.value.document;
|
|
414
|
+
const nodes = doc.nodes;
|
|
415
|
+
const edges = doc.edges;
|
|
416
|
+
let sig = `${nodes.length}:${edges.length}`;
|
|
417
|
+
for (const node of nodes) {
|
|
418
|
+
sig += `|${node.id}:${resolveRunnableNodeType(node)}:${node.lifecyclePhase ?? ""}`;
|
|
419
|
+
}
|
|
420
|
+
for (const edge of edges) {
|
|
421
|
+
sig += `>${edge.id}:${edge.source}:${edge.target}`;
|
|
422
|
+
}
|
|
423
|
+
return sig;
|
|
424
|
+
});
|
|
425
|
+
|
|
412
426
|
const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
|
|
413
427
|
graph: blueprintGraph,
|
|
414
428
|
active: blueprintOpen,
|
|
415
429
|
bootKey: computed(() => workspaceProjects.activeProjectId.value ?? undefined),
|
|
416
|
-
onUpdated: computed(() => `${activeLayerId.value}|${
|
|
430
|
+
onUpdated: computed(() => `${activeLayerId.value}|${blueprintLifecycleToken.value}`),
|
|
417
431
|
resolveLibraryBlueprint,
|
|
418
432
|
libraryNameById: blueprintLibraryNameById,
|
|
419
433
|
rootLibraryBlueprintId: activeBlueprintLibraryId,
|
|
@@ -583,8 +597,14 @@ async function handleBlueprintImportFile(file: File) {
|
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
async function handleWorkspaceCreateProject() {
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
try {
|
|
601
|
+
const result = await workspaceProjects.handleCreateProject();
|
|
602
|
+
if (result?.name) message.success(panelMessages().workspaceCreated(result.name));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
message.error(
|
|
605
|
+
error instanceof Error ? error.message : panelMessages().createWorkspaceFailed
|
|
606
|
+
);
|
|
607
|
+
}
|
|
588
608
|
}
|
|
589
609
|
|
|
590
610
|
async function handleWorkspaceOpenProject(id: string) {
|
|
@@ -609,8 +629,14 @@ async function handleWorkspaceSyncProject() {
|
|
|
609
629
|
}
|
|
610
630
|
|
|
611
631
|
async function handleWorkspaceDeleteProject(id: string) {
|
|
612
|
-
|
|
613
|
-
|
|
632
|
+
try {
|
|
633
|
+
await workspaceProjects.handleDeleteProject(id);
|
|
634
|
+
message.success(panelMessages().workspaceDeleted);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
message.error(
|
|
637
|
+
error instanceof Error ? error.message : panelMessages().deleteWorkspaceFailed
|
|
638
|
+
);
|
|
639
|
+
}
|
|
614
640
|
}
|
|
615
641
|
|
|
616
642
|
async function openOnlinePreviewForProject(
|
|
@@ -185,6 +185,7 @@ function hasViewEvent(el: PanelElement) {
|
|
|
185
185
|
<div
|
|
186
186
|
v-for="el in sortedElements"
|
|
187
187
|
:key="el.id"
|
|
188
|
+
v-memo="[el, selectedIds.includes(el.id), previewMode, layerLocked, hasViewEvent(el), previewLayoutKey]"
|
|
188
189
|
:class="[
|
|
189
190
|
'absolute select-none',
|
|
190
191
|
previewMode ? '' : 'rv-selectable',
|
|
@@ -202,6 +203,8 @@ function hasViewEvent(el: PanelElement) {
|
|
|
202
203
|
transformOrigin: 'center center',
|
|
203
204
|
boxSizing: 'border-box',
|
|
204
205
|
overflow: el.materialType === 'viewport' ? 'hidden' : undefined,
|
|
206
|
+
contentVisibility: 'auto',
|
|
207
|
+
containIntrinsicSize: `${Math.max(1, el.width)}px ${Math.max(1, el.height)}px`,
|
|
205
208
|
...getNodeVisualStyle(el),
|
|
206
209
|
}"
|
|
207
210
|
@mousedown="onSelect(el.id, $event)"
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import Moveable from "moveable";
|
|
12
12
|
import type { PanelElement } from "../types";
|
|
13
13
|
import { uniformViewportZoom } from "../viewportZoom";
|
|
14
|
-
import {
|
|
14
|
+
import { setCanvasInteractionBusy } from "../utils/canvas-interaction-busy";
|
|
15
15
|
|
|
16
16
|
const { t, locale } = useI18n();
|
|
17
17
|
|
|
@@ -444,6 +444,7 @@ function createMoveable() {
|
|
|
444
444
|
const point = readClientPoint(e);
|
|
445
445
|
e.datas.__startClientX = point?.x ?? null;
|
|
446
446
|
e.datas.__startClientY = point?.y ?? null;
|
|
447
|
+
setCanvasInteractionBusy(true);
|
|
447
448
|
});
|
|
448
449
|
|
|
449
450
|
moveable.on("drag", (e: any) => {
|
|
@@ -479,6 +480,7 @@ function createMoveable() {
|
|
|
479
480
|
ev.datas.__startClientX = point?.x ?? null;
|
|
480
481
|
ev.datas.__startClientY = point?.y ?? null;
|
|
481
482
|
});
|
|
483
|
+
setCanvasInteractionBusy(true);
|
|
482
484
|
});
|
|
483
485
|
|
|
484
486
|
moveable.on("dragGroup", (e: any) => {
|
|
@@ -513,6 +515,7 @@ function createMoveable() {
|
|
|
513
515
|
e.datas.__startY = data.y;
|
|
514
516
|
e.datas.__startClientX = e.inputEvent?.clientX ?? null;
|
|
515
517
|
e.datas.__startClientY = e.inputEvent?.clientY ?? null;
|
|
518
|
+
setCanvasInteractionBusy(true);
|
|
516
519
|
});
|
|
517
520
|
|
|
518
521
|
moveable.on("resize", (e: any) => {
|
|
@@ -526,7 +529,6 @@ function createMoveable() {
|
|
|
526
529
|
const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
|
|
527
530
|
e.target.style.left = `${sx + tx}px`;
|
|
528
531
|
e.target.style.top = `${sy + ty}px`;
|
|
529
|
-
notifyPreviewLayoutChanged();
|
|
530
532
|
});
|
|
531
533
|
|
|
532
534
|
moveable.on("resizeGroupStart", (e: any) => {
|
|
@@ -543,6 +545,7 @@ function createMoveable() {
|
|
|
543
545
|
ev.datas.__startClientX = input?.clientX ?? null;
|
|
544
546
|
ev.datas.__startClientY = input?.clientY ?? null;
|
|
545
547
|
});
|
|
548
|
+
setCanvasInteractionBusy(true);
|
|
546
549
|
});
|
|
547
550
|
|
|
548
551
|
moveable.on("resizeGroup", (e: any) => {
|
|
@@ -557,7 +560,6 @@ function createMoveable() {
|
|
|
557
560
|
ev.target.style.left = `${sx + tx}px`;
|
|
558
561
|
ev.target.style.top = `${sy + ty}px`;
|
|
559
562
|
});
|
|
560
|
-
notifyPreviewLayoutChanged();
|
|
561
563
|
});
|
|
562
564
|
|
|
563
565
|
moveable.on("rotateStart", (e: any) => {
|
|
@@ -566,6 +568,7 @@ function createMoveable() {
|
|
|
566
568
|
const data = props.elementsById.get(id);
|
|
567
569
|
if (!data) return;
|
|
568
570
|
e.set(data.rotate ?? 0);
|
|
571
|
+
setCanvasInteractionBusy(true);
|
|
569
572
|
});
|
|
570
573
|
|
|
571
574
|
moveable.on("rotate", (e: any) => {
|
|
@@ -573,6 +576,17 @@ function createMoveable() {
|
|
|
573
576
|
e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
|
|
574
577
|
});
|
|
575
578
|
|
|
579
|
+
moveable.on("rotateGroupStart", (e: any) => {
|
|
580
|
+
e.events.forEach((ev: any) => {
|
|
581
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
582
|
+
if (!id) return;
|
|
583
|
+
const data = props.elementsById.get(id);
|
|
584
|
+
if (!data) return;
|
|
585
|
+
ev.set(data.rotate ?? 0);
|
|
586
|
+
});
|
|
587
|
+
setCanvasInteractionBusy(true);
|
|
588
|
+
});
|
|
589
|
+
|
|
576
590
|
moveable.on("rotateGroup", (e: any) => {
|
|
577
591
|
e.events.forEach((ev: any) => {
|
|
578
592
|
if (!ev?.target?.style) return;
|
|
@@ -581,175 +595,199 @@ function createMoveable() {
|
|
|
581
595
|
});
|
|
582
596
|
|
|
583
597
|
moveable.on("dragEnd", (e: any) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const data = props.elementsById.get(id);
|
|
587
|
-
if (!data) return;
|
|
588
|
-
const sx = e.datas.__startX ?? data.x;
|
|
589
|
-
const sy = e.datas.__startY ?? data.y;
|
|
590
|
-
const point = readClientPoint(e);
|
|
591
|
-
const hasClient =
|
|
592
|
-
typeof e.datas.__startClientX === "number" &&
|
|
593
|
-
typeof e.datas.__startClientY === "number" &&
|
|
594
|
-
!!point;
|
|
595
|
-
const tx = hasClient
|
|
596
|
-
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
597
|
-
: toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
598
|
-
const ty = hasClient
|
|
599
|
-
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
600
|
-
: toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
601
|
-
const nextX = sx + tx;
|
|
602
|
-
const nextY = sy + ty;
|
|
603
|
-
const size = readTargetCanvasSize(e.target as HTMLElement | null, {
|
|
604
|
-
width: data.width,
|
|
605
|
-
height: data.height,
|
|
606
|
-
});
|
|
607
|
-
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
608
|
-
props.updateElement(id, { x: nextX, y: nextY, ...patch });
|
|
609
|
-
if (data.materialType === "grid") {
|
|
610
|
-
const dx = nextX - data.x;
|
|
611
|
-
const dy = nextY - data.y;
|
|
612
|
-
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
613
|
-
const children = getGridDirectChildren(id);
|
|
614
|
-
const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
615
|
-
children.forEach((child) => {
|
|
616
|
-
props.updateElement(
|
|
617
|
-
child.id,
|
|
618
|
-
{ x: child.x + dx, y: child.y + dy },
|
|
619
|
-
{ batchId, meta: { type: "node.group-drag" } }
|
|
620
|
-
);
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
scheduleViewportSync();
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
moveable.on("dragGroupEnd", (e: any) => {
|
|
628
|
-
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
629
|
-
const selectedSet = new Set(
|
|
630
|
-
e.events
|
|
631
|
-
.map((ev: any) => (ev.target ? getId(ev.target) : null))
|
|
632
|
-
.filter((id: string | null): id is string => !!id)
|
|
633
|
-
);
|
|
634
|
-
const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
|
|
635
|
-
e.events.forEach((ev: any) => {
|
|
636
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
598
|
+
try {
|
|
599
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
637
600
|
if (!id) return;
|
|
638
601
|
const data = props.elementsById.get(id);
|
|
639
602
|
if (!data) return;
|
|
640
|
-
const sx =
|
|
641
|
-
const sy =
|
|
642
|
-
const point = readClientPoint(
|
|
603
|
+
const sx = e.datas.__startX ?? data.x;
|
|
604
|
+
const sy = e.datas.__startY ?? data.y;
|
|
605
|
+
const point = readClientPoint(e);
|
|
643
606
|
const hasClient =
|
|
644
|
-
typeof
|
|
645
|
-
typeof
|
|
607
|
+
typeof e.datas.__startClientX === "number" &&
|
|
608
|
+
typeof e.datas.__startClientY === "number" &&
|
|
646
609
|
!!point;
|
|
647
610
|
const tx = hasClient
|
|
648
|
-
? toCanvasDeltaX(point.x -
|
|
649
|
-
: toCanvasDeltaX(
|
|
611
|
+
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
612
|
+
: toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
650
613
|
const ty = hasClient
|
|
651
|
-
? toCanvasDeltaY(point.y -
|
|
652
|
-
: toCanvasDeltaY(
|
|
614
|
+
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
615
|
+
: toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
653
616
|
const nextX = sx + tx;
|
|
654
617
|
const nextY = sy + ty;
|
|
655
|
-
const size = readTargetCanvasSize(
|
|
618
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, {
|
|
656
619
|
width: data.width,
|
|
657
620
|
height: data.height,
|
|
658
621
|
});
|
|
659
622
|
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
660
|
-
props.updateElement(
|
|
661
|
-
id,
|
|
662
|
-
{ x: nextX, y: nextY, ...patch },
|
|
663
|
-
{ batchId, meta: { type: "node.group-drag" } }
|
|
664
|
-
);
|
|
623
|
+
props.updateElement(id, { x: nextX, y: nextY, ...patch });
|
|
665
624
|
if (data.materialType === "grid") {
|
|
666
625
|
const dx = nextX - data.x;
|
|
667
626
|
const dy = nextY - data.y;
|
|
668
627
|
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
669
|
-
|
|
628
|
+
const children = getGridDirectChildren(id);
|
|
629
|
+
const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
630
|
+
children.forEach((child) => {
|
|
631
|
+
props.updateElement(
|
|
632
|
+
child.id,
|
|
633
|
+
{ x: child.x + dx, y: child.y + dy },
|
|
634
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
635
|
+
);
|
|
636
|
+
});
|
|
670
637
|
}
|
|
671
638
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
639
|
+
scheduleViewportSync();
|
|
640
|
+
} finally {
|
|
641
|
+
setCanvasInteractionBusy(false);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
moveable.on("dragGroupEnd", (e: any) => {
|
|
646
|
+
try {
|
|
647
|
+
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
648
|
+
const selectedSet = new Set(
|
|
649
|
+
e.events
|
|
650
|
+
.map((ev: any) => (ev.target ? getId(ev.target) : null))
|
|
651
|
+
.filter((id: string | null): id is string => !!id)
|
|
652
|
+
);
|
|
653
|
+
const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
|
|
654
|
+
e.events.forEach((ev: any) => {
|
|
655
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
656
|
+
if (!id) return;
|
|
657
|
+
const data = props.elementsById.get(id);
|
|
658
|
+
if (!data) return;
|
|
659
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
660
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
661
|
+
const point = readClientPoint(ev) ?? readClientPoint(e);
|
|
662
|
+
const hasClient =
|
|
663
|
+
typeof ev.datas.__startClientX === "number" &&
|
|
664
|
+
typeof ev.datas.__startClientY === "number" &&
|
|
665
|
+
!!point;
|
|
666
|
+
const tx = hasClient
|
|
667
|
+
? toCanvasDeltaX(point.x - ev.datas.__startClientX)
|
|
668
|
+
: toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
669
|
+
const ty = hasClient
|
|
670
|
+
? toCanvasDeltaY(point.y - ev.datas.__startClientY)
|
|
671
|
+
: toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
672
|
+
const nextX = sx + tx;
|
|
673
|
+
const nextY = sy + ty;
|
|
674
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
|
|
675
|
+
width: data.width,
|
|
676
|
+
height: data.height,
|
|
677
|
+
});
|
|
678
|
+
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
677
679
|
props.updateElement(
|
|
678
|
-
|
|
679
|
-
{ x:
|
|
680
|
+
id,
|
|
681
|
+
{ x: nextX, y: nextY, ...patch },
|
|
680
682
|
{ batchId, meta: { type: "node.group-drag" } }
|
|
681
683
|
);
|
|
684
|
+
if (data.materialType === "grid") {
|
|
685
|
+
const dx = nextX - data.x;
|
|
686
|
+
const dy = nextY - data.y;
|
|
687
|
+
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
688
|
+
gridDeltaMap.set(id, { dx, dy });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
682
691
|
});
|
|
692
|
+
for (const [gridId, delta] of gridDeltaMap.entries()) {
|
|
693
|
+
const children = getGridDirectChildren(gridId);
|
|
694
|
+
children.forEach((child) => {
|
|
695
|
+
if (selectedSet.has(child.id)) return;
|
|
696
|
+
props.updateElement(
|
|
697
|
+
child.id,
|
|
698
|
+
{ x: child.x + delta.dx, y: child.y + delta.dy },
|
|
699
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
scheduleViewportSync();
|
|
704
|
+
} finally {
|
|
705
|
+
setCanvasInteractionBusy(false);
|
|
683
706
|
}
|
|
684
|
-
scheduleViewportSync();
|
|
685
707
|
});
|
|
686
708
|
|
|
687
709
|
moveable.on("resizeEnd", (e: any) => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const data = props.elementsById.get(id);
|
|
691
|
-
if (!data) return;
|
|
692
|
-
const width = e.lastEvent?.width ?? data.width;
|
|
693
|
-
const height = e.lastEvent?.height ?? data.height;
|
|
694
|
-
const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
695
|
-
const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
696
|
-
const sx = e.datas.__startX ?? data.x;
|
|
697
|
-
const sy = e.datas.__startY ?? data.y;
|
|
698
|
-
const nextX = sx + tx;
|
|
699
|
-
const nextY = sy + ty;
|
|
700
|
-
const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
|
|
701
|
-
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
702
|
-
props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
703
|
-
scheduleViewportSync();
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
moveable.on("resizeGroupEnd", (e: any) => {
|
|
707
|
-
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
708
|
-
e.events.forEach((ev: any) => {
|
|
709
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
710
|
+
try {
|
|
711
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
710
712
|
if (!id) return;
|
|
711
713
|
const data = props.elementsById.get(id);
|
|
712
714
|
if (!data) return;
|
|
713
|
-
const width =
|
|
714
|
-
const height =
|
|
715
|
-
const tx = toCanvasDeltaX(
|
|
716
|
-
const ty = toCanvasDeltaY(
|
|
717
|
-
const sx =
|
|
718
|
-
const sy =
|
|
715
|
+
const width = e.lastEvent?.width ?? data.width;
|
|
716
|
+
const height = e.lastEvent?.height ?? data.height;
|
|
717
|
+
const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
718
|
+
const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
719
|
+
const sx = e.datas.__startX ?? data.x;
|
|
720
|
+
const sy = e.datas.__startY ?? data.y;
|
|
719
721
|
const nextX = sx + tx;
|
|
720
722
|
const nextY = sy + ty;
|
|
721
|
-
const size = readTargetCanvasSize(
|
|
723
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
|
|
722
724
|
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
723
|
-
props.updateElement(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
scheduleViewportSync();
|
|
725
|
+
props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
726
|
+
scheduleViewportSync();
|
|
727
|
+
} finally {
|
|
728
|
+
setCanvasInteractionBusy(false);
|
|
729
|
+
}
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
-
moveable.on("
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
732
|
+
moveable.on("resizeGroupEnd", (e: any) => {
|
|
733
|
+
try {
|
|
734
|
+
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
735
|
+
e.events.forEach((ev: any) => {
|
|
736
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
737
|
+
if (!id) return;
|
|
738
|
+
const data = props.elementsById.get(id);
|
|
739
|
+
if (!data) return;
|
|
740
|
+
const width = ev.lastEvent?.width ?? data.width;
|
|
741
|
+
const height = ev.lastEvent?.height ?? data.height;
|
|
742
|
+
const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
743
|
+
const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
744
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
745
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
746
|
+
const nextX = sx + tx;
|
|
747
|
+
const nextY = sy + ty;
|
|
748
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
|
|
749
|
+
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
750
|
+
props.updateElement(
|
|
751
|
+
id,
|
|
752
|
+
{ width, height, x: nextX, y: nextY, ...snapPatch },
|
|
753
|
+
{ batchId, meta: { type: "node.group-resize" } }
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
scheduleViewportSync();
|
|
757
|
+
} finally {
|
|
758
|
+
setCanvasInteractionBusy(false);
|
|
759
|
+
}
|
|
740
760
|
});
|
|
741
761
|
|
|
742
|
-
moveable.on("
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
762
|
+
moveable.on("rotateEnd", (e: any) => {
|
|
763
|
+
try {
|
|
764
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
746
765
|
if (!id) return;
|
|
747
766
|
const data = props.elementsById.get(id);
|
|
748
767
|
if (!data) return;
|
|
749
|
-
const rotate =
|
|
750
|
-
props.updateElement(id, { rotate }
|
|
751
|
-
|
|
752
|
-
|
|
768
|
+
const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
769
|
+
props.updateElement(id, { rotate });
|
|
770
|
+
scheduleViewportSync();
|
|
771
|
+
} finally {
|
|
772
|
+
setCanvasInteractionBusy(false);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
moveable.on("rotateGroupEnd", (e: any) => {
|
|
777
|
+
try {
|
|
778
|
+
const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
779
|
+
e.events.forEach((ev: any) => {
|
|
780
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
781
|
+
if (!id) return;
|
|
782
|
+
const data = props.elementsById.get(id);
|
|
783
|
+
if (!data) return;
|
|
784
|
+
const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
785
|
+
props.updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
|
|
786
|
+
});
|
|
787
|
+
scheduleViewportSync();
|
|
788
|
+
} finally {
|
|
789
|
+
setCanvasInteractionBusy(false);
|
|
790
|
+
}
|
|
753
791
|
});
|
|
754
792
|
|
|
755
793
|
moveableRef.value = moveable;
|
|
@@ -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) =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, onMounted } from "vue";
|
|
1
|
+
import { computed, onMounted, shallowRef } from "vue";
|
|
2
2
|
import { store, type Node, type State } from "@arronqzy/rx-store";
|
|
3
3
|
import { useStoreRef } from "@arronqzy/vue-rx-store";
|
|
4
4
|
import { useI18nOptional } from "@arronqzy/i18n/vue";
|
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
applyGridLayoutPatchAcrossMappingFamily,
|
|
48
48
|
applyMappingFamilySyncPatch,
|
|
49
49
|
} from "../utils/updateElementDraft";
|
|
50
|
+
import {
|
|
51
|
+
materializePanelElements,
|
|
52
|
+
stabilizeElementList,
|
|
53
|
+
} from "../utils/materializePanelElements";
|
|
50
54
|
|
|
51
55
|
export type { PanelActionResult, PanelHistoryItem, PanelLayer } from "../types";
|
|
52
56
|
|
|
@@ -126,19 +130,24 @@ export function usePanelElements() {
|
|
|
126
130
|
(stateRef.value.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID
|
|
127
131
|
);
|
|
128
132
|
|
|
133
|
+
const allElementsCache = shallowRef<PanelElement[]>([]);
|
|
134
|
+
const elementsCache = shallowRef<PanelElement[]>([]);
|
|
135
|
+
|
|
129
136
|
const allElements = computed(() => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
+
const next = materializePanelElements(
|
|
138
|
+
stateRef.value.root.children,
|
|
139
|
+
allElementsCache.value
|
|
140
|
+
);
|
|
141
|
+
allElementsCache.value = next;
|
|
142
|
+
return next;
|
|
137
143
|
});
|
|
138
144
|
|
|
139
|
-
const elements = computed(() =>
|
|
140
|
-
allElements.value.filter((el) => el.layerId === activeLayerId.value)
|
|
141
|
-
|
|
145
|
+
const elements = computed(() => {
|
|
146
|
+
const next = allElements.value.filter((el) => el.layerId === activeLayerId.value);
|
|
147
|
+
const stable = stabilizeElementList(next, elementsCache.value);
|
|
148
|
+
elementsCache.value = stable;
|
|
149
|
+
return stable;
|
|
150
|
+
});
|
|
142
151
|
|
|
143
152
|
const byId = computed(() => {
|
|
144
153
|
const map = new Map<string, PanelElement>();
|
|
@@ -1047,7 +1056,14 @@ export function usePanelElements() {
|
|
|
1047
1056
|
/* fall through */
|
|
1048
1057
|
}
|
|
1049
1058
|
}
|
|
1050
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
return JSON.parse(JSON.stringify(current)) as State;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
throw Object.assign(new Error("indexeddb-serialize-failed"), {
|
|
1063
|
+
name: "DataCloneError",
|
|
1064
|
+
cause: error,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1051
1067
|
}
|
|
1052
1068
|
|
|
1053
1069
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* While Moveable is actively dragging/resizing/rotating, pause expensive
|
|
3
|
+
* canvas-wide work (e.g. ellipsis overflow measurement via MutationObserver).
|
|
4
|
+
*/
|
|
5
|
+
let busyDepth = 0;
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
export function isCanvasInteractionBusy(): boolean {
|
|
9
|
+
return busyDepth > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setCanvasInteractionBusy(busy: boolean): void {
|
|
13
|
+
const prev = busyDepth > 0;
|
|
14
|
+
if (busy) busyDepth += 1;
|
|
15
|
+
else busyDepth = Math.max(0, busyDepth - 1);
|
|
16
|
+
const next = busyDepth > 0;
|
|
17
|
+
if (prev === next) return;
|
|
18
|
+
listeners.forEach((listener) => listener());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subscribeCanvasInteractionBusy(listener: () => void): () => void {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(listener);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PanelElement } from "../types";
|
|
2
|
+
import { isPanelElementNode } from "./panelElementNodes";
|
|
3
|
+
import type { Node } from "@arronqzy/rx-store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preserve Immer structural sharing: unchanged nodes keep the same props
|
|
7
|
+
* object reference so React.memo / Vue can skip re-rendering them.
|
|
8
|
+
*/
|
|
9
|
+
export function materializePanelElements(
|
|
10
|
+
nodes: Node[] | undefined,
|
|
11
|
+
previous?: readonly PanelElement[] | null
|
|
12
|
+
): PanelElement[] {
|
|
13
|
+
const list = nodes ?? [];
|
|
14
|
+
const prevById = previous?.length
|
|
15
|
+
? new Map(previous.map((el) => [el.id, el] as const))
|
|
16
|
+
: null;
|
|
17
|
+
const next: PanelElement[] = [];
|
|
18
|
+
|
|
19
|
+
for (const n of list) {
|
|
20
|
+
if (!isPanelElementNode(n) || !n.props) continue;
|
|
21
|
+
const props = n.props as PanelElement;
|
|
22
|
+
const normalized: PanelElement =
|
|
23
|
+
typeof props.zIndex === "number" ? props : { ...props, zIndex: 1 };
|
|
24
|
+
|
|
25
|
+
const prev = prevById?.get(normalized.id);
|
|
26
|
+
if (prev && prev === normalized) {
|
|
27
|
+
next.push(prev);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
prev &&
|
|
32
|
+
prev.id === normalized.id &&
|
|
33
|
+
prev.x === normalized.x &&
|
|
34
|
+
prev.y === normalized.y &&
|
|
35
|
+
prev.width === normalized.width &&
|
|
36
|
+
prev.height === normalized.height &&
|
|
37
|
+
prev.rotate === normalized.rotate &&
|
|
38
|
+
prev.zIndex === normalized.zIndex &&
|
|
39
|
+
prev.layerId === normalized.layerId &&
|
|
40
|
+
prev.locked === normalized.locked &&
|
|
41
|
+
prev.materialType === normalized.materialType &&
|
|
42
|
+
prev.name === normalized.name &&
|
|
43
|
+
prev.chart === normalized.chart &&
|
|
44
|
+
prev.table === normalized.table &&
|
|
45
|
+
prev.style === normalized.style &&
|
|
46
|
+
prev.textHtml === normalized.textHtml &&
|
|
47
|
+
prev.textAllowInput === normalized.textAllowInput &&
|
|
48
|
+
prev.gridRows === normalized.gridRows &&
|
|
49
|
+
prev.gridCols === normalized.gridCols &&
|
|
50
|
+
prev.gridGap === normalized.gridGap &&
|
|
51
|
+
prev.gridPadding === normalized.gridPadding &&
|
|
52
|
+
prev.parentGridId === normalized.parentGridId &&
|
|
53
|
+
prev.gridSlotIndex === normalized.gridSlotIndex &&
|
|
54
|
+
prev.gridColSpan === normalized.gridColSpan &&
|
|
55
|
+
prev.gridRowSpan === normalized.gridRowSpan &&
|
|
56
|
+
prev.viewportOverflow === normalized.viewportOverflow &&
|
|
57
|
+
prev.scene3d === normalized.scene3d &&
|
|
58
|
+
prev.refLayerId === normalized.refLayerId &&
|
|
59
|
+
prev.refCopyMode === normalized.refCopyMode &&
|
|
60
|
+
prev.refSnapshot === normalized.refSnapshot &&
|
|
61
|
+
prev.audioSrc === normalized.audioSrc &&
|
|
62
|
+
prev.videoSrc === normalized.videoSrc &&
|
|
63
|
+
prev.geometryShape === normalized.geometryShape &&
|
|
64
|
+
prev.geometryColor === normalized.geometryColor &&
|
|
65
|
+
prev.geometrySketchDataUrl === normalized.geometrySketchDataUrl
|
|
66
|
+
) {
|
|
67
|
+
next.push(prev);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
next.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
previous &&
|
|
75
|
+
previous.length === next.length &&
|
|
76
|
+
previous.every((el, i) => el === next[i])
|
|
77
|
+
) {
|
|
78
|
+
return previous as PanelElement[];
|
|
79
|
+
}
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keep previous array identity when item references are unchanged (order-sensitive). */
|
|
84
|
+
export function stabilizeElementList(
|
|
85
|
+
next: PanelElement[],
|
|
86
|
+
previous?: readonly PanelElement[] | null
|
|
87
|
+
): PanelElement[] {
|
|
88
|
+
if (
|
|
89
|
+
previous &&
|
|
90
|
+
previous.length === next.length &&
|
|
91
|
+
previous.every((el, i) => el === next[i])
|
|
92
|
+
) {
|
|
93
|
+
return previous as PanelElement[];
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
@@ -137,6 +137,26 @@ export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBou
|
|
|
137
137
|
|
|
138
138
|
export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
let previewLayoutRaf = 0;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Notify chart/preview hosts that layout changed.
|
|
144
|
+
* Default path is rAF-coalesced (safe during Moveable resize).
|
|
145
|
+
* Pass `{ flush: true }` after resize ends so charts catch the final size.
|
|
146
|
+
*/
|
|
147
|
+
export function notifyPreviewLayoutChanged(options?: { flush?: boolean }) {
|
|
148
|
+
if (typeof window === "undefined") return;
|
|
149
|
+
if (options?.flush) {
|
|
150
|
+
if (previewLayoutRaf) {
|
|
151
|
+
window.cancelAnimationFrame(previewLayoutRaf);
|
|
152
|
+
previewLayoutRaf = 0;
|
|
153
|
+
}
|
|
154
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (previewLayoutRaf) return;
|
|
158
|
+
previewLayoutRaf = window.requestAnimationFrame(() => {
|
|
159
|
+
previewLayoutRaf = 0;
|
|
160
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
161
|
+
});
|
|
142
162
|
}
|