[ADF-643] upload enhancements (#1949)

* rework folder uploading

- flatterns hierarchy on folder upload
- performs a single traversal for the entire folder heirarchy and ends with a comple file list
- allows now dropping folders on existing folders
- overall code improvements

* fix unit tests

* readme updates

* clean old and unused code

* code cleanup

* limit concurrent uploads

* update code as per review

* fix upload button for Safari

* fixes for Safari

- Safari compatibility
- code updates based on review

* fix code

* fix unit tests
This commit is contained in:
Denys Vuika
2017-06-11 12:02:05 +01:00
committed by Eugenio Romano
parent e7a1f46ac8
commit a02ba4ad71
16 changed files with 393 additions and 631 deletions

View File

@@ -46,11 +46,10 @@
[contextMenuActions]="true" [contextMenuActions]="true"
[contentActions]="true" [contentActions]="true"
[allowDropFiles]="true" [allowDropFiles]="true"
[sorting]="['name', 'desc']"
(error)="onNavigationError($event)" (error)="onNavigationError($event)"
(success)="resetError()" (success)="resetError()"
(preview)="showFile($event)" (preview)="showFile($event)"
(permissionError)="onPermissionsFailed($event)"> (permissionError)="handlePermissionError($event)">
<data-columns> <data-columns>
<data-column key="$thumbnail" type="image" [sortable]="false"></data-column> <data-column key="$thumbnail" type="image" [sortable]="false"></data-column>
<data-column <data-column
@@ -100,22 +99,12 @@
<content-actions> <content-actions>
<!-- folder actions --> <!-- folder actions -->
<content-action
target="folder"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.SYSTEM_1' | translate}}"
handler="system1">
</content-action>
<content-action
target="folder"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.CUSTOM' | translate}}"
(execute)="myFolderAction1($event)">
</content-action>
<content-action <content-action
target="folder" target="folder"
permission="delete" permission="delete"
[disableWithNoPermission]="true" [disableWithNoPermission]="true"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}" title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
(permissionEvent)="onPermissionsFailed($event)" (permissionEvent)="handlePermissionError($event)"
handler="delete"> handler="delete">
</content-action> </content-action>
<!-- document actions --> <!-- document actions -->
@@ -124,29 +113,14 @@
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}" title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}"
handler="download"> handler="download">
</content-action> </content-action>
<content-action
target="document"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.SYSTEM_2' | translate}}"
handler="system2">
</content-action>
<content-action
target="document"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.CUSTOM' | translate}}"
(execute)="myCustomAction1($event)">
</content-action>
<content-action <content-action
target="document" target="document"
permission="delete" permission="delete"
[disableWithNoPermission]="true" [disableWithNoPermission]="true"
(permissionEvent)="onPermissionsFailed($event)" (permissionEvent)="handlePermissionError($event)"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}" title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
handler="delete"> handler="delete">
</content-action> </content-action>
<content-action
target="folder"
title="Activiti: View Form"
(execute)="viewActivitiForm($event)">
</content-action>
</content-actions> </content-actions>
</alfresco-document-list> </alfresco-document-list>
</alfresco-upload-drag-area> </alfresco-upload-drag-area>
@@ -195,7 +169,7 @@
[uploadFolders]="folderUpload" [uploadFolders]="folderUpload"
[versioning]="versioning" [versioning]="versioning"
[disableWithNoPermission]="disableWithNoPermission" [disableWithNoPermission]="disableWithNoPermission"
(permissionEvent)="onUploadPermissionFailed($event)"> (permissionEvent)="handlePermissionError($event)">
</alfresco-upload-button> </alfresco-upload-button>
</div> </div>
<div *ngIf="acceptedFilesTypeShow"> <div *ngIf="acceptedFilesTypeShow">
@@ -209,7 +183,7 @@
[uploadFolders]="folderUpload" [uploadFolders]="folderUpload"
[versioning]="versioning" [versioning]="versioning"
[disableWithNoPermission]="disableWithNoPermission" [disableWithNoPermission]="disableWithNoPermission"
(permissionEvent)="onUploadPermissionFailed($event)"> (permissionEvent)="handlePermissionError($event)">
</alfresco-upload-button> </alfresco-upload-button>
</div> </div>
<section> <section>

View File

