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 { Button } from "./components/ui/button";
|
||||||
import { Label } from "./components/ui/label";
|
import { Label } from "./components/ui/label";
|
||||||
import { Input } from "./components/ui/input";
|
import { Input } from "./components/ui/input";
|
||||||
import { useState, useEffect } from "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 } from "lucide-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 { toast } from "sonner";
|
||||||
import { PackageSelector } from "./PackageSelector";
|
import { PackageSelector } from "./PackageSelector";
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
@@ -52,6 +52,19 @@ interface AnalysisResult {
|
|||||||
errorMessage?: string;
|
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
|
// Helper function to sort versions using semver-like logic
|
||||||
function sortVersions(versions: string[]): string[] {
|
function sortVersions(versions: string[]): string[] {
|
||||||
return versions.sort((a, b) => {
|
return versions.sort((a, b) => {
|
||||||
@@ -425,6 +438,11 @@ export function PackageDetails() {
|
|||||||
const [analysisError, setAnalysisError] = useState<string | null>(null);
|
const [analysisError, setAnalysisError] = useState<string | null>(null);
|
||||||
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
||||||
const [copiedCommand, setCopiedCommand] = 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[] => {
|
const recalculateCompatibility = (results: AnalysisResult[], selections: Map<string, string>, targetNodeVersion: string): AnalysisResult[] => {
|
||||||
return results.map(result => {
|
return results.map(result => {
|
||||||
@@ -579,6 +597,9 @@ export function PackageDetails() {
|
|||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
// Save the filename for later use
|
||||||
|
setPackageJsonFileName(file.name);
|
||||||
|
|
||||||
// Reset previous state
|
// Reset previous state
|
||||||
setAnalysisResults([]);
|
setAnalysisResults([]);
|
||||||
setVersionSelections(new Map());
|
setVersionSelections(new Map());
|
||||||
@@ -652,11 +673,120 @@ export function PackageDetails() {
|
|||||||
setAnalysisResults([]);
|
setAnalysisResults([]);
|
||||||
setVersionSelections(new Map());
|
setVersionSelections(new Map());
|
||||||
setAnalysisError(null);
|
setAnalysisError(null);
|
||||||
|
setPackageJsonFileName(undefined);
|
||||||
|
setProjectName('');
|
||||||
|
setProjectDescription('');
|
||||||
const fileInput = document.getElementById('package-file') as HTMLInputElement;
|
const fileInput = document.getElementById('package-file') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
toast.info('Ready to upload a new package.json file');
|
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>) => {
|
const handleSelectionConfirm = (selected: Set<string>) => {
|
||||||
setSelectedPackages(selected);
|
setSelectedPackages(selected);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
@@ -946,6 +1076,41 @@ export function PackageDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{/* Node.js Version */}
|
{/* Node.js Version */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -1053,6 +1218,28 @@ export function PackageDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Analyze Button */}
|
||||||
{selectedPackages.size > 0 && (
|
{selectedPackages.size > 0 && (
|
||||||
<div className="space-y-2 pt-1">
|
<div className="space-y-2 pt-1">
|
||||||
@@ -1065,7 +1252,7 @@ export function PackageDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={fetchPackageDetails}
|
onClick={fetchPackageDetails}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -1091,6 +1278,18 @@ export function PackageDetails() {
|
|||||||
<Edit2 className="mr-2 h-4 w-4" />
|
<Edit2 className="mr-2 h-4 w-4" />
|
||||||
Edit Selection
|
Edit Selection
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user