[ADF-1403] Upload Dialog - Delete files event (#2234)

* remove provisional service

* Error and Delete events

* delete status

* separate upload events

* update demo

* files upload counter and errors

* pendig files icon and action

* remove multiple files error key

* handle cancel files

* fixed component theme

* remove fdescribe

* resolved comments

* throw error
This commit is contained in:
Cilibiu Bogdan
2017-08-24 12:45:13 +03:00
committed by Mario Romano
parent 1deaa22570
commit 07ba8bc15f
17 changed files with 385 additions and 302 deletions

View File

@@ -21,7 +21,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { import {
AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, CreateFolderDialogComponent, AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, CreateFolderDialogComponent,
DownloadZipDialogComponent, FileUploadCompleteEvent, FolderCreatedEvent, NotificationService, DownloadZipDialogComponent, FileUploadEvent, FolderCreatedEvent, NotificationService,
SiteModel, UploadService SiteModel, UploadService
} from 'ng2-alfresco-core'; } from 'ng2-alfresco-core';
import { DataColumn, DataRow } from 'ng2-alfresco-datatable'; import { DataColumn, DataRow } from 'ng2-alfresco-datatable';
@@ -136,7 +136,8 @@ export class FilesComponent implements OnInit {
}); });
} }
this.uploadService.fileUploadComplete.debounceTime(300).subscribe(value => this.onFileUploadComplete(value)); this.uploadService.fileUploadComplete.debounceTime(300).subscribe(value => this.onFileUploadEvent(value));
this.uploadService.fileUploadDeleted.subscribe((value) => this.onFileUploadEvent(value));
this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value)); this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value));
// this.permissionsStyle.push(new PermissionStyleModel('document-list__create', PermissionsEnum.CREATE)); // this.permissionsStyle.push(new PermissionStyleModel('document-list__create', PermissionsEnum.CREATE));
@@ -161,7 +162,7 @@ export class FilesComponent implements OnInit {
this.errorMessage = null; this.errorMessage = null;
} }
onFileUploadComplete(event: FileUploadCompleteEvent) { onFileUploadEvent(event: FileUploadEvent) {
if (event && event.file.options.parentId === this.documentList.currentFolderId) { if (event && event.file.options.parentId === this.documentList.currentFolderId) {
this.documentList.reload(); this.documentList.reload();
} }

View File

@@ -34,3 +34,19 @@ export class FileUploadCompleteEvent extends FileUploadEvent {
} }
} }
export class FileUploadDeleteEvent extends FileUploadEvent {
constructor(file: FileModel, public totalComplete: number = 0) {
super(file, FileUploadStatus.Deleted);
}
}
export class FileUploadErrorEvent extends FileUploadEvent {
constructor(file: FileModel, public error: any, public totalError: number = 0) {
super(file, FileUploadStatus.Error);
}
}

View File

@@ -34,7 +34,8 @@ export enum FileUploadStatus {
Progress = 3, Progress = 3,
Cancelled = 4, Cancelled = 4,
Aborted = 5, Aborted = 5,
Error = 6 Error = 6,
Deleted = 7
} }
export class FileModel { export class FileModel {

View File

@@ -17,7 +17,7 @@
import { EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { FileModel, FileUploadOptions } from '../models/file.model'; import { FileModel, FileUploadOptions, FileUploadStatus } from '../models/file.model';
import { AlfrescoApiService } from './alfresco-api.service'; import { AlfrescoApiService } from './alfresco-api.service';
import { AlfrescoSettingsService } from './alfresco-settings.service'; import { AlfrescoSettingsService } from './alfresco-settings.service';
import { AppConfigModule } from './app-config.service'; import { AppConfigModule } from './app-config.service';
@@ -214,4 +214,31 @@ describe('UploadService', () => {
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0]).toBe(file4); expect(result[0]).toBe(file4);
}); });
it('should call onUploadDeleted if file was deleted', () => {
const file = <any> ({ status: FileUploadStatus.Deleted });
spyOn(service.fileUploadDeleted, 'next');
service.cancelUpload(file);
expect(service.fileUploadDeleted.next).toHaveBeenCalled();
});
it('should call fileUploadError if file has error status', () => {
const file = <any> ({ status: FileUploadStatus.Error });
spyOn(service.fileUploadError, 'next');
service.cancelUpload(file);
expect(service.fileUploadError.next).toHaveBeenCalled();
});
it('should call fileUploadCancelled if file is in pending', () => {
const file = <any> ({ status: FileUploadStatus.Pending });
spyOn(service.fileUploadCancelled, 'next');
service.cancelUpload(file);
expect(service.fileUploadCancelled.next).toHaveBeenCalled();
});
}); });

View File

