Add Option to Save and Load the upgrade progress

* Implements Save and Load functionality
This commit is contained in:
2026-04-14 23:15:20 +05:30
parent bbdb2a3a61
commit be8d796bb3

View File

@@ -1,8 +1,8 @@
import { Button } from "./components/ui/button";
import { Label } from "./components/ui/label";
import { Input } from "./components/ui/input";
import { useState, useEffect } from "react";
import { Edit2, ChevronDown, ChevronUp, AlertCircle, Search, Package, Upload, CheckCircle2, XCircle, AlertTriangle, FileJson, TrendingUp, TrendingDown, Copy, Check, Terminal, X } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { Edit2, ChevronDown, ChevronUp, AlertCircle, Search, Package, Upload, CheckCircle2, XCircle, AlertTriangle, FileJson, TrendingUp, TrendingDown, Copy, Check, Terminal, X, Save, FolderOpen } from "lucide-react";
import { toast } from "sonner";
import { PackageSelector } from "./PackageSelector";
import * as Collapsible from "@radix-ui/react-collapsible";
@@ -52,6 +52,19 @@ interface AnalysisResult {
errorMessage?: string;
}
interface SavedProgress {
version: string; // File format version
timestamp: string;
projectName?: string;
projectDescription?: string;
nodeVersion: string;
packageJsonName?: string;
packages: PackageData[];
selectedPackages: string[];
analysisResults: AnalysisResult[];
versionSelections: Record<string, string>;
}
// Helper function to sort versions using semver-like logic
function sortVersions(versions: string[]): string[] {
return versions.sort((a, b) => {
@@ -425,6 +438,11 @@ export function PackageDetails() {
const [analysisError, setAnalysisError] = useState<string | null>(null);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [copiedCommand, setCopiedCommand] = useState(false);
const [packageJsonFileName, setPackageJsonFileName] = useState<string>();
const [projectName, setProjectName] = useState<string>('');
const [projectDescription, setProjectDescription] = useState<string>('');
const loadProgressInputRef = useRef<HTMLInputElement>(null);
const recalculateCompatibility = (results: AnalysisResult[], selections: Map<string, string>, targetNodeVersion: string): AnalysisResult[] => {
return results.map(result => {
@@ -579,6 +597,9 @@ export function PackageDetails() {
const file = e.target.files?.[0];
if (!file) return;
// Save the filename for later use
setPackageJsonFileName(file.name);
// Reset previous state
setAnalysisResults([]);
setVersionSelections(new Map());
@@ -652,11 +673,120 @@ export function PackageDetails() {
setAnalysisResults([]);
setVersionSelections(new Map());
setAnalysisError(null);
setPackageJsonFileName(undefined);
setProjectName('');
setProjectDescription('');
const fileInput = document.getElementById('package-file') as HTMLInputElement;
if (fileInput) fileInput.value = '';
toast.info('Ready to upload a new package.json file');
};
const handleSaveProgress = async () => {
try {
const progressData: SavedProgress = {
version: '1.0',
timestamp: new Date().toISOString(),
projectName: projectName || undefined,
projectDescription: projectDescription || undefined,
nodeVersion,
packageJsonName: packageJsonFileName,
packages,
selectedPackages: Array.from(selectedPackages),
analysisResults,
versionSelections: Object.fromEntries(versionSelections)
};
const dataStr = JSON.stringify(progressData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const baseFileName = projectName
? `${projectName.replace(/[^a-z0-9]/gi, '-').toLowerCase()}-upgrade`
: packageJsonFileName
? `${packageJsonFileName.replace('.json', '')}-upgrade`
: `npm-upgrade-${new Date().toISOString().split('T')[0]}`;
const fileName = `${baseFileName}.npmupgrade`;
// Use File System Access API if available
if ('showSaveFilePicker' in window) {
const handle = await (window as any).showSaveFilePicker({
suggestedName: fileName,
types: [{
description: 'NPM Upgrade Progress',
accept: { 'application/json': ['.npmupgrade'] }
}]
});
const writable = await handle.createWritable();
await writable.write(dataBlob);
await writable.close();
toast.success('Progress saved successfully!');
} else {
// Fallback to download for browsers without File System Access API
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success('Progress downloaded successfully!');
}
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('Error saving progress:', error);
toast.error('Failed to save progress');
}
}
};
const handleLoadProgress = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.npmupgrade')) {
toast.error('Please select a valid .npmupgrade file');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const progressData: SavedProgress = JSON.parse(content);
// Validate the loaded data
if (!progressData.version || !progressData.packages) {
toast.error('Invalid progress file format');
return;
}
// Restore the state
setNodeVersion(progressData.nodeVersion);
setPackageJsonFileName(progressData.packageJsonName);
setProjectName(progressData.projectName || '');
setProjectDescription(progressData.projectDescription || '');
setPackages(progressData.packages);
setSelectedPackages(new Set(progressData.selectedPackages));
setAnalysisResults(progressData.analysisResults);
setVersionSelections(new Map(Object.entries(progressData.versionSelections)));
setFileUploaded(true);
const projectInfo = progressData.projectName ? ` - ${progressData.projectName}` : '';
toast.success(`Progress loaded!${projectInfo}`, {
description: `${progressData.selectedPackages.length} packages restored`
});
} catch (error) {
console.error('Error loading progress:', error);
toast.error('Failed to load progress file');
}
};
reader.readAsText(file);
// Reset the input so the same file can be loaded again
event.target.value = '';
};
const handleSelectionConfirm = (selected: Set<string>) => {
setSelectedPackages(selected);
setDialogOpen(false);
@@ -946,6 +1076,41 @@ export function PackageDetails() {
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Project Info Section */}
<div className="p-3 rounded-lg bg-muted/50 space-y-3">
<div className="space-y-2">
<Label htmlFor="projectName" className="text-xs font-medium">
Project Name (Optional)
</Label>
<Input
id="projectName"
type="text"
placeholder="e.g., Frontend Upgrade Q1 2026"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
className="h-9"
/>
</div>
<div className="space-y-2">
<Label htmlFor="projectDescription" className="text-xs font-medium">
Description (Optional)
</Label>
<textarea
id="projectDescription"
placeholder="Describe what you're upgrading and why..."
value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)}
className="flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-none"
rows={2}
/>
</div>
{(projectName || projectDescription) && (
<p className="text-xs text-muted-foreground">
This information will be saved with your progress file
</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Node.js Version */}
<div className="space-y-2">
@@ -1053,6 +1218,28 @@ export function PackageDetails() {
</div>
</div>
{/* Load Progress Button - Always Visible */}
<div className="pt-2 border-t">
<Button
variant="outline"
onClick={() => loadProgressInputRef.current?.click()}
className="gap-2 w-full"
>
<FolderOpen className="h-4 w-4" />
Load Saved Progress
</Button>
<input
ref={loadProgressInputRef}
type="file"
accept=".npmupgrade"
onChange={handleLoadProgress}
className="hidden"
/>
<p className="text-xs text-muted-foreground mt-2 text-center">
Load a previously saved upgrade progress file
</p>
</div>
{/* Analyze Button */}
{selectedPackages.size > 0 && (
<div className="space-y-2 pt-1">
@@ -1065,7 +1252,7 @@ export function PackageDetails() {
</div>
</div>
)}
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap">
<Button
onClick={fetchPackageDetails}
disabled={isLoading}
@@ -1091,6 +1278,18 @@ export function PackageDetails() {
<Edit2 className="mr-2 h-4 w-4" />
Edit Selection
</Button>
{/* Save Progress Button */}
{analysisResults.length > 0 && (
<Button
variant="outline"
onClick={handleSaveProgress}
className="gap-2"
>
<Save className="h-4 w-4" />
Save Progress
</Button>
)}
</div>
</div>
)}