Compare commits

...

4 Commits

Author SHA1 Message Date
Zimeng Xiong dd0f381ed1 chore: pre-release v0.4.5-dev 2026-02-07 12:09:21 -08:00
Zimeng Xiong c40a5f46a0 fix colliding drawing IDs 2026-02-07 12:09:02 -08:00
Zimeng Xiong 8fcca43b0d chore: pre-release v0.4.4-dev 2026-02-07 11:58:09 -08:00
Zimeng Xiong f20412cdfb separate debounced autosave 2026-02-07 11:57:32 -08:00
9 changed files with 222 additions and 45 deletions
+1 -1
View File
@@ -1 +1 @@
0.4.3 0.4.5
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "backend", "name": "backend",
"version": "0.4.3", "version": "0.4.5",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+35 -5
View File
@@ -334,7 +334,20 @@ export const registerDashboardRoutes = (
} }
} }
const updatedDrawing = await prisma.drawing.update({ where: { id }, data }); const updateResult = await prisma.drawing.updateMany({
where: { id, userId: req.user.id },
data,
});
if (updateResult.count === 0) {
return res.status(404).json({ error: "Drawing not found" });
}
const updatedDrawing = await prisma.drawing.findFirst({
where: { id, userId: req.user.id },
});
if (!updatedDrawing) {
return res.status(404).json({ error: "Drawing not found" });
}
invalidateDrawingsCache(); invalidateDrawingsCache();
return res.json({ return res.json({
@@ -352,7 +365,12 @@ export const registerDashboardRoutes = (
const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } }); const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
if (!drawing) return res.status(404).json({ error: "Drawing not found" }); if (!drawing) return res.status(404).json({ error: "Drawing not found" });
await prisma.drawing.delete({ where: { id } }); const deleteResult = await prisma.drawing.deleteMany({
where: { id, userId: req.user.id },
});
if (deleteResult.count === 0) {
return res.status(404).json({ error: "Drawing not found" });
}
invalidateDrawingsCache(); invalidateDrawingsCache();
if (config.enableAuditLogging) { if (config.enableAuditLogging) {
@@ -375,6 +393,9 @@ export const registerDashboardRoutes = (
const { id } = req.params; const { id } = req.params;
const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } }); const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } });
if (!original) return res.status(404).json({ error: "Original drawing not found" }); if (!original) return res.status(404).json({ error: "Original drawing not found" });
if (original.collectionId === "trash") {
await ensureTrashCollection(prisma, req.user.id);
}
const newDrawing = await prisma.drawing.create({ const newDrawing = await prisma.drawing.create({
data: { data: {
@@ -443,10 +464,19 @@ export const registerDashboardRoutes = (
} }
const sanitizedName = sanitizeText(parsed.data, 100); const sanitizedName = sanitizeText(parsed.data, 100);
const updatedCollection = await prisma.collection.update({ const updateResult = await prisma.collection.updateMany({
where: { id }, where: { id, userId: req.user.id },
data: { name: sanitizedName }, data: { name: sanitizedName },
}); });
if (updateResult.count === 0) {
return res.status(404).json({ error: "Collection not found" });
}
const updatedCollection = await prisma.collection.findFirst({
where: { id, userId: req.user.id },
});
if (!updatedCollection) {
return res.status(404).json({ error: "Collection not found" });
}
return res.json(updatedCollection); return res.json(updatedCollection);
})); }));
@@ -464,7 +494,7 @@ export const registerDashboardRoutes = (
where: { collectionId: id, userId: req.user.id }, where: { collectionId: id, userId: req.user.id },
data: { collectionId: null }, data: { collectionId: null },
}), }),
prisma.collection.delete({ where: { id } }), prisma.collection.deleteMany({ where: { id, userId: req.user.id } }),
]); ]);
invalidateDrawingsCache(); invalidateDrawingsCache();
+106
View File
@@ -131,6 +131,21 @@ const makeUniqueName = (base: string, used: Set<string>): string => {
return candidate; return candidate;
}; };
const findFirstDuplicate = (values: string[]): string | null => {
const seen = new Set<string>();
for (const value of values) {
if (seen.has(value)) return value;
seen.add(value);
}
return null;
};
const normalizeNonEmptyId = (value: unknown): string | null => {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
const findSqliteTable = (tables: string[], candidates: string[]): string | null => { const findSqliteTable = (tables: string[], candidates: string[]): string | null => {
const byLower = new Map(tables.map((t) => [t.toLowerCase(), t])); const byLower = new Map(tables.map((t) => [t.toLowerCase(), t]));
for (const candidate of candidates) { for (const candidate of candidates) {
@@ -439,6 +454,28 @@ Drawings: ${drawings.length}
message: `Too many drawings (max ${MAX_IMPORT_DRAWINGS})`, message: `Too many drawings (max ${MAX_IMPORT_DRAWINGS})`,
}); });
} }
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
});
}
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
});
}
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
if (duplicateDrawingPath) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
});
}
for (const drawing of manifest.drawings) { for (const drawing of manifest.drawings) {
if (!getSafeZipEntry(zip, drawing.filePath)) { if (!getSafeZipEntry(zip, drawing.filePath)) {
return res.status(400).json({ return res.status(400).json({
@@ -532,6 +569,28 @@ Drawings: ${drawings.length}
}); });
} }
const duplicateCollectionId = findFirstDuplicate(manifest.collections.map((c) => c.id));
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate collection id in manifest: ${duplicateCollectionId}`,
});
}
const duplicateDrawingId = findFirstDuplicate(manifest.drawings.map((d) => d.id));
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing id in manifest: ${duplicateDrawingId}`,
});
}
const duplicateDrawingPath = findFirstDuplicate(manifest.drawings.map((d) => d.filePath));
if (duplicateDrawingPath) {
return res.status(400).json({
error: "Invalid backup manifest",
message: `Duplicate drawing file path in manifest: ${duplicateDrawingPath}`,
});
}
type PreparedImportDrawing = { type PreparedImportDrawing = {
id: string; id: string;
name: string; name: string;
@@ -772,6 +831,31 @@ Drawings: ${drawings.length}
}); });
} }
const duplicateDrawingIdRow = db
.prepare(
`SELECT id FROM "${drawingTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
)
.get();
if (duplicateDrawingIdRow?.id) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate drawing id in legacy DB: ${String(duplicateDrawingIdRow.id)}`,
});
}
if (collectionTable) {
const duplicateCollectionIdRow = db
.prepare(
`SELECT id FROM "${collectionTable}" WHERE id IS NOT NULL GROUP BY id HAVING COUNT(1) > 1 LIMIT 1`
)
.get();
if (duplicateCollectionIdRow?.id) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate collection id in legacy DB: ${String(duplicateCollectionIdRow.id)}`,
});
}
}
let latestMigration: string | null = null; let latestMigration: string | null = null;
const migrationsTable = findSqliteTable(tables, ["_prisma_migrations"]); const migrationsTable = findSqliteTable(tables, ["_prisma_migrations"]);
if (migrationsTable) { if (migrationsTable) {
@@ -862,6 +946,28 @@ Drawings: ${drawings.length}
}); });
} }
const importedCollectionIds = importedCollections
.map((c) => normalizeNonEmptyId(c?.id))
.filter((id): id is string => id !== null);
const duplicateCollectionId = findFirstDuplicate(importedCollectionIds);
if (duplicateCollectionId) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate collection id in legacy DB: ${duplicateCollectionId}`,
});
}
const importedDrawingIds = importedDrawings
.map((d) => normalizeNonEmptyId(d?.id))
.filter((id): id is string => id !== null);
const duplicateDrawingId = findFirstDuplicate(importedDrawingIds);
if (duplicateDrawingId) {
return res.status(400).json({
error: "Invalid legacy DB",
message: `Duplicate drawing id in legacy DB: ${duplicateDrawingId}`,
});
}
type PreparedLegacyDrawing = { type PreparedLegacyDrawing = {
importedId: string | null; importedId: string | null;
name: string; name: string;
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.4.3", "version": "0.4.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 6767", "dev": "vite --port 6767",
+21 -2
View File
@@ -36,6 +36,8 @@ export const Layout: React.FC<LayoutProps> = ({
const sidebarRef = useRef<HTMLDivElement>(null); const sidebarRef = useRef<HTMLDivElement>(null);
const startXRef = useRef(0); const startXRef = useRef(0);
const startWidthRef = useRef(0); const startWidthRef = useRef(0);
const resizeMouseMoveHandlerRef = useRef<((e: MouseEvent) => void) | null>(null);
const resizeMouseUpHandlerRef = useRef<(() => void) | null>(null);
// Handle mouse down on resize handle // Handle mouse down on resize handle
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
@@ -44,6 +46,13 @@ export const Layout: React.FC<LayoutProps> = ({
startXRef.current = e.clientX; startXRef.current = e.clientX;
startWidthRef.current = sidebarWidth; startWidthRef.current = sidebarWidth;
if (resizeMouseMoveHandlerRef.current) {
document.removeEventListener('mousemove', resizeMouseMoveHandlerRef.current);
}
if (resizeMouseUpHandlerRef.current) {
document.removeEventListener('mouseup', resizeMouseUpHandlerRef.current);
}
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
const diff = e.clientX - startXRef.current; const diff = e.clientX - startXRef.current;
const newWidth = Math.max(200, Math.min(600, startWidthRef.current + diff)); const newWidth = Math.max(200, Math.min(600, startWidthRef.current + diff));
@@ -54,8 +63,12 @@ export const Layout: React.FC<LayoutProps> = ({
setIsResizing(false); setIsResizing(false);
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
resizeMouseMoveHandlerRef.current = null;
resizeMouseUpHandlerRef.current = null;
}; };
resizeMouseMoveHandlerRef.current = handleMouseMove;
resizeMouseUpHandlerRef.current = handleMouseUp;
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
@@ -63,8 +76,14 @@ export const Layout: React.FC<LayoutProps> = ({
// Cleanup event listeners on unmount // Cleanup event listeners on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
document.removeEventListener('mousemove', () => {}); if (resizeMouseMoveHandlerRef.current) {
document.removeEventListener('mouseup', () => {}); document.removeEventListener('mousemove', resizeMouseMoveHandlerRef.current);
resizeMouseMoveHandlerRef.current = null;
}
if (resizeMouseUpHandlerRef.current) {
document.removeEventListener('mouseup', resizeMouseUpHandlerRef.current);
resizeMouseUpHandlerRef.current = null;
}
}; };
}, []); }, []);
+3 -5
View File
@@ -60,12 +60,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return; return;
} }
} catch { } catch {
// If status fails (backend down / schema mismatch), avoid locking the UI // If status fails, default to auth-enabled mode to avoid exposing
// behind login. Backend still enforces auth when enabled. // single-user UI paths accidentally. Backend remains the source of truth.
setAuthEnabled(false); setAuthEnabled(true);
setBootstrapRequired(false); setBootstrapRequired(false);
setUser(null);
return;
} }
const storedUser = localStorage.getItem(USER_KEY); const storedUser = localStorage.getItem(USER_KEY);
+13 -2
View File
@@ -73,12 +73,14 @@ export const Dashboard: React.FC = () => {
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const listRequestVersionRef = useRef(0);
const { uploadFiles } = useUpload(); const { uploadFiles } = useUpload();
const hasMore = drawings.length < totalCount; const hasMore = drawings.length < totalCount;
const refreshData = useCallback(async () => { const refreshData = useCallback(async () => {
const requestVersion = ++listRequestVersionRef.current;
setIsLoading(true); setIsLoading(true);
try { try {
const [drawingsRes, collectionsData] = await Promise.all([ const [drawingsRes, collectionsData] = await Promise.all([
@@ -90,6 +92,7 @@ export const Dashboard: React.FC = () => {
}), }),
api.getCollections() api.getCollections()
]); ]);
if (requestVersion !== listRequestVersionRef.current) return;
setDrawings(drawingsRes.drawings); setDrawings(drawingsRes.drawings);
setTotalCount(drawingsRes.totalCount); setTotalCount(drawingsRes.totalCount);
setCollections(collectionsData); setCollections(collectionsData);
@@ -97,12 +100,15 @@ export const Dashboard: React.FC = () => {
} catch (err) { } catch (err) {
console.error('Failed to fetch data:', err); console.error('Failed to fetch data:', err);
} finally { } finally {
setIsLoading(false); if (requestVersion === listRequestVersionRef.current) {
setIsLoading(false);
}
} }
}, [debouncedSearch, selectedCollectionId, sortConfig.field, sortConfig.direction]); }, [debouncedSearch, selectedCollectionId, sortConfig.field, sortConfig.direction]);
const fetchMore = useCallback(async () => { const fetchMore = useCallback(async () => {
if (isFetchingMore || !hasMore || isLoading) return; if (isFetchingMore || !hasMore || isLoading) return;
const requestVersion = listRequestVersionRef.current;
setIsFetchingMore(true); setIsFetchingMore(true);
try { try {
const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, { const drawingsRes = await api.getDrawings(debouncedSearch, selectedCollectionId, {
@@ -111,7 +117,12 @@ export const Dashboard: React.FC = () => {
sortField: sortConfig.field, sortField: sortConfig.field,
sortDirection: sortConfig.direction, sortDirection: sortConfig.direction,
}); });
setDrawings(prev => [...prev, ...drawingsRes.drawings]); if (requestVersion !== listRequestVersionRef.current) return;
setDrawings(prev => {
const seen = new Set(prev.map((d) => d.id));
const nextPage = drawingsRes.drawings.filter((d) => !seen.has(d.id));
return [...prev, ...nextPage];
});
setTotalCount(drawingsRes.totalCount); setTotalCount(drawingsRes.totalCount);
} catch (err) { } catch (err) {
console.error('Failed to fetch more data:', err); console.error('Failed to fetch more data:', err);
+41 -28
View File
@@ -124,7 +124,7 @@ export const Editor: React.FC = () => {
const latestFilesRef = useRef<any>(null); const latestFilesRef = useRef<any>(null);
const lastSyncedFilesRef = useRef<Record<string, any>>({}); const lastSyncedFilesRef = useRef<Record<string, any>>({});
const latestAppStateRef = useRef<any>(null); const latestAppStateRef = useRef<any>(null);
const debouncedSaveRef = useRef<((elements: readonly any[], appState: any) => void) | null>(null); const debouncedSaveRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => void) | null>(null);
const emitFilesDeltaIfNeeded = useCallback( const emitFilesDeltaIfNeeded = useCallback(
(nextFiles: Record<string, any>) => { (nextFiles: Record<string, any>) => {
@@ -361,13 +361,13 @@ export const Editor: React.FC = () => {
const didEmit = emitFilesDeltaIfNeeded(nextFiles); const didEmit = emitFilesDeltaIfNeeded(nextFiles);
// Persist after file data becomes available so new tabs (tab3) load correctly. // Persist after file data becomes available so new tabs (tab3) load correctly.
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) { if (didEmit && id && latestAppStateRef.current && debouncedSaveRef.current) {
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current); debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
} }
}; };
} }
setIsReady(true); setIsReady(true);
}, [emitFilesDeltaIfNeeded]); }, [emitFilesDeltaIfNeeded, id]);
// Handle #addLibrary URL hash parameter for importing libraries from links // Handle #addLibrary URL hash parameter for importing libraries from links
useEffect(() => { useEffect(() => {
@@ -428,12 +428,12 @@ export const Editor: React.FC = () => {
scrollToContent: true, scrollToContent: true,
}), []); }), []);
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null); const saveDataRef = useRef<((drawingId: string, elements: readonly any[], appState: any) => Promise<void>) | null>(null);
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null); const savePreviewRef = useRef<((drawingId: string, elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null); const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
saveDataRef.current = async (elements: readonly any[], appState: any) => { saveDataRef.current = async (drawingId: string, elements: readonly any[], appState: any) => {
if (!id) return; if (!drawingId) return;
try { try {
const persistableAppState = { const persistableAppState = {
@@ -446,27 +446,27 @@ export const Editor: React.FC = () => {
const persistableElements = Array.isArray(snapshot) ? snapshot : []; const persistableElements = Array.isArray(snapshot) ? snapshot : [];
console.log("[Editor] Saving drawing", { console.log("[Editor] Saving drawing", {
drawingId: id, drawingId,
elementCount: persistableElements.length, elementCount: persistableElements.length,
hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted), hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted),
appState: persistableAppState, appState: persistableAppState,
}); });
await api.updateDrawing(id, { await api.updateDrawing(drawingId, {
elements: persistableElements, elements: persistableElements,
appState: persistableAppState, appState: persistableAppState,
files: latestFilesRef.current || {}, files: latestFilesRef.current || {},
}); });
console.log("[Editor] Save complete", { drawingId: id }); console.log("[Editor] Save complete", { drawingId });
} catch (err) { } catch (err) {
console.error('Failed to save drawing', err); console.error('Failed to save drawing', err);
toast.error("Failed to save changes"); toast.error("Failed to save changes");
} }
}; };
savePreviewRef.current = async (elements: readonly any[], appState: any, files: any) => { savePreviewRef.current = async (drawingId: string, elements: readonly any[], appState: any, files: any) => {
if (!id) return; if (!drawingId) return;
try { try {
const currentSnapshot = latestElementsRef.current ?? elements; const currentSnapshot = latestElementsRef.current ?? elements;
@@ -484,13 +484,13 @@ export const Editor: React.FC = () => {
const preview = svg.outerHTML; const preview = svg.outerHTML;
console.log("[Editor] Saving preview", { console.log("[Editor] Saving preview", {
drawingId: id, drawingId,
elementCount: currentSnapshot.length, elementCount: currentSnapshot.length,
}); });
await api.updateDrawing(id, { preview }); await api.updateDrawing(drawingId, { preview });
console.log("[Editor] Preview save complete", { drawingId: id }); console.log("[Editor] Preview save complete", { drawingId });
} catch (err) { } catch (err) {
console.error('Failed to save preview', err); console.error('Failed to save preview', err);
} }
@@ -509,9 +509,9 @@ export const Editor: React.FC = () => {
const debouncedSave = useCallback( const debouncedSave = useCallback(
debounce((elements, appState) => { debounce((drawingId, elements, appState) => {
if (saveDataRef.current) { if (saveDataRef.current) {
saveDataRef.current(elements, appState); saveDataRef.current(drawingId, elements, appState);
} }
}, 1000), }, 1000),
[] // Empty dependency array = Stable across renders [] // Empty dependency array = Stable across renders
@@ -519,9 +519,9 @@ export const Editor: React.FC = () => {
// Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves. // Allow non-hook code (e.g., Excalidraw API wrappers) to trigger debounced saves.
debouncedSaveRef.current = debouncedSave; debouncedSaveRef.current = debouncedSave;
const debouncedSavePreview = useCallback( const debouncedSavePreview = useCallback(
debounce((elements, appState, files) => { debounce((drawingId, elements, appState, files) => {
if (savePreviewRef.current) { if (savePreviewRef.current) {
savePreviewRef.current(elements, appState, files); savePreviewRef.current(drawingId, elements, appState, files);
} }
}, 10000), }, 10000),
[] []
@@ -536,6 +536,13 @@ export const Editor: React.FC = () => {
[] []
); );
useEffect(() => {
return () => {
debouncedSave.cancel();
debouncedSavePreview.cancel();
};
}, [debouncedSave, debouncedSavePreview]);
const broadcastChanges = useCallback( const broadcastChanges = useCallback(
throttle((elements: readonly any[], currentFiles?: Record<string, any>) => { throttle((elements: readonly any[], currentFiles?: Record<string, any>) => {
if (!socketRef.current || !id) return; if (!socketRef.current || !id) return;
@@ -670,8 +677,9 @@ export const Editor: React.FC = () => {
const files = excalidrawAPI.current.getFiles() || {}; const files = excalidrawAPI.current.getFiles() || {};
latestElementsRef.current = elements; latestElementsRef.current = elements;
latestFilesRef.current = files; latestFilesRef.current = files;
await saveDataRef.current(elements, appState); if (!id) return;
savePreviewRef.current(elements, appState, files); await saveDataRef.current(id, elements, appState);
savePreviewRef.current(id, elements, appState, files);
toast.success("Saved changes to server"); toast.success("Saved changes to server");
} }
} }
@@ -739,7 +747,9 @@ export const Editor: React.FC = () => {
elementCount: allElements.length, elementCount: allElements.length,
hasRenderableElements, hasRenderableElements,
}); });
debouncedSave(allElements, appState); if (id) {
debouncedSave(id, allElements, appState);
}
// Trigger Slow Preview Gen // Trigger Slow Preview Gen
const filesSnapshot = currentFiles; const filesSnapshot = currentFiles;
@@ -748,8 +758,10 @@ export const Editor: React.FC = () => {
drawingId: id, drawingId: id,
fileCount: Object.keys(filesSnapshot).length, fileCount: Object.keys(filesSnapshot).length,
}); });
debouncedSavePreview(allElements, appState, filesSnapshot); if (id) {
}, [debouncedSave, debouncedSavePreview, broadcastChanges]); debouncedSavePreview(id, allElements, appState, filesSnapshot);
}
}, [debouncedSave, debouncedSavePreview, broadcastChanges, id]);
// Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously) // Ensure file-only updates (e.g. pasted image dataURL arriving asynchronously)
// are still broadcast to collaborators AND persisted to the server. // are still broadcast to collaborators AND persisted to the server.
@@ -767,7 +779,7 @@ export const Editor: React.FC = () => {
// Persist after file data becomes available (covers the "tab 3" case). // Persist after file data becomes available (covers the "tab 3" case).
if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) { if (didEmit && latestAppStateRef.current && debouncedSaveRef.current) {
debouncedSaveRef.current(latestElementsRef.current, latestAppStateRef.current); debouncedSaveRef.current(id, latestElementsRef.current, latestAppStateRef.current);
} }
}, 1000); }, 1000);
@@ -803,6 +815,7 @@ export const Editor: React.FC = () => {
// Save drawing and generate preview before navigating // Save drawing and generate preview before navigating
try { try {
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) { if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
if (!id) return;
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted(); const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
const appState = excalidrawAPI.current.getAppState(); const appState = excalidrawAPI.current.getAppState();
const files = excalidrawAPI.current.getFiles() || {}; const files = excalidrawAPI.current.getFiles() || {};
@@ -810,8 +823,8 @@ export const Editor: React.FC = () => {
latestFilesRef.current = files; latestFilesRef.current = files;
await Promise.all([ await Promise.all([
saveDataRef.current(elements, appState), saveDataRef.current(id, elements, appState),
savePreviewRef.current(elements, appState, files) savePreviewRef.current(id, elements, appState, files)
]); ]);
console.log("[Editor] Saved on back navigation", { drawingId: id }); console.log("[Editor] Saved on back navigation", { drawingId: id });
} }