persistence

This commit is contained in:
2025-11-02 15:34:26 +01:00
parent 4f296ac1e3
commit 866a31b89d
3 changed files with 142 additions and 20 deletions
+113 -5
View File
@@ -583,6 +583,7 @@ const DesktopWorkspace = ({
onClearSelection = null, onClearSelection = null,
helpOpen = false, helpOpen = false,
onHelpClose = null, onHelpClose = null,
tenantId = null,
}) => { }) => {
const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]); const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]);
@@ -591,6 +592,7 @@ const DesktopWorkspace = ({
const layoutRef = useRef(new Map()); const layoutRef = useRef(new Map());
const itemRefs = useRef(new Map()); const itemRefs = useRef(new Map());
const zCounterRef = useRef(10); const zCounterRef = useRef(10);
const layoutDirtyRef = useRef(false);
const [layoutSnapshot, setLayoutSnapshot] = useState(() => new Map()); const [layoutSnapshot, setLayoutSnapshot] = useState(() => new Map());
@@ -626,6 +628,62 @@ const DesktopWorkspace = ({
documentLookupRef.current = documentLookup; documentLookupRef.current = documentLookup;
}, [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 applySnapshotDimensions = useCallback((docKey, snapshot) => {
const width = Number(snapshot?.width); const width = Number(snapshot?.width);
const height = Number(snapshot?.height); const height = Number(snapshot?.height);
@@ -1107,9 +1165,49 @@ const recalcVisibleDocIds = useCallback(() => {
documentLookup, documentLookup,
]); ]);
const syncLayoutSnapshot = useCallback(() => { const persistLayoutSnapshot = useCallback(
setLayoutSnapshot(new Map(layoutRef.current)); (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(() => { useLayoutEffect(() => {
const container = containerRef.current; const container = containerRef.current;
@@ -1185,7 +1283,14 @@ const syncLayoutSnapshot = useCallback(() => {
const minCenterY = CANVAS_PADDING + halfHeight; const minCenterY = CANVAS_PADDING + halfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - 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) { if (existing) {
const defaultCenterX = (minCenterX + maxCenterX) / 2; const defaultCenterX = (minCenterX + maxCenterX) / 2;
const defaultCenterY = (minCenterY + maxCenterY) / 2; const defaultCenterY = (minCenterY + maxCenterY) / 2;
@@ -1258,10 +1363,11 @@ const syncLayoutSnapshot = useCallback(() => {
if (!entry) return; if (!entry) return;
const updated = { ...entry, z: zCounterRef.current }; const updated = { ...entry, z: zCounterRef.current };
layoutRef.current.set(docId, updated); layoutRef.current.set(docId, updated);
markLayoutDirty();
syncLayoutSnapshot(); syncLayoutSnapshot();
recalcVisibleDocIds(); recalcVisibleDocIds();
}, },
[syncLayoutSnapshot, recalcVisibleDocIds], [syncLayoutSnapshot, recalcVisibleDocIds, markLayoutDirty],
); );
const openOverlayForDoc = useCallback( const openOverlayForDoc = useCallback(
@@ -1740,6 +1846,7 @@ const syncLayoutSnapshot = useCallback(() => {
closeOverlay, closeOverlay,
overlayOriginRect, overlayOriginRect,
overlayOriginTransform, overlayOriginTransform,
markLayoutDirty,
selectedDocumentIds, selectedDocumentIds,
onClearSelection, onClearSelection,
}), }),
@@ -1789,6 +1896,7 @@ const syncLayoutSnapshot = useCallback(() => {
selectedDocumentIds, selectedDocumentIds,
onClearSelection, onClearSelection,
onDocumentStackSelect, onDocumentStackSelect,
markLayoutDirty,
], ],
); );
+14 -3
View File
@@ -195,8 +195,13 @@ const AppLayout = () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return 'list'; return 'list';
} }
const stored = window.localStorage.getItem('papercrate_view_mode'); try {
return stored === 'grid' || stored === 'desk' ? stored : 'list'; 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 [deskHelpOpen, setDeskHelpOpen] = useState(false);
const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode); const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode);
@@ -4481,7 +4486,11 @@ const AppLayout = () => {
const next = mode === 'grid' ? 'grid' : mode === 'desk' ? 'desk' : 'list'; const next = mode === 'grid' ? 'grid' : mode === 'desk' ? 'desk' : 'list';
setDocumentsViewMode((previous) => { setDocumentsViewMode((previous) => {
if (next !== previous && typeof window !== 'undefined') { 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; return next;
}); });
@@ -4873,6 +4882,7 @@ const AppLayout = () => {
onOpenHelp: handleDeskHelpOpen, onOpenHelp: handleDeskHelpOpen,
helpOpen: deskHelpOpen, helpOpen: deskHelpOpen,
onHelpClose: handleDeskHelpClose, onHelpClose: handleDeskHelpClose,
tenantId: currentTenantId,
selectedDocumentIds, selectedDocumentIds,
onClearSelection: clearDocumentSelection, onClearSelection: clearDocumentSelection,
resolveThumbnailUrl: resolveThumbnailUrlForDoc, resolveThumbnailUrl: resolveThumbnailUrlForDoc,
@@ -4897,6 +4907,7 @@ const AppLayout = () => {
handleDeskDocumentStackSelect, handleDeskDocumentStackSelect,
handleDeskHelpOpen, handleDeskHelpOpen,
handleDeskHelpClose, handleDeskHelpClose,
currentTenantId,
deskHelpOpen, deskHelpOpen,
selectedDocumentIds, selectedDocumentIds,
clearDocumentSelection, clearDocumentSelection,
+15 -12
View File
@@ -33,6 +33,7 @@ const useDocumentDrag = () => {
onDocumentOpen, onDocumentOpen,
onInspectDocument, onInspectDocument,
selectedDocumentIds, selectedDocumentIds,
markLayoutDirty,
} = useDesktopContext(); } = useDesktopContext();
const applyTransform = useCallback( const applyTransform = useCallback(
@@ -74,18 +75,20 @@ const useDocumentDrag = () => {
rotation, rotation,
}); });
applyTransform( applyTransform(
item.docId, item.docId,
centerX, centerX,
centerY, centerY,
item.width, item.width,
item.height, item.height,
rotation, rotation,
item.docId === dragState.docKey ? dragState.dragScale || 1 : 1, item.docId === dragState.docKey ? dragState.dragScale || 1 : 1,
); );
}); });
},
[applyTransform, layoutRef], markLayoutDirty?.();
},
[applyTransform, layoutRef, markLayoutDirty],
); );
const tapHandler = usePointerTap({ const tapHandler = usePointerTap({