feat: enhance upload component with pause, resume, and cancel functionality

- Added buttons to pause, resume, and cancel file uploads.
- Improved upload progress display to show current file status.
- Implemented session storage to restore upload state after page refresh.
- Refactored file upload logic to handle individual file uploads and track progress.
This commit is contained in:
vee1e 2025-09-03 17:14:34 +05:30
parent 95ae4eb5ee
commit d9d5b43edc
2 changed files with 270 additions and 34 deletions

View file

@ -39,7 +39,7 @@ import { FormPreviewService } from '../../services/form-preview.service';
<div class="progress-counter" <div class="progress-counter"
[ngClass]="{'counter-orange': isValidating || isUploading, 'counter-purple': isParsing, 'counter-red': isDeletingAll}" [ngClass]="{'counter-orange': isValidating || isUploading, 'counter-purple': isParsing, 'counter-red': isDeletingAll}"
*ngIf="isUploading"> *ngIf="isUploading">
Processing {{uploadProgress.total}} form(s) concurrently... {{uploadPaused ? 'Paused' : 'Processing'}} {{uploadProgress.current}}/{{uploadProgress.total}} form(s)...
</div> </div>
<div class="progress-counter" <div class="progress-counter"
[ngClass]="{'counter-orange': isValidating || isUploading, 'counter-purple': isParsing, 'counter-red': isDeletingAll}" [ngClass]="{'counter-orange': isValidating || isUploading, 'counter-purple': isParsing, 'counter-red': isDeletingAll}"
@ -102,16 +102,36 @@ import { FormPreviewService } from '../../services/form-preview.service';
<mat-icon>upload</mat-icon> <mat-icon>upload</mat-icon>
{{isUploading ? 'Uploading...' : 'Upload'}} {{isUploading ? 'Uploading...' : 'Upload'}}
</button> </button>
<button *ngIf="isUploading && !uploadPaused" mat-icon-button color="warn" (click)="pauseUpload()" matTooltip="Pause Upload">
<mat-icon>pause</mat-icon>
</button>
<button *ngIf="uploadPaused" mat-icon-button color="accent" (click)="resumeUpload()" matTooltip="Resume Upload">
<mat-icon>play_arrow</mat-icon>
</button>
<button *ngIf="isUploading || uploadPaused" mat-icon-button color="warn" (click)="cancelUpload()" matTooltip="Cancel Upload">
<mat-icon>stop</mat-icon>
</button>
</div> </div>
<div class="selected-file" *ngFor="let file of selectedFiles"> <div class="selected-file" *ngFor="let file of selectedFiles; let i = index">
<div class="file-info"> <div class="file-info">
<mat-icon>description</mat-icon> <mat-icon>description</mat-icon>
<span class="file-name" [title]="file.name">{{file.name}}</span> <span class="file-name" [title]="file.name">{{file.name}}</span>
<span *ngIf="validationResults[file.name]" <div class="status-container">
[ngClass]="{'valid': validationResults[file.name].valid, 'invalid': !validationResults[file.name].valid}" <span *ngIf="isUploading || uploadPaused" class="upload-status"
class="validation-status"> [ngClass]="{
{{validationResults[file.name].message}} 'processing': i === currentUploadIndex && !uploadPaused,
</span> 'completed': processedFiles.includes(file.name),
'pending': i > currentUploadIndex,
'paused': i === currentUploadIndex && uploadPaused
}">
{{getFileUploadStatus(file.name, i)}}
</span>
<span *ngIf="validationResults[file.name]"
[ngClass]="{'valid': validationResults[file.name].valid, 'invalid': !validationResults[file.name].valid}"
class="validation-status">
{{validationResults[file.name].message}}
</span>
</div>
</div> </div>
<!-- Detailed Error Messages --> <!-- Detailed Error Messages -->
@ -532,12 +552,18 @@ import { FormPreviewService } from '../../services/form-preview.service';
cursor: help; cursor: help;
} }
.status-container {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.validation-status { .validation-status {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 8px; border-radius: 8px;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: bold; font-weight: bold;
margin-left: auto;
white-space: nowrap; white-space: nowrap;
display: flex; display: flex;
align-items: center; align-items: center;
@ -554,6 +580,40 @@ import { FormPreviewService } from '../../services/form-preview.service';
background: #1c0000; background: #1c0000;
border: 1px solid #660000; border: 1px solid #660000;
} }
.upload-status {
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: bold;
white-space: nowrap;
display: flex;
align-items: center;
}
.upload-status.processing {
color: #2196F3;
background: rgba(33, 150, 243, 0.1);
border: 1px solid rgba(33, 150, 243, 0.3);
}
.upload-status.completed {
color: #4CAF50;
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.upload-status.pending {
color: #FFC107;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
}
.upload-status.paused {
color: #FF9800;
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.3);
}
} }
.validation-details { .validation-details {
@ -643,6 +703,7 @@ import { FormPreviewService } from '../../services/form-preview.service';
display: flex; display: flex;
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
align-items: center;
button[disabled] { button[disabled] {
opacity: 0.5; opacity: 0.5;
@ -652,6 +713,27 @@ import { FormPreviewService } from '../../services/form-preview.service';
cursor: not-allowed !important; cursor: not-allowed !important;
} }
} }
::ng-deep .mat-mdc-icon-button {
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
.mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
::ng-deep .mat-mdc-raised-button {
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
} }
} }
@ -1490,6 +1572,11 @@ export class UploadComponent implements OnInit, OnChanges {
isParsing = false; isParsing = false;
allValid = false; allValid = false;
uploadPaused = false;
uploadQueue: File[] = [];
processedFiles: string[] = [];
currentUploadIndex = 0;
// Schema modal properties // Schema modal properties
showSchemaModal = false; showSchemaModal = false;
selectedSchemaFileName = ''; selectedSchemaFileName = '';
@ -1503,6 +1590,7 @@ export class UploadComponent implements OnInit, OnChanges {
ngOnInit(): void { ngOnInit(): void {
this.loadForms(); this.loadForms();
this.restoreUploadState();
// Subscribe to currently previewed form // Subscribe to currently previewed form
this.formPreviewService.currentPreviewedFormId$.subscribe(formId => { this.formPreviewService.currentPreviewedFormId$.subscribe(formId => {
@ -1628,47 +1716,186 @@ export class UploadComponent implements OnInit, OnChanges {
uploadFiles() { uploadFiles() {
if (this.selectedFiles.length === 0 || !this.allValid) return; if (this.selectedFiles.length === 0 || !this.allValid) return;
this.isUploading = true; this.isUploading = true;
this.uploadPaused = false;
this.uploadQueue = [...this.selectedFiles];
this.processedFiles = [];
this.currentUploadIndex = 0;
this.uploadProgress = { current: 0, total: this.selectedFiles.length }; this.uploadProgress = { current: 0, total: this.selectedFiles.length };
this.saveUploadState();
this.processNextFile();
}
// Send all files at once for concurrent processing private processNextFile() {
this.formService.uploadFiles(this.selectedFiles).subscribe({ if (this.uploadPaused || this.currentUploadIndex >= this.uploadQueue.length) {
next: (results: any[]) => { if (this.currentUploadIndex >= this.uploadQueue.length) {
this.uploadProgress.current = this.selectedFiles.length; this.completeUpload();
}
this.saveUploadState();
return;
}
// Show results for each file const file = this.uploadQueue[this.currentUploadIndex];
const successful = results.filter(r => !r.error).length; this.formService.uploadSingleFile(file).subscribe({
const failed = results.filter(r => r.error).length; next: (result: any) => {
this.processedFiles.push(file.name);
if (failed === 0) { this.currentUploadIndex++;
this.snackBar.open(`✅ Successfully processed ${successful} form(s)!`, 'Close', { duration: 5000 }); this.uploadProgress.current = this.currentUploadIndex;
this.saveUploadState();
if (!result.error) {
this.snackBar.open(`✅ Successfully processed ${file.name}`, 'Close', { duration: 2000 });
} else { } else {
this.snackBar.open(`⚠️ Processed ${successful} form(s), ${failed} failed. Check console for details.`, 'Close', { duration: 7000 }); this.snackBar.open(`❌ Failed to process ${file.name}`, 'Close', { duration: 3000 });
} }
// Wait a moment for database operations to complete, then refresh the forms list setTimeout(() => this.processNextFile(), 500);
setTimeout(() => {
this.isUploading = false;
this.selectedFiles = [];
this.validationResults = {};
this.allValid = false;
this.loadForms();
}, 1000); // 1 second delay to ensure database operations complete
}, },
error: (error: any) => { error: (error: any) => {
this.isUploading = false; console.error(`Upload failed for ${file.name}:`, error);
console.error('Upload failed:', error); this.snackBar.open(`❌ Upload failed for ${file.name}`, 'Close', { duration: 3000 });
this.snackBar.open(`❌ Upload failed: ${error.error?.message || error.message || 'Unknown error'}`, 'Close', { duration: 5000 }); this.currentUploadIndex++;
this.uploadProgress.current = this.currentUploadIndex;
this.saveUploadState();
setTimeout(() => this.processNextFile(), 500);
} }
}); });
} }
private resetUploadState(): void { private completeUpload() {
this.isUploading = false; const successful = this.processedFiles.length;
this.selectedFiles = []; const total = this.uploadQueue.length;
const failed = total - successful;
if (failed === 0) {
this.snackBar.open(`✅ Successfully processed all ${successful} form(s)!`, 'Close', { duration: 5000 });
} else {
this.snackBar.open(`⚠️ Processed ${successful} form(s), ${failed} failed.`, 'Close', { duration: 7000 });
}
this.resetUploadState();
this.loadForms(); this.loadForms();
} }
pauseUpload() {
this.uploadPaused = true;
this.saveUploadState();
this.snackBar.open('⏸️ Upload paused', 'Close', { duration: 2000 });
}
resumeUpload() {
this.uploadPaused = false;
this.saveUploadState();
this.snackBar.open('▶️ Upload resumed', 'Close', { duration: 2000 });
this.processNextFile();
}
cancelUpload() {
if (confirm('Are you sure you want to cancel the upload? Progress will be lost.')) {
this.resetUploadState();
this.snackBar.open('🛑 Upload cancelled', 'Close', { duration: 3000 });
}
}
private resetUploadState() {
this.isUploading = false;
this.uploadPaused = false;
this.uploadQueue = [];
this.processedFiles = [];
this.currentUploadIndex = 0;
this.selectedFiles = [];
this.validationResults = {};
this.allValid = false;
this.uploadProgress = { current: 0, total: 0 };
this.clearUploadSession();
}
private saveUploadState() {
if (typeof window === 'undefined' || !window.sessionStorage) {
return;
}
if (this.isUploading || this.uploadPaused) {
const state = {
isUploading: this.isUploading,
uploadPaused: this.uploadPaused,
uploadQueue: this.uploadQueue.map(file => ({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
})),
processedFiles: this.processedFiles,
currentUploadIndex: this.currentUploadIndex,
uploadProgress: this.uploadProgress,
validationResults: this.validationResults,
allValid: this.allValid,
timestamp: Date.now()
};
sessionStorage.setItem('uploadState', JSON.stringify(state));
}
}
private restoreUploadState() {
if (typeof window === 'undefined' || !window.sessionStorage) {
return;
}
const savedState = sessionStorage.getItem('uploadState');
if (savedState) {
try {
const state = JSON.parse(savedState);
const timeDiff = Date.now() - state.timestamp;
if (timeDiff < 30 * 60 * 1000) {
this.isUploading = state.isUploading;
this.uploadPaused = state.uploadPaused;
this.processedFiles = state.processedFiles || [];
this.currentUploadIndex = state.currentUploadIndex || 0;
this.uploadProgress = state.uploadProgress || { current: 0, total: 0 };
this.validationResults = state.validationResults || {};
this.allValid = state.allValid || false;
if (this.isUploading || this.uploadPaused) {
this.snackBar.open('📋 Previous upload session restored. You can resume or cancel.', 'Close', {
duration: 5000,
panelClass: ['custom-snackbar']
});
}
} else {
this.clearUploadSession();
}
} catch (error) {
console.error('Failed to restore upload state:', error);
this.clearUploadSession();
}
}
}
private clearUploadSession() {
if (typeof window === 'undefined' || !window.sessionStorage) {
return;
}
sessionStorage.removeItem('uploadState');
}
getFileUploadStatus(fileName: string, index: number): string {
if (this.processedFiles.includes(fileName)) {
return 'Completed';
} else if (index === this.currentUploadIndex) {
return this.uploadPaused ? 'Paused' : 'Processing...';
} else if (index < this.currentUploadIndex) {
return 'Completed';
} else {
return 'Pending';
}
}
loadForms(): void { loadForms(): void {
console.log('Loading forms from database...'); console.log('Loading forms from database...');
this.formService.getAllForms().subscribe({ this.formService.getAllForms().subscribe({

View file

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FormValidation } from '../models/form.model'; import { FormValidation } from '../models/form.model';
export interface FormData { export interface FormData {
@ -83,6 +84,14 @@ export class FormService {
return this.http.post<FormDetails>(`${this.apiUrl}/upload`, formData); return this.http.post<FormDetails>(`${this.apiUrl}/upload`, formData);
} }
uploadSingleFile(file: File): Observable<any> {
const formData = new FormData();
formData.append('files', file);
return this.http.post<any[]>(`${this.apiUrl}/upload`, formData).pipe(
map((results: any[]) => results[0])
);
}
uploadFiles(files: File[]): Observable<any[]> { uploadFiles(files: File[]): Observable<any[]> {
const formData = new FormData(); const formData = new FormData();
files.forEach(file => formData.append('files', file)); files.forEach(file => formData.append('files', file));