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,
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,
],
);
+14 -3
View File
@@ -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,
+15 -12
View File
@@ -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({