mirror of
https://github.com/vee1e/bulk-questionnaire-upload.git
synced 2026-09-01 09:50:06 +00:00
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:
parent
95ae4eb5ee
commit
d9d5b43edc
2 changed files with 270 additions and 34 deletions
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue