import { useState, useEffect, useRef } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { CategoryAutocomplete } from '@/components/ui/category-autocomplete'; import { Upload, AlertTriangle, CheckCircle2, XCircle, FileJson, FileText } from 'lucide-react'; import { toast } from 'sonner'; import { getHttpApiClient } from '@/lib/http-api-client'; import { cn } from '@/lib/utils'; interface ConflictInfo { featureId: string; title?: string; existingTitle?: string; hasConflict: boolean; } interface ImportResult { success: boolean; featureId?: string; importedAt: string; warnings?: string[]; errors?: string[]; wasOverwritten?: boolean; } interface ImportFeaturesDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectPath: string; categorySuggestions: string[]; onImportComplete?: () => void; } type ImportStep = 'upload' | 'review' | 'result'; export function ImportFeaturesDialog({ open, onOpenChange, projectPath, categorySuggestions, onImportComplete, }: ImportFeaturesDialogProps) { const fileInputRef = useRef(null); const [step, setStep] = useState('upload'); const [fileData, setFileData] = useState(''); const [fileName, setFileName] = useState(''); const [fileFormat, setFileFormat] = useState<'json' | 'yaml' | null>(null); // Options const [overwrite, setOverwrite] = useState(false); const [targetCategory, setTargetCategory] = useState(''); // Conflict check results const [conflicts, setConflicts] = useState([]); const [isCheckingConflicts, setIsCheckingConflicts] = useState(false); // Import results const [importResults, setImportResults] = useState([]); const [isImporting, setIsImporting] = useState(false); // Parse error const [parseError, setParseError] = useState(''); // Reset state when dialog opens useEffect(() => { if (open) { setStep('upload'); setFileData(''); setFileName(''); setFileFormat(null); setOverwrite(false); setTargetCategory(''); setConflicts([]); setImportResults([]); setParseError(''); } }, [open]); const handleFileSelect = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // Check file extension const ext = file.name.split('.').pop()?.toLowerCase(); if (ext !== 'json' && ext !== 'yaml' && ext !== 'yml') { setParseError('Please select a JSON or YAML file'); return; } try { const content = await file.text(); setFileData(content); setFileName(file.name); setFileFormat(ext === 'yml' ? 'yaml' : (ext as 'json' | 'yaml')); setParseError(''); // Check for conflicts await checkConflicts(content); } catch { setParseError('Failed to read file'); } // Reset input if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const checkConflicts = async (data: string) => { setIsCheckingConflicts(true); try { const api = getHttpApiClient(); const result = await api.features.checkConflicts(projectPath, data); if (!result.success) { setParseError(result.error || 'Failed to parse import file'); setConflicts([]); return; } setConflicts(result.conflicts || []); setStep('review'); } catch (error) { setParseError(error instanceof Error ? error.message : 'Failed to check conflicts'); } finally { setIsCheckingConflicts(false); } }; const handleImport = async () => { setIsImporting(true); try { const api = getHttpApiClient(); const result = await api.features.import(projectPath, fileData, { overwrite, targetCategory: targetCategory || undefined, }); if (!result.success && result.failedCount === result.results?.length) { toast.error(result.error || 'Failed to import features'); return; } setImportResults(result.results || []); setStep('result'); const successCount = result.importedCount || 0; const failCount = result.failedCount || 0; if (failCount === 0) { toast.success(`Successfully imported ${successCount} feature(s)`); } else if (successCount > 0) { toast.warning(`Imported ${successCount} feature(s), ${failCount} failed`); } else { toast.error(`Failed to import features`); } onImportComplete?.(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to import features'); } finally { setIsImporting(false); } }; const handleDrop = async (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); const file = event.dataTransfer.files[0]; if (!file) return; const ext = file.name.split('.').pop()?.toLowerCase(); if (ext !== 'json' && ext !== 'yaml' && ext !== 'yml') { setParseError('Please drop a JSON or YAML file'); return; } try { const content = await file.text(); setFileData(content); setFileName(file.name); setFileFormat(ext === 'yml' ? 'yaml' : (ext as 'json' | 'yaml')); setParseError(''); await checkConflicts(content); } catch { setParseError('Failed to read file'); } }; const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); }; const conflictingFeatures = conflicts.filter((c) => c.hasConflict); const hasConflicts = conflictingFeatures.length > 0; const renderUploadStep = () => (
{/* Drop Zone */}
fileInputRef.current?.click()} data-testid="import-drop-zone" >
Click to upload or drag and drop
JSON or YAML
{parseError && (
{parseError}
)} {isCheckingConflicts && (
Analyzing file...
)}
); const renderReviewStep = () => (
{/* File Info */}
{fileFormat === 'json' ? ( ) : ( )}
{fileName}
{conflicts.length} feature(s) to import
{/* Conflict Warning */} {hasConflicts && (
{conflictingFeatures.length} conflict(s) detected
The following features already exist in this project:
    {conflictingFeatures.map((c) => (
  • {c.existingTitle || c.featureId}
  • ))}
)} {/* Options */}
{hasConflicts && (
setOverwrite(!!checked)} data-testid="import-overwrite" />
)}
{/* Features Preview */}
{conflicts.map((c) => (
{c.hasConflict ? ( overwrite ? ( ) : ( ) ) : ( )} {c.title || c.featureId} {c.hasConflict && !overwrite && ( (will skip) )}
))}
); const renderResultStep = () => { const successResults = importResults.filter((r) => r.success); const failedResults = importResults.filter((r) => !r.success); return (
{/* Summary */}
{successResults.length > 0 && (
{successResults.length} imported
)} {failedResults.length > 0 && (
{failedResults.length} failed
)}
{/* Results List */}
{importResults.map((result, idx) => (
{result.success ? ( ) : ( )} {result.featureId || `Feature ${idx + 1}`} {result.wasOverwritten && ( (overwritten) )}
{result.warnings && result.warnings.length > 0 && (
{result.warnings.map((w, i) => (
{w}
))}
)} {result.errors && result.errors.length > 0 && (
{result.errors.map((e, i) => (
{e}
))}
)}
))}
); }; return ( Import Features {step === 'upload' && 'Import features from a JSON or YAML export file.'} {step === 'review' && 'Review and configure import options.'} {step === 'result' && 'Import completed.'} {step === 'upload' && renderUploadStep()} {step === 'review' && renderReviewStep()} {step === 'result' && renderResultStep()} {step === 'upload' && ( )} {step === 'review' && ( <> )} {step === 'result' && ( )} ); }