From 866a31b89dae390746fe422da0dde50b82ac4cba Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 2 Nov 2025 15:34:26 +0100 Subject: [PATCH] persistence --- frontend/src/DesktopWorkspace.jsx | 118 +++++++++++++++++++++++- frontend/src/app/AppLayout.jsx | 17 +++- frontend/src/desktop/useDocumentDrag.js | 27 +++--- 3 files changed, 142 insertions(+), 20 deletions(-) diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index 65b37b1..776bcf6 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -583,6 +583,7 @@ const DesktopWorkspace = ({ onClearSelection = null, helpOpen = false, onHelpClose = null, + tenantId = null, }) => { const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]); @@ -591,6 +592,7 @@ const DesktopWorkspace = ({ const layoutRef = useRef(new Map()); const itemRefs = useRef(new Map()); const zCounterRef = useRef(10); + const layoutDirtyRef = useRef(false); const [layoutSnapshot, setLayoutSnapshot] = useState(() => new Map()); @@ -626,6 +628,62 @@ const DesktopWorkspace = ({ documentLookupRef.current = documentLookup; }, [documentLookup]); + const storageKey = useMemo(() => { + if (!tenantId) { + return null; + } + return `papercrate.desk-layout.${tenantId}`; + }, [tenantId]); + + const initialPersistedLayout = useMemo(() => { + if (!storageKey || typeof window === 'undefined') { + return new Map(); + } + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) { + return new Map(); + } + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object') { + return new Map(); + } + const map = new Map(); + Object.entries(parsed).forEach(([docId, value]) => { + if (!value || typeof value !== 'object') { + return; + } + const centerX = Number(value.centerX); + const centerY = Number(value.centerY); + if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) { + return; + } + const rotation = Number.isFinite(Number(value.rotation)) ? Number(value.rotation) : 0; + const z = Number.isFinite(Number(value.z)) ? Number(value.z) : undefined; + map.set(String(docId), { + centerX, + centerY, + rotation, + z, + }); + }); + return map; + } catch (error) { + console.warn('[desk] Failed to parse persisted layout', error); + return new Map(); + } + }, [storageKey]); + + const persistedLayoutRef = useRef(initialPersistedLayout); + + useEffect(() => { + persistedLayoutRef.current = initialPersistedLayout; + }, [initialPersistedLayout]); + + const markLayoutDirty = useCallback(() => { + layoutDirtyRef.current = true; + }, []); + const applySnapshotDimensions = useCallback((docKey, snapshot) => { const width = Number(snapshot?.width); const height = Number(snapshot?.height); @@ -1107,9 +1165,49 @@ const recalcVisibleDocIds = useCallback(() => { documentLookup, ]); -const syncLayoutSnapshot = useCallback(() => { - setLayoutSnapshot(new Map(layoutRef.current)); - }, []); + const persistLayoutSnapshot = useCallback( + (snapshot, force = false) => { + if (!storageKey || typeof window === 'undefined') { + return; + } + if (!force && !layoutDirtyRef.current) { + return; + } + layoutDirtyRef.current = false; + const payload = {}; + snapshot.forEach((entry, docId) => { + if (!docId || !entry) { + return; + } + const centerX = Number(entry.centerX); + const centerY = Number(entry.centerY); + if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) { + return; + } + payload[docId] = { + centerX, + centerY, + rotation: Number.isFinite(Number(entry.rotation)) ? Number(entry.rotation) : 0, + z: Number.isFinite(Number(entry.z)) ? Number(entry.z) : undefined, + }; + }); + try { + window.localStorage.setItem(storageKey, JSON.stringify(payload)); + persistedLayoutRef.current = new Map( + Object.entries(payload).map(([id, value]) => [id, value]), + ); + } catch (error) { + console.warn('[desk] Failed to persist desk layout', error); + } + }, + [storageKey], + ); + + const syncLayoutSnapshot = useCallback((force = false) => { + const snapshot = new Map(layoutRef.current); + setLayoutSnapshot(snapshot); + persistLayoutSnapshot(snapshot, force); + }, [persistLayoutSnapshot]); useLayoutEffect(() => { const container = containerRef.current; @@ -1185,7 +1283,14 @@ const syncLayoutSnapshot = useCallback(() => { const minCenterY = CANVAS_PADDING + halfHeight; const maxCenterY = Math.max(minCenterY, canvasHeight - CANVAS_PADDING - halfHeight); - const existing = previous.get(doc.id); + const docKey = doc?.id != null ? String(doc.id) : null; + const persisted = docKey ? persistedLayoutRef.current.get(docKey) : null; + let existing = previous.get(doc.id) || null; + if (persisted) { + existing = existing + ? { ...existing, ...persisted } + : { ...persisted }; + } if (existing) { const defaultCenterX = (minCenterX + maxCenterX) / 2; const defaultCenterY = (minCenterY + maxCenterY) / 2; @@ -1258,10 +1363,11 @@ const syncLayoutSnapshot = useCallback(() => { if (!entry) return; const updated = { ...entry, z: zCounterRef.current }; layoutRef.current.set(docId, updated); + markLayoutDirty(); syncLayoutSnapshot(); recalcVisibleDocIds(); }, - [syncLayoutSnapshot, recalcVisibleDocIds], + [syncLayoutSnapshot, recalcVisibleDocIds, markLayoutDirty], ); const openOverlayForDoc = useCallback( @@ -1740,6 +1846,7 @@ const syncLayoutSnapshot = useCallback(() => { closeOverlay, overlayOriginRect, overlayOriginTransform, + markLayoutDirty, selectedDocumentIds, onClearSelection, }), @@ -1789,6 +1896,7 @@ const syncLayoutSnapshot = useCallback(() => { selectedDocumentIds, onClearSelection, onDocumentStackSelect, + markLayoutDirty, ], ); diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx index 974e981..01cf362 100644 --- a/frontend/src/app/AppLayout.jsx +++ b/frontend/src/app/AppLayout.jsx @@ -195,8 +195,13 @@ const AppLayout = () => { if (typeof window === 'undefined') { return 'list'; } - const stored = window.localStorage.getItem('papercrate_view_mode'); - return stored === 'grid' || stored === 'desk' ? stored : 'list'; + try { + const stored = window.sessionStorage.getItem('papercrate_view_mode'); + return stored === 'grid' || stored === 'desk' ? stored : 'list'; + } catch (error) { + console.warn('[view-mode] failed to read stored mode', error); + return 'list'; + } }); const [deskHelpOpen, setDeskHelpOpen] = useState(false); const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode); @@ -4481,7 +4486,11 @@ const AppLayout = () => { const next = mode === 'grid' ? 'grid' : mode === 'desk' ? 'desk' : 'list'; setDocumentsViewMode((previous) => { if (next !== previous && typeof window !== 'undefined') { - window.localStorage.setItem('papercrate_view_mode', next); + try { + window.sessionStorage.setItem('papercrate_view_mode', next); + } catch (error) { + console.warn('[view-mode] failed to persist mode', error); + } } return next; }); @@ -4873,6 +4882,7 @@ const AppLayout = () => { onOpenHelp: handleDeskHelpOpen, helpOpen: deskHelpOpen, onHelpClose: handleDeskHelpClose, + tenantId: currentTenantId, selectedDocumentIds, onClearSelection: clearDocumentSelection, resolveThumbnailUrl: resolveThumbnailUrlForDoc, @@ -4897,6 +4907,7 @@ const AppLayout = () => { handleDeskDocumentStackSelect, handleDeskHelpOpen, handleDeskHelpClose, + currentTenantId, deskHelpOpen, selectedDocumentIds, clearDocumentSelection, diff --git a/frontend/src/desktop/useDocumentDrag.js b/frontend/src/desktop/useDocumentDrag.js index 29e2738..a2d6369 100644 --- a/frontend/src/desktop/useDocumentDrag.js +++ b/frontend/src/desktop/useDocumentDrag.js @@ -33,6 +33,7 @@ const useDocumentDrag = () => { onDocumentOpen, onInspectDocument, selectedDocumentIds, + markLayoutDirty, } = useDesktopContext(); const applyTransform = useCallback( @@ -74,18 +75,20 @@ const useDocumentDrag = () => { rotation, }); - applyTransform( - item.docId, - centerX, - centerY, - item.width, - item.height, - rotation, - item.docId === dragState.docKey ? dragState.dragScale || 1 : 1, - ); - }); - }, - [applyTransform, layoutRef], + applyTransform( + item.docId, + centerX, + centerY, + item.width, + item.height, + rotation, + item.docId === dragState.docKey ? dragState.dragScale || 1 : 1, + ); + }); + + markLayoutDirty?.(); + }, + [applyTransform, layoutRef, markLayoutDirty], ); const tapHandler = usePointerTap({