[ADF-523] Fix delete operation in the search component (#2020)

* Search delete permission notification fix

* Support content deletion inside search results

* Forgotten broken test fix.

* Update alfresco-document-list READDME.md

* Update alfresco-document-list READDME.md II

* Adding TOC to README.md

* Build fix

* Fix the build for now and ever!
This commit is contained in:
Popovics András 2017-07-06 09:45:03 +01:00 committed by Eugenio Romano
parent ee871ba578
commit 843afdbcc6
14 changed files with 214 additions and 46 deletions

View File

@ -25,6 +25,7 @@
* [Actions](#actions)
+ [Menu actions](#menu-actions)
+ [Default action handlers](#default-action-handlers)
- [Delete - System handler combined with custom handler](#delete---system-handler-combined-with-custom-handler)
- [Delete - Show notification message with no permission](#delete---show-notification-message-with-no-permission)
- [Delete - Disable button checking the permission](#delete---disable-button-checking-the-permission)
- [Download](#download)
@ -519,8 +520,24 @@ In the Example below will add the [ng2-alfresco-tag](https://www.npmjs.com/packa
### Actions
Properties:
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `target` | string | | "document" or "folder" |
| `title` | string | | The title of the action as shown in the menu |
| `handler` | string | | System type actions. Can be "delete" or "download" |
| `permission` | string | | Then name of the permission |
Events:
| Name | Description |
| --- | --- |
| `execute` | Emitted when user clicks on the action. For combined handlers see below |
| `permissionEvent` | Emitted when a permission error happens |
DocumentList supports declarative actions for Documents and Folders.
Each action can be bound to either default out-of-box handler or a custom behavior.
Each action can be bound to either default out-of-box handler, to a custom behavior or to both of them.
You can define both folder and document actions at the same time.
#### Menu actions
@ -529,18 +546,28 @@ You can define both folder and document actions at the same time.
<adf-document-list ...>
<content-actions>
<!-- system handler -->
<content-action
target="document"
title="System action"
handler="system2">
target="folder"
title="Delete"
handler="delete">
</content-action>
<!-- custom handler -->
<content-action
target="document"
title="Custom action"
(execute)="myCustomAction1($event)">
</content-action>
<!-- combined handler -->
<content-action
target="document"
title="Delete with additional custom callback"
handler="delete"
(execute)="myCustomActionAfterDelete($event)">
</content-action>
</content-actions>
</adf-document-list>
```
@ -553,6 +580,11 @@ export class MyView {
let entry = event.value.entry;
alert(`Custom document action for ${entry.name}`);
}
myCustomActionAfterDelete(event) {
let entry = event.value.entry;
alert(`Custom callback after delete system action for ${entry.name}`);
}
}
```
@ -570,6 +602,10 @@ The following action handlers are provided out-of-box:
All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"`
will trigger the same `download` action.
##### Delete - System handler combined with custom handler
If you specify both of the **handler="delete"** and your custom **(execute)="myCustomActionAfterDelete($event)"**, your callback will be invoked after a successful delete happened. A successful delete operation happens if there is neither permission error, neither other network related error for the delete operation request. For handling permission errors see the section below.
##### Delete - Show notification message with no permission
You can show a notification error when the user don't have the right permission to perform the action.
@ -650,24 +686,34 @@ Initiates download of the corresponding document file.
#### Folder actions
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value.
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value. You can define system, custom or combined handlers as well just as with the document actions.
```html
<adf-document-list ...>
<content-actions>
<!-- system handler -->
<content-action
target="folder"
title="Default folder action 1"
handler="system1">
</content-action>
<!-- custom handler -->
<content-action
target="folder"
title="Custom folder action"
(execute)="myFolderAction1($event)">
</content-action>
<!-- combined handler -->
<content-action
target="folder"
title="Delete with additional custom callback"
handler="delete"
(execute)="myCustomActionAfterDelete($event)">
</content-action>
</content-actions>
</adf-document-list>
```
@ -680,6 +726,11 @@ export class MyView {
let entry = event.value.entry;
alert(`Custom folder action for ${entry.name}`);
}
myCustomActionAfterDelete(event) {
let entry = event.value.entry;
alert(`Custom callback after delete system action for ${entry.name}`);
}
}
```

View File

@ -160,7 +160,7 @@ describe('ContentAction', () => {
expect(documentList.actions.length).toBe(1);
let model = documentList.actions[0];
model.handler('<obj>');
model.execute('<obj>');
});
it('should sync localizable fields with model', () => {
@ -226,7 +226,7 @@ describe('ContentAction', () => {
action.execute = handler;
action.ngOnInit();
action.model.handler(file);
action.model.execute(file);
});
it('should allow registering model without handler', () => {

View File

@ -77,11 +77,11 @@ export class ContentActionComponent implements OnInit, OnChanges {
if (this.handler) {
this.model.handler = this.getSystemHandler(this.target, this.handler);
} else if (this.execute) {
this.model.handler = (document: any): void => {
this.execute.emit({
value: document
});
}
if (this.execute) {
this.model.execute = (value: any): void => {
this.execute.emit({ value });
};
}
@ -106,6 +106,9 @@ export class ContentActionComponent implements OnInit, OnChanges {
if (ltarget === 'document') {
if (this.documentActions) {
this.documentActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
return this.documentActions.getHandler(name);
}
return null;

View File

@ -27,7 +27,7 @@ import { NodeMinimalEntry, NodeMinimal, NodePaging } from '../models/document-li
import { ShareDataRow, RowFilter, ImageResolver } from './../data/share-datatable-adapter';
import { DataTableModule } from 'ng2-alfresco-datatable';
import { DocumentMenuActionComponent } from './document-menu-action.component';
import { Observable } from 'rxjs/Rx';
import { Observable, Subject } from 'rxjs/Rx';
import {
fakeNodeAnswerWithNOEntries,
fakeNodeAnswerWithEntries,
@ -125,12 +125,10 @@ describe('DocumentList', () => {
expect(columns[2]).toBe(column);
});
it('should execute action with node', () => {
it('should call action\'s handler with node', () => {
let node = new FileNode();
let action = new ContentActionModel();
action.handler = function () {
console.log('mock handler');
};
action.handler = () => {};
spyOn(action, 'handler').and.stub();
@ -139,19 +137,42 @@ describe('DocumentList', () => {
});
it('should execute action with node and permission', () => {
it('should call action\'s handler with node and permission', () => {
let node = new FileNode();
let action = new ContentActionModel();
action.handler = function () {
console.log('mock handler');
};
action.handler = () => {};
action.permission = 'fake-permission';
spyOn(action, 'handler').and.stub();
documentList.executeContentAction(node, action);
expect(action.handler).toHaveBeenCalledWith(node, documentList, 'fake-permission');
expect(action.handler).toHaveBeenCalledWith(node, documentList, 'fake-permission');
});
it('should call action\'s execute with node if it is defined', () => {
let node = new FileNode();
let action = new ContentActionModel();
action.execute = () => {};
spyOn(action, 'execute').and.stub();
documentList.executeContentAction(node, action);
expect(action.execute).toHaveBeenCalledWith(node);
});
it('should call action\'s execute only after the handler has been executed', () => {
const deleteObservable: Subject<any> = new Subject<any>();
let node = new FileNode();
let action = new ContentActionModel();
action.handler = () => deleteObservable;
action.execute = () => {};
spyOn(action, 'execute').and.stub();
documentList.executeContentAction(node, action);
expect(action.execute).not.toHaveBeenCalled();
deleteObservable.next();
expect(action.execute).toHaveBeenCalledWith(node);
});
it('should show the loading state during the loading of new elements', (done) => {

View File

@ -19,7 +19,7 @@ import {
Component, OnInit, Input, OnChanges, Output, SimpleChanges, EventEmitter, ElementRef,
AfterContentInit, TemplateRef, NgZone, ViewChild, HostListener, ContentChild
} from '@angular/core';
import { Subject } from 'rxjs/Rx';
import { Subject, Observable } from 'rxjs/Rx';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core';
import {
@ -343,7 +343,17 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
*/
executeContentAction(node: MinimalNodeEntity, action: ContentActionModel) {
if (node && node.entry && action) {
action.handler(node, this, action.permission);
let handlerSub;
if (typeof action.handler === 'function') {
handlerSub = action.handler(node, this, action.permission);
} else {
handlerSub = Observable.of(true);
}
if (typeof action.execute === 'function') {
handlerSub.subscribe(() => { action.execute(node); });
}
}
}

View File

@ -19,6 +19,7 @@ export class ContentActionModel {
icon: string;
title: string;
handler: ContentActionHandler;
execute: Function;
target: string;
permission: string;
disableWithNoPermission: boolean = false;
@ -29,6 +30,7 @@ export class ContentActionModel {
this.icon = obj.icon;
this.title = obj.title;
this.handler = obj.handler;
this.execute = obj.execute;
this.target = obj.target;
this.permission = obj.permission;
this.disableWithNoPermission = obj.disableWithNoPermission;

View File

@ -116,6 +116,20 @@ describe('DocumentActionsService', () => {
});
it('should call the error on the returned Observable if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let file = new FileNode();
const deleteObservable = service.getHandler('delete')(file);
deleteObservable.subscribe({
error: (error) => {
expect(error.message).toEqual('No permission to delete');
done();
}
});
});
it('should delete the file node if there is the delete permission', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
@ -190,19 +204,26 @@ describe('DocumentActionsService', () => {
let actionService = new DocumentActionsService(null, contentService);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
expect(result).toBeFalsy();
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should require content service for download action', () => {
let actionService = new DocumentActionsService(documentListService, null);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
expect(result).toBeFalsy();
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should require file node for download action', () => {
let folder = new FolderNode();
expect(service.getHandler('download')(folder)).toBeFalsy();
let result = service.getHandler('download')(folder);
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should delete file node', () => {
@ -212,9 +233,10 @@ describe('DocumentActionsService', () => {
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
const deleteObservale = service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
expect(deleteObservale.subscribe).toBeDefined;
});
it('should support deletion only file node', () => {

View File

@ -16,6 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ContentActionHandler } from '../models/content-action.model';
import { DocumentListService } from './document-list.service';
import { AlfrescoContentService } from 'ng2-alfresco-core';
@ -74,7 +75,7 @@ export class DocumentActionsService {
window.alert('standard document action 2');
}
private download(obj: any): boolean {
private download(obj: any): Observable<boolean> {
if (this.canExecuteAction(obj) && this.contentService) {
let link = document.createElement('a');
document.body.appendChild(link);
@ -82,21 +83,26 @@ export class DocumentActionsService {
link.href = this.contentService.getContentUrl(obj);
link.click();
document.body.removeChild(link);
return true;
return Observable.of(true);
}
return false;
return Observable.of(false);
}
private deleteNode(obj: any, target?: any, permission?: string) {
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {
let handlerObservale;
if (this.canExecuteAction(obj)) {
if (this.hasPermission(obj.entry, permission)) {
this.documentListService.deleteNode(obj.entry.id).subscribe(() => {
handlerObservale = this.documentListService.deleteNode(obj.entry.id);
handlerObservale.subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
});
return handlerObservale;
} else {
this.permissionEvent.next(new PermissionModel({type: 'content', action: 'delete', permission: permission}));
return Observable.throw(new Error('No permission to delete'));
}
}
}

View File

@ -119,9 +119,10 @@ describe('FolderActionsService', () => {
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [ permission ];
service.getHandler('delete')(folderWithPermission, null, permission);
const deleteObservale = service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(folder.entry.id);
expect(deleteObservale.subscribe).toBeDefined;
});
it('should not delete the folder node if there is no delete permission', (done) => {
@ -140,6 +141,23 @@ describe('FolderActionsService', () => {
service.getHandler('delete')(folderWithPermission);
});
it('should call the error on the returned Observable if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update'];
const deleteObservable = service.getHandler('delete')(folderWithPermission);
deleteObservable.subscribe({
error: (error) => {
expect(error.message).toEqual('No permission to delete');
done();
}
});
});
it('should delete the folder node if there is the delete and others permission ', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();

View File

@ -19,7 +19,7 @@ import { Injectable } from '@angular/core';
import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service';
import { Subject } from 'rxjs/Rx';
import { Subject, Observable } from 'rxjs/Rx';
@Injectable()
export class FolderActionsService {
@ -71,16 +71,21 @@ export class FolderActionsService {
window.alert('standard folder action 2');
}
private deleteNode(obj: any, target?: any, permission?: string) {
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {
let handlerObservale: Observable<any>;
if (this.canExecuteAction(obj)) {
if (this.hasPermission(obj.entry, permission)) {
this.documentListService.deleteNode(obj.entry.id).subscribe(() => {
handlerObservale = this.documentListService.deleteNode(obj.entry.id);
handlerObservale.subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
});
return handlerObservale;
} else {
this.permissionEvent.next(new PermissionModel({type: 'folder', action: 'delete', permission: permission}));
return Observable.throw(new Error('No permission to delete'));
}
}
}

View File

@ -50,7 +50,9 @@
<content-action
target="folder"
title="{{'SEARCH.DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
handler="delete">
permission="delete"
handler="delete"
(permissionEvent)="handlePermission($event)">
</content-action>
<!-- document actions -->
<content-action
@ -61,7 +63,10 @@
<content-action
target="document"
title="{{'SEARCH.DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
handler="delete">
permission="delete"
handler="delete"
(execute)="onContentDelete($event)"
(permissionEvent)="handlePermission($event)">
</content-action>
</content-actions>
</adf-document-list>

View File

@ -22,8 +22,9 @@ import { Observable } from 'rxjs/Rx';
import { AlfrescoSearchComponent } from './alfresco-search.component';
import { TranslationMock } from './../assets/translation.service.mock';
import { AlfrescoSearchService } from '../services/alfresco-search.service';
import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, CoreModule, NotificationService } from 'ng2-alfresco-core';
import { DocumentListModule } from 'ng2-alfresco-documentlist';
import { PermissionModel } from 'ng2-alfresco-documentlist';
describe('AlfrescoSearchComponent', () => {
@ -104,7 +105,8 @@ describe('AlfrescoSearchComponent', () => {
declarations: [AlfrescoSearchComponent], // declare the test component
providers: [
AlfrescoSearchService,
{ provide: AlfrescoTranslationService, useClass: TranslationMock }
{ provide: AlfrescoTranslationService, useClass: TranslationMock },
{ provide: NotificationService, useClass: NotificationService }
]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(AlfrescoSearchComponent);
@ -126,7 +128,7 @@ describe('AlfrescoSearchComponent', () => {
{provide: ActivatedRoute, useValue: {params: Observable.from([{q: 'exampleTerm692'}])}}
]);
let search = new AlfrescoSearchComponent(null, null, injector.get(ActivatedRoute));
let search = new AlfrescoSearchComponent(null, null, null, injector.get(ActivatedRoute));
search.ngOnInit();
@ -142,11 +144,20 @@ describe('AlfrescoSearchComponent', () => {
expect(translationService.addTranslationFolder).toHaveBeenCalledWith('ng2-alfresco-search', 'assets/ng2-alfresco-search');
});
it('should show the Notification snackbar on permission error', () => {
const notoficationService = TestBed.get(NotificationService);
spyOn(notoficationService, 'openSnackMessage');
component.handlePermission(new PermissionModel());
expect(notoficationService.openSnackMessage).toHaveBeenCalledWith('PERMISSON.LACKOF', 3000);
});
describe('Search results', () => {
it('should call search service with the correct parameters', (done) => {
let searchTerm = 'searchTerm63688', options = {
include: ['path'],
include: ['path', 'allowableOperations'],
skipCount: 0,
rootNodeId: '-my-',
nodeType: 'my:type',

View File

@ -18,7 +18,8 @@
import { Component, EventEmitter, Input, Output, Optional, OnChanges, SimpleChanges, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { AlfrescoSearchService, SearchOptions } from './../services/alfresco-search.service';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, NotificationService } from 'ng2-alfresco-core';
import { PermissionModel } from 'ng2-alfresco-documentlist';
import { NodePaging, Pagination } from 'alfresco-js-api';
@Component({
@ -66,6 +67,7 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
constructor(private searchService: AlfrescoSearchService,
private translateService: AlfrescoTranslationService,
private notificationService: NotificationService,
@Optional() private route: ActivatedRoute) {
}
@ -107,7 +109,7 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
private displaySearchResults(searchTerm) {
if (searchTerm && this.searchService) {
let searchOpts: SearchOptions = {
include: ['path'],
include: ['path', 'allowableOperations'],
skipCount: this.skipCount,
rootNodeId: this.rootNodeId,
nodeType: this.resultType,
@ -147,4 +149,13 @@ export class AlfrescoSearchComponent implements OnChanges, OnInit {
this.skipCount = event.skipCount;
this.displaySearchResults(this.searchTerm);
}
public onContentDelete(entry: any) {
this.displaySearchResults(this.searchTerm);
}
public handlePermission(permission: PermissionModel): void {
let permissionErrorMessage: any = this.translateService.get('PERMISSON.LACKOF', permission);
this.notificationService.openSnackMessage(permissionErrorMessage.value, 3000);
}
}

View File

@ -42,5 +42,8 @@
}
}
}
},
"PERMISSON": {
"LACKOF": "You don't have the {{permission}} permission to {{action}} the {{type}}"
}
}