persistence
This commit is contained in:
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user