import { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Upload, X, AlertTriangle, CheckCircle, SkipForward, RefreshCw, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; interface RestoreResult { success: boolean; message: string; restored?: Record; skipped?: Record; skipped_details?: Record; files_restored?: number; total_skipped?: number; new_api_keys?: Array<{ name: string; key: string; key_prefix: string }>; } interface RestoreModalProps { onClose: () => void; onRestore: (file: File, overwrite: boolean) => Promise; onSuccess: () => void; } type ModalState = 'options' | 'restoring' | 'result'; export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProps) { const { t } = useTranslation(); const [state, setState] = useState('options'); const [overwrite, setOverwrite] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [result, setResult] = useState(null); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const fileInputRef = useRef(null); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && state !== 'restoring') { // Use handleClose for result state to trigger onSuccess if (state === 'result' && result?.success) { onSuccess(); } onClose(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose, onSuccess, state, result]); const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setSelectedFile(file); } }; const handleRestore = async () => { if (!selectedFile) return; setState('restoring'); try { const restoreResult = await onRestore(selectedFile, overwrite); setResult(restoreResult); setState('result'); // Don't call onSuccess here - wait until modal closes // This prevents race condition with query cache } catch { setResult({ success: false, message: t('backup.failedToRestore'), }); setState('result'); } }; const handleClose = () => { // If restore was successful, trigger refresh before closing if (result?.success) { onSuccess(); } onClose(); }; const toggleCategory = (category: string) => { setExpandedCategories(prev => { const next = new Set(prev); if (next.has(category)) { next.delete(category); } else { next.add(category); } return next; }); }; const totalRestored = result?.restored ? Object.values(result.restored).reduce((a, b) => a + b, 0) + (result.files_restored || 0) : 0; const totalSkipped = result?.total_skipped || 0; return (
{ // Only close if clicking directly on the backdrop, not on children if (e.target === e.currentTarget && state !== 'restoring') { onClose(); } }} > {/* Header */}
{state === 'result' && result?.success ? ( ) : state === 'result' && !result?.success ? ( ) : ( )}

{state === 'options' && t('backup.restoreBackup')} {state === 'restoring' && t('backup.restoring')} {state === 'result' && (result?.success ? t('backup.restoreComplete') : t('backup.restoreFailed2'))}

{state === 'options' && t('backup.importSettings')} {state === 'restoring' && t('backup.pleaseWaitRestoring')} {state === 'result' && result?.message}

{state !== 'restoring' && ( )}
{/* Options State */} {state === 'options' && ( <>
{/* File Selection */}
{/* Info Box */}

{t('backup.duplicateHandling')}

  • {t('backup.matchPrinters')} - {t('backup.matchPrintersBy')}
  • {t('backup.matchSmartPlugs')} - {t('backup.matchSmartPlugsBy')}
  • {t('backup.matchNotificationProviders')} - {t('backup.matchNotificationProvidersBy')}
  • {t('backup.matchFilaments')} - {t('backup.matchFilamentsBy')}
  • {t('backup.matchArchives')} - {t('backup.matchArchivesBy')}
  • {t('backup.matchPendingUploads')} - {t('backup.matchPendingUploadsBy')}
  • {t('backup.matchSettingsTemplates')} - {t('backup.matchSettingsTemplatesBy')}
{/* Overwrite Toggle */}

{overwrite ? ( ) : ( )} {overwrite ? t('backup.replaceExisting') : t('backup.keepExisting')}

{overwrite ? t('backup.overwriteDescription') : t('backup.keepDescription')}

{overwrite && (
{t('backup.overwriteCaution')} {t('backup.overwriteWarning')}
)}
{/* Footer */}
)} {/* Restoring State */} {state === 'restoring' && (

{t('backup.processingBackup')}

)} {/* Result State */} {state === 'result' && result && ( <>
{/* Summary */}
{totalRestored}
{t('backup.itemsRestored')}
{totalSkipped}
{t('backup.itemsSkipped')}
{/* Restored Details */} {result.restored && Object.entries(result.restored).some(([, count]) => count > 0) && (

{t('backup.restored')}

{Object.entries(result.restored) .filter(([, count]) => count > 0) .map(([key, count]) => (
{t(`backup.categories.${key}`, key)} {count}
))} {(result.files_restored || 0) > 0 && (
{t('backup.filesCategory')} {result.files_restored}
)}
)} {/* Skipped Details */} {result.skipped && Object.entries(result.skipped).some(([, count]) => count > 0) && (

{t('backup.skippedAlreadyExist')}

{Object.entries(result.skipped) .filter(([, count]) => count > 0) .map(([key, count]) => { const details = result.skipped_details?.[key] || []; const isExpanded = expandedCategories.has(key); return (
{isExpanded && details.length > 0 && (
{details.slice(0, 10).map((item, i) => (
{item}
))} {details.length > 10 && (
{t('backup.andMore', { count: details.length - 10 })}
)}
)}
); })}
)} {/* Newly Generated API Keys */} {result.new_api_keys && result.new_api_keys.length > 0 && (

{t('backup.newApiKeysGenerated')}

{t('backup.keysShownOnce')}

{result.new_api_keys.map((apiKey: { name: string; key: string; key_prefix: string }, i: number) => (
{apiKey.name}
{apiKey.key}
))}
)} {totalRestored === 0 && totalSkipped === 0 && (
{t('backup.noDataFound')}
)}
{/* Footer */}
)}
); }