diff --git a/demo-shell-ng2/app/components/files/files.component.html b/demo-shell-ng2/app/components/files/files.component.html index 76c08ea4c0..c1e3b86349 100644 --- a/demo-shell-ng2/app/components/files/files.component.html +++ b/demo-shell-ng2/app/components/files/files.component.html @@ -44,7 +44,9 @@ + + + + ... + +``` + +### Properties + +| Name | Type | Default | Description | +| ----------------- | ------------------- | ------- | --------------------------- | +| adf-delete | MinimalNodeEntity[] | [] | Nodes to delete | +| permanent | boolean | false | Permanent delete | + +### Events + +| Name | Description | +| ------------------------- | -------------------------------------------- | +| delete | emitted when delete process is done | + +## Details + +See **Demo Shell** diff --git a/ng2-components/ng2-alfresco-core/index.ts b/ng2-components/ng2-alfresco-core/index.ts index f5ad28fe73..c837101b32 100644 --- a/ng2-components/ng2-alfresco-core/index.ts +++ b/ng2-components/ng2-alfresco-core/index.ts @@ -55,6 +55,7 @@ import { UploadService } from './src/services/upload.service'; import { HighlightDirective } from './src/directives/highlight.directive'; import { LogoutDirective } from './src/directives/logout.directive'; +import { NodeDeleteDirective } from './src/directives/node-delete.directive'; import { NodeFavoriteDirective } from './src/directives/node-favorite.directive'; import { AppsProcessService } from './src/services/apps-process.service'; import { DeletedNodesApiService } from './src/services/deleted-nodes-api.service'; @@ -265,6 +266,7 @@ export function createTranslateLoader(http: Http, logService: LogService) { NodeRestoreDirective, NodePermissionDirective, NodeFavoriteDirective, + NodeDeleteDirective, HighlightDirective, DataColumnComponent, DataColumnListComponent, @@ -310,6 +312,7 @@ export function createTranslateLoader(http: Http, logService: LogService) { NodeRestoreDirective, NodePermissionDirective, NodeFavoriteDirective, + NodeDeleteDirective, HighlightDirective, DataColumnComponent, DataColumnListComponent, diff --git a/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.spec.ts b/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.spec.ts new file mode 100644 index 0000000000..00af6f5fb4 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.spec.ts @@ -0,0 +1,220 @@ +/*! + * @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 { Component, DebugElement } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Observable } from 'rxjs/Rx'; +import { AlfrescoTranslationService } from '../../index'; +import { CoreModule } from '../../index'; +import { AlfrescoApiService } from '../services/alfresco-api.service'; +import { NotificationService } from '../services/notification.service'; +import { NodeDeleteDirective } from './node-delete.directive'; + +@Component({ + template: ` +
+
` +}) +class TestComponent { + selection = []; + + done = jasmine.createSpy('done'); +} + +describe('NodeDeleteDirective', () => { + let fixture: ComponentFixture; + let element: DebugElement; + let component: TestComponent; + let alfrescoApi: AlfrescoApiService; + let translation: AlfrescoTranslationService; + let notification: NotificationService; + let nodeApi; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CoreModule + ], + declarations: [ + TestComponent + ] + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + element = fixture.debugElement.query(By.directive(NodeDeleteDirective)); + + alfrescoApi = TestBed.get(AlfrescoApiService); + nodeApi = alfrescoApi.getInstance().nodes; + translation = TestBed.get(AlfrescoTranslationService); + notification = TestBed.get(NotificationService); + }); + })); + + beforeEach(() => { + spyOn(translation, 'get').and.callFake((key) => { + return Observable.of(key); + }); + }); + + describe('Delete', () => { + beforeEach(() => { + spyOn(notification, 'openSnackMessage'); + }); + + it('should do nothing if selection is empty', () => { + spyOn(nodeApi, 'deleteNode'); + component.selection = []; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + + expect(nodeApi.deleteNode).not.toHaveBeenCalled(); + }); + + it('should process node successfully', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve()); + + component.selection = [{ entry: { id: '1', name: 'name1' } }]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.SINGULAR' + ); + })); + + it('should notify failed node deletion', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.reject('error')); + + component.selection = [{ entry: { id: '1', name: 'name1' } }]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.ERROR_SINGULAR' + ); + })); + + it('should notify nodes deletion', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve()); + + component.selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.PLURAL' + ); + })); + + it('should notify failed nodes deletion', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.reject('error')); + + component.selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.ERROR_PLURAL' + ); + })); + + it('should notify partial deletion when only one node is successful', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.callFake((id) => { + if (id === '1') { + return Promise.reject('error'); + } else { + return Promise.resolve(); + } + }); + + component.selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } } + ]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.PARTIAL_SINGULAR' + ); + })); + + it('should notify partial deletion when some nodes are successful', fakeAsync(() => { + spyOn(nodeApi, 'deleteNode').and.callFake((id) => { + if (id === '1') { + return Promise.reject(null); + } + + if (id === '2') { + return Promise.resolve(); + } + + if (id === '3') { + return Promise.resolve(); + } + }); + + component.selection = [ + { entry: { id: '1', name: 'name1' } }, + { entry: { id: '2', name: 'name2' } }, + { entry: { id: '3', name: 'name3' } } + ]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(notification.openSnackMessage).toHaveBeenCalledWith( + 'CORE.DELETE_NODE.PARTIAL_PLURAL' + ); + })); + + it('should emit event when delete is done', fakeAsync(() => { + component.done.calls.reset(); + spyOn(nodeApi, 'deleteNode').and.returnValue(Promise.resolve()); + + component.selection = [{ entry: { id: '1', name: 'name1' } }]; + + fixture.detectChanges(); + element.triggerEventHandler('click', null); + tick(); + + expect(component.done).toHaveBeenCalled(); + })); + }); +}); diff --git a/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.ts b/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.ts new file mode 100644 index 0000000000..844731b067 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/directives/node-delete.directive.ts @@ -0,0 +1,200 @@ +/*! + * @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 { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { Observable } from 'rxjs/Rx'; +import { AlfrescoApiService } from '../services/alfresco-api.service'; +import { NotificationService } from '../services/notification.service'; +import { TranslationService } from '../services/translation.service'; + +interface ProcessedNodeData { + entry: MinimalNodeEntryEntity; + status: number; +} + +interface ProcessStatus { + success: ProcessedNodeData[]; + failed: ProcessedNodeData[]; + someFailed(); + someSucceeded(); + oneFailed(); + oneSucceeded(); + allSucceeded(); + allFailed(); +} + +@Directive({ + selector: '[adf-delete]' +}) +export class NodeDeleteDirective { + private nodesApi; + + @Input('adf-delete') + selection: MinimalNodeEntity[]; + + @Input() permanent: boolean = false; + + @Output() delete: EventEmitter = new EventEmitter(); + + @HostListener('click') + onClick() { + this.process(this.selection); + } + + constructor( + private notification: NotificationService, + private alfrescoApiService: AlfrescoApiService, + private translation: TranslationService + ) { + this.nodesApi = this.alfrescoApiService.getInstance().nodes; + } + + private process(selection: MinimalNodeEntity[]) { + if (!selection.length) { + return; + } + + const batch = this.getDeleteNodesBatch(selection); + + Observable.forkJoin(...batch) + .subscribe((data: ProcessedNodeData[]) => { + const processedItems: ProcessStatus = this.processStatus(data); + + this.notify(processedItems); + + if (processedItems.someSucceeded) { + this.delete.emit(); + } + }); + } + + private getDeleteNodesBatch(selection: MinimalNodeEntity[]): Observable[] { + return selection.map((node) => this.deleteNode(node)); + } + + private deleteNode(node: MinimalNodeEntity): Observable { + // shared nodes support + const id = ( node.entry).nodeId || node.entry.id; + + const promise = this.nodesApi.deleteNode(id, { permanent: this.permanent }); + + return Observable.fromPromise(promise) + .map(() => ({ + entry: node.entry, + status: 1 + })) + .catch((error: any) => { + return Observable.of({ + entry: node.entry, + status: 0 + }); + }); + } + + private processStatus(data): ProcessStatus { + const deleteStatus = { + success: [], + failed: [], + get someFailed() { + return !!(this.failed.length); + }, + get someSucceeded() { + return !!(this.success.length); + }, + get oneFailed() { + return this.failed.length === 1; + }, + get oneSucceeded() { + return this.success.length === 1; + }, + get allSucceeded() { + return this.someSucceeded && !this.someFailed; + }, + get allFailed() { + return this.someFailed && !this.someSucceeded; + } + }; + + return data.reduce( + (acc, next) => { + if (next.status === 1) { + acc.success.push(next); + } else { + acc.failed.push(next); + } + + return acc; + }, + deleteStatus + ); + } + + private notify(status) { + this.getMessage(status).subscribe((message) => this.notification.openSnackMessage(message)); + } + + private getMessage(status): Observable { + if (status.allFailed && !status.oneFailed) { + return this.translation.get( + 'CORE.DELETE_NODE.ERROR_PLURAL', + { number: status.failed.length } + ); + } + + if (status.allSucceeded && !status.oneSucceeded) { + return this.translation.get( + 'CORE.DELETE_NODE.PLURAL', + { number: status.success.length } + ); + } + + if (status.someFailed && status.someSucceeded && !status.oneSucceeded) { + return this.translation.get( + 'CORE.DELETE_NODE.PARTIAL_PLURAL', + { + success: status.success.length, + failed: status.failed.length + } + ); + } + + if (status.someFailed && status.oneSucceeded) { + return this.translation.get( + 'CORE.DELETE_NODE.PARTIAL_SINGULAR', + { + success: status.success.length, + failed: status.failed.length + } + ); + } + + if (status.oneFailed && !status.someSucceeded) { + return this.translation.get( + 'CORE.DELETE_NODE.ERROR_SINGULAR', + { name: status.failed[0].entry.name } + ); + } + + if (status.oneSucceeded && !status.someFailed) { + return this.translation.get( + 'CORE.DELETE_NODE.SINGULAR', + { name: status.success[0].entry.name } + ); + } + } +} diff --git a/ng2-components/ng2-alfresco-core/src/i18n/en.json b/ng2-components/ng2-alfresco-core/src/i18n/en.json index 86c7663bf6..72f9d19148 100644 --- a/ng2-components/ng2-alfresco-core/src/i18n/en.json +++ b/ng2-components/ng2-alfresco-core/src/i18n/en.json @@ -22,6 +22,14 @@ "GENERIC": "There was a problem restoring {{ name }} item", "PLURAL": "Restore successful", "SINGULAR": "{{ name }} item restored" + }, + "DELETE_NODE": { + "SINGULAR": "{{ name }} deleted", + "PLURAL": "{{ number }} items deleted", + "PARTIAL_SINGULAR": "Deleted {{ success }} item, {{ failed }} couldn't be deleted", + "PARTIAL_PLURAL": "Deleted {{ success }} items, {{ failed }} couldn't be deleted", + "ERROR_SINGULAR": "{{ name }} couldn't be deleted", + "ERROR_PLURAL": "{{ number }} items couldn't be deleted" } } }