export/import functionality

This commit is contained in:
Zimeng Xiong
2025-11-22 10:06:58 -08:00
parent 05d472189c
commit d08dfe9b56
7 changed files with 1446 additions and 18 deletions
+13 -4
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { AlertTriangle, X } from 'lucide-react';
import { AlertTriangle, CheckCircle, X } from 'lucide-react';
interface ConfirmModalProps {
isOpen: boolean;
@@ -12,6 +12,7 @@ interface ConfirmModalProps {
onCancel: () => void;
isDangerous?: boolean; // Makes confirm button red
showCancel?: boolean;
variant?: 'warning' | 'success'; // Controls icon and styling
}
export const ConfirmModal: React.FC<ConfirmModalProps> = ({
@@ -23,10 +24,18 @@ export const ConfirmModal: React.FC<ConfirmModalProps> = ({
onConfirm,
onCancel,
isDangerous = true,
showCancel = true
showCancel = true,
variant = 'warning'
}) => {
if (!isOpen) return null;
// Icon and styling based on variant
const isSuccess = variant === 'success';
const IconComponent = isSuccess ? CheckCircle : AlertTriangle;
const iconClasses = isSuccess
? "w-12 h-12 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center text-emerald-600 dark:text-emerald-300 border-2 border-emerald-200 dark:border-emerald-900/30"
: "w-12 h-12 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-300 border-2 border-rose-200 dark:border-rose-900/30";
return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop */}
@@ -45,8 +54,8 @@ export const ConfirmModal: React.FC<ConfirmModalProps> = ({
</button>
<div className="flex flex-col items-center text-center gap-4">
<div className="w-12 h-12 rounded-full bg-rose-100 dark:bg-rose-900/30 flex items-center justify-center text-rose-600 dark:text-rose-300 border-2 border-rose-200 dark:border-rose-900/30">
<AlertTriangle size={24} strokeWidth={2.5} />
<div className={iconClasses}>
<IconComponent size={24} strokeWidth={2.5} />
</div>
<div className="space-y-2">
+72 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Layout } from '../components/Layout';
import { DrawingCard } from '../components/DrawingCard';
import { Plus, Search, Loader2, Inbox, Trash2, Folder, ArrowRight, Copy } from 'lucide-react';
import { Plus, Search, Loader2, Inbox, Trash2, Folder, ArrowRight, Copy, Upload } from 'lucide-react';
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom';
import * as api from '../api';
import type { Drawing, Collection } from '../types';
@@ -79,6 +79,10 @@ export const Dashboard: React.FC = () => {
const [drawingToDelete, setDrawingToDelete] = useState<string | null>(null);
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
// Import state
const [showImportError, setShowImportError] = useState<{ isOpen: boolean; message: string }>({ isOpen: false, message: '' });
const [showImportSuccess, setShowImportSuccess] = useState(false);
// Drag Selection State
const [isDragSelecting, setIsDragSelecting] = useState(false);
const [dragStart, setDragStart] = useState<Point | null>(null);
@@ -306,6 +310,24 @@ export const Dashboard: React.FC = () => {
}
};
const handleImportDrawings = async (files: FileList | null) => {
if (!files || isTrashView) return;
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);
}
};
const handleRenameDrawing = async (id: string, name: string) => {
setDrawings(prev => prev.map(d => d.id === id ? { ...d, name } : d));
await api.updateDrawing(id, { name });
@@ -784,6 +806,32 @@ export const Dashboard: React.FC = () => {
</div>
</div>
<input
type="file"
multiple
accept=".json,.excalidraw"
className="hidden"
id="dashboard-import"
onChange={(e) => {
handleImportDrawings(e.target.files);
e.target.value = ''; // Reset input
}}
/>
<button
onClick={() => document.getElementById('dashboard-import')?.click()}
disabled={isTrashView}
className={clsx(
"h-[42px] w-full sm:w-auto flex items-center justify-center gap-2 px-6 rounded-xl border-2 border-black dark:border-neutral-700 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] transition-all font-bold text-sm whitespace-nowrap",
isTrashView
? "bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-600 border-slate-300 dark:border-slate-700 shadow-none cursor-not-allowed"
: "bg-emerald-600 dark:bg-neutral-800 text-white hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] dark:hover:shadow-[4px_4px_0px_0px_rgba(255,255,255,0.2)] hover:-translate-y-1 active:translate-y-0 active:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:active:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)]"
)}
>
<Upload size={18} strokeWidth={2.5} />
Import
</button>
<button
onClick={handleCreateDrawing}
disabled={isTrashView}
@@ -911,6 +959,29 @@ export const Dashboard: React.FC = () => {
onConfirm={executeBulkPermanentDelete}
onCancel={() => setShowBulkDeleteConfirm(false)}
/>
<ConfirmModal
isOpen={showImportError.isOpen}
title="Import Failed"
message={showImportError.message}
confirmText="OK"
showCancel={false}
isDangerous={false}
onConfirm={() => setShowImportError({ isOpen: false, message: '' })}
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
View File
@@ -259,6 +259,7 @@ export const Settings: React.FC = () => {
confirmText="OK"
showCancel={false}
isDangerous={false}
variant="success"
onConfirm={() => setImportSuccess(false)}
onCancel={() => setImportSuccess(false)}
/>