From be8d796bb3fde4c9a1fcf1945147d7934591d43c Mon Sep 17 00:00:00 2001 From: Indrajith K L Date: Tue, 14 Apr 2026 23:15:20 +0530 Subject: [PATCH] Add Option to Save and Load the upgrade progress * Implements Save and Load functionality --- src/PackageDetails.tsx | 205 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 3 deletions(-) diff --git a/src/PackageDetails.tsx b/src/PackageDetails.tsx index 7b97465..c0f73e9 100644 --- a/src/PackageDetails.tsx +++ b/src/PackageDetails.tsx @@ -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; +} + // 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(null); const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); const [copiedCommand, setCopiedCommand] = useState(false); + const [packageJsonFileName, setPackageJsonFileName] = useState(); + const [projectName, setProjectName] = useState(''); + const [projectDescription, setProjectDescription] = useState(''); + + const loadProgressInputRef = useRef(null); const recalculateCompatibility = (results: AnalysisResult[], selections: Map, 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) => { + 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) => { setSelectedPackages(selected); setDialogOpen(false); @@ -946,6 +1076,41 @@ export function PackageDetails() { + {/* Project Info Section */} +
+
+ + setProjectName(e.target.value)} + className="h-9" + /> +
+
+ +