|
|
@@ -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 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|