From 39e3ac2a87d21dba95cb135df6e21f62518fd65a Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 26 Nov 2025 20:24:00 +0100 Subject: [PATCH] feat: Implement layout persistence for card positions and rotation, loading from and saving to the database. --- frontend/src/desktop/CardDragLogic.ts | 1 + frontend/src/desktop/DesktopWorkspace.tsx | 10 +++ frontend/src/desktop/LayoutSystem.ts | 85 +++++++++++++++++-- .../src/documents/panel/DocumentsPanel.tsx | 2 +- 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/frontend/src/desktop/CardDragLogic.ts b/frontend/src/desktop/CardDragLogic.ts index 2392472..0d53c96 100644 --- a/frontend/src/desktop/CardDragLogic.ts +++ b/frontend/src/desktop/CardDragLogic.ts @@ -55,4 +55,5 @@ export const handleDragEnd = (store: LayoutStore, selection: string[]) => { card.finishDrag(); } }); + store.saveLayout(); }; diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index d250807..bdcd74d 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -54,6 +54,8 @@ export interface DesktopWorkspaceProps { onDocumentActivate?: (doc: DeskDocument, event?: unknown) => void; onSelectionChange?: (selectedIds: Identifier[]) => void; onDocumentTagDrop?: (docId: Identifier, tag: any) => void; + tenantId?: Identifier | null; + viewId?: string | null; } // Wrapper to handle hooks per card @@ -84,6 +86,8 @@ const DesktopWorkspace: React.FC = ({ onDocumentActivate, onSelectionChange, onDocumentTagDrop, + tenantId, + viewId, }) => { const containerRef = useRef(null); @@ -99,6 +103,12 @@ const DesktopWorkspace: React.FC = ({ const layoutStore = useMemo(() => new LayoutStore(), []); const layoutRef = useRef>(new Map()); + useEffect(() => { + if (tenantId && viewId) { + layoutStore.loadLayout(String(tenantId), viewId); + } + }, [layoutStore, tenantId, viewId]); + // Selection Context const { selectedDocumentIds, diff --git a/frontend/src/desktop/LayoutSystem.ts b/frontend/src/desktop/LayoutSystem.ts index 26c0796..ae0ae25 100644 --- a/frontend/src/desktop/LayoutSystem.ts +++ b/frontend/src/desktop/LayoutSystem.ts @@ -1,4 +1,5 @@ import { constrainDimensions } from './utils/layoutUtils'; +import { fetchLayoutRecords, upsertLayoutRecords } from './db'; export interface LayoutCardState { id: string; @@ -225,6 +226,10 @@ export class LayoutStore { items = new Map(); zCounter = 100; + private savedLayouts = new Map(); + private tenantId: string | null = null; + private viewId: string | null = null; + initialize(id: string, ref: HTMLElement | null, config: { width: number; height: number; @@ -238,10 +243,27 @@ export class LayoutStore { ); if (!card) { - // Apply defaults if not provided - const x = Math.random() * 500; - const y = Math.random() * 500; - const rotation = Math.random() * 10 - 5; + // Check for saved layout + const saved = this.savedLayouts.get(id); + + let x, y, rotation, z; + + if (saved) { + x = saved.x; + y = saved.y; + rotation = saved.rotation; + z = saved.z; + // Ensure zCounter is higher than any loaded z + if (z >= this.zCounter) { + this.zCounter = z + 1; + } + } else { + // Apply defaults if not provided + x = Math.random() * 500; + y = Math.random() * 500; + rotation = Math.random() * 10 - 5; + z = this.zCounter++; + } card = new LayoutCard(id, this, { x, @@ -249,7 +271,7 @@ export class LayoutStore { rotation, width, height, - z: this.zCounter++ + z }, ref); this.items.set(id, card); } else { @@ -319,4 +341,57 @@ export class LayoutStore { getSnapshot() { return Array.from(this.items.values()).map(card => card.toSnapshot()); } + + async loadLayout(tenantId: string, viewId: string) { + this.tenantId = tenantId; + this.viewId = viewId; + + const records = await fetchLayoutRecords({ tenantId, viewId }); + + this.savedLayouts.clear(); + let maxZ = this.zCounter; + + for (const record of records) { + if (record.documentId && record.centerX !== undefined && record.centerY !== undefined) { + this.savedLayouts.set(record.documentId, { + x: record.centerX, + y: record.centerY, + rotation: record.rotation || 0, + z: record.zIndex || 0 + }); + if (record.zIndex && record.zIndex >= maxZ) { + maxZ = record.zIndex + 1; + } + } + } + + this.zCounter = maxZ; + + // Apply to existing items if any (though usually this runs before items are created) + for (const [id, card] of this.items) { + const saved = this.savedLayouts.get(id); + if (saved) { + card.update(saved); + } + } + } + + async saveLayout() { + if (!this.tenantId || !this.viewId) return; + + const entries = Array.from(this.items.values()).map(card => ({ + documentId: card.id, + centerX: card.x, + centerY: card.y, + rotation: card.rotation, + zIndex: card.z, + updatedAt: Date.now() + })); + + await upsertLayoutRecords({ + tenantId: this.tenantId, + viewId: this.viewId, + entries + }); + } } diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index 70c75f2..20f35f0 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -839,7 +839,7 @@ const DocumentsPanelInner: React.FC = ({ switch (viewMode) { case 'desk': - return ; + return ; case 'grid': return ; case 'list':