allow importing of libraries via URL, update db schema
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Library" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
|
||||
"items" TEXT NOT NULL DEFAULT '[]',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
Binary file not shown.
Binary file not shown.
@@ -33,3 +33,10 @@ model Drawing {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Library {
|
||||
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||
items String @default("[]") // Stored as JSON string array of library items
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -139,6 +139,13 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LibraryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
items: 'items',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -152,7 +159,8 @@ exports.Prisma.NullsOrder = {
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
Collection: 'Collection',
|
||||
Drawing: 'Drawing'
|
||||
Drawing: 'Drawing',
|
||||
Library: 'Library'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+1083
-2
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-6afe3d9baa793154c8d01c79f8418d91423e5ccaec794547bf848a451459cf53",
|
||||
"name": "prisma-client-8eed3ee5004eaec649fc60571177778f25acb4a3cdc2c238bbb8e70dd820d0ff",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
@@ -33,3 +33,10 @@ model Drawing {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Library {
|
||||
id String @id @default("default") // Singleton pattern - use "default" ID
|
||||
items String @default("[]") // Stored as JSON string array of library items
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -139,6 +139,13 @@ exports.Prisma.DrawingScalarFieldEnum = {
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.LibraryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
items: 'items',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -152,7 +159,8 @@ exports.Prisma.NullsOrder = {
|
||||
|
||||
exports.Prisma.ModelName = {
|
||||
Collection: 'Collection',
|
||||
Drawing: 'Drawing'
|
||||
Drawing: 'Drawing',
|
||||
Library: 'Library'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -801,6 +801,58 @@ app.delete("/collections/:id", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Library ---
|
||||
|
||||
// GET /library - Fetch stored library items
|
||||
app.get("/library", async (req, res) => {
|
||||
try {
|
||||
const library = await prisma.library.findUnique({
|
||||
where: { id: "default" },
|
||||
});
|
||||
|
||||
if (!library) {
|
||||
// Return empty array if no library exists yet
|
||||
return res.json({ items: [] });
|
||||
}
|
||||
|
||||
res.json({
|
||||
items: JSON.parse(library.items),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch library:", error);
|
||||
res.status(500).json({ error: "Failed to fetch library" });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /library - Update/create library items
|
||||
app.put("/library", async (req, res) => {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
return res.status(400).json({ error: "Items must be an array" });
|
||||
}
|
||||
|
||||
const library = await prisma.library.upsert({
|
||||
where: { id: "default" },
|
||||
update: {
|
||||
items: JSON.stringify(items),
|
||||
},
|
||||
create: {
|
||||
id: "default",
|
||||
items: JSON.stringify(items),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
items: JSON.parse(library.items),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update library:", error);
|
||||
res.status(500).json({ error: "Failed to update library" });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Export/Import Endpoints ---
|
||||
|
||||
// GET /export - Export SQLite database (supports .sqlite and .db extensions)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user