@@ -15,13 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, Input, OnInit, AfterViewInit, Optional, ViewChild, ChangeDetectorRef } from '@angular/core'; import { Component, Input, OnInit, Optional, ViewChild, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { MdDialog } from '@angular/material'; import { MdDialog } from '@angular/material';
import { AlfrescoAuthenticationService, AlfrescoContentService, FolderCreatedEvent, LogService, NotificationService } from 'ng2-alfresco-core'; import { AlfrescoContentService, FolderCreatedEvent, NotificationService } from 'ng2-alfresco-core';
import { DocumentActionsService, DocumentListComponent, ContentActionHandler, DocumentActionModel, FolderActionModel } from 'ng2-alfresco-documentlist'; import { DocumentListComponent } from 'ng2-alfresco-documentlist';
import { FormService } from 'ng2-activiti-form'; import { UploadService, FileUploadCompleteEvent } from 'ng2-alfresco-upload';
import { UploadService, UploadButtonComponent, UploadDragAreaComponent } from 'ng2-alfresco-upload';
import { CreateFolderDialog } from '../../dialogs/create-folder.dialog'; import { CreateFolderDialog } from '../../dialogs/create-folder.dialog';
@@ -30,7 +29,7 @@ import { CreateFolderDialog } from '../../dialogs/create-folder.dialog';
templateUrl: './files.component.html', templateUrl: './files.component.html',
styleUrls: ['./files.component.css'] styleUrls: ['./files.component.css']
}) })
export class FilesComponent implements OnInit, AfterViewInit { export class FilesComponent implements OnInit {
// The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root- // The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root-
currentFolderId: string = '-my-'; currentFolderId: string = '-my-';
@@ -38,7 +37,7 @@ export class FilesComponent implements OnInit, AfterViewInit {
fileNodeId: any; fileNodeId: any;
fileShowed: boolean = false; fileShowed: boolean = false;
useCustomToolbar = false; useCustomToolbar = true;
@Input() @Input()
multipleFileUpload: boolean = false; multipleFileUpload: boolean = false;
@@ -64,36 +63,12 @@ export class FilesComponent implements OnInit, AfterViewInit {
@ViewChild(DocumentListComponent) @ViewChild(DocumentListComponent)
documentList: DocumentListComponent; documentList: DocumentListComponent;
@ViewChild(UploadButtonComponent) constructor(private changeDetector: ChangeDetectorRef,
uploadButton: UploadButtonComponent;
@ViewChild(UploadDragAreaComponent)
uploadDragArea: UploadDragAreaComponent;
constructor(private documentActions: DocumentActionsService,
private authService: AlfrescoAuthenticationService,
private formService: FormService,
private logService: LogService,
private changeDetector: ChangeDetectorRef,
private router: Router,
private notificationService: NotificationService, private notificationService: NotificationService,
private uploadService: UploadService, private uploadService: UploadService,
private contentService: AlfrescoContentService, private contentService: AlfrescoContentService,
private dialog: MdDialog, private dialog: MdDialog,
@Optional() private route: ActivatedRoute) { @Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this));
}
myDocumentActionHandler(obj: any) {
window.alert('my custom action handler');
}
myCustomAction1(event) {
alert('Custom document action for ' + event.value.entry.name);
}
myFolderAction1(event) {
alert('Custom folder action for ' + event.value.entry.name);
} }
showFile(event) { showFile(event) {
@@ -120,36 +95,11 @@ export class FilesComponent implements OnInit, AfterViewInit {
} }
}); });
} }
if (this.authService.isBpmLoggedIn()) {
this.formService.getProcessDefinitions().subscribe(
defs => this.setupBpmActions(defs || []),
err => this.logService.error(err)
);
} else {
this.logService.warn('You are not logged in to BPM');
}
this.uploadService.fileUploadComplete.debounceTime(300).subscribe(value => this.onFileUploadComplete(value));
this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value)); this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value));
} }
ngAfterViewInit() {
this.uploadButton.onSuccess
.debounceTime(100)
.subscribe((event) => {
this.reload(event);
});
this.uploadDragArea.onSuccess
.debounceTime(100)
.subscribe((event) => {
this.reload(event);
});
}
viewActivitiForm(event?: any) {
this.router.navigate(['/activiti/tasksnode', event.value.entry.id]);
}
onNavigationError(err: any) { onNavigationError(err: any) {
if (err) { if (err) {
this.errorMessage = err.message || 'Navigation error'; this.errorMessage = err.message || 'Navigation error';
@@ -160,24 +110,10 @@ export class FilesComponent implements OnInit, AfterViewInit {
this.errorMessage = null; this.errorMessage = null;
} }
private setupBpmActions(actions: any[]) { onFileUploadComplete(event: FileUploadCompleteEvent) {
actions.map(def => { if (event && event.file.options.parentId === this.documentList.currentFolderId) {
let documentAction = new DocumentActionModel(); this.documentList.reload();
documentAction.title = 'Activiti: ' + (def.name || 'Unknown process');
documentAction.handler = this.getBpmActionHandler(def);
this.documentList.actions.push(documentAction);
let folderAction = new FolderActionModel();
folderAction.title = 'Activiti: ' + (def.name || 'Unknown process');
folderAction.handler = this.getBpmActionHandler(def);
this.documentList.actions.push(folderAction);
});
} }
private getBpmActionHandler(processDefinition: any): ContentActionHandler {
return function (obj: any, target?: any) {
window.alert(`Starting BPM process: ${processDefinition.id}`);
}.bind(this);
} }
onFolderCreated(event: FolderCreatedEvent) { onFolderCreated(event: FolderCreatedEvent) {
@@ -188,20 +124,11 @@ export class FilesComponent implements OnInit, AfterViewInit {
} }
} }
onPermissionsFailed(event: any) { handlePermissionError(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000); 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);
}
reload(event: any) {
if (event && event.value && event.value.entry && event.value.entry.parentId) {
if (this.documentList.currentFolderId === event.value.entry.parentId) {
this.documentList.reload();
}
}
} }
onCreateFolderClicked(event: Event) { onCreateFolderClicked(event: Event) {
@@ -209,12 +136,8 @@ export class FilesComponent implements OnInit, AfterViewInit {
dialogRef.afterClosed().subscribe(folderName => { dialogRef.afterClosed().subscribe(folderName => {
if (folderName) { if (folderName) {
this.contentService.createFolder('', folderName, this.documentList.currentFolderId).subscribe( this.contentService.createFolder('', folderName, this.documentList.currentFolderId).subscribe(
node => { node => console.log(node),
console.log(node); err => console.log(err)
},
err => {
console.log(err);
}
); );
} }
}); });

View File

@@ -17,6 +17,7 @@
import { ElementRef } from '@angular/core'; import { ElementRef } from '@angular/core';
import { UploadDirective } from './upload.directive'; import { UploadDirective } from './upload.directive';
import { FileInfo } from './../utils/file-utils';
describe('UploadDirective', () => { describe('UploadDirective', () => {
@@ -106,30 +107,35 @@ describe('UploadDirective', () => {
expect(event.preventDefault).not.toHaveBeenCalled(); expect(event.preventDefault).not.toHaveBeenCalled();
}); });
it('should raise upload-files event on files drop', () => { it('should raise upload-files event on files drop', (done) => {
directive.enabled = true; directive.enabled = true;
let files = [<File> {}];
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']); let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
spyOn(directive, 'getDataTransfer').and.returnValue({}); spyOn(directive, 'getDataTransfer').and.returnValue({});
spyOn(directive, 'getFilesDropped').and.returnValue(files); spyOn(directive, 'getFilesDropped').and.returnValue(Promise.resolve([
spyOn(nativeElement, 'dispatchEvent').and.stub(); <FileInfo> {},
<FileInfo> {}
]));
spyOn(nativeElement, 'dispatchEvent').and.callFake(_ => {
done();
});
directive.onDrop(event); directive.onDrop(event);
expect(nativeElement.dispatchEvent).toHaveBeenCalled();
}); });
it('should provide dropped files in upload-files event', () => { it('should provide dropped files in upload-files event', (done) => {
directive.enabled = true; directive.enabled = true;
let files = [<File> {}]; let files = [
<FileInfo> {}
];
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']); let event = jasmine.createSpyObj('event', ['preventDefault', 'stopPropagation']);
spyOn(directive, 'getDataTransfer').and.returnValue({}); spyOn(directive, 'getDataTransfer').and.returnValue({});
spyOn(directive, 'getFilesDropped').and.returnValue(files); spyOn(directive, 'getFilesDropped').and.returnValue(Promise.resolve(files));
spyOn(nativeElement, 'dispatchEvent').and.callFake(e => { spyOn(nativeElement, 'dispatchEvent').and.callFake(e => {
expect(e.detail.files.length).toBe(1); expect(e.detail.files.length).toBe(1);
expect(e.detail.files[0]).toBe(files[0]); expect(e.detail.files[0]).toBe(files[0]);
done();
}); });
directive.onDrop(event); directive.onDrop(event);
expect(nativeElement.dispatchEvent).toHaveBeenCalled();
}); });
}); });

View File

@@ -16,6 +16,7 @@
*/ */
import { Directive, Input, HostListener, ElementRef, Renderer, OnInit, NgZone, OnDestroy } from '@angular/core'; import { Directive, Input, HostListener, ElementRef, Renderer, OnInit, NgZone, OnDestroy } from '@angular/core';
import { FileUtils, FileInfo } from '../utils/file-utils';
@Directive({ @Directive({
selector: '[adf-upload]' selector: '[adf-upload]'
@@ -129,14 +130,16 @@ export class UploadDirective implements OnInit, OnDestroy {
const dataTranfer = this.getDataTransfer(event); const dataTranfer = this.getDataTransfer(event);
if (dataTranfer) { if (dataTranfer) {
const files = this.getFilesDropped(dataTranfer); this.getFilesDropped(dataTranfer).then(files => {
this.onUploadFiles(files); this.onUploadFiles(files);
});
} }
} }
return false; return false;
} }
onUploadFiles(files: File[]) { onUploadFiles(files: FileInfo[]) {
if (this.enabled && files.length > 0) { if (this.enabled && files.length > 0) {
let e = new CustomEvent('upload-files', { let e = new CustomEvent('upload-files', {
detail: { detail: {
@@ -177,34 +180,55 @@ export class UploadDirective implements OnInit, OnDestroy {
* Extract files from the DataTransfer object used to hold the data that is being dragged during a drag and drop operation. * Extract files from the DataTransfer object used to hold the data that is being dragged during a drag and drop operation.
* @param dataTransfer DataTransfer object * @param dataTransfer DataTransfer object
*/ */
protected getFilesDropped(dataTransfer: DataTransfer): File[] { protected getFilesDropped(dataTransfer: DataTransfer): Promise<FileInfo[]> {
const result: File[] = []; return new Promise(resolve => {
const iterations = [];
if (dataTransfer) { if (dataTransfer) {
const items: FileList = dataTransfer.files; const items = dataTransfer.items;
if (items) {
if (items && items.length > 0) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
result.push(items[i]); if (typeof items[i].webkitGetAsEntry !== 'undefined') {
let item = items[i].webkitGetAsEntry();
if (item) {
if (item.isFile) {
iterations.push(Promise.resolve(<FileInfo> {
entry: item,
file: items[i].getAsFile(),
relativeFolder: '/'
}));
} else if (item.isDirectory) {
iterations.push(new Promise(resolveFolder => {
FileUtils.flattern(item).then(files => resolveFolder(files));
}));
} }
} }
} else {
iterations.push(Promise.resolve(<FileInfo>{
entry: null,
file: items[i].getAsFile(),
relativeFolder: '/'
}));
}
}
} else {
// safari or FF
let files = FileUtils
.toFileArray(dataTransfer.files)
.map(file => <FileInfo> {
entry: null,
file: file,
relativeFolder: '/'
});
iterations.push(Promise.resolve(files));
}
} }
return result; Promise.all(iterations).then(result => {
} resolve(result.reduce((a, b) => a.concat(b), []));
});
/** });
* Extract files from the FileList object used to hold files that user selected by means of File Dialog.
* @param fileList List of selected files
*/
protected getFilesSelected(fileList: FileList) {
let result: File[] = [];
if (fileList && fileList.length > 0) {
for (let i = 0; i < fileList.length; i++) {
result.push(fileList[i]);
}
}
return result;
} }
/** /**
@@ -214,8 +238,12 @@ export class UploadDirective implements OnInit, OnDestroy {
protected onSelectFiles(e: Event) { protected onSelectFiles(e: Event) {
if (this.isClickMode()) { if (this.isClickMode()) {
const input = (<HTMLInputElement>e.currentTarget); const input = (<HTMLInputElement>e.currentTarget);
const files = this.getFilesSelected(input.files); const files = FileUtils.toFileArray(input.files);
this.onUploadFiles(files); this.onUploadFiles(files.map(file => <FileInfo> {
entry: null,
file: file,
relativeFolder: '/'
}));
} }
} }
} }

View File

@@ -0,0 +1,73 @@
/*!
* @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.
*/
export interface FileInfo {
entry?: WebKitFileEntry;
file?: File;
relativeFolder?: string;
}
export class FileUtils {
static flattern(folder: any): Promise<FileInfo[]> {
let reader = folder.createReader();
let files: FileInfo[] = [];
return new Promise(resolve => {
let iterations = [];
(function traverse() {
reader.readEntries((entries) => {
if (!entries.length) {
Promise.all(iterations).then(result => resolve(files));
} else {
iterations.push(Promise.all(entries.map(entry => {
if (entry.isFile) {
return new Promise(resolveFile => {
entry.file(function (f: File) {
files.push({
entry: entry,
file: f,
relativeFolder: entry.fullPath.replace(/\/[^\/]*$/, '')
});
resolveFile();
});
});
} else {
return FileUtils.flattern(entry).then(result => {
files.push(...result);
});
}
})));
// Try calling traverse() again for the same dir, according to spec
traverse();
}
});
})();
});
}
static toFileArray(fileList: FileList): File[] {
let result = [];
if (fileList && fileList.length > 0) {
for (let i = 0; i < fileList.length; i++) {
result.push(fileList[i]);
}
}
return result;
}
}

View File

@@ -16,3 +16,4 @@
*/ */
export * from './object-utils'; export * from './object-utils';
export * from './file-utils';

View File

@@ -102,70 +102,16 @@ Follow the 3 steps below:
```html ```html
<alfresco-upload-button <alfresco-upload-button
[showNotificationBar]="true" [rootFolderId]="-my-"
[uploadFolders]="true" [uploadFolders]="true"
[multipleFiles]="false" [multipleFiles]="false"
[acceptedFilesType]=".jpg,.gif,.png,.svg" [acceptedFilesType]=".jpg,.gif,.png,.svg"
[currentFolderPath]="/Sites/swsdp/documentLibrary"
[versioning]="false" [versioning]="false"
(onSuccess)="customMethod($event)"> (onSuccess)="customMethod($event)">
</alfresco-upload-button> </alfresco-upload-button>
<file-uploading-dialog></file-uploading-dialog> <file-uploading-dialog></file-uploading-dialog>
``` ```
Example of an App that declares upload button component :
```ts
import { NgModule, Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { CoreModule, AlfrescoSettingsService, AlfrescoAuthenticationService } from 'ng2-alfresco-core';
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>
`
})
export class MyDemoApp {
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)
);
}
public onSuccess(event: Object): void {
console.log('File uploaded');
}
}
@NgModule({
imports: [
BrowserModule,
CoreModule.forRoot(),
UploadModule.forRoot()
],
declarations: [ MyDemoApp ],
bootstrap: [ MyDemoApp ]
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
```
### Events ### Events
| Name | Description | | Name | Description |
@@ -177,11 +123,12 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `disabled` | *boolean* | false | Toggle component disabled state | | `disabled` | *boolean* | false | Toggle component disabled state |
| `showNotificationBar` | *boolean* | true | Hide/show notification bar | | **(deprecated)** `showNotificationBar` | *boolean* | true | Hide/show notification bar. **Deprecated in 1.6.0: use UploadService events and NotificationService api instead.** |
| `uploadFolders` | *boolean* | false | Allow/disallow upload folders (only for chrome) | | `uploadFolders` | *boolean* | false | Allow/disallow upload folders (only for chrome) |
| `multipleFiles` | *boolean* | false | Allow/disallow multiple files | | `multipleFiles` | *boolean* | false | Allow/disallow multiple files |
| `acceptedFilesType` | *string* | * | array of allowed file extensions , example: ".jpg,.gif,.png,.svg" | | `acceptedFilesType` | *string* | * | array of allowed file extensions , example: ".jpg,.gif,.png,.svg" |
| `currentFolderPath` | *string* | '/Sites/swsdp/documentLibrary' | define the path where the files are uploaded | | **(deprecated)** `currentFolderPath` | *string* | '/Sites/swsdp/documentLibrary' | define the path where the files are uploaded. **Deprecated in 1.6.0: use rootFolderId instead.** |
| `rootFolderId` | *string* | '-root-' | The ID of the root folder node. |
| `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 | | `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 |
| `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 | | `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 | | `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 |
@@ -199,7 +146,9 @@ You can subscribe to this event from your component and use the NotificationServ
[rootFolderId]="currentFolderId" [rootFolderId]="currentFolderId"
(permissionEvent)="onUploadPermissionFailed($event)"> (permissionEvent)="onUploadPermissionFailed($event)">
</alfresco-upload-button> </alfresco-upload-button>
```
```ts
export class MyComponent { export class MyComponent {
onUploadPermissionFailed(event: any) { onUploadPermissionFailed(event: any) {
@@ -232,61 +181,22 @@ The UploadButtonComponent provides the property disableWithNoPermission that can
This component, provide a drag and drop are to upload files to alfresco. This component, provide a drag and drop are to upload files to alfresco.
```html ```html
<alfresco-upload-drag-area <alfresco-upload-drag-area (onSuccess)="customMethod($event)">
(onSuccess)="customMethod($event)"> <div style="width: 200px; height: 100px; border: 1px solid #888888">
DRAG HERE
</div>
</alfresco-upload-drag-area> </alfresco-upload-drag-area>
<file-uploading-dialog></file-uploading-dialog> <file-uploading-dialog></file-uploading-dialog>
``` ```
Example of an App that declares upload drag and drop component:
```ts ```ts
import { NgModule, Component } from '@angular/core'; export class AppComponent {
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { CoreModule, AlfrescoSettingsService, AlfrescoAuthenticationService } from 'ng2-alfresco-core';
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>
`
})
export class MyDemoApp {
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)
);
}
public onSuccess(event: Object): void { public onSuccess(event: Object): void {
console.log('File uploaded'); console.log('File uploaded');
} }
} }
@NgModule({
imports: [
BrowserModule,
CoreModule.forRoot(),
UploadModule.forRoot()
],
declarations: [ MyDemoApp ],
bootstrap: [ MyDemoApp ]
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
``` ```
### Events ### Events
@@ -300,9 +210,9 @@ platformBrowserDynamic().bootstrapModule(AppModule);
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `enabled` | *boolean* | true | Toggle component enabled state | | `enabled` | *boolean* | true | Toggle component enabled state |
| `showNotificationBar` | *boolean* | true | Hide/show notification bar | | **(deprecated)** `showNotificationBar` | *boolean* | true | Hide/show notification bar. **Deprecated in 1.6.0: use UploadService events and NotificationService api instead.** |
| `rootFolderId` | *string* | '-root-' | The ID of the root folder node. | `rootFolderId` | *string* | '-root-' | The ID of the root folder node. |
| `currentFolderPath` | *string* | '/' | define the path where the files are uploaded | | **(deprecated)** `currentFolderPath` | *string* | '/' | define the path where the files are uploaded. **Deprecated in 1.6.0: use rootFolderId instead.** |
| `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 | | `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 |
## FileUploadingDialogComponent ## FileUploadingDialogComponent

View File

@@ -67,7 +67,6 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
}); });
this.uploadService.fileUpload.subscribe(e => { this.uploadService.fileUpload.subscribe(e => {
console.log(e);
this.cd.detectChanges(); this.cd.detectChanges();
}); });
} }

View File

@@ -186,7 +186,7 @@ describe('UploadButtonComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
component.onFilesAdded(fakeEvent); component.onFilesAdded(fakeEvent);
expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith('-root-', '/root-fake-/sites-fake/folder-fake', null); expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith(null);
}); });
it('should call uploadFile with a custom root folder', () => { it('should call uploadFile with a custom root folder', () => {
@@ -202,7 +202,7 @@ describe('UploadButtonComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
component.onFilesAdded(fakeEvent); component.onFilesAdded(fakeEvent);
expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith('-my-', '/root-fake-/sites-fake/folder-fake', null); expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith(null);
}); });
it('should create a folder and emit an File uploaded event', (done) => { it('should create a folder and emit an File uploaded event', (done) => {
@@ -228,21 +228,6 @@ describe('UploadButtonComponent', () => {
component.onDirectoryAdded(fakeEvent); component.onDirectoryAdded(fakeEvent);
}); });
it('should emit an onError event when the folder already exist', (done) => {
component.rootFolderId = '-my-';
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('Error');
done();
});
component.onDirectoryAdded(fakeEvent);
});
it('should by default the title of the button get from the JSON file', () => { it('should by default the title of the button get from the JSON file', () => {
let compiled = fixture.debugElement.nativeElement; let compiled = fixture.debugElement.nativeElement;
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -17,14 +17,12 @@
import { Component, ElementRef, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { Component, ElementRef, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { Observable, Subject } from 'rxjs/Rx'; import { Observable, Subject } from 'rxjs/Rx';
import { AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, LogService, NotificationService, AlfrescoSettingsService } from 'ng2-alfresco-core'; import { AlfrescoApiService, AlfrescoContentService, AlfrescoTranslationService, LogService, NotificationService, AlfrescoSettingsService, FileUtils } from 'ng2-alfresco-core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { UploadService } from '../services/upload.service'; import { UploadService } from '../services/upload.service';
import { FileModel } from '../models/file.model'; import { FileModel } from '../models/file.model';
import { PermissionModel } from '../models/permissions.model'; import { PermissionModel } from '../models/permissions.model';
const ERROR_FOLDER_ALREADY_EXIST = 409;
@Component({ @Component({
selector: 'alfresco-upload-button', selector: 'alfresco-upload-button',
templateUrl: './upload-button.component.html', templateUrl: './upload-button.component.html',
@@ -32,11 +30,15 @@ const ERROR_FOLDER_ALREADY_EXIST = 409;
}) })
export class UploadButtonComponent implements OnInit, OnChanges { export class UploadButtonComponent implements OnInit, OnChanges {
private static DEFAULT_ROOT_ID: string = '-root-';
@Input() @Input()
disabled: boolean = false; disabled: boolean = false;
/**
* @deprecated Deprecated in 1.6.0, you can use UploadService events and NotificationService api instead.
*
* @type {boolean}
* @memberof UploadButtonComponent
*/
@Input() @Input()
showNotificationBar: boolean = true; showNotificationBar: boolean = true;
@@ -55,11 +57,17 @@ export class UploadButtonComponent implements OnInit, OnChanges {
@Input() @Input()
staticTitle: string; staticTitle: string;
/**
* @deprecated Deprecated in 1.6.0, this property is not used for couple of releases already.
*
* @type {string}
* @memberof UploadDragAreaComponent
*/
@Input() @Input()
currentFolderPath: string = '/'; currentFolderPath: string = '/';
@Input() @Input()
rootFolderId: string = UploadButtonComponent.DEFAULT_ROOT_ID; rootFolderId: string = '-root-';
@Input() @Input()
disableWithNoPermission: boolean = false; disableWithNoPermission: boolean = false;
@@ -122,16 +130,11 @@ export class UploadButtonComponent implements OnInit, OnChanges {
return !this.hasPermission && this.disableWithNoPermission ? true : undefined; return !this.hasPermission && this.disableWithNoPermission ? true : undefined;
} }
/**
* Method called when files are dropped in the drag area.
*
* @param {File[]} files - files dropped in the drag area.
*/
onFilesAdded($event: any): void { onFilesAdded($event: any): void {
let files: File[] = this.getFiles($event.currentTarget.files); let files: File[] = FileUtils.toFileArray($event.currentTarget.files);
if (this.hasPermission) { if (this.hasPermission) {
this.uploadFiles(this.currentFolderPath, files); this.uploadFiles(files);
} else { } else {
this.permissionEvent.emit(new PermissionModel({type: 'content', action: 'upload', permission: 'create'})); this.permissionEvent.emit(new PermissionModel({type: 'content', action: 'upload', permission: 'create'}));
} }
@@ -139,38 +142,10 @@ export class UploadButtonComponent implements OnInit, OnChanges {
$event.target.value = ''; $event.target.value = '';
} }
/**
* Method called when a folder is dropped in the drag area.
*
* @param {File[]} files - files of a folder dropped in the drag area.
*/
onDirectoryAdded($event: any): void { onDirectoryAdded($event: any): void {
let files: File[] = this.getFiles($event.currentTarget.files);
if (this.hasPermission) { if (this.hasPermission) {
let hashMapDir = this.convertIntoHashMap(files); let files: File[] = FileUtils.toFileArray($event.currentTarget.files);
this.uploadFiles(files);
hashMapDir.forEach((filesDir, directoryPath) => {
let directoryName = this.getDirectoryName(directoryPath);
let absolutePath = this.currentFolderPath + this.getDirectoryPath(directoryPath);
this.contentService.createFolder(absolutePath, directoryName, this.rootFolderId)
.subscribe(
_ => {
let relativeDir = this.currentFolderPath + '/' + directoryPath;
this.uploadFiles(relativeDir, filesDir);
},
error => {
let errorMessagePlaceholder = this.getErrorMessage(error.response);
if (errorMessagePlaceholder) {
this.onError.emit({value: errorMessagePlaceholder});
let errorMessage = this.formatString(errorMessagePlaceholder, [directoryName]);
if (errorMessage) {
this.showErrorNotificationBar(errorMessage);
}
}
}
);
});
} else { } else {
this.permissionEvent.emit(new PermissionModel({type: 'content', action: 'upload', permission: 'create'})); this.permissionEvent.emit(new PermissionModel({type: 'content', action: 'upload', permission: 'create'}));
} }
@@ -180,78 +155,24 @@ export class UploadButtonComponent implements OnInit, OnChanges {
/** /**
* Upload a list of file in the specified path * Upload a list of file in the specified path
* @param path
* @param files * @param files
* @param path
*/ */
uploadFiles(path: string, files: File[]): void { uploadFiles(files: File[]): void {
if (files.length) { if (files.length > 0) {
const latestFilesAdded = files.map(f => new FileModel(f, { newVersion: this.versioning })); const latestFilesAdded = files.map(file => new FileModel(file, {
newVersion: this.versioning,
parentId: this.rootFolderId,
path: (file.webkitRelativePath || '').replace(/\/[^\/]*$/, '')
}));
this.uploadService.addToQueue(...latestFilesAdded); this.uploadService.addToQueue(...latestFilesAdded);
this.uploadService.uploadFilesInTheQueue(this.rootFolderId, path, this.onSuccess); this.uploadService.uploadFilesInTheQueue(this.onSuccess);
if (this.showNotificationBar) { if (this.showNotificationBar) {
this.showUndoNotificationBar(latestFilesAdded); this.showUndoNotificationBar(latestFilesAdded);
} }
} }
} }
/**
* It converts the array given as input into a map. The map is a key values pairs, where the key is the directory name and the value are
* all the files that the directory contains.
* @param files - array of files
* @returns {Map}
*/
private convertIntoHashMap(files: File[]): Map<string, File[]> {
let directoryMap = new Map<string, File[]>();
for (let file of files) {
let directory = this.getDirectoryPath(file.webkitRelativePath);
let filesSomeDir = directoryMap.get(directory) || [];
filesSomeDir.push(file);
directoryMap.set(directory, filesSomeDir);
}
return directoryMap;
}
private getFiles(fileList: FileList): File[] {
const result: File[] = [];
if (fileList && fileList.length > 0) {
for (let i = 0; i < fileList.length; i++) {
result.push(fileList[i]);
}
}
return result;
}
/**
* Split the directory path given as input and cut the last directory name
* @param directory
* @returns {string}
*/
private getDirectoryPath(directory: string): string {
let relativeDirPath = '';
let dirPath = directory.split('/');
if (dirPath.length > 1) {
dirPath.pop();
relativeDirPath = '/' + dirPath.join('/');
}
return relativeDirPath;
}
/**
* Split a directory path passed in input and return the first directory name
* @param directory
* @returns {string}
*/
private getDirectoryName(directory: string): string {
let dirPath = directory.split('/');
if (dirPath.length > 1) {
return dirPath.pop();
} else {
return dirPath[0];
}
}
/** /**
* Show undo notification bar. * Show undo notification bar.
* *
@@ -267,43 +188,6 @@ export class UploadButtonComponent implements OnInit, OnChanges {
}); });
} }
/**
* Retrive the error message using the error status code
* @param response - object that contain the HTTP response
* @returns {string}
*/
private getErrorMessage(response: any): string {
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';
}
/**
* Show the error inside Notification bar
* @param Error message
* @private
*/
private showErrorNotificationBar(errorMessage: string): void {
this.notificationService.openSnackMessage(errorMessage, 3000);
}
/**
* Replace a placeholder {0} in a message with the input keys
* @param message - the message that conains the placeholder
* @param keys - array of value
* @returns {string} - The message without placeholder
*/
private formatString(message: string, keys: any []): string {
let i = keys.length;
while (i--) {
message = message.replace(new RegExp('\\{' + i + '\\}', 'gm'), keys[i]);
}
return message;
}
checkPermission() { checkPermission() {
if (this.rootFolderId) { if (this.rootFolderId) {
this.getFolderNode(this.rootFolderId).subscribe( this.getFolderNode(this.rootFolderId).subscribe(
@@ -313,6 +197,7 @@ export class UploadButtonComponent implements OnInit, OnChanges {
} }
} }
// TODO: move to AlfrescoContentService
getFolderNode(nodeId: string): Observable<MinimalNodeEntryEntity> { getFolderNode(nodeId: string): Observable<MinimalNodeEntryEntity> {
let opts: any = { let opts: any = {
includeSource: true, includeSource: true,
@@ -331,13 +216,9 @@ export class UploadButtonComponent implements OnInit, OnChanges {
} }
private hasCreatePermission(node: any): boolean { private hasCreatePermission(node: any): boolean {
if (this.hasPermissions(node)) { if (node && node.allowableOperations) {
return node.allowableOperations.find(permision => permision === 'create') ? true : false; return node.allowableOperations.find(permision => permision === 'create') ? true : false;
} }
return false; return false;
} }
private hasPermissions(node: any): boolean {
return node && node.allowableOperations ? true : false;
}
} }

View File

@@ -67,21 +67,22 @@ describe('UploadDragAreaComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('should upload the list of files dropped', () => { it('should upload the list of files dropped', (done) => {
component.currentFolderPath = '/root-fake-/sites-fake/folder-fake'; component.currentFolderPath = '/root-fake-/sites-fake/folder-fake';
component.onSuccess = null; component.onSuccess = null;
component.showNotificationBar = false; component.showNotificationBar = false;
uploadService.addToQueue = jasmine.createSpy('addToQueue');
uploadService.uploadFilesInTheQueue = jasmine.createSpy('uploadFilesInTheQueue'); uploadService.uploadFilesInTheQueue = jasmine.createSpy('uploadFilesInTheQueue');
fixture.detectChanges(); fixture.detectChanges();
const file = <File> {name: 'fake-name-1', size: 10, webkitRelativePath: 'fake-folder1/fake-name-1.json'}; const file = <File> {name: 'fake-name-1', size: 10, webkitRelativePath: 'fake-folder1/fake-name-1.json'};
let fileFake = new FileModel(file); let filesList = [file];
let filesList = [fileFake];
spyOn(uploadService, 'addToQueue').and.callFake((f: FileModel) => {
expect(f.file).toBe(file);
done();
});
component.onFilesDropped(filesList); component.onFilesDropped(filesList);
expect(uploadService.addToQueue).toHaveBeenCalledWith(fileFake);
expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith('-root-', '/root-fake-/sites-fake/folder-fake', null);
}); });
it('should show the loading messages in the notification bar when the files are dropped', () => { it('should show the loading messages in the notification bar when the files are dropped', () => {
@@ -92,11 +93,11 @@ describe('UploadDragAreaComponent', () => {
component.showUndoNotificationBar = jasmine.createSpy('_showUndoNotificationBar'); component.showUndoNotificationBar = jasmine.createSpy('_showUndoNotificationBar');
fixture.detectChanges(); fixture.detectChanges();
let fileFake = new FileModel(<File> {name: 'fake-name-1', size: 10, webkitRelativePath: 'fake-folder1/fake-name-1.json'}); let fileFake = <File> {name: 'fake-name-1', size: 10, webkitRelativePath: 'fake-folder1/fake-name-1.json'};
let filesList = [fileFake]; let filesList = [fileFake];
component.onFilesDropped(filesList); component.onFilesDropped(filesList);
expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith('-root-', '/root-fake-/sites-fake/folder-fake', null); expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith(null);
expect(component.showUndoNotificationBar).toHaveBeenCalled(); expect(component.showUndoNotificationBar).toHaveBeenCalled();
}); });
@@ -119,8 +120,7 @@ describe('UploadDragAreaComponent', () => {
}; };
component.onFilesEntityDropped(itemEntity); component.onFilesEntityDropped(itemEntity);
expect(uploadService.uploadFilesInTheQueue) expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith(null);
.toHaveBeenCalledWith('-root-', '/root-fake-/sites-fake/document-library-fake/folder-fake/', null);
}); });
it('should upload a file with a custom root folder ID when dropped', () => { it('should upload a file with a custom root folder ID when dropped', () => {
@@ -143,7 +143,6 @@ describe('UploadDragAreaComponent', () => {
}; };
component.onFilesEntityDropped(itemEntity); component.onFilesEntityDropped(itemEntity);
expect(uploadService.uploadFilesInTheQueue) expect(uploadService.uploadFilesInTheQueue).toHaveBeenCalledWith(null);
.toHaveBeenCalledWith('-my-', '/root-fake-/sites-fake/document-library-fake/folder-fake/', null);
}); });
}); });

View File

@@ -16,12 +16,10 @@
*/ */
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { AlfrescoTranslationService, AlfrescoContentService, LogService, NotificationService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, NotificationService, FileUtils, FileInfo } from 'ng2-alfresco-core';
import { UploadService } from '../services/upload.service'; import { UploadService } from '../services/upload.service';
import { FileModel } from '../models/file.model'; import { FileModel } from '../models/file.model';
const ERROR_FOLDER_ALREADY_EXIST = 409;
@Component({ @Component({
selector: 'alfresco-upload-drag-area', selector: 'alfresco-upload-drag-area',
templateUrl: './upload-drag-area.component.html', templateUrl: './upload-drag-area.component.html',
@@ -29,31 +27,39 @@ const ERROR_FOLDER_ALREADY_EXIST = 409;
}) })
export class UploadDragAreaComponent { export class UploadDragAreaComponent {
private static DEFAULT_ROOT_ID: string = '-root-';
@Input() @Input()
enabled: boolean = true; enabled: boolean = true;
/**
* @deprecated Deprecated in 1.6.0, you can use UploadService events and NotificationService api instead.
*
* @type {boolean}
* @memberof UploadButtonComponent
*/
@Input() @Input()
showNotificationBar: boolean = true; showNotificationBar: boolean = true;
@Input() @Input()
versioning: boolean = false; versioning: boolean = false;
/**
* @deprecated Deprecated in 1.6.0, this property is not used for couple of releases already. Use rootFolderId instead.
*
* @type {string}
* @memberof UploadDragAreaComponent
*/
@Input() @Input()
currentFolderPath: string = '/'; currentFolderPath: string = '/';
@Input() @Input()
rootFolderId: string = UploadDragAreaComponent.DEFAULT_ROOT_ID; rootFolderId: string = '-root-';
@Output() @Output()
onSuccess = new EventEmitter(); onSuccess = new EventEmitter();
constructor(private uploadService: UploadService, constructor(private uploadService: UploadService,
private translateService: AlfrescoTranslationService, private translateService: AlfrescoTranslationService,
private logService: LogService, private notificationService: NotificationService) {
private notificationService: NotificationService,
private contentService: AlfrescoContentService) {
if (translateService) { if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-upload', 'assets/ng2-alfresco-upload'); translateService.addTranslationFolder('ng2-alfresco-upload', 'assets/ng2-alfresco-upload');
} }
@@ -67,15 +73,18 @@ export class UploadDragAreaComponent {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (this.enabled) { if (this.enabled) {
let files: File[] = e.detail.files; let files: FileInfo[] = e.detail.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const fileModels = files.map(f => new FileModel(f, { newVersion: this.versioning })); let parentId = this.rootFolderId;
if (e.detail.data.obj.entry.isFolder) { if (e.detail.data && e.detail.data.obj.entry.isFolder) {
let id = e.detail.data.obj.entry.id; parentId = e.detail.data.obj.entry.id || this.rootFolderId;
this.onFilesDropped(fileModels, id, '/');
} else {
this.onFilesDropped(fileModels);
} }
const fileModels = files.map(fileInfo => new FileModel(fileInfo.file, {
newVersion: this.versioning,
path: fileInfo.relativeFolder,
parentId: parentId
}));
this.uploadFiles(fileModels);
} }
} }
} }
@@ -85,10 +94,15 @@ export class UploadDragAreaComponent {
* *
* @param {File[]} files - files dropped in the drag area. * @param {File[]} files - files dropped in the drag area.
*/ */
onFilesDropped(files: FileModel[], rootId?: string, directory?: string): void { onFilesDropped(files: File[]): void {
if (this.enabled && files.length) { if (this.enabled && files.length) {
this.uploadService.addToQueue(...files); const fileModels = files.map(file => new FileModel(file, {
this.uploadService.uploadFilesInTheQueue(rootId || this.rootFolderId, directory || this.currentFolderPath, this.onSuccess); newVersion: this.versioning,
path: '/',
parentId: this.rootFolderId
}));
this.uploadService.addToQueue(...fileModels);
this.uploadService.uploadFilesInTheQueue(this.onSuccess);
let latestFilesAdded = this.uploadService.getQueue(); let latestFilesAdded = this.uploadService.getQueue();
if (this.showNotificationBar) { if (this.showNotificationBar) {
this.showUndoNotificationBar(latestFilesAdded); this.showUndoNotificationBar(latestFilesAdded);
@@ -103,11 +117,13 @@ export class UploadDragAreaComponent {
onFilesEntityDropped(item: any): void { onFilesEntityDropped(item: any): void {
if (this.enabled) { if (this.enabled) {
item.file((file: File) => { item.file((file: File) => {
const fileModel = new FileModel(file, { newVersion: this.versioning }); const fileModel = new FileModel(file, {
newVersion: this.versioning,
parentId: this.rootFolderId,
path: item.fullPath.replace(item.name, '')
});
this.uploadService.addToQueue(fileModel); this.uploadService.addToQueue(fileModel);
let path = item.fullPath.replace(item.name, ''); this.uploadService.uploadFilesInTheQueue(this.onSuccess);
let filePath = this.currentFolderPath + path;
this.uploadService.uploadFilesInTheQueue(this.rootFolderId, filePath, this.onSuccess);
}); });
} }
} }
@@ -118,52 +134,22 @@ export class UploadDragAreaComponent {
*/ */
onFolderEntityDropped(folder: any): void { onFolderEntityDropped(folder: any): void {
if (this.enabled && folder.isDirectory) { if (this.enabled && folder.isDirectory) {
let relativePath = folder.fullPath.replace(folder.name, ''); FileUtils.flattern(folder).then(entries => {
relativePath = this.currentFolderPath + relativePath; let files = entries.map(entry => {
return new FileModel(entry.file, {
this.contentService.createFolder(relativePath, folder.name, this.rootFolderId) newVersion: this.versioning,
.subscribe( parentId: this.rootFolderId,
message => { path: entry.relativeFolder
this.onSuccess.emit({
value: 'Created folder'
}); });
let dirReader = folder.createReader(); });
dirReader.readEntries((entries: any) => { this.uploadService.addToQueue(...files);
for (let i = 0; i < entries.length; i++) { /* @deprecated in 1.6.0 */
this._traverseFileTree(entries[i]);
}
if (this.showNotificationBar) { if (this.showNotificationBar) {
let latestFilesAdded = this.uploadService.getQueue(); let latestFilesAdded = this.uploadService.getQueue();
this.showUndoNotificationBar(latestFilesAdded); this.showUndoNotificationBar(latestFilesAdded);
} }
this.uploadService.uploadFilesInTheQueue(this.onSuccess);
}); });
},
error => {
let errorMessagePlaceholder = this.getErrorMessage(error.response);
let errorMessage = this.formatString(errorMessagePlaceholder, [folder.name]);
if (this.showNotificationBar) {
this.showErrorNotificationBar(errorMessage);
} else {
this.logService.error(errorMessage);
}
}
);
}
}
/**
* Travers all the files and folders, and create it on the alfresco.
*
* @param {Object} item - can contains files or folders.
*/
private _traverseFileTree(item: any): void {
if (item.isFile) {
this.onFilesEntityDropped(item);
} else {
if (item.isDirectory) {
this.onFolderEntityDropped(item);
}
} }
} }
@@ -182,41 +168,14 @@ export class UploadDragAreaComponent {
}); });
} }
/** private uploadFiles(files: FileModel[]): void {
* Show the error inside Notification bar if (this.enabled && files.length) {
* @param Error message this.uploadService.addToQueue(...files);
* @private this.uploadService.uploadFilesInTheQueue(this.onSuccess);
*/ let latestFilesAdded = this.uploadService.getQueue();
showErrorNotificationBar(errorMessage: string) { if (this.showNotificationBar) {
this.notificationService.openSnackMessage(errorMessage, 3000); this.showUndoNotificationBar(latestFilesAdded);
}
/**
* Retrive the error message using the error status code
* @param response - object that contain the HTTP response
* @returns {string}
*/
private getErrorMessage(response: any): string {
if (response.body.error.statusCode === ERROR_FOLDER_ALREADY_EXIST) {
let errorMessage: any;
errorMessage = this.translateService.get('FILE_UPLOAD.MESSAGES.FOLDER_ALREADY_EXIST');
return errorMessage.value;
} }
} }
/**
* Replace a placeholder {0} in a message with the input keys
* @param message - the message that conains the placeholder
* @param keys - array of value
* @returns {string} - The message without placeholder
*/
private formatString(message: string, keys: any []) {
if (message) {
let i = keys.length;
while (i--) {
message = message.replace(new RegExp('\\{' + i + '\\}', 'gm'), keys[i]);
}
}
return message;
} }
} }

View File

@@ -16,6 +16,7 @@
*/ */
import { Directive, EventEmitter, Input, Output, OnInit, OnDestroy, ElementRef, NgZone } from '@angular/core'; import { Directive, EventEmitter, Input, Output, OnInit, OnDestroy, ElementRef, NgZone } from '@angular/core';
import { FileUtils } from 'ng2-alfresco-core';
@Directive({ @Directive({
selector: '[file-draggable]' selector: '[file-draggable]'
@@ -28,7 +29,7 @@ export class FileDraggableDirective implements OnInit, OnDestroy {
enabled: boolean = true; enabled: boolean = true;
@Output() @Output()
onFilesDropped: EventEmitter<any> = new EventEmitter(); onFilesDropped: EventEmitter<File[]> = new EventEmitter<File[]>();
@Output() @Output()
onFilesEntityDropped: EventEmitter<any> = new EventEmitter(); onFilesEntityDropped: EventEmitter<any> = new EventEmitter();
@@ -73,16 +74,20 @@ export class FileDraggableDirective implements OnInit, OnDestroy {
if (typeof items[i].webkitGetAsEntry !== 'undefined') { if (typeof items[i].webkitGetAsEntry !== 'undefined') {
let item = items[i].webkitGetAsEntry(); let item = items[i].webkitGetAsEntry();
if (item) { if (item) {
this.traverseFileTree(item); if (item.isFile) {
this.onFilesEntityDropped.emit(item);
} else if (item.isDirectory) {
this.onFolderEntityDropped.emit(item);
}
} }
} else { } else {
let files = event.dataTransfer.files; let files = FileUtils.toFileArray(event.dataTransfer.files);
this.onFilesDropped.emit(files); this.onFilesDropped.emit(files);
} }
} }
} else { } else {
// safari or FF // safari or FF
let files = event.dataTransfer.files; let files = FileUtils.toFileArray(event.dataTransfer.files);
this.onFilesDropped.emit(files); this.onFilesDropped.emit(files);
} }
@@ -90,22 +95,6 @@ export class FileDraggableDirective implements OnInit, OnDestroy {
} }
} }
/**
* Travers all the files and folders, and emit an event for each file or directory.
*
* @param {Object} item - can contains files or folders.
*/
private traverseFileTree(item: any): void {
if (item.isFile) {
let self = this;
self.onFilesEntityDropped.emit(item);
} else {
if (item.isDirectory) {
this.onFolderEntityDropped.emit(item);
}
}
}
/** /**
* Change the style of the drag area when a file drag in. * Change the style of the drag area when a file drag in.
* *

View File

@@ -23,6 +23,8 @@ export interface FileUploadProgress {
export interface FileUploadOptions { export interface FileUploadOptions {
newVersion?: boolean; newVersion?: boolean;
parentId?: string;
path?: string;
} }
export enum FileUploadStatus { export enum FileUploadStatus {

View File

@@ -19,7 +19,7 @@ import { EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core'; import { CoreModule } from 'ng2-alfresco-core';
import { UploadService } from './upload.service'; import { UploadService } from './upload.service';
import { FileModel } from '../models/file.model'; import { FileModel, FileUploadOptions } from '../models/file.model';
declare let jasmine: any; declare let jasmine: any;
@@ -77,9 +77,12 @@ describe('UploadService', () => {
expect(e.value).toBe('File uploaded'); expect(e.value).toBe('File uploaded');
done(); done();
}); });
let fileFake = new FileModel(<File>{name: 'fake-name', size: 10}); let fileFake = new FileModel(
<File>{name: 'fake-name', size: 10},
<FileUploadOptions> { parentId: '-root-', path: 'fake-dir' }
);
service.addToQueue(fileFake); service.addToQueue(fileFake);
service.uploadFilesInTheQueue('-root-', 'fake-dir', emitter); service.uploadFilesInTheQueue(emitter);
let request = jasmine.Ajax.requests.mostRecent(); let request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true'); expect(request.url).toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true');
@@ -99,9 +102,12 @@ describe('UploadService', () => {
expect(e.value).toBe('Error file uploaded'); expect(e.value).toBe('Error file uploaded');
done(); done();
}); });
let fileFake = new FileModel(<File>{name: 'fake-name', size: 10}); let fileFake = new FileModel(
<File>{name: 'fake-name', size: 10},
<FileUploadOptions> { parentId: '-root-' }
);
service.addToQueue(fileFake); service.addToQueue(fileFake);
service.uploadFilesInTheQueue('-root-', '', emitter); service.uploadFilesInTheQueue(emitter);
expect(jasmine.Ajax.requests.mostRecent().url) expect(jasmine.Ajax.requests.mostRecent().url)
.toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true'); .toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true');
@@ -121,7 +127,7 @@ describe('UploadService', () => {
}); });
let fileFake = new FileModel(<File>{name: 'fake-name', size: 10}); let fileFake = new FileModel(<File>{name: 'fake-name', size: 10});
service.addToQueue(fileFake); service.addToQueue(fileFake);
service.uploadFilesInTheQueue('-root-', '', emitter); service.uploadFilesInTheQueue(emitter);
let file = service.getQueue(); let file = service.getQueue();
service.cancelUpload(...file); service.cancelUpload(...file);
@@ -132,7 +138,7 @@ describe('UploadService', () => {
const filesFake = new FileModel(<File>{name: 'fake-name', size: 10}, { newVersion: true }); const filesFake = new FileModel(<File>{name: 'fake-name', size: 10}, { newVersion: true });
service.addToQueue(filesFake); service.addToQueue(filesFake);
service.uploadFilesInTheQueue('-root-', '', emitter); service.uploadFilesInTheQueue(emitter);
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('autoRename=true')).toBe(false); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('autoRename=true')).toBe(false);
expect(jasmine.Ajax.requests.mostRecent().params.has('majorVersion')).toBe(true); expect(jasmine.Ajax.requests.mostRecent().params.has('majorVersion')).toBe(true);
@@ -145,9 +151,12 @@ describe('UploadService', () => {
expect(e.value).toBe('File uploaded'); expect(e.value).toBe('File uploaded');
done(); done();
}); });
let filesFake = new FileModel(<File>{name: 'fake-name', size: 10}); let filesFake = new FileModel(
<File>{name: 'fake-name', size: 10},
<FileUploadOptions> { parentId: '123', path: 'fake-dir' }
);
service.addToQueue(filesFake); service.addToQueue(filesFake);
service.uploadFilesInTheQueue('123', 'fake-dir', emitter); service.uploadFilesInTheQueue(emitter);
let request = jasmine.Ajax.requests.mostRecent(); let request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/123/children?autoRename=true'); expect(request.url).toBe('http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/123/children?autoRename=true');

View File

@@ -17,7 +17,7 @@
import { EventEmitter, Injectable } from '@angular/core'; import { EventEmitter, Injectable } from '@angular/core';
import { Subject } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { AlfrescoApiService, LogService } from 'ng2-alfresco-core'; import { AlfrescoApiService } from 'ng2-alfresco-core';
import { FileUploadEvent, FileUploadCompleteEvent } from '../events/file.event'; import { FileUploadEvent, FileUploadCompleteEvent } from '../events/file.event';
import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model'; import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model';
@@ -27,6 +27,7 @@ export class UploadService {
private queue: FileModel[] = []; private queue: FileModel[] = [];
private cache: { [key: string]: any } = {}; private cache: { [key: string]: any } = {};
private totalComplete: number = 0; private totalComplete: number = 0;
private activeTask: Promise<any> = null;
queueChanged: Subject<FileModel[]> = new Subject<FileModel[]>(); queueChanged: Subject<FileModel[]> = new Subject<FileModel[]>();
fileUpload: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUpload: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
@@ -37,8 +38,18 @@ export class UploadService {
fileUploadError: Subject<FileUploadEvent> = new Subject<FileUploadEvent>(); fileUploadError: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>(); fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>();
constructor(private apiService: AlfrescoApiService, constructor(private apiService: AlfrescoApiService) {
private logService: LogService) { }
/**
* Checks whether the service is uploading a file.
*
* @returns {boolean}
*
* @memberof UploadService
*/
isUploading(): boolean {
return this.activeTask ? true : false;
} }
/** /**
@@ -67,52 +78,32 @@ export class UploadService {
/** /**
* Pick all the files in the queue that are not been uploaded yet and upload it into the directory folder. * Pick all the files in the queue that are not been uploaded yet and upload it into the directory folder.
*
* @param {EventEmitter<any>} emitter @deprecated emitter to invoke on file status change
*
* @memberof UploadService
*/ */
uploadFilesInTheQueue(rootId: string, directory: string, elementEmit: EventEmitter<any>): void { uploadFilesInTheQueue(emitter: EventEmitter<any>): void {
const files = this.getFilesToUpload(); if (!this.activeTask) {
let file = this.queue.find(f => f.status === FileUploadStatus.Pending);
files.forEach((file: FileModel) => { if (file) {
this.onUploadStarting(file); this.onUploadStarting(file);
const opts: any = { const promise = this.beginUpload(file, emitter);
renditions: 'doclib' this.activeTask = promise;
this.cache[file.id] = promise;
let next = () => {
this.activeTask = null;
setTimeout(() => this.uploadFilesInTheQueue(emitter), 100);
}; };
if (file.options.newVersion === true) { promise.then(
opts.overwrite = true; () => next(),
opts.majorVersion = true; () => next()
} else { );
opts.autoRename = true; }
} }
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);
});
this.cache[file.id] = promise;
});
} }
cancelUpload(...files: FileModel[]) { cancelUpload(...files: FileModel[]) {
@@ -131,6 +122,46 @@ export class UploadService {
}); });
} }
private beginUpload(file: FileModel, /* @deprecated */emitter: EventEmitter<any>): any {
let opts: any = {
renditions: 'doclib'
};
if (file.options.newVersion === true) {
opts.overwrite = true;
opts.majorVersion = true;
} else {
opts.autoRename = true;
}
let promise = this.apiService.getInstance().upload.uploadFile(
file.file,
file.options.path,
file.options.parentId,
null,
opts
);
promise.on('progress', (progress: FileUploadProgress) => {
this.onUploadProgress(file, progress);
})
.on('abort', () => {
this.onUploadAborted(file);
emitter.emit({ value: 'File aborted' });
})
.on('error', err => {
this.onUploadError(file, err);
emitter.emit({ value: 'Error file uploaded' });
})
.on('success', data => {
this.onUploadComplete(file);
emitter.emit({ value: data });
})
.catch(err => {
this.onUploadError(file, err);
});
return promise;
}
private onUploadStarting(file: FileModel): void { private onUploadStarting(file: FileModel): void {
if (file) { if (file) {
file.status = FileUploadStatus.Starting; file.status = FileUploadStatus.Starting;
@@ -200,11 +231,4 @@ export class UploadService {
this.fileUploadAborted.next(event); this.fileUploadAborted.next(event);
} }
} }
private getFilesToUpload(): FileModel[] {
let filesToUpload = this.queue.filter(file => {
return file.status === FileUploadStatus.Pending;
});
return filesToUpload;
}
} }