diff --git a/backend/src/index.ts b/backend/src/index.ts index f078561..0f5dc0c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -112,12 +112,18 @@ const io = new Server(httpServer, { maxHttpBufferSize: 1e8, // 100 MB }); const prisma = new PrismaClient(); -const parseJsonField = (rawValue: string | null | undefined, fallback: T): T => { +const parseJsonField = ( + rawValue: string | null | undefined, + fallback: T +): T => { if (!rawValue) return fallback; try { return JSON.parse(rawValue) as T; } catch (error) { - console.warn("Failed to parse JSON field", { error, valuePreview: rawValue.slice(0, 50) }); + console.warn("Failed to parse JSON field", { + error, + valuePreview: rawValue.slice(0, 50), + }); return fallback; } }; @@ -132,6 +138,11 @@ const DRAWINGS_CACHE_TTL_MS = (() => { type DrawingsCacheEntry = { body: Buffer; expiresAt: number }; const drawingsCache = new Map(); +/** + * Builds a cache key for the drawings list endpoint. + * NOTE: This key does NOT include sort order. If sorting options are added + * to the endpoint in the future, they must be included in this key. + */ const buildDrawingsCacheKey = (keyParts: { searchTerm: string; collectionFilter: string; @@ -798,7 +809,7 @@ app.put("/drawings/:id", async (req, res) => { where: { id }, data, }); - await invalidateDrawingsCache(); + invalidateDrawingsCache(); console.log("[API] Update complete", { id, @@ -927,7 +938,7 @@ app.delete("/collections/:id", async (req, res) => { where: { id }, }), ]); - await invalidateDrawingsCache(); + invalidateDrawingsCache(); res.json({ success: true }); } catch (error) { diff --git a/frontend/src/components/DrawingCard.tsx b/frontend/src/components/DrawingCard.tsx index 9e02e26..f30eb1f 100644 --- a/frontend/src/components/DrawingCard.tsx +++ b/frontend/src/components/DrawingCard.tsx @@ -58,6 +58,7 @@ export const DrawingCard: React.FC = ({ const [previewSvg, setPreviewSvg] = useState(drawing.preview ?? null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [isExporting, setIsExporting] = useState(false); + const [exportError, setExportError] = useState(null); const [fullData, setFullData] = useState(null); const fullDataRef = React.useRef(fullData); @@ -69,14 +70,18 @@ export const DrawingCard: React.FC = ({ fullDataPromiseRef.current = null; }, [drawing.id]); - const ensureFullData = useCallback(async () => { + const drawingIdRef = React.useRef(drawing.id); + drawingIdRef.current = drawing.id; + + const ensureFullData = useCallback(async (): Promise => { if (fullDataRef.current) { return fullDataRef.current; } if (fullDataPromiseRef.current) { return fullDataPromiseRef.current; } - const promise = api.getDrawing(drawing.id).then((fullDrawing) => { + const currentDrawingId = drawingIdRef.current; + const promise = api.getDrawing(currentDrawingId).then((fullDrawing) => { const payload: HydratedDrawingData = { elements: fullDrawing.elements || [], appState: fullDrawing.appState || {}, @@ -91,7 +96,7 @@ export const DrawingCard: React.FC = ({ }); fullDataPromiseRef.current = promise; return promise; - }, [drawing.id]); + }, []); // Stable identity - uses refs internally useEffect(() => { let cancelled = false; @@ -136,11 +141,13 @@ export const DrawingCard: React.FC = ({ return () => { cancelled = true; }; - }, [drawing.id, drawing.preview, ensureFullData, onPreviewGenerated]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [drawing.id, drawing.preview, onPreviewGenerated]); // ensureFullData has stable identity via refs const handleExport = useCallback(async () => { try { setIsExporting(true); + setExportError(null); const data = await ensureFullData(); const drawingPayload: Drawing = { ...drawing, @@ -151,6 +158,9 @@ export const DrawingCard: React.FC = ({ exportDrawingToFile(drawingPayload); } catch (error) { console.error("Failed to export drawing", error); + setExportError("Failed to export drawing. Please try again."); + // Clear error after 3 seconds + setTimeout(() => setExportError(null), 3000); } finally { setIsExporting(false); } @@ -409,6 +419,11 @@ export const DrawingCard: React.FC = ({ {isExporting ? : } {isExporting ? 'Exporting...' : 'Export'} + {exportError && ( +
+ {exportError} +
+ )}