@@ -18,7 +18,7 @@
import { EventEmitter, Injectable } from '@angular/core'; import { EventEmitter, Injectable } from '@angular/core';
import * as minimatch from 'minimatch'; import * as minimatch from 'minimatch';
import { Subject } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { FileUploadCompleteEvent, FileUploadEvent } from '../events/file.event'; import { FileUploadCompleteEvent, FileUploadDeleteEvent, FileUploadErrorEvent, FileUploadEvent } from '../events/file.event';
import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model'; import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model';
import { AlfrescoApiService } from './alfresco-api.service'; import { AlfrescoApiService } from './alfresco-api.service';
import { AppConfigService } from './app-config.service'; import { AppConfigService } from './app-config.service';
@@ -30,6 +30,7 @@ export class UploadService {
private cache: { [key: string]: any } = {}; private cache: { [key: string]: any } = {};
private totalComplete: number = 0; private totalComplete: number = 0;
private totalAborted: number = 0; private totalAborted: number = 0;
private totalError: number = 0;
private activeTask: Promise<any> = null; private activeTask: Promise<any> = null;
private excludedFileList: String[] = []; private excludedFileList: String[] = [];
@@ -39,8 +40,9 @@ export class UploadService {
fileUploadCancelled: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUploadCancelled: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadProgress: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUploadProgress: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadAborted: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUploadAborted: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadError: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUploadError: Subject<FileUploadErrorEvent> = new Subject<FileUploadErrorEvent>();
fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>(); fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>();
fileUploadDeleted: Subject<FileUploadDeleteEvent> = new Subject<FileUploadDeleteEvent>();
constructor(private apiService: AlfrescoApiService, private appConfigService: AppConfigService) { constructor(private apiService: AlfrescoApiService, private appConfigService: AppConfigService) {
this.excludedFileList = <String[]> this.appConfigService.get('files.excluded'); this.excludedFileList = <String[]> this.appConfigService.get('files.excluded');
@@ -123,17 +125,15 @@ export class UploadService {
cancelUpload(...files: FileModel[]) { cancelUpload(...files: FileModel[]) {
files.forEach(file => { files.forEach(file => {
file.status = FileUploadStatus.Cancelled;
const promise = this.cache[file.id]; const promise = this.cache[file.id];
if (promise) { if (promise) {
promise.abort(); promise.abort();
delete this.cache[file.id]; delete this.cache[file.id];
} else {
const performAction = this.getAction(file);
performAction();
} }
const event = new FileUploadEvent(file, FileUploadStatus.Cancelled);
this.fileUpload.next(event);
this.fileUploadCancelled.next(event);
}); });
} }
@@ -141,6 +141,7 @@ export class UploadService {
this.queue = []; this.queue = [];
this.totalComplete = 0; this.totalComplete = 0;
this.totalAborted = 0; this.totalAborted = 0;
this.totalError = 0;
} }
getUploadPromise(file: FileModel) { getUploadPromise(file: FileModel) {
@@ -183,7 +184,7 @@ export class UploadService {
emitter.emit({ value: data }); emitter.emit({ value: data });
}) })
.catch(err => { .catch(err => {
this.onUploadError(file, err); throw err;
}); });
return promise; return promise;
@@ -212,13 +213,14 @@ export class UploadService {
private onUploadError(file: FileModel, error: any): void { private onUploadError(file: FileModel, error: any): void {
if (file) { if (file) {
file.status = FileUploadStatus.Error; file.status = FileUploadStatus.Error;
this.totalError++;
const promise = this.cache[file.id]; const promise = this.cache[file.id];
if (promise) { if (promise) {
delete this.cache[file.id]; delete this.cache[file.id];
} }
const event = new FileUploadEvent(file, FileUploadStatus.Error, error); const event = new FileUploadErrorEvent(file, error, this.totalError);
this.fileUpload.next(event); this.fileUpload.next(event);
this.fileUploadError.next(event); this.fileUploadError.next(event);
} }
@@ -257,4 +259,35 @@ export class UploadService {
promise.next(); promise.next();
} }
} }
private onUploadCancelled(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Cancelled;
const event = new FileUploadEvent(file, FileUploadStatus.Cancelled);
this.fileUpload.next(event);
this.fileUploadCancelled.next(event);
}
}
private onUploadDeleted(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Deleted;
this.totalComplete--;
const event = new FileUploadDeleteEvent(file, this.totalComplete);
this.fileUpload.next(event);
this.fileUploadDeleted.next(event);
}
}
private getAction(file) {
const actions = {
[FileUploadStatus.Pending]: () => this.onUploadCancelled(file),
[FileUploadStatus.Deleted]: () => this.onUploadDeleted(file),
[FileUploadStatus.Error]: () => this.onUploadError(file, null)
};
return actions[file.status];
}
} }

View File

@@ -43,7 +43,7 @@
@include mat-comment-list-theme($theme); @include mat-comment-list-theme($theme);
@include mat-start-task-theme($theme); @include mat-start-task-theme($theme);
@include mat-people-search-theme($theme); @include mat-people-search-theme($theme);
@include mat-file-uploading-list-theme($theme); @include mat-file-uploading-row-theme($theme);
@include mat-people-theme($theme); @include mat-people-theme($theme);
@include mat-login-theme($theme); @include mat-login-theme($theme);
@include mat-accordion-theme($theme); @include mat-accordion-theme($theme);

View File

@@ -240,6 +240,7 @@ Note:
| fileUploadAborted | FileUploadEvent | Raised when file upload gets aborted by the server. | | fileUploadAborted | FileUploadEvent | Raised when file upload gets aborted by the server. |
| fileUploadError | FileUploadEvent | Raised when an error occurs to file upload. | | fileUploadError | FileUploadEvent | Raised when an error occurs to file upload. |
| fileUploadComplete | FileUploadCompleteEvent | Raised when file upload is complete. | | fileUploadComplete | FileUploadCompleteEvent | Raised when file upload is complete. |
| fileUploadDelete | FileUploadDeleteEvent | Raised when uploaded file is removed from server. |
## Build from sources ## Build from sources

View File

@@ -26,7 +26,6 @@ import { FileUploadingListComponent } from './src/components/file-uploading-list
import { UploadButtonComponent } from './src/components/upload-button.component'; import { UploadButtonComponent } from './src/components/upload-button.component';
import { UploadDragAreaComponent } from './src/components/upload-drag-area.component'; import { UploadDragAreaComponent } from './src/components/upload-drag-area.component';
import { FileDraggableDirective } from './src/directives/file-draggable.directive'; import { FileDraggableDirective } from './src/directives/file-draggable.directive';
import { FileUploadService } from './src/services/file-uploading.service';
export * from './src/components/upload-button.component'; export * from './src/components/upload-button.component';
export * from './src/components/file-uploading-dialog.component'; export * from './src/components/file-uploading-dialog.component';
@@ -35,7 +34,6 @@ export * from './src/directives/file-draggable.directive';
export * from './src/components/file-uploading-list.component'; export * from './src/components/file-uploading-list.component';
export * from './src/components/file-uploading-list-row.component'; export * from './src/components/file-uploading-list-row.component';
export * from './src/models/permissions.model'; export * from './src/models/permissions.model';
export * from './src/services/file-uploading.service';
export const UPLOAD_DIRECTIVES: any[] = [ export const UPLOAD_DIRECTIVES: any[] = [
FileDraggableDirective, FileDraggableDirective,
@@ -46,10 +44,6 @@ export const UPLOAD_DIRECTIVES: any[] = [
FileUploadingListRowComponent FileUploadingListRowComponent
]; ];
export const UPLOAD_PROVIDERS: any[] = [
FileUploadService
];
@NgModule({ @NgModule({
imports: [ imports: [
CoreModule, CoreModule,
@@ -59,7 +53,6 @@ export const UPLOAD_PROVIDERS: any[] = [
...UPLOAD_DIRECTIVES ...UPLOAD_DIRECTIVES
], ],
providers: [ providers: [
...UPLOAD_PROVIDERS,
{ {
provide: TRANSLATION_PROVIDER, provide: TRANSLATION_PROVIDER,
multi: true, multi: true,
@@ -76,10 +69,7 @@ export const UPLOAD_PROVIDERS: any[] = [
export class UploadModule { export class UploadModule {
static forRoot(): ModuleWithProviders { static forRoot(): ModuleWithProviders {
return { return {
ngModule: UploadModule, ngModule: UploadModule
providers: [
...UPLOAD_PROVIDERS
]
}; };
} }
} }

View File

@@ -27,7 +27,7 @@
*ngIf="uploadList.isUploadCompleted()"> *ngIf="uploadList.isUploadCompleted()">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_COMPLETED' {{ 'FILE_UPLOAD.MESSAGES.UPLOAD_COMPLETED'
| translate: { | translate: {
completed: (totalCompleted - uploadList.uploadCancelledFiles.length), completed: totalCompleted,
total: filesUploadingList.length total: filesUploadingList.length
} }
}} }}
@@ -42,12 +42,12 @@
<section <section
class="upload-dialog__info" class="upload-dialog__info"
*ngIf="uploadList.uploadErrorFiles.length"> *ngIf="totalErrors">
{{ {{
(uploadList.uploadErrorFiles.length > 1 (totalErrors > 1
? 'FILE_UPLOAD.MESSAGES.UPLOAD_ERRORS' ? 'FILE_UPLOAD.MESSAGES.UPLOAD_ERRORS'
: 'FILE_UPLOAD.MESSAGES.UPLOAD_ERROR') : 'FILE_UPLOAD.MESSAGES.UPLOAD_ERROR')
| translate: { total: uploadList.uploadErrorFiles.length } | translate: { total: totalErrors }
}} }}
</section> </section>
@@ -59,7 +59,7 @@
<adf-file-uploading-list-row <adf-file-uploading-list-row
[file]="file" [file]="file"
(remove)="uploadList.removeFile(file)" (remove)="uploadList.removeFile(file)"
(cancel)="uploadList.cancelFileUpload(file)"> (cancel)="uploadList.cancelFile(file)">
</adf-file-uploading-list-row> </adf-file-uploading-list-row>
</ng-template> </ng-template>
</adf-file-uploading-list> </adf-file-uploading-list>
@@ -70,7 +70,7 @@
color="primary" color="primary"
*ngIf="!uploadList.isUploadCompleted() && !uploadList.isUploadCancelled()" *ngIf="!uploadList.isUploadCompleted() && !uploadList.isUploadCancelled()"
md-button md-button
(click)="uploadList.cancelAllFiles($event)"> (click)="uploadList.cancelAllFiles()">
{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_ALL' | translate }} {{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_ALL' | translate }}
</button> </button>

View File

@@ -16,8 +16,9 @@
*/ */
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FileModel, FileUploadCompleteEvent, UploadService } from 'ng2-alfresco-core'; import { FileModel, FileUploadCompleteEvent, FileUploadDeleteEvent,
import { Subscription } from 'rxjs/Rx'; FileUploadErrorEvent, UploadService } from 'ng2-alfresco-core';
import { Observable, Subscription } from 'rxjs/Rx';
@Component({ @Component({
selector: 'adf-file-uploading-dialog, file-uploading-dialog', selector: 'adf-file-uploading-dialog, file-uploading-dialog',
@@ -31,35 +32,47 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
filesUploadingList: FileModel[] = []; filesUploadingList: FileModel[] = [];
isDialogActive: boolean = false; isDialogActive: boolean = false;
totalCompleted: number = 0; totalCompleted: number = 0;
totalErrors: number = 0;
isDialogMinimized: boolean = false; isDialogMinimized: boolean = false;
uploadFilesCompleted: boolean = false;
private listSubscription: Subscription; private listSubscription: Subscription;
private counterSubscription: Subscription; private counterSubscription: Subscription;
private fileUploadSubscription: Subscription; private fileUploadSubscription: Subscription;
private errorSubscription: Subscription;
constructor( constructor(
private uploadService: UploadService, private uploadService: UploadService,
private changeDetecor: ChangeDetectorRef) { private changeDetecor: ChangeDetectorRef) {}
}
ngOnInit() { ngOnInit() {
this.listSubscription = this.uploadService this.listSubscription = this.uploadService
.queueChanged.subscribe((fileList: FileModel[]) => { .queueChanged.subscribe((fileList: FileModel[]) => {
this.filesUploadingList = fileList; this.filesUploadingList = fileList;
if (this.filesUploadingList.length > 0) { if (this.filesUploadingList.length) {
this.isDialogActive = true; this.isDialogActive = true;
} }
}); });
this.counterSubscription = this.uploadService this.counterSubscription = Observable
.fileUploadComplete.subscribe((event: FileUploadCompleteEvent) => { .merge(
this.uploadService.fileUploadComplete,
this.uploadService.fileUploadDeleted
)
.subscribe((event: (FileUploadDeleteEvent|FileUploadCompleteEvent)) => {
this.totalCompleted = event.totalComplete; this.totalCompleted = event.totalComplete;
}); });
this.errorSubscription = this.uploadService.fileUploadError
.subscribe((event: FileUploadErrorEvent) => {
this.totalErrors = event.totalError;
this.changeDetecor.detectChanges();
});
this.fileUploadSubscription = this.uploadService this.fileUploadSubscription = this.uploadService
.fileUpload.subscribe(() => this.changeDetecor.detectChanges()); .fileUpload.subscribe(() => {
this.changeDetecor.detectChanges();
});
} }
/** /**
@@ -75,6 +88,7 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
*/ */
close(): void { close(): void {
this.totalCompleted = 0; this.totalCompleted = 0;
this.totalErrors = 0;
this.filesUploadingList = []; this.filesUploadingList = [];
this.isDialogActive = false; this.isDialogActive = false;
this.isDialogMinimized = false; this.isDialogMinimized = false;
@@ -87,5 +101,6 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
this.listSubscription.unsubscribe(); this.listSubscription.unsubscribe();
this.counterSubscription.unsubscribe(); this.counterSubscription.unsubscribe();
this.fileUploadSubscription.unsubscribe(); this.fileUploadSubscription.unsubscribe();
this.errorSubscription.unsubscribe();
} }
} }

View File

@@ -1,28 +1,28 @@
<div class="adf-file-uploading-list"> <div class="adf-file-uploading-row">
<md-icon <md-icon
md-list-icon md-list-icon
class="list-row__type"> class="adf-file-uploading-row__type">
insert_drive_file insert_drive_file
</md-icon> </md-icon>
<span <span
class="list-row__name" class="adf-file-uploading-row__name"
title="{{ file.name }}"> title="{{ file.name }}">
{{ file.name }} {{ file.name }}
</span> </span>
<div <div
*ngIf="file.status === FileUploadStatus.Progress" *ngIf="file.status === FileUploadStatus.Progress || file.status === FileUploadStatus.Starting"
(click)="onCancel(file)" (click)="onCancel(file)"
class="list-row__group list-row__group--toggle" class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_FILE' | translate }}"> title="{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_FILE' | translate }}">
<span class="list-row__status"> <span class="adf-file-uploading-row__status">
{{ file.progress.loaded | adfFileSize }} / {{ file.progress.total | adfFileSize }} {{ file.progress.loaded | adfFileSize }} / {{ file.progress.total | adfFileSize }}
</span> </span>
<md-icon <md-icon
md-list-icon md-list-icon
class="list-row__action list-row__action--cancel"> class="adf-file-uploading-row__action adf-file-uploading-row__action--cancel">
clear clear
</md-icon> </md-icon>
</div> </div>
@@ -30,24 +30,41 @@
<div <div
*ngIf="file.status === FileUploadStatus.Complete" *ngIf="file.status === FileUploadStatus.Complete"
(click)="onRemove(file)" (click)="onRemove(file)"
class="list-row__group list-row__group--toggle" class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.REMOVE_FILE' | translate }}"> title="{{ 'ADF_FILE_UPLOAD.BUTTON.REMOVE_FILE' | translate }}">
<md-icon <md-icon
md-list-icon md-list-icon
class="list-row__status list-row__status--done"> class="adf-file-uploading-row__status adf-file-uploading-row__status--done">
check_circle check_circle
</md-icon> </md-icon>
<md-icon <md-icon
md-list-icon md-list-icon
class="list-row__action list-row__action--remove"> class="adf-file-uploading-row__action adf-file-uploading-row__action--remove">
remove_circle
</md-icon>
</div>
<div
*ngIf="file.status === FileUploadStatus.Pending"
(click)="onCancel(file)"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle">
<md-icon
md-list-icon
class="adf-file-uploading-row__status adf-file-uploading-row__status--pending">
schedule
</md-icon>
<md-icon
md-list-icon
class="adf-file-uploading-row__action adf-file-uploading-row__action--remove">
remove_circle remove_circle
</md-icon> </md-icon>
</div> </div>
<div <div
*ngIf="file.status === FileUploadStatus.Error" *ngIf="file.status === FileUploadStatus.Error"
class="list-row__block list-row__status--error" class="adf-file-uploading-row__block adf-file-uploading-row__status--error"
title="{{ file.response }}"> title="{{ file.response }}">
<md-icon md-list-icon> <md-icon md-list-icon>
report_problem report_problem
@@ -55,8 +72,10 @@
</div> </div>
<div <div
*ngIf="file.status === FileUploadStatus.Cancelled || file.status === FileUploadStatus.Aborted" *ngIf="file.status === FileUploadStatus.Cancelled ||
class="list-row__block list-row__status--cancelled"> file.status === FileUploadStatus.Aborted ||
file.status === FileUploadStatus.Deleted"
class="adf-file-uploading-row__block adf-file-uploading-row__status--cancelled">
{{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }} {{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }}
</div> </div>
</div> <div>

View File

@@ -1,23 +1,21 @@
@import 'colors'; @import 'colors';
@mixin mat-file-uploading-list-theme($theme) { @mixin mat-file-uploading-row-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$accent: map-get($theme, accent); $accent: map-get($theme, accent);
$warn: map-get($theme, warn); $warn: map-get($theme, warn);
.adf-file-uploading-list { .adf-file-uploading-row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.5em 1em 0.5em 1em; padding: 0.5em 1em 0.5em 1em;
cursor: default;
&:not(:first-child) { &:not(:first-child) {
border-top: 1px solid ; border-top: 1px solid ;
} }
.list-row {
cursor: default;
&__name { &__name {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -35,20 +33,20 @@
&__group--toggle { &__group--toggle {
cursor: pointer; cursor: pointer;
.list-row__status { .adf-file-uploading-row__status {
display: flex; display: flex;
} }
.list-row__action { .adf-file-uploading-row__action {
display: none; display: none;
} }
&:hover { &:hover {
.list-row__status { .adf-file-uploading-row__status {
display: none; display: none;
} }
.list-row__action { .adf-file-uploading-row__action {
display: flex; display: flex;
} }
} }
@@ -71,4 +69,3 @@
} }
} }
} }
}

View File

@@ -16,10 +16,9 @@
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlfrescoTranslationService, FileModel, FileUploadStatus, NodesApiService, NotificationService, UploadService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, FileUploadStatus, NodesApiService, NotificationService, UploadService } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { UploadModule } from '../../index'; import { UploadModule } from '../../index';
import { FileUploadService } from '../services/file-uploading.service';
import { FileUploadingListComponent } from './file-uploading-list.component'; import { FileUploadingListComponent } from './file-uploading-list.component';
describe('FileUploadingListComponent', () => { describe('FileUploadingListComponent', () => {
@@ -27,10 +26,13 @@ describe('FileUploadingListComponent', () => {
let component: FileUploadingListComponent; let component: FileUploadingListComponent;
let uploadService: UploadService; let uploadService: UploadService;
let nodesApiService: NodesApiService; let nodesApiService: NodesApiService;
let fileUploadService: FileUploadService;
let notificationService: NotificationService; let notificationService: NotificationService;
let translateService: AlfrescoTranslationService; let translateService: AlfrescoTranslationService;
let file = new FileModel(<File> { name: 'fake-name' }); let file: any;
beforeEach(() => {
file = { data: { entry: { id: 'x' } } };
});
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -43,66 +45,128 @@ describe('FileUploadingListComponent', () => {
beforeEach(() => { beforeEach(() => {
nodesApiService = TestBed.get(NodesApiService); nodesApiService = TestBed.get(NodesApiService);
uploadService = TestBed.get(UploadService); uploadService = TestBed.get(UploadService);
fileUploadService = TestBed.get(FileUploadService);
notificationService = TestBed.get(NotificationService); notificationService = TestBed.get(NotificationService);
translateService = TestBed.get(AlfrescoTranslationService); translateService = TestBed.get(AlfrescoTranslationService);
fixture = TestBed.createComponent(FileUploadingListComponent); fixture = TestBed.createComponent(FileUploadingListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.files = [ file ];
file.data = { entry: { id: 'x' } };
spyOn(translateService, 'get').and.returnValue(Observable.of('some error message')); spyOn(translateService, 'get').and.returnValue(Observable.of('some error message'));
spyOn(notificationService, 'openSnackMessage');
spyOn(uploadService, 'cancelUpload');
}); });
describe('cancelFileUpload()', () => { describe('cancelFile()', () => {
it('should call uploadService api when cancelling a file', () => { it('should call uploadService api when cancelling a file', () => {
spyOn(uploadService, 'cancelUpload'); component.cancelFile(file);
component.cancelFileUpload(file);
expect(uploadService.cancelUpload).toHaveBeenCalledWith(file); expect(uploadService.cancelUpload).toHaveBeenCalledWith(file);
}); });
}); });
describe('removeFile()', () => { describe('removeFile()', () => {
it('should remove file successfully when api returns success', () => { it('should change file status when api returns success', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of('success')); spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of(file));
spyOn(fileUploadService, 'emitFileRemoved');
component.removeFile(file); component.removeFile(file);
fixture.detectChanges(); fixture.detectChanges();
expect(fileUploadService.emitFileRemoved).toHaveBeenCalledWith(file); expect(file.status).toBe(FileUploadStatus.Deleted);
}); });
it('should notify on remove file fail when api returns error', () => { it('should change file status when api returns error', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw({})); spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(file));
spyOn(notificationService, 'openSnackMessage');
component.removeFile(file);
fixture.detectChanges();
expect(file.status).toBe(FileUploadStatus.Error);
});
it('should notify fail when api returns error', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(file));
component.removeFile(file); component.removeFile(file);
fixture.detectChanges(); fixture.detectChanges();
expect(notificationService.openSnackMessage).toHaveBeenCalled(); expect(notificationService.openSnackMessage).toHaveBeenCalled();
}); });
it('should call uploadService on error', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw(file));
component.removeFile(file);
fixture.detectChanges();
expect(uploadService.cancelUpload).toHaveBeenCalled();
});
it('should call uploadService on success', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of(file));
component.removeFile(file);
fixture.detectChanges();
expect(uploadService.cancelUpload).toHaveBeenCalled();
});
}); });
describe('cancelAllFiles()', () => { describe('cancelAllFiles()', () => {
beforeEach(() => { beforeEach(() => {
spyOn(component, 'removeFile'); component.files = <any> [
spyOn(component, 'cancelFileUpload'); {
data: {
entry: { id: '1' }
},
status: FileUploadStatus.Cancelled
},
{
data: {
entry: { id: '2' }
},
status: FileUploadStatus.Error
}
];
}); });
it('should call removeFile() if file was uploaded', () => { it('should not call deleteNode if there are no competed uploads', () => {
file.status = FileUploadStatus.Complete; spyOn(nodesApiService, 'deleteNode');
component.cancelAllFiles(null);
expect(component.removeFile).toHaveBeenCalledWith(file); component.cancelAllFiles();
expect(nodesApiService.deleteNode).not.toHaveBeenCalled();
}); });
it('should call cancelFileUpload() if file is being uploaded', () => { it('should not call uploadService if there are no uploading files', () => {
file.status = FileUploadStatus.Progress; component.cancelAllFiles();
component.cancelAllFiles(null);
expect(component.cancelFileUpload).toHaveBeenCalledWith(file); expect(uploadService.cancelUpload).not.toHaveBeenCalled();
});
it('should call deleteNode when there are completed uploads', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of({}));
component.files[0].status = FileUploadStatus.Complete;
component.cancelAllFiles();
expect(nodesApiService.deleteNode).toHaveBeenCalled();
});
it('should call uploadService when there are uploading files', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.of({}));
component.files[0].status = FileUploadStatus.Progress;
component.cancelAllFiles();
expect(uploadService.cancelUpload).toHaveBeenCalled();
});
it('should notify on deleting file error', () => {
spyOn(nodesApiService, 'deleteNode').and.returnValue(Observable.throw({}));
component.files[0].status = FileUploadStatus.Complete;
component.cancelAllFiles();
expect(notificationService.openSnackMessage).toHaveBeenCalled();
}); });
}); });
@@ -110,7 +174,6 @@ describe('FileUploadingListComponent', () => {
it('should return false when at least one file is in progress', () => { it('should return false when at least one file is in progress', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Progress }, { status: FileUploadStatus.Progress },
{ status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Complete } { status: FileUploadStatus.Complete }
]; ];
@@ -120,23 +183,39 @@ describe('FileUploadingListComponent', () => {
it('should return false when at least one file is in pending', () => { it('should return false when at least one file is in pending', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Pending }, { status: FileUploadStatus.Pending },
{ status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Complete } { status: FileUploadStatus.Complete }
]; ];
expect(component.isUploadCompleted()).toBe(false); expect(component.isUploadCompleted()).toBe(false);
}); });
it('should return false when none of the files is completed', () => { it('should return false when at least one file is in starting state', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Error }, { status: FileUploadStatus.Starting },
{ status: FileUploadStatus.Error }, { status: FileUploadStatus.Complete }
];
expect(component.isUploadCompleted()).toBe(false);
});
it('should return false when files are cancelled', () => {
component.files = <any> [
{ status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Cancelled } { status: FileUploadStatus.Cancelled }
]; ];
expect(component.isUploadCompleted()).toBe(false); expect(component.isUploadCompleted()).toBe(false);
}); });
it('should return true when there are deleted files', () => {
component.files = <any> [
{ status: FileUploadStatus.Complete },
{ status: FileUploadStatus.Deleted }
];
expect(component.isUploadCompleted()).toBe(true);
});
it('should return true when none of the files is in progress', () => { it('should return true when none of the files is in progress', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Error }, { status: FileUploadStatus.Error },
@@ -191,7 +270,6 @@ describe('FileUploadingListComponent', () => {
it('should return true when all files are aborted', () => { it('should return true when all files are aborted', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Aborted },
{ status: FileUploadStatus.Aborted } { status: FileUploadStatus.Aborted }
]; ];
@@ -202,53 +280,10 @@ describe('FileUploadingListComponent', () => {
component.files = <any> [ component.files = <any> [
{ status: FileUploadStatus.Cancelled }, { status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Cancelled }, { status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Error } { status: FileUploadStatus.Aborted }
]; ];
expect(component.isUploadCancelled()).toBe(true); expect(component.isUploadCancelled()).toBe(true);
}); });
}); });
describe('uploadErrorFiles()', () => {
it('should return array of error files', () => {
component.files = <any> [
{ status: FileUploadStatus.Complete },
{ status: FileUploadStatus.Error },
{ status: FileUploadStatus.Error }
];
expect(component.uploadErrorFiles.length).toEqual(2);
});
it('should return empty array when no error files found', () => {
component.files = <any> [
{ status: FileUploadStatus.Complete },
{ status: FileUploadStatus.Pending }
];
expect(component.uploadErrorFiles.length).toEqual(0);
});
});
describe('uploadCancelledFiles()', () => {
it('should return array of cancelled files', () => {
component.files = <any> [
{ status: FileUploadStatus.Cancelled },
{ status: FileUploadStatus.Complete },
{ status: FileUploadStatus.Error }
];
expect(component.uploadCancelledFiles.length).toEqual(1);
});
it('should return emty array when no cancelled files found', () => {
component.files = <any> [
{ status: FileUploadStatus.Error },
{ status: FileUploadStatus.Complete },
{ status: FileUploadStatus.Pending }
];
expect(component.uploadCancelledFiles.length).toEqual(0);
});
});
}); });

View File

@@ -17,7 +17,7 @@
import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { AlfrescoTranslationService, FileModel, FileUploadStatus, NodesApiService, NotificationService, UploadService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, FileModel, FileUploadStatus, NodesApiService, NotificationService, UploadService } from 'ng2-alfresco-core';
import { FileUploadService } from '../services/file-uploading.service'; import { Observable } from 'rxjs/Rx';
@Component({ @Component({
selector: 'adf-file-uploading-list, alfresco-file-uploading-list', selector: 'adf-file-uploading-list, alfresco-file-uploading-list',
@@ -35,7 +35,6 @@ export class FileUploadingListComponent {
files: FileModel[] = []; files: FileModel[] = [];
constructor( constructor(
private fileUploadService: FileUploadService,
private uploadService: UploadService, private uploadService: UploadService,
private nodesApi: NodesApiService, private nodesApi: NodesApiService,
private notificationService: NotificationService, private notificationService: NotificationService,
@@ -49,40 +48,42 @@ export class FileUploadingListComponent {
* *
* @memberOf FileUploadingListComponent * @memberOf FileUploadingListComponent
*/ */
cancelFileUpload(file: FileModel): void { cancelFile(file: FileModel): void {
this.uploadService.cancelUpload(file); this.uploadService.cancelUpload(file);
} }
removeFile(file: FileModel): void { removeFile(file: FileModel): void {
const { id } = file.data.entry; this.deleteNode(file)
this.nodesApi .subscribe(() => {
.deleteNode(id, { permanent: true }) if ( file.status === FileUploadStatus.Error) {
.subscribe( this.notifyError(file);
() => this.onRemoveSuccess(file), }
() => this.onRemoveFail(file)
); this.uploadService.cancelUpload(file);
});
} }
/** /**
* Call the abort method for each file * Call the appropriate method for each file, depending on state
*/ */
cancelAllFiles(event: Event): void { cancelAllFiles(): void {
if (event) { this.getUploadingFiles()
event.preventDefault(); .forEach((file) => this.uploadService.cancelUpload(file));
}
const deletedFiles = this.files
this.files.forEach((file) => { .filter((file) => file.status === FileUploadStatus.Complete)
const { status } = file; .map((file) => this.deleteNode(file));
const { Complete, Progress, Pending } = FileUploadStatus;
Observable.forkJoin(...deletedFiles)
if (status === Complete) { .subscribe((files: FileModel[]) => {
this.removeFile(file); const errors = files
} .filter((file) => file.status === FileUploadStatus.Error);
if (status === Progress || status === Pending) { if (errors.length) {
this.cancelFileUpload(file); this.notifyError(...errors);
} }
this.uploadService.cancelUpload(...files);
}); });
} }
@@ -92,7 +93,7 @@ export class FileUploadingListComponent {
*/ */
isUploadCompleted(): boolean { isUploadCompleted(): boolean {
return !this.isUploadCancelled() && return !this.isUploadCancelled() &&
!!this.files.length && Boolean(this.files.length) &&
!this.files !this.files
.some(({status}) => .some(({status}) =>
status === FileUploadStatus.Starting || status === FileUploadStatus.Starting ||
@@ -111,38 +112,55 @@ export class FileUploadingListComponent {
.every(({status}) => .every(({status}) =>
status === FileUploadStatus.Aborted || status === FileUploadStatus.Aborted ||
status === FileUploadStatus.Cancelled || status === FileUploadStatus.Cancelled ||
status === FileUploadStatus.Error status === FileUploadStatus.Deleted
); );
} }
/** private deleteNode(file: FileModel): Observable<FileModel> {
* Gets all the files with status Error. const { id } = file.data.entry;
* @returns {boolean} - false if there is none
*/ return this.nodesApi
get uploadErrorFiles(): FileModel[] { .deleteNode(id, { permanent: true })
return this.files.filter(({status}) => status === FileUploadStatus.Error); .map(() => {
file.status = FileUploadStatus.Deleted;
return file;
})
.catch((error) => {
file.status = FileUploadStatus.Error;
return Observable.of(file);
});
} }
/** private notifyError(...files: FileModel[]) {
* Gets all the files with status Cancelled. let translateSubscription = null;
* @returns {boolean} - false if there is none
*/ if (files.length === 1) {
get uploadCancelledFiles(): FileModel[] { translateSubscription = this.translateService
return this.files.filter(({status}) => status === FileUploadStatus.Cancelled); .get(
'FILE_UPLOAD.MESSAGES.REMOVE_FILE_ERROR',
{ fileName: files[0].name}
);
} else {
translateSubscription = this.translateService
.get(
'FILE_UPLOAD.MESSAGES.REMOVE_FILES_ERROR',
{ total: files.length }
);
} }
private onRemoveSuccess(file: FileModel): void { translateSubscription
const { uploadService, fileUploadService } = this; .subscribe(message => this.notificationService.openSnackMessage(message, 4000));
uploadService.cancelUpload(file);
fileUploadService.emitFileRemoved(file);
} }
private onRemoveFail(file: FileModel): void { private getUploadingFiles() {
this.translateService return this.files.filter((item) => {
.get('FILE_UPLOAD.MESSAGES.REMOVE_FILE_ERROR', { fileName: file.name}) if (
.subscribe((message) => { item.status === FileUploadStatus.Pending ||
this.notificationService.openSnackMessage(message, 4000); item.status === FileUploadStatus.Progress ||
item.status === FileUploadStatus.Starting
) {
return item;
}
}); });
} }
} }

View File

@@ -26,7 +26,8 @@
"PROGRESS": "Upload in progress...", "PROGRESS": "Upload in progress...",
"FOLDER_ALREADY_EXIST": "The folder {0} already exist", "FOLDER_ALREADY_EXIST": "The folder {0} already exist",
"FOLDER_NOT_SUPPORTED": "Folder upload isn't supported by your browser", "FOLDER_NOT_SUPPORTED": "Folder upload isn't supported by your browser",
"REMOVE_FILE_ERROR": "Error removing file {{ fileName }}" "REMOVE_FILE_ERROR": "Error removing file {{ fileName }}",
"REMOVE_FILES_ERROR": "Error removing {{ total }} files"
}, },
"ACTION": { "ACTION": {
"UNDO": "Undo" "UNDO": "Undo"

View File

@@ -1,43 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FileModel } from 'ng2-alfresco-core';
import { FileUploadService } from './file-uploading.service';
describe('FileUploadService', () => {
let service: FileUploadService;
let file = new FileModel(<File> { name: 'fake-name' });
beforeEach(() => {
service = new FileUploadService();
});
it('emits file remove event', () => {
spyOn(service.remove, 'next');
service.emitFileRemoved(file);
expect(service.remove.next).toHaveBeenCalledWith(file);
});
it('passes removed file data', () => {
service.emitFileRemoved(file);
service.remove.subscribe((data) => {
expect(data).toEqual(file);
});
});
});

View File

@@ -1,28 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Rx';
@Injectable()
export class FileUploadService {
remove = new Subject<string>();
emitFileRemoved(item: any) {
this.remove.next(item);
}
}