[ADF-571] upload feature rework (#1922)

* upload feature rework

lots of improvements for upload dialog and underlying services

* readme update

- readme cleanup
- remove some old comments from code
- update readme with new events for Upload Service

* restore prerequisites section in readme
This commit is contained in:
Denys Vuika
2017-06-02 14:05:55 +01:00
committed by Eugenio Romano
parent b4c9710e71
commit c2fee79724
23 changed files with 560 additions and 769 deletions

View File

@@ -17,10 +17,10 @@
import { Component, Input, OnInit, AfterViewInit, Optional, ViewChild, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AlfrescoAuthenticationService, LogService, NotificationService } from 'ng2-alfresco-core';
import { AlfrescoAuthenticationService, AlfrescoContentService, FolderCreatedEvent, LogService, NotificationService } from 'ng2-alfresco-core';
import { DocumentActionsService, DocumentListComponent, ContentActionHandler, DocumentActionModel, FolderActionModel } from 'ng2-alfresco-documentlist';
import { FormService } from 'ng2-activiti-form';
import { UploadService, UploadButtonComponent, UploadDragAreaComponent, FolderCreatedEvent } from 'ng2-alfresco-upload';
import { UploadService, UploadButtonComponent, UploadDragAreaComponent } from 'ng2-alfresco-upload';
@Component({
selector: 'files-component',
@@ -73,6 +73,7 @@ export class FilesComponent implements OnInit, AfterViewInit {
private router: Router,
private notificationService: NotificationService,
private uploadService: UploadService,
private contentService: AlfrescoContentService,
@Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this));
}
@@ -122,7 +123,7 @@ export class FilesComponent implements OnInit, AfterViewInit {
this.logService.warn('You are not logged in to BPM');
}
this.uploadService.folderCreated.subscribe(value => this.onFolderCreated(value));
this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value));
}
ngAfterViewInit() {

View File

@@ -41,6 +41,7 @@ import {
ContentService
} from './src/services/index';
import { FileSizePipe } from './src/pipes/file-size.pipe';
import { UploadDirective } from './src/directives/upload.directive';
import { DataColumnComponent } from './src/components/data-column/data-column.component';
import { DataColumnListComponent } from './src/components/data-column/data-column-list.component';
@@ -57,6 +58,7 @@ export * from './src/directives/upload.directive';
export * from './src/utils/index';
export * from './src/events/base.event';
export * from './src/events/base-ui.event';
export * from './src/events/folder-created.event';
export const ALFRESCO_CORE_PROVIDERS: any[] = [
NotificationService,
@@ -101,7 +103,8 @@ export function createTranslateLoader(http: Http, logService: LogService) {
...COLLAPSABLE_DIRECTIVES,
UploadDirective,
DataColumnComponent,
DataColumnListComponent
DataColumnListComponent,
FileSizePipe
],
providers: [
...ALFRESCO_CORE_PROVIDERS
@@ -119,7 +122,8 @@ export function createTranslateLoader(http: Http, logService: LogService) {
UploadDirective,
DataColumnComponent,
DataColumnListComponent,
MdSnackBarModule
MdSnackBarModule,
FileSizePipe
]
})
export class CoreModule {

View File

@@ -0,0 +1,36 @@
/*!
* @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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'adfFileSize'
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number = 0, decimals: number = 2): string {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024,
dm = decimals || 2,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}

View File

@@ -16,15 +16,18 @@
*/
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs/Rx';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoAuthenticationService } from './alfresco-authentication.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { LogService } from './log.service.ts';
import { Observable } from 'rxjs/Rx';
import { LogService } from './log.service';
import { FolderCreatedEvent } from '../events/folder-created.event';
@Injectable()
export class AlfrescoContentService {
folderCreated: Subject<FolderCreatedEvent> = new Subject<FolderCreatedEvent>();
constructor(public authService: AlfrescoAuthenticationService,
public apiService: AlfrescoApiService,
private logService: LogService) {
@@ -70,6 +73,23 @@ export class AlfrescoContentService {
})).catch(this.handleError);
}
/**
* Create a folder
* @param name - the folder name
*/
createFolder(relativePath: string, name: string, parentId?: string): Observable<MinimalNodeEntity> {
return Observable.fromPromise(this.apiService.getInstance().nodes.createFolder(name, relativePath, parentId))
.do(data => {
this.folderCreated.next({
relativePath: relativePath,
name: name,
parentId: parentId,
node: data
});
})
.catch(err => this.handleError(err));
}
private handleError(error: any) {
this.logService.error(error);
return Observable.throw(error || 'Server error');

View File

@@ -18,20 +18,25 @@
<a href='https://github.com/Alfresco/alfresco-ng2-components/blob/master/LICENSE'>
<img src='https://img.shields.io/hexpm/l/plug.svg' alt='license' />
</a>
<a href='https://www.alfresco.com/'>
<img src='https://img.shields.io/badge/style-component-green.svg?label=alfresco' alt='alfresco component' />
</a>
<a href='https://angular.io/'>
<img src='https://img.shields.io/badge/style-2-red.svg?label=angular' alt='angular 2' />
</a>
<a href='https://www.typescriptlang.org/docs/tutorial.html'>
<img src='https://img.shields.io/badge/style-lang-blue.svg?label=typescript' alt='typescript' />
</a>
<a href='https://www.alfresco.com/'>
<img src='https://img.shields.io/badge/style-%3E5.0.0-blue.svg?label=node%20version' alt='node version' />
</a>
</p>
## Content
### Components
- [FileUploadingDialogComponent](#fileuploadingdialogcomponent)
- FileUploadingListComponent
- [UploadButtonComponent](#uploadbuttoncomponent)
- [UploadDragAreaComponent](#uploaddragareacomponent)
### Services
- [UploadService](#uploadservice)
### Directives
- FileDraggableDirective
## Prerequisites
Before you start using this development framework, make sure you have installed all required software and done all the
@@ -93,19 +98,17 @@ Follow the 3 steps below:
Please refer to the following example file: [systemjs.config.js](demo/systemjs.config.js) .
#### Basic usage
## UploadButtonComponent
```html
<alfresco-upload-button [showNotificationBar]="true"
[uploadFolders]="true"
[multipleFiles]="false"
[acceptedFilesType]=".jpg,.gif,.png,.svg"
[currentFolderPath]="/Sites/swsdp/documentLibrary"
[versioning]="false"
(onSuccess)="customMethod($event)">
<alfresco-upload-button
[showNotificationBar]="true"
[uploadFolders]="true"
[multipleFiles]="false"
[acceptedFilesType]=".jpg,.gif,.png,.svg"
[currentFolderPath]="/Sites/swsdp/documentLibrary"
[versioning]="false"
(onSuccess)="customMethod($event)">
</alfresco-upload-button>
<file-uploading-dialog></file-uploading-dialog>
```
@@ -121,26 +124,27 @@ import { UploadModule } from 'ng2-alfresco-upload';
@Component({
selector: 'alfresco-app-demo',
template: `<alfresco-upload-button [showNotificationBar]="true"
[uploadFolders]="false"
[multipleFiles]="false"
[acceptedFilesType]="'.jpg,.gif,.png,.svg'"
(onSuccess)="onSuccess($event)">
</alfresco-upload-button>
<file-uploading-dialog></file-uploading-dialog>`
template: `
<alfresco-upload-button
[showNotificationBar]="true"
[uploadFolders]="false"
[multipleFiles]="false"
[acceptedFilesType]="'.jpg,.gif,.png,.svg'"
(onSuccess)="onSuccess($event)">
</alfresco-upload-button>
<file-uploading-dialog></file-uploading-dialog>
`
})
export class MyDemoApp {
constructor(private authService: AlfrescoAuthenticationService, private settingsService: AlfrescoSettingsService) {
constructor(private authService: AlfrescoAuthenticationService,
private settingsService: AlfrescoSettingsService) {
settingsService.ecmHost = 'http://localhost:8080';
this.authService.login('admin', 'admin').subscribe(
ticket => {
console.log(ticket);
},
error => {
console.log(error);
});
ticket => console.log(ticket),
error => console.log(error)
);
}
public onSuccess(event: Object): void {
@@ -160,15 +164,15 @@ export class MyDemoApp {
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
```
#### Events
### Events
| Name | Description |
| --- | --- |
| `onSuccess` | The event is emitted when the file is uploaded |
#### Properties
### Properties
| Name | Type | Default | Description |
| --- | --- | --- | --- |
@@ -182,7 +186,10 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| `staticTitle` | *string* | 'FILE_UPLOAD.BUTTON.UPLOAD_FILE' or 'FILE_UPLOAD.BUTTON.UPLOAD_FOLDER' string in the JSON text file | define the text of the upload button |
| `disableWithNoPermission` | *boolean* | false | If the value is true and the user doesn't have the permission to delete the node the button will be disabled |
### How to show notification message with no permission
### Advanced usage
#### How to show notification message with no permission
You can show a notification error when the user doesn't have the right permission to perform the action.
The UploadButtonComponent provides the event permissionEvent that is raised when the delete permission is missing
You can subscribe to this event from your component and use the NotificationService to show a message.
@@ -195,9 +202,11 @@ You can subscribe to this event from your component and use the NotificationServ
export class MyComponent {
onUploadPermissionFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
onUploadPermissionFailed(event: any) {
this.notificationService.openSnackMessage(
`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000
);
}
}
```
@@ -205,6 +214,7 @@ onUploadPermissionFailed(event: any) {
![Upload notification message](docs/assets/upload-notification-message.png)
#### How to disable the button when the delete permission is missing
You can easily disable the button when the user doesn't own the permission to perform the action.
The UploadButtonComponent provides the property disableWithNoPermission that can be true. In this way the button should be disabled if the delete permission is missing for the node.
@@ -217,19 +227,18 @@ The UploadButtonComponent provides the property disableWithNoPermission that can
![Upload disable button](docs/assets/upload-disable-button.png)
## UploadDragAreaComponent
### Drag and drop
This component, provide a drag and drop are to upload files to alfresco.
#### Basic usage
```html
<alfresco-upload-drag-area (onSuccess)="customMethod($event)"></alfresco-upload-drag-area>
<alfresco-upload-drag-area
(onSuccess)="customMethod($event)">
</alfresco-upload-drag-area>
<file-uploading-dialog></file-uploading-dialog>
```
Example of an App that declares upload drag and drop component :
Example of an App that declares upload drag and drop component:
```ts
import { NgModule, Component } from '@angular/core';
@@ -240,25 +249,25 @@ import { UploadModule } from 'ng2-alfresco-upload';
@Component({
selector: 'alfresco-app-demo',
template: `<alfresco-upload-drag-area (onSuccess)="customMethod($event)" >
<div style="width: 200px; height: 100px; border: 1px solid #888888">
DRAG HERE
</div>
</alfresco-upload-drag-area>
<file-uploading-dialog></file-uploading-dialog>`
template: `
<alfresco-upload-drag-area (onSuccess)="customMethod($event)" >
<div style="width: 200px; height: 100px; border: 1px solid #888888">
DRAG HERE
</div>
</alfresco-upload-drag-area>
<file-uploading-dialog></file-uploading-dialog>
`
})
export class MyDemoApp {
constructor(private authService: AlfrescoAuthenticationService, private settingsService: AlfrescoSettingsService) {
constructor(private authService: AlfrescoAuthenticationService,
private settingsService: AlfrescoSettingsService) {
settingsService.ecmHost = 'http://localhost:8080';
this.authService.login('admin', 'admin').subscribe(
ticket => {
console.log(ticket);
},
error => {
console.log(error);
});
ticket => console.log(ticket),
error => console.log(error)
);
}
public onSuccess(event: Object): void {
@@ -278,16 +287,15 @@ export class MyDemoApp {
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
```
#### Events
### Events
| Name | Description |
| --- | --- |
| `onSuccess` | The event is emitted when the file is uploaded |
#### Properties
### Properties
| Name | Type | Default | Description |
| --- | --- | --- | --- |
@@ -297,27 +305,31 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| `currentFolderPath` | *string* | '/' | define the path where the files are uploaded |
| `versioning` | *boolean* | false | Versioning false is the default uploader behaviour and it rename using an integer suffix if there is a name clash. Versioning true to indicate that a major version should be created |
### Files Dialog
## FileUploadingDialogComponent
This component provides a dialog that shows all the files uploaded with upload button or drag & drop area components.
This component should be used in combination with upload button or drag & drop area.
#### Basic usage
```html
<file-uploading-dialog></file-uploading-dialog>
```
### UploadService service
## UploadService
Provides access to various APIs related to file upload features.
#### Events
### Events
| Name | Type | Description |
| --- | --- | --- |
| folderCreated | FolderCreatedEvent | Raised when dropped folder gets created |
| queueChanged | FileModel[] | Raised every time the file queue changes. |
| fileUpload | FileUploadEvent | Raised every time a File model changes its state. |
| fileUploadStarting | FileUploadEvent | Raised when upload starts. |
| fileUploadCancelled | FileUploadEvent | Raised when upload gets cancelled by user. |
| fileUploadProgress | FileUploadEvent | Raised during file upload process and contains the current progress for the particular File model. |
| fileUploadAborted | FileUploadEvent | Raised when file upload gets aborted by the server. |
| fileUploadError | FileUploadEvent | Raised when an error occurs to file upload. |
| fileUploadComplete | FileUploadCompleteEvent | Raised when file upload is complete. |
## Build from sources
@@ -331,7 +343,7 @@ npm run build
### Build the files and keep watching for changes
```sh
$ npm run build:w
npm run build:w
```
## Running unit tests

View File

@@ -16,7 +16,7 @@
*/
import { NgModule, ModuleWithProviders } from '@angular/core';
import { MdIconModule } from '@angular/material';
import { MdIconModule, MdProgressSpinnerModule, MdButtonModule } from '@angular/material';
import { CoreModule } from 'ng2-alfresco-core';
import { UploadDragAreaComponent } from './src/components/upload-drag-area.component';
@@ -26,22 +26,6 @@ import { FileUploadingDialogComponent } from './src/components/file-uploading-di
import { FileUploadingListComponent } from './src/components/file-uploading-list.component';
import { UploadService } from './src/services/upload.service';
/**
* ng2-alfresco-upload, provide components to upload files to alfresco repository.
*
* Components provided:
* - A button to upload files
* <alfresco-upload-button [showDialogUpload]="boolean"
* [showNotificationBar]="boolean"
* [uploadFolders]="boolean"
* [multipleFiles]="boolean"
* [acceptedFilesType]="string">
* </alfresco-upload-button>
*
* - Drag and drop area to upload files:
* <alfresco-upload-drag-area [showDialogUpload]="boolean" ></alfresco-upload-drag-area>
*/
export * from './src/components/upload-button.component';
export * from './src/components/file-uploading-dialog.component';
export * from './src/components/upload-drag-area.component';
@@ -50,7 +34,7 @@ export * from './src/directives/file-draggable.directive';
export * from './src/components/file-uploading-list.component';
export * from './src/models/file.model';
export * from './src/models/permissions.model';
export * from './src/events/folder-created.event';
export * from './src/events/file.event';
export const UPLOAD_DIRECTIVES: any[] = [
FileDraggableDirective,
@@ -67,7 +51,9 @@ export const UPLOAD_PROVIDERS: any[] = [
@NgModule({
imports: [
CoreModule,
MdIconModule
MdIconModule,
MdProgressSpinnerModule,
MdButtonModule
],
declarations: [
...UPLOAD_DIRECTIVES

View File

@@ -14,7 +14,7 @@
}
:host .file-dialog {
width: 700px;
width: 550px;
display: none;
-webkit-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .2);
box-shadow: -2px -1px 8px 3px rgba(0, 0, 0, .2);

View File

@@ -16,12 +16,14 @@
*/
import { DebugElement } from '@angular/core';
import { MdProgressSpinnerModule } from '@angular/material';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { FileUploadingDialogComponent } from './file-uploading-dialog.component';
import { FileUploadingListComponent } from './file-uploading-list.component';
import { UploadService } from '../services/upload.service';
import { FileModel } from '../models/file.model';
import { FileUploadCompleteEvent } from '../events/file.event';
describe('FileUploadingDialogComponent', () => {
@@ -35,7 +37,8 @@ describe('FileUploadingDialogComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot()
CoreModule.forRoot(),
MdProgressSpinnerModule
],
declarations: [
FileUploadingDialogComponent,
@@ -48,8 +51,6 @@ describe('FileUploadingDialogComponent', () => {
}));
beforeEach(() => {
window['componentHandler'] = null;
const fileFake = new File([''], 'fake-name');
file = new FileModel(fileFake);
@@ -70,7 +71,7 @@ describe('FileUploadingDialogComponent', () => {
});
it('should render completed upload 1 when an element is added to Observer', () => {
uploadService.updateFileCounterStream(1);
uploadService.fileUploadComplete.next(new FileUploadCompleteEvent(null, 1));
fixture.detectChanges();
expect(element.querySelector('#total-upload-completed').innerText).toEqual('1');

View File

@@ -15,24 +15,15 @@
* limitations under the License.
*/
import { Component, Input, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, Input, ChangeDetectorRef, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { FileModel } from '../models/file.model';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { UploadService } from '../services/upload.service';
import { FileUploadCompleteEvent } from '../events/file.event';
/**
* <file-uploading-dialog [filesUploadingList]="FileModel[]"></file-uploading-dialog>
*
* This component is a hideable and minimizable wich contains the list of the uploading
* files contained in the filesUploadingList.
*
* @InputParam {FileModel[]} filesUploadingList - list of the uploading files .
*
*
* @returns {FileUploadingDialogComponent} .
*/
@Component({
selector: 'file-uploading-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './file-uploading-dialog.component.html',
styleUrls: ['./file-uploading-dialog.component.css']
})
@@ -55,27 +46,30 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-upload', 'assets/ng2-alfresco-upload');
}
cd.detach();
}
ngOnInit() {
if (this.uploadService.filesUpload$) {
this.listSubscription = this.uploadService.filesUpload$.subscribe((fileList: FileModel[]) => {
this.filesUploadingList = fileList;
if (this.filesUploadingList.length > 0) {
this.isDialogActive = true;
this.cd.detectChanges();
}
});
}
if (this.uploadService.totalCompleted$) {
this.counterSubscription = this.uploadService.totalCompleted$.subscribe((total: number) => {
this.totalCompleted = total;
if (this.totalCompleted > 1) {
this.totalCompletedMsg = 'FILE_UPLOAD.MESSAGES.COMPLETED';
}
this.listSubscription = this.uploadService.queueChanged.subscribe((fileList: FileModel[]) => {
this.filesUploadingList = fileList;
if (this.filesUploadingList.length > 0) {
this.isDialogActive = true;
this.cd.detectChanges();
});
}
}
});
this.counterSubscription = this.uploadService.fileUploadComplete.subscribe((e: FileUploadCompleteEvent) => {
this.totalCompleted = e.totalComplete;
if (this.totalCompleted > 1) {
this.totalCompletedMsg = 'FILE_UPLOAD.MESSAGES.COMPLETED';
}
this.cd.detectChanges();
});
this.uploadService.fileUpload.subscribe(e => {
console.log(e);
this.cd.detectChanges();
});
}
/**
@@ -83,6 +77,7 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
*/
toggleVisible(): void {
this.isDialogActive = !this.isDialogActive;
this.cd.detectChanges();
}
/**
@@ -90,11 +85,11 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
*/
toggleMinimized(): void {
this.isDialogMinimized = !this.isDialogMinimized;
this.cd.detectChanges();
}
ngOnDestroy() {
this.listSubscription.unsubscribe();
this.counterSubscription.unsubscribe();
this.cd.detach();
}
}

View File

@@ -3,8 +3,8 @@
border: 0px;
}
.cursor {
cursor: pointer;
.center {
text-align: center;
}
.body-dialog-header {
@@ -42,62 +42,40 @@
width: 100%;
}
:host .truncate {
margin-left: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.cancel-upload-button {
cursor: pointer;
}
:host .mdl-progress {
width: 150px;
}
@media (max-device-width: 360px) {
.truncate {
max-width: 50px;
margin-left: 0px;
}
}
@media (max-device-width: 568px) {
.truncate {
width: 60px;
}
.mdl-progress {
width: 60px;
}
}
@media (max-width: 740px) {
.truncate {
max-width: 80px;
}
.mdl-progress {
max-width: 70px;
}
.size-column {
display: none;
}
}
@media (min-width: 740px) {
.truncate {
width: 249px;
}
.size-column {
display: table-cell;
}
.file-progress-spinner {
height: 24px;
width: 100%;
text-align: center;
}
.full-width {
width: 100%;
}
.no-width {
width: 0%;
.ellipsis-cell .cell-container {
height: 1em;
}
/* visible content */
.ellipsis-cell .cell-value {
display: block;
position: absolute;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1em; /* for vertical align of text */
}
/* cell stretching content */
.ellipsis-cell > div:after {
content: attr(title);
overflow: hidden;
height: 0;
display: block;
}

View File

@@ -6,32 +6,37 @@
</div>
<table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
<tr>
<th class="mdl-data-table__cell--non-numeric">{{'FILE_UPLOAD.FILE_INFO.NAME' | translate}}</th>
<th class="mdl-data-table__cell--non-numeric">{{'FILE_UPLOAD.FILE_INFO.PROGRESS' | translate}}</th>
<th class="mdl-data-table__cell--non-numeric mdl-cell--hide-phone size-column">{{'FILE_UPLOAD.FILE_INFO.SIZE' | translate}}</th>
<th class="mdl-data-table__cell--non-numeric">{{'FILE_UPLOAD.FILE_INFO.ACTION' | translate}}</th>
<th class="mdl-data-table__cell--non-numeric full-width">{{'ADF_FILE_UPLOAD.FILE_LIST.NAME' | translate}}</th>
<th class="mdl-data-table__cell center">{{'ADF_FILE_UPLOAD.FILE_LIST.PROGRESS' | translate}}</th>
<th class="mdl-data-table__cell mdl-cell--hide-phone size-column center">{{'ADF_FILE_UPLOAD.FILE_LIST.SIZE' | translate}}</th>
<th class="mdl-data-table__cell center">{{'ADF_FILE_UPLOAD.FILE_LIST.ACTION' | translate}}</th>
</tr>
<tr *ngFor="let file of files" tabindex="0">
<td class="mdl-data-table__cell--non-numeric" attr.data-automation-id="dialog_{{file.name}}">
<div class="truncate">{{file.name}}</div>
</td>
<td class="mdl-data-table__cell--non-numeric">
<div class="mdl-progress mdl-js-progress is-upgraded" id="{{file.id}}">
<div class="progressbar bar bar1" attr.data-automation-id="dialog_progress_{{file.name}}" [style.width.%]="file.progress.percent"></div>
<div class="bufferbar bar bar2" class="full-width"></div>
<div class="auxbar bar bar3" class="no-width"></div>
<td class="mdl-data-table__cell--non-numeric full-width ellipsis-cell" attr.data-automation-id="dialog_{{file.name}}">
<div class="cell-container">
<div class="cell-value" [title]="file.name">{{file.name}}</div>
</div>
</td>
<td class="mdl-data-table__cell--non-numeric mdl-cell--hide-phone size-column" attr.data-automation-id="{{file.name}}_filesize">{{file.size}}</td>
<td class="mdl-data-table__cell--non-numeric">
<span *ngIf="file.done && !file.abort">
<i data-automation-id="done_icon" class="material-icons action-icons">done</i>
<td class="mdl-data-table__cell center">
<md-icon *ngIf="file.status === FileUploadStatus.Error || file.status === FileUploadStatus.Aborted">error_outline</md-icon>
<md-icon *ngIf="file.status === FileUploadStatus.Cancelled">block</md-icon>
<ng-container *ngIf="file.status === FileUploadStatus.Progress">
<md-progress-spinner
class="file-progress-spinner"
[mode]="'determinate'"
[value]="file.progress.percent">
</md-progress-spinner>
</ng-container>
</td>
<td class="mdl-data-table__cell mdl-cell--hide-phone size-column center" attr.data-automation-id="{{file.name}}_filesize">
{{ file.size | adfFileSize }}
</td>
<td class="mdl-data-table__cell center">
<span *ngIf="file.status === FileUploadStatus.Complete">
<md-icon>done</md-icon>
</span>
<span *ngIf="file.uploading" (click)="cancelFileUpload(file)" class="cursor" tabindex="0">
<i data-automation-id="abort_cancel_upload" class="material-icons action-icons">remove_circle_outline</i>
</span>
<span *ngIf="file.abort">
<i class="material-icons action-icons" data-automation-id="upload_stopped" tabindex="0">remove_circle</i>
<span *ngIf="file.status === FileUploadStatus.Progress" (click)="cancelFileUpload(file)" tabindex="0" class="cancel-upload-button">
<md-icon>remove_circle_outline</md-icon>
</span>
</td>
</tr>

View File

@@ -16,18 +16,9 @@
*/
import { Component, Input } from '@angular/core';
import { FileModel } from '../models/file.model';
import { FileModel, FileUploadStatus } from '../models/file.model';
import { UploadService } from '../services/upload.service';
/**
* <alfresco-file-uploading-list [files]="files"></alfresco-file-uploading-list>
*
* This component show a list of the uploading files contained in the filesUploadingList.
*
* @InputParam {FileModel[]} filesUploadingList - list of the uploading files .
*
*
* @returns {FileUploadingListComponent} .
*/
@Component({
selector: 'alfresco-file-uploading-list',
templateUrl: './file-uploading-list.component.html',
@@ -35,9 +26,14 @@ import { FileModel } from '../models/file.model';
})
export class FileUploadingListComponent {
FileUploadStatus = FileUploadStatus;
@Input()
files: FileModel[];
constructor(private uploadService: UploadService) {
}
/**
* Cancel file upload
*
@@ -46,9 +42,7 @@ export class FileUploadingListComponent {
* @memberOf FileUploadingListComponent
*/
cancelFileUpload(file: FileModel): void {
if (file) {
file.emitAbort();
}
this.uploadService.cancelUpload(file);
}
/**
@@ -58,21 +52,20 @@ export class FileUploadingListComponent {
if (event) {
event.preventDefault();
}
this.files.forEach((uploadingFileModel: FileModel) => {
uploadingFileModel.emitAbort();
});
this.uploadService.cancelUpload(...this.files);
}
/**
* Verify if all the files are in state done or abort
* @returns {boolean} - false if there is a file in progress
* Check if all the files are not in the Progress state.
* @returns {boolean} - false if there is at least one file in Progress
*/
isUploadCompleted(): boolean {
let isPending = false;
let isAllCompleted = true;
for (let i = 0; i < this.files.length && !isPending; i++) {
let file = this.files[i];
if (!file.done && !file.abort) {
if (file.status === FileUploadStatus.Progress) {
isPending = true;
isAllCompleted = false;
}

View File

@@ -15,10 +15,10 @@
* limitations under the License.
*/
import { DebugElement, SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { UploadButtonComponent } from './upload-button.component';
import { DebugElement, SimpleChange } from '@angular/core';
import { CoreModule, AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { CoreModule, AlfrescoTranslationService, AlfrescoContentService} from 'ng2-alfresco-core';
import { TranslationMock } from '../assets/translation.service.mock';
import { UploadService } from '../services/upload.service';
import { Observable } from 'rxjs/Rx';
@@ -33,27 +33,6 @@ describe('UploadButtonComponent', () => {
target: {value: 'fake-name-1'}
};
let fakeResolveRest = {
entry: {
isFile: false,
isFolder: true,
name: 'fake-folder1'
}
};
let fakeResolvePromise = new Promise(function (resolve, reject) {
resolve(fakeResolveRest);
});
let fakeRejectRest = {
response: {
body: {
error: {
statusCode: 409
}
}
}
};
let fakeFolderNodeWithoutPermission = {
allowableOperations: [
'update'
@@ -73,15 +52,12 @@ describe('UploadButtonComponent', () => {
nodeType: 'cm:folder'
};
let fakeRejectPromise = new Promise(function (resolve, reject) {
reject(fakeRejectRest);
});
let component: UploadButtonComponent;
let fixture: ComponentFixture<UploadButtonComponent>;
let debug: DebugElement;
let element: HTMLElement;
let uploadService: UploadService;
let contentService: AlfrescoContentService;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -93,7 +69,6 @@ describe('UploadButtonComponent', () => {
],
providers: [
UploadService,
NotificationService,
{provide: AlfrescoTranslationService, useClass: TranslationMock}
]
}).compileComponents();
@@ -104,6 +79,7 @@ describe('UploadButtonComponent', () => {
fixture = TestBed.createComponent(UploadButtonComponent);
uploadService = TestBed.get(UploadService);
contentService = TestBed.get(AlfrescoContentService);
debug = fixture.debugElement;
element = fixture.nativeElement;
@@ -141,7 +117,7 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.disableWithNoPermission = false;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithoutPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithoutPermission));
fixture.detectChanges();
@@ -160,7 +136,7 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.disableWithNoPermission = true;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithoutPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithoutPermission));
component.onFilesAdded(fakeEvent);
let compiled = fixture.debugElement.nativeElement;
@@ -173,7 +149,7 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.disableWithNoPermission = true;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
component.onFilesAdded(fakeEvent);
@@ -187,7 +163,7 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.disableWithNoPermission = false;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
component.onFilesAdded(fakeEvent);
@@ -202,7 +178,7 @@ describe('UploadButtonComponent', () => {
component.currentFolderPath = '/root-fake-/sites-fake/folder-fake';
component.onSuccess = null;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
uploadService.uploadFilesInTheQueue = jasmine.createSpy('uploadFilesInTheQueue');
@@ -218,7 +194,7 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.onSuccess = null;
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
uploadService.uploadFilesInTheQueue = jasmine.createSpy('uploadFilesInTheQueue');
@@ -233,11 +209,12 @@ describe('UploadButtonComponent', () => {
component.rootFolderId = '-my-';
component.currentFolderPath = '/fake-root-path';
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(uploadService, 'callApiCreateFolder').and.returnValue(fakeResolvePromise);
spyOn(contentService, 'createFolder').and.returnValue(Observable.of(true));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
fixture.detectChanges();
component.onSuccess.subscribe(e => {
expect(e.value).toEqual('File uploaded');
done();
@@ -245,22 +222,21 @@ describe('UploadButtonComponent', () => {
spyOn(component, 'uploadFiles').and.callFake(() => {
component.onSuccess.emit({
value: 'File uploaded'
}
);
value: 'File uploaded'
});
});
component.onDirectoryAdded(fakeEvent);
});
it('should emit an onError event when the folder already exist', (done) => {
component.rootFolderId = '-my-';
spyOn(uploadService, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
spyOn(uploadService, 'callApiCreateFolder').and.returnValue(fakeRejectPromise);
spyOn(contentService, 'createFolder').and.returnValue(Observable.throw(new Error('')));
spyOn(component, 'getFolderNode').and.returnValue(Observable.of(fakeFolderNodeWithPermission));
component.ngOnChanges({ rootFolderId: new SimpleChange(null, component.rootFolderId, true) });
component.onError.subscribe(e => {
expect(e.value).toEqual('FILE_UPLOAD.MESSAGES.FOLDER_ALREADY_EXIST');
expect(e.value).toEqual('Error');
done();
});

View File

@@ -16,36 +16,15 @@
*/
import { Component, ElementRef, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { Subject } from 'rxjs/Rx';
import { AlfrescoTranslationService, LogService, NotificationService, AlfrescoSettingsService } from 'ng2-alfresco-core';
import { Observable, Subject } from 'rxjs/Rx';
import { AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, LogService, NotificationService, AlfrescoSettingsService } from 'ng2-alfresco-core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { UploadService } from '../services/upload.service';
import { FileModel } from '../models/file.model';
import { PermissionModel } from '../models/permissions.model';
declare let componentHandler: any;
const ERROR_FOLDER_ALREADY_EXIST = 409;
/**
* <alfresco-upload-button [showNotificationBar]="boolean"
* [uploadFolders]="boolean"
* [multipleFiles]="boolean"
* [acceptedFilesType]="string"
* (onSuccess)="customMethod($event)">
* </alfresco-upload-button>
*
* This component, provide a set of buttons to upload files to alfresco.
*
* @InputParam {boolean} [true] showNotificationBar - hide/show notification bar.
* @InputParam {boolean} [false] versioning - true to indicate that a major version should be created
* @InputParam {boolean} [false] uploadFolders - allow/disallow upload folders (only for chrome).
* @InputParam {boolean} [false] multipleFiles - allow/disallow multiple files.
* @InputParam {string} [*] acceptedFilesType - array of allowed file extensions.
* @InputParam {boolean} [false] versioning - true to indicate that a major version should be created
* @Output - onSuccess - The event is emitted when the file is uploaded
*
* @returns {UploadButtonComponent} .
*/
@Component({
selector: 'alfresco-upload-button',
templateUrl: './upload-button.component.html',
@@ -106,7 +85,9 @@ export class UploadButtonComponent implements OnInit, OnChanges {
private translateService: AlfrescoTranslationService,
private logService: LogService,
private notificationService: NotificationService,
private settingsService: AlfrescoSettingsService) {
private settingsService: AlfrescoSettingsService,
private apiService: AlfrescoApiService,
private contentService: AlfrescoContentService) {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-upload', 'assets/ng2-alfresco-upload');
}
@@ -172,9 +153,9 @@ export class UploadButtonComponent implements OnInit, OnChanges {
let directoryName = this.getDirectoryName(directoryPath);
let absolutePath = this.currentFolderPath + this.getDirectoryPath(directoryPath);
this.uploadService.createFolder(absolutePath, directoryName, this.rootFolderId)
this.contentService.createFolder(absolutePath, directoryName, this.rootFolderId)
.subscribe(
res => {
_ => {
let relativeDir = this.currentFolderPath + '/' + directoryPath;
this.uploadFiles(relativeDir, filesDir);
},
@@ -269,10 +250,8 @@ export class UploadButtonComponent implements OnInit, OnChanges {
messageTranslate = this.translateService.get('FILE_UPLOAD.MESSAGES.PROGRESS');
actionTranslate = this.translateService.get('FILE_UPLOAD.ACTION.UNDO');
this.notificationService.openSnackMessageAction(messageTranslate.value, actionTranslate.value, 3000).afterDismissed().subscribe(() => {
latestFilesAdded.forEach((uploadingFileModel: FileModel) => {
uploadingFileModel.emitAbort();
});
this.notificationService.openSnackMessageAction(messageTranslate.value, actionTranslate.value, 3000).onAction().subscribe(() => {
this.uploadService.cancelUpload(...latestFilesAdded);
});
}
@@ -282,11 +261,12 @@ export class UploadButtonComponent implements OnInit, OnChanges {
* @returns {string}
*/
private getErrorMessage(response: any): string {
if (response.body && response.body.error.statusCode === ERROR_FOLDER_ALREADY_EXIST) {
if (response && response.body && response.body.error.statusCode === ERROR_FOLDER_ALREADY_EXIST) {
let errorMessage: any;
errorMessage = this.translateService.get('FILE_UPLOAD.MESSAGES.FOLDER_ALREADY_EXIST');
return errorMessage.value;
}
return 'Error';
}
/**
@@ -314,17 +294,30 @@ export class UploadButtonComponent implements OnInit, OnChanges {
checkPermission() {
if (this.rootFolderId) {
this.uploadService.getFolderNode(this.rootFolderId).subscribe(
(res) => {
this.permissionValue.next(this.hasCreatePermission(res));
},
(error) => {
this.onError.emit(error);
}
this.getFolderNode(this.rootFolderId).subscribe(
res => this.permissionValue.next(this.hasCreatePermission(res)),
error => this.onError.emit(error)
);
}
}
getFolderNode(nodeId: string): Observable<MinimalNodeEntryEntity> {
let opts: any = {
includeSource: true,
include: ['allowableOperations']
};
return Observable.fromPromise(this.apiService.getInstance().nodes.getNodeInfo(nodeId, opts))
.catch(err => this.handleError(err));
}
private handleError(error: Response) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
this.logService.error(error);
return Observable.throw(error || 'Server error');
}
private hasCreatePermission(node: any): boolean {
if (this.hasPermissions(node)) {
return node.allowableOperations.find(permision => permision === 'create') ? true : false;

View File

@@ -17,7 +17,7 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { EventEmitter, DebugElement } from '@angular/core';
import { AlfrescoTranslationService, CoreModule, LogService, LogServiceMock, NotificationService } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, CoreModule, LogService, LogServiceMock } from 'ng2-alfresco-core';
import { UploadDragAreaComponent } from './upload-drag-area.component';
import { FileDraggableDirective } from '../directives/file-draggable.directive';
@@ -45,7 +45,6 @@ describe('UploadDragAreaComponent', () => {
],
providers: [
UploadService,
NotificationService,
{ provide: AlfrescoTranslationService, useClass: TranslationMock },
{ provide: LogService, useClass: LogServiceMock }
]
@@ -147,92 +146,4 @@ describe('UploadDragAreaComponent', () => {
expect(uploadService.uploadFilesInTheQueue)
.toHaveBeenCalledWith('-my-', '/root-fake-/sites-fake/document-library-fake/folder-fake/', null);
});
xit('should throws an exception and show it in the notification bar when the folder already exist', done => {
component.currentFolderPath = '/root-fake-/sites-fake/folder-fake';
component.showNotificationBar = true;
fixture.detectChanges();
let fakeRest = {
response: {
body: {
error: {
statusCode: 409
}
}
}
};
let fakePromise = new Promise(function (resolve, reject) {
reject(fakeRest);
});
spyOn(uploadService, 'callApiCreateFolder').and.returnValue(fakePromise);
spyOn(component, 'showErrorNotificationBar').and.callFake( () => {
expect(component.showErrorNotificationBar).toHaveBeenCalledWith('FILE_UPLOAD.MESSAGES.FOLDER_ALREADY_EXIST');
done();
});
let folderEntry = {
fullPath: '/folder-duplicate-fake',
isDirectory: true,
isFile: false,
name: 'folder-duplicate-fake'
};
component.onFolderEntityDropped(folderEntry);
});
it('should create a folder and call onFilesEntityDropped with the file inside the folder', done => {
component.currentFolderPath = '/root-fake-/sites-fake/document-library-fake';
component.onSuccess = new EventEmitter();
fixture.detectChanges();
let itemEntity = {
fullPath: '/folder-fake/file-fake.png',
isDirectory: false,
isFile: true,
name: 'file-fake.png',
file: (callbackFile) => {
let fileFake = new File(['fakefake'], 'file-fake.png', {type: 'image/png'});
callbackFile(fileFake);
}
};
let fakeRest = {
entry: {
isFile: false,
isFolder: true,
name: 'folder-fake'
}
};
let fakePromise = new Promise(function (resolve, reject) {
resolve(fakeRest);
});
spyOn(uploadService, 'callApiCreateFolder').and.returnValue(fakePromise);
spyOn(component, 'onFilesEntityDropped').and.callFake( () => {
expect(component.onFilesEntityDropped).toHaveBeenCalledWith(itemEntity);
});
spyOn(component, 'showUndoNotificationBar').and.callFake( () => {
expect(component.showUndoNotificationBar).toHaveBeenCalled();
done();
});
let folderEntry = {
fullPath: '/folder-fake',
isDirectory: true,
isFile: false,
name: 'folder-fake',
createReader: () => {
return {
readEntries: (callback) => {
let entries = [itemEntity, itemEntity];
callback(entries);
}
};
}
};
component.onFolderEntityDropped(folderEntry);
});
});

View File

@@ -16,21 +16,12 @@
*/
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AlfrescoTranslationService, LogService, NotificationService } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, AlfrescoContentService, LogService, NotificationService } from 'ng2-alfresco-core';
import { UploadService } from '../services/upload.service';
import { FileModel } from '../models/file.model';
const ERROR_FOLDER_ALREADY_EXIST = 409;
/**
* <alfresco-upload-drag-area (onSuccess)="customMethod($event)></alfresco-upload-drag-area>
*
* This component, provide a drag and drop are to upload files to alfresco.
*
* @Output - onSuccess - The event is emitted when the file is uploaded
*
* @returns {UploadDragAreaComponent} .
*/
@Component({
selector: 'alfresco-upload-drag-area',
templateUrl: './upload-drag-area.component.html',
@@ -61,7 +52,8 @@ export class UploadDragAreaComponent {
constructor(private uploadService: UploadService,
private translateService: AlfrescoTranslationService,
private logService: LogService,
private notificationService: NotificationService) {
private notificationService: NotificationService,
private contentService: AlfrescoContentService) {
if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-upload', 'assets/ng2-alfresco-upload');
}
@@ -129,7 +121,7 @@ export class UploadDragAreaComponent {
let relativePath = folder.fullPath.replace(folder.name, '');
relativePath = this.currentFolderPath + relativePath;
this.uploadService.createFolder(relativePath, folder.name, this.rootFolderId)
this.contentService.createFolder(relativePath, folder.name, this.rootFolderId)
.subscribe(
message => {
this.onSuccess.emit({
@@ -186,9 +178,7 @@ export class UploadDragAreaComponent {
actionTranslate = this.translateService.get('FILE_UPLOAD.ACTION.UNDO');
this.notificationService.openSnackMessageAction(messageTranslate.value, actionTranslate.value, 3000).onAction().subscribe(() => {
latestFilesAdded.forEach((uploadingFileModel: FileModel) => {
uploadingFileModel.emitAbort();
});
this.uploadService.cancelUpload(...latestFilesAdded);
});
}

View File

@@ -17,17 +17,6 @@
import { Directive, EventEmitter, Input, Output, OnInit, OnDestroy, ElementRef, NgZone } from '@angular/core';
/**
* [file-draggable]
*
* This directive, provide a drag and drop area for files and folders.
*
* @OutputEvent {EventEmitter} onFilesDropped(File)- event fired fot each file dropped
* in the drag and drop area.
*
*
* @returns {FileDraggableDirective} .
*/
@Directive({
selector: '[file-draggable]'
})

View File

@@ -0,0 +1,36 @@
/*!
* @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, FileUploadStatus } from '../models/file.model';
export class FileUploadEvent {
constructor(
public readonly file: FileModel,
public readonly status: FileUploadStatus = FileUploadStatus.Pending,
public readonly error: any = null) {
}
}
export class FileUploadCompleteEvent extends FileUploadEvent {
constructor(file: FileModel, public totalComplete: number = 0) {
super(file, FileUploadStatus.Complete);
}
}

View File

@@ -1,25 +1,27 @@
{
"FILE_UPLOAD": {
"BUTTON": {
"UPLOAD_FILE": "Upload file",
"UPLOAD_FOLDER": "Upload folder",
"CANCEL_ALL": "Cancell all"
"ADF_FILE_UPLOAD": {
"FILE_LIST": {
"NAME": "Name",
"PROGRESS": "Progress",
"SIZE": "Size",
"ACTION": "Action"
}
},
"MESSAGES": {
"SINGLE_COMPLETED": "upload complete",
"COMPLETED": "uploads complete",
"PROGRESS": "Upload in progress...",
"FOLDER_ALREADY_EXIST": "The folder {0} already exist",
"FOLDER_NOT_SUPPORTED": "Folder upload isn't supported by your browser"
},
"FILE_INFO": {
"NAME": "File name",
"PROGRESS": "File progress",
"SIZE": "File size",
"ACTION": "Actions"
},
"ACTION": {
"UNDO": "Undo"
"FILE_UPLOAD": {
"BUTTON": {
"UPLOAD_FILE": "Upload file",
"UPLOAD_FOLDER": "Upload folder",
"CANCEL_ALL": "Cancell all"
},
"MESSAGES": {
"SINGLE_COMPLETED": "upload complete",
"COMPLETED": "uploads complete",
"PROGRESS": "Upload in progress...",
"FOLDER_ALREADY_EXIST": "The folder {0} already exist",
"FOLDER_NOT_SUPPORTED": "Folder upload isn't supported by your browser"
},
"ACTION": {
"UNDO": "Undo"
}
}
}
}

View File

@@ -15,130 +15,58 @@
* limitations under the License.
*/
/**
*
* This object represent the status of an uploading file.
*
*
* @returns {FileModel} .
*/
export class FileModel {
id: string;
status: number;
statusText: string;
progress: Object;
name: string;
size: string;
response: string;
done: boolean = false;
error: boolean = false;
abort: boolean = false;
uploading: boolean = false;
file: File;
promiseUpload: any;
options: FileUploadOptions;
constructor(file: File, options?: FileUploadOptions) {
this.file = file;
this.options = Object.assign({}, {
newVersion: false
}, options);
this.id = this.generateId();
this.name = file.name;
this.size = this.getFileSize(file.size);
this.progress = {
loaded: 0,
total: 0,
percent: 0
};
}
setProgres(progress: any): void {
this.progress = progress;
}
/**
* Emit an event progress on the promise
*/
emitProgres(progress: any): void {
this.setProgres(progress);
this.promiseUpload.emit('progress', progress);
}
setError(): void {
this.error = true;
}
/**
* Emit an event progress on the promise
*/
emitError(): void {
this.setError();
this.promiseUpload.emit('error');
}
setUploading() {
this.uploading = true;
}
setPromiseUpload(promiseUpload: any) {
this.promiseUpload = promiseUpload;
}
/**
* Stop the uploading of the file.
*/
setAbort(): void {
if (!this.done && !this.error) {
this.abort = true;
this.uploading = false;
}
}
/**
* Emit an event abort on the promise
*/
emitAbort(): void {
this.setAbort();
this.promiseUpload.abort();
}
/**
* Update status of the file when upload finish or is ended.
*/
onFinished(status: number, statusText: string, response: string): void {
this.status = status;
this.statusText = statusText;
this.response = response;
this.done = true;
this.uploading = false;
}
/**
* Calculate the size of the file in kb,mb and gb.
*
* @param {number} sizeinbytes - size in bytes of the file.
*/
private getFileSize(sizeinbytes: number): string {
let fSExt = new Array('Bytes', 'KB', 'MB', 'GB');
let size = sizeinbytes;
let i = 0;
while (size > 900) {
size /= 1000;
i++;
}
return Math.round((Math.round(size * 100) / 100)) + ' ' + fSExt[i];
}
private generateId(): string {
return 'uploading-file-' + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export interface FileUploadProgress {
loaded: number;
total: number;
percent: number;
}
export interface FileUploadOptions {
newVersion?: boolean;
}
export enum FileUploadStatus {
Pending = 0,
Complete = 1,
Starting = 2,
Progress = 3,
Cancelled = 4,
Aborted = 5,
Error = 6
}
export class FileModel {
readonly id: string;
readonly name: string;
readonly size: number;
readonly file: File;
status: FileUploadStatus = FileUploadStatus.Pending;
progress: FileUploadProgress;
options: FileUploadOptions;
constructor(file: File, options?: FileUploadOptions) {
this.file = file;
this.id = this.generateId();
this.name = file.name;
this.size = file.size;
this.progress = {
loaded: 0,
total: 0,
percent: 0
};
this.options = Object.assign({}, {
newVersion: false
}, options);
}
private generateId(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}

View File

@@ -124,89 +124,7 @@ describe('UploadService', () => {
service.uploadFilesInTheQueue('-root-', '', emitter);
let file = service.getQueue();
file[0].emitAbort();
});
it('should make XHR error request after the xhr error is called', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toBe('Error file uploaded');
done();
});
let fileFake = new FileModel(<File>{name: 'fake-name', size: 10});
service.addToQueue(fileFake);
service.uploadFilesInTheQueue('-root-', '', emitter);
let file = service.getQueue();
file[0].emitError();
});
it('should make XHR progress request after the onprogress is called', (done) => {
let fakeProgress = {
loaded: 500,
total: 1234,
percent: 44
};
let filesFake = new FileModel(<File>{name: 'fake-name', size: 10});
service.addToQueue(filesFake);
service.filesUpload$.subscribe((file) => {
expect(file).toBeDefined();
expect(file[0]).toBeDefined();
expect(file[0].progress).toEqual(fakeProgress);
done();
});
service.uploadFilesInTheQueue('-root-', '', null);
let file = service.getQueue();
file[0].emitProgres(fakeProgress);
});
it('should make XHR done request after the folder is created', (done) => {
let fakeRest = {
entry: {
isFile: false,
isFolder: true,
name: 'fake-folder'
}
};
let fakePromise = new Promise(function (resolve, reject) {
resolve(fakeRest);
});
spyOn(service, 'callApiCreateFolder').and.returnValue(fakePromise);
let defaultPath = '';
let folderName = 'fake-folder';
service.createFolder(defaultPath, folderName).subscribe(res => {
expect(res).toEqual(fakeRest);
done();
});
});
it('should throws an exception when a folder already exist', (done) => {
let fakeRest = {
response: {
body: {
error: {
statusCode: 409
}
}
}
};
let fakePromise = new Promise(function (resolve, reject) {
reject(fakeRest);
});
spyOn(service, 'callApiCreateFolder').and.returnValue(fakePromise);
let defaultPath = '';
let folderName = 'folder-duplicate-fake';
service.createFolder(defaultPath, folderName).subscribe(
res => {
},
error => {
expect(error).toEqual(fakeRest);
done();
}
);
service.cancelUpload(...file);
});
it('If versioning is true autoRename should not be present and majorVersion should be a param', () => {

View File

@@ -16,36 +16,38 @@
*/
import { EventEmitter, Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { Observer, Observable, Subject } from 'rxjs/Rx';
import { Subject } from 'rxjs/Rx';
import { AlfrescoApiService, LogService } from 'ng2-alfresco-core';
import { FolderCreatedEvent } from '../events/folder-created.event';
import { FileModel } from '../models/file.model';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
import { FileUploadEvent, FileUploadCompleteEvent } from '../events/file.event';
import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model';
/**
*
* UploadService keep the queue of the file to upload and uploads them.
*
* @returns {UploadService} .
*/
@Injectable()
export class UploadService {
private queue: FileModel[] = [];
private filesUploadObserverProgressBar: Observer<FileModel[]>;
private totalCompletedObserver: Observer<number>;
private cache: { [key: string]: any } = {};
private totalComplete: number = 0;
totalCompleted: number = 0;
filesUpload$: Observable<FileModel[]>;
totalCompleted$: Observable<any>;
folderCreated: Subject<FolderCreatedEvent> = new Subject<FolderCreatedEvent>();
queueChanged: Subject<FileModel[]> = new Subject<FileModel[]>();
fileUpload: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadStarting: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadCancelled: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadProgress: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadAborted: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadError: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>();
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
this.filesUpload$ = new Observable<FileModel[]>(observer => this.filesUploadObserverProgressBar = observer).share();
this.totalCompleted$ = new Observable<number>(observer => this.totalCompletedObserver = observer).share();
}
/**
* Returns the file Queue
*
* @return {FileModel[]} - files in the upload queue.
*/
getQueue(): FileModel[] {
return this.queue;
}
/**
@@ -59,9 +61,7 @@ export class UploadService {
addToQueue(...files: FileModel[]): FileModel[] {
const allowedFiles = files.filter(f => !f.name.startsWith('.'));
this.queue = this.queue.concat(allowedFiles);
if (this.filesUploadObserverProgressBar) {
this.filesUploadObserverProgressBar.next(this.queue);
}
this.queueChanged.next(this.queue);
return allowedFiles;
}
@@ -69,125 +69,142 @@ export class UploadService {
* Pick all the files in the queue that are not been uploaded yet and upload it into the directory folder.
*/
uploadFilesInTheQueue(rootId: string, directory: string, elementEmit: EventEmitter<any>): void {
let filesToUpload = this.queue.filter((file) => {
return !file.uploading && !file.done && !file.abort && !file.error;
});
const files = this.getFilesToUpload();
filesToUpload.forEach((uploadingFileModel: FileModel) => {
uploadingFileModel.setUploading();
files.forEach((file: FileModel) => {
this.onUploadStarting(file);
const opts: any = {
renditions: 'doclib'
};
if (uploadingFileModel.options.newVersion === true) {
if (file.options.newVersion === true) {
opts.overwrite = true;
opts.majorVersion = true;
} else {
opts.autoRename = true;
}
let promiseUpload = this.apiService.getInstance().upload.uploadFile(uploadingFileModel.file, directory, rootId, null, opts)
.on('progress', (progress: any) => {
uploadingFileModel.setProgres(progress);
this.updateFileListStream(this.queue);
})
.on('abort', () => {
uploadingFileModel.setAbort();
elementEmit.emit({
value: 'File aborted'
});
})
.on('error', () => {
uploadingFileModel.setError();
elementEmit.emit({
value: 'Error file uploaded'
});
})
.on('success', (data: any) => {
elementEmit.emit({
value: data
});
uploadingFileModel.onFinished(
data.status,
data.statusText,
data.response
);
this.updateFileListStream(this.queue);
if (!uploadingFileModel.abort && !uploadingFileModel.error) {
this.updateFileCounterStream(++this.totalCompleted);
}
const promise = this.apiService.getInstance().upload.uploadFile(file.file, directory, rootId, null, opts);
promise.on('progress', (progress: FileUploadProgress) => {
this.onUploadProgress(file, progress);
})
.on('abort', () => {
this.onUploadAborted(file);
elementEmit.emit({
value: 'File aborted'
});
})
.on('error', err => {
this.onUploadError(file, err);
elementEmit.emit({
value: 'Error file uploaded'
});
})
.on('success', data => {
this.onUploadComplete(file);
elementEmit.emit({
value: data
});
})
.catch((err) => {
this.onUploadError(file, err);
});
uploadingFileModel.setPromiseUpload(promiseUpload);
this.cache[file.id] = promise;
});
}
/**
* Return all the files in the uploading queue.
*
* @return {FileModel[]} - files in the upload queue.
*/
getQueue(): FileModel[] {
return this.queue;
cancelUpload(...files: FileModel[]) {
files.forEach(file => {
file.status = FileUploadStatus.Cancelled;
const promise = this.cache[file.id];
if (promise) {
promise.abort();
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Cancelled);
this.fileUpload.next(event);
this.fileUploadCancelled.next(event);
});
}
/**
* Create a folder
* @param name - the folder name
*/
createFolder(relativePath: string, name: string, parentId?: string): Observable<MinimalNodeEntity> {
return Observable.fromPromise(this.callApiCreateFolder(relativePath, name, parentId))
.do(data => {
this.folderCreated.next({
relativePath: relativePath,
name: name,
parentId: parentId,
node: data
});
})
.catch(err => this.handleError(err));
}
callApiCreateFolder(relativePath: string, name: string, parentId?: string): Promise<MinimalNodeEntity> {
return this.apiService.getInstance().nodes.createFolder(name, relativePath, parentId);
}
/**
* Throw the error
* @param error
* @returns {ErrorObservable}
*/
private handleError(error: Response) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
this.logService.error(error);
return Observable.throw(error || 'Server error');
}
private updateFileListStream(fileList: FileModel[]) {
if (this.filesUploadObserverProgressBar) {
this.filesUploadObserverProgressBar.next(fileList);
private onUploadStarting(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Starting;
const event = new FileUploadEvent(file, FileUploadStatus.Starting);
this.fileUpload.next(event);
this.fileUploadStarting.next(event);
}
}
updateFileCounterStream(total: number) {
if (this.totalCompletedObserver) {
this.totalCompletedObserver.next(total);
private onUploadProgress(file: FileModel, progress: FileUploadProgress): void {
if (file) {
file.progress = progress;
file.status = FileUploadStatus.Progress;
const event = new FileUploadEvent(file, FileUploadStatus.Progress);
this.fileUpload.next(event);
this.fileUploadProgress.next(event);
this.queueChanged.next(this.queue);
}
}
getFolderNode(nodeId: string): Observable<MinimalNodeEntryEntity> {
let opts: any = {
includeSource: true,
include: ['allowableOperations']
};
private onUploadError(file: FileModel, error: any): void {
if (file) {
file.status = FileUploadStatus.Error;
return Observable.fromPromise(this.apiService.getInstance().nodes.getNodeInfo(nodeId, opts))
.map((response: any) => {
return response;
})
.catch(err => this.handleError(err));
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Error, error);
this.fileUpload.next(event);
this.fileUploadError.next(event);
}
}
private onUploadComplete(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Complete;
this.totalComplete++;
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadCompleteEvent(file, this.totalComplete);
this.fileUpload.next(event);
this.fileUploadComplete.next(event);
this.queueChanged.next(this.queue);
}
}
private onUploadAborted(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Aborted;
const promise = this.cache[file.id];
if (promise) {
delete this.cache[file.id];
}
const event = new FileUploadEvent(file, FileUploadStatus.Aborted);
this.fileUpload.next(event);
this.fileUploadAborted.next(event);
}
}
private getFilesToUpload(): FileModel[] {
let filesToUpload = this.queue.filter(file => {
return file.status === FileUploadStatus.Pending;
});
return filesToUpload;
}
}