allow importing of libraries via URL, update db schema

This commit is contained in:
Zimeng Xiong
2025-11-24 14:32:37 -08:00
parent ee8204532d
commit fa73708d97
14 changed files with 1301 additions and 16 deletions
+12
View File
@@ -84,3 +84,15 @@ export const deleteCollection = async (id: string) => {
const response = await api.delete<{ success: true }>(`/collections/${id}`);
return response.data;
};
// --- Library ---
export const getLibrary = async () => {
const response = await api.get<{ items: any[] }>("/library");
return response.data.items;
};
export const updateLibrary = async (items: any[]) => {
const response = await api.put<{ items: any[] }>("/library", { items });
return response.data.items;
};
+90 -1
View File
@@ -241,6 +241,59 @@ export const Editor: React.FC = () => {
setIsReady(true);
}, []);
// Handle #addLibrary URL hash parameter for importing libraries from links
useEffect(() => {
if (!isReady || !excalidrawAPI.current) return;
const hash = window.location.hash;
if (!hash.includes('addLibrary=')) return;
const params = new URLSearchParams(hash.slice(1)); // Remove the leading #
const libraryUrl = params.get('addLibrary');
if (!libraryUrl) return;
const importLibraryFromUrl = async () => {
try {
console.log('[Editor] Importing library from URL:', libraryUrl);
toast.loading('Importing library...', { id: 'library-import' });
const response = await fetch(libraryUrl);
if (!response.ok) {
throw new Error(`Failed to fetch library: ${response.statusText}`);
}
const blob = await response.blob();
// Use Excalidraw's updateLibrary API with proper settings:
// - defaultStatus: "published" puts items in "Excalidraw library" section
// - merge: true preserves existing library items
// - openLibraryMenu: true shows the library sidebar after import
await excalidrawAPI.current.updateLibrary({
libraryItems: blob,
merge: true,
defaultStatus: "published",
openLibraryMenu: true,
});
// Get the updated library items and persist to server
const updatedItems = excalidrawAPI.current.getAppState().libraryItems || [];
await api.updateLibrary([...updatedItems]);
toast.success('Library imported successfully', { id: 'library-import' });
console.log('[Editor] Library import complete');
// Clear the hash to prevent re-importing on refresh
window.history.replaceState(null, '', window.location.pathname + window.location.search);
} catch (err) {
console.error('[Editor] Failed to import library:', err);
toast.error('Failed to import library', { id: 'library-import' });
}
};
importLibraryFromUrl();
}, [isReady]);
const buildEmptyScene = useCallback(() => ({
elements: [],
appState: {
@@ -259,6 +312,7 @@ export const Editor: React.FC = () => {
// ------------------------------------------------------------------
const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise<void>) | null>(null);
const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise<void>) | null>(null);
const saveLibraryRef = useRef<((items: any[]) => Promise<void>) | null>(null);
// Update the ref on every render to ensure it has access to the latest props/state
saveDataRef.current = async (elements: readonly any[], appState: any) => {
@@ -326,6 +380,17 @@ export const Editor: React.FC = () => {
}
};
saveLibraryRef.current = async (items: any[]) => {
try {
console.log("[Editor] Saving library", { itemCount: items.length });
await api.updateLibrary(items);
console.log("[Editor] Library save complete");
} catch (err) {
console.error('Failed to save library', err);
toast.error("Failed to save library");
}
};
// Create the debounced function ONLY ONCE.
// It simply calls whatever is currently in saveDataRef.current
const debouncedSave = useCallback(
@@ -346,6 +411,15 @@ export const Editor: React.FC = () => {
[]
);
const debouncedSaveLibrary = useCallback(
debounce((items: any[]) => {
if (saveLibraryRef.current) {
saveLibraryRef.current(items);
}
}, 1000),
[]
);
const broadcastChanges = useCallback(
throttle((elements: readonly any[]) => {
if (!socketRef.current || !id) return;
@@ -391,7 +465,14 @@ export const Editor: React.FC = () => {
return;
}
try {
const data = await api.getDrawing(id);
// Fetch drawing and library in parallel
const [data, libraryItems] = await Promise.all([
api.getDrawing(id),
api.getLibrary().catch((err) => {
console.warn('Failed to load library, using empty:', err);
return [];
})
]);
setDrawingName(data.name);
// Use elements directly without converting - they're already normalized during import
@@ -417,6 +498,7 @@ export const Editor: React.FC = () => {
appState: hydratedAppState,
files,
scrollToContent: true,
libraryItems,
});
} catch (err) {
console.error('Failed to load drawing', err);
@@ -537,6 +619,12 @@ export const Editor: React.FC = () => {
}
};
// Handle library changes and persist to server
const handleLibraryChange = useCallback((items: readonly any[]) => {
console.log("[Editor] Library changed", { itemCount: items.length });
debouncedSaveLibrary([...items]);
}, [debouncedSaveLibrary]);
// Disable native Excalidraw save dialogs
// UIOptions is now defined outside the component
@@ -639,6 +727,7 @@ export const Editor: React.FC = () => {
initialData={initialData}
onChange={handleCanvasChange}
onPointerUpdate={onPointerUpdate}
onLibraryChange={handleLibraryChange}
excalidrawAPI={setExcalidrawAPI}
UIOptions={UIOptions}
/>