Add Option to Save and Load the upgrade progress
* Implements Save and Load functionality
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user