0.2.1 Release (#32)
* feat(security): implement CSRF protection * chore: clean up CSRF implementation - Remove unused generateCsrfToken export from security.ts - Remove redundant /csrf-token path check (GET already exempt) - Restore defineConfig wrapper in vitest.config.ts for type safety * add K8S note in README, fix broken e2e * feat/upload-bar (#30) * feat/upload-bar: add a upload bar when user upload file, indicate the upload process * feat/save-loading-status: add save status when click back button from editor * fix: address PR review issues in upload and save features - Replace deprecated substr() with substring() in UploadContext - Fix broken error handling that checked stale task status - Fix missing useEffect dependency in UploadStatus - Fix CSS class conflict in progress bar styling - Add error recovery for save state in Editor (reset on failure) - Use .finally() instead of .then() to ensure refresh on upload failure - Fix inconsistent indentation in UploadContext * fix e2e tests --------- Co-authored-by: Zimeng Xiong <zxzimeng@gmail.com> * chore: pre-release v0.2.1-dev * Update backend/src/security.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix filename/math random UUID generation --------- Co-authored-by: AdrianAcala <adrianacala017@gmail.com> Co-authored-by: adamant368 <60790941+Yiheng-Liu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import type { DrawingSummary, Collection } from '../types';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
import clsx from 'clsx';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { importDrawings } from '../utils/importUtils';
|
||||
import { useUpload } from '../context/UploadContext';
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
@@ -78,7 +78,6 @@ export const Dashboard: React.FC = () => {
|
||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
||||
|
||||
const [showImportError, setShowImportError] = useState<{ isOpen: boolean; message: string }>({ isOpen: false, message: '' });
|
||||
const [showImportSuccess, setShowImportSuccess] = useState(false);
|
||||
|
||||
const [isDragSelecting, setIsDragSelecting] = useState(false);
|
||||
const [dragStart, setDragStart] = useState<Point | null>(null);
|
||||
@@ -99,6 +98,8 @@ export const Dashboard: React.FC = () => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { uploadFiles } = useUpload();
|
||||
|
||||
const refreshData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -303,17 +304,12 @@ export const Dashboard: React.FC = () => {
|
||||
|
||||
const fileArray = Array.from(files);
|
||||
const targetCollectionId = selectedCollectionId === undefined ? null : selectedCollectionId;
|
||||
|
||||
const result = await importDrawings(fileArray, targetCollectionId, refreshData);
|
||||
|
||||
if (result.failed > 0) {
|
||||
setShowImportError({
|
||||
isOpen: true,
|
||||
message: `Import complete with errors.\nSuccess: ${result.success}\nFailed: ${result.failed}\nErrors:\n${result.errors.join('\n')}`
|
||||
});
|
||||
} else {
|
||||
setShowImportSuccess(true);
|
||||
}
|
||||
|
||||
// Use the global upload context
|
||||
uploadFiles(fileArray, targetCollectionId).finally(() => {
|
||||
// Refresh after all uploads complete (success or failure)
|
||||
refreshData();
|
||||
});
|
||||
};
|
||||
|
||||
const handleRenameDrawing = async (id: string, name: string) => {
|
||||
@@ -525,8 +521,7 @@ export const Dashboard: React.FC = () => {
|
||||
// Handle Files
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
const libFiles = files.filter(f => f.name.endsWith('.excalidrawlib'));
|
||||
if (libFiles.length > 0) {
|
||||
setShowImportError({
|
||||
@@ -537,13 +532,11 @@ export const Dashboard: React.FC = () => {
|
||||
|
||||
const drawingFiles = files.filter(f => !f.name.endsWith('.excalidrawlib'));
|
||||
if (drawingFiles.length > 0) {
|
||||
const result = await importDrawings(drawingFiles, targetCollectionId, refreshData);
|
||||
if (result.failed > 0) {
|
||||
alert(`Import complete with errors.\nSuccess: ${result.success}\nFailed: ${result.failed}\nErrors:\n${result.errors.join('\n')}`);
|
||||
}
|
||||
uploadFiles(drawingFiles, targetCollectionId).finally(() => {
|
||||
refreshData();
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -938,17 +931,6 @@ export const Dashboard: React.FC = () => {
|
||||
onCancel={() => setShowImportError({ isOpen: false, message: '' })}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={showImportSuccess}
|
||||
title="Import Successful"
|
||||
message="Drawings imported successfully."
|
||||
confirmText="OK"
|
||||
showCancel={false}
|
||||
isDangerous={false}
|
||||
variant="success"
|
||||
onConfirm={() => setShowImportSuccess(false)}
|
||||
onCancel={() => setShowImportSuccess(false)}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Download } from 'lucide-react';
|
||||
import { ArrowLeft, Download, Loader2 } from 'lucide-react';
|
||||
import { Excalidraw, exportToSvg } from '@excalidraw/excalidraw';
|
||||
import '@excalidraw/excalidraw/index.css';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -56,6 +56,7 @@ export const Editor: React.FC = () => {
|
||||
const [newName, setNewName] = useState('');
|
||||
const [initialData, setInitialData] = useState<any>(null);
|
||||
const [isSceneLoading, setIsSceneLoading] = useState(true);
|
||||
const [isSavingOnLeave, setIsSavingOnLeave] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${drawingName} - ExcaliDash`;
|
||||
@@ -591,23 +592,29 @@ export const Editor: React.FC = () => {
|
||||
// Disable native Excalidraw save dialogs
|
||||
|
||||
const handleBackClick = async () => {
|
||||
if (isSavingOnLeave) return; // Prevent double clicks
|
||||
|
||||
setIsSavingOnLeave(true);
|
||||
|
||||
// Save drawing and generate preview before navigating
|
||||
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
|
||||
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
||||
const appState = excalidrawAPI.current.getAppState();
|
||||
const files = excalidrawAPI.current.getFiles() || {};
|
||||
latestElementsRef.current = elements;
|
||||
latestFilesRef.current = files;
|
||||
|
||||
try {
|
||||
try {
|
||||
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
|
||||
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
||||
const appState = excalidrawAPI.current.getAppState();
|
||||
const files = excalidrawAPI.current.getFiles() || {};
|
||||
latestElementsRef.current = elements;
|
||||
latestFilesRef.current = files;
|
||||
|
||||
await Promise.all([
|
||||
saveDataRef.current(elements, appState),
|
||||
savePreviewRef.current(elements, appState, files)
|
||||
]);
|
||||
console.log("[Editor] Saved on back navigation", { drawingId: id });
|
||||
} catch (err) {
|
||||
console.error('Failed to save on back navigation', err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save on back navigation', err);
|
||||
} finally {
|
||||
setIsSavingOnLeave(false);
|
||||
}
|
||||
navigate('/');
|
||||
};
|
||||
@@ -616,8 +623,19 @@ export const Editor: React.FC = () => {
|
||||
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
|
||||
<header className="h-14 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={handleBackClick} className="p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-full text-gray-600 dark:text-gray-300">
|
||||
<ArrowLeft size={20} />
|
||||
<button
|
||||
onClick={handleBackClick}
|
||||
disabled={isSavingOnLeave}
|
||||
className={`flex items-center gap-2 p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-full text-gray-600 dark:text-gray-300 disabled:opacity-50 disabled:cursor-wait transition-all duration-200 ${isSavingOnLeave ? 'pr-4' : ''}`}
|
||||
>
|
||||
{isSavingOnLeave ? (
|
||||
<>
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span className="text-sm font-medium">Saving changes...</span>
|
||||
</>
|
||||
) : (
|
||||
<ArrowLeft size={20} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isRenaming ? (
|
||||
|
||||
Reference in New Issue
Block a user