Added permission check on documentlist action menu (#1832)

* #ADF-166 - add permissin check on folder creation

* #ADF-166 - removed wrong pushed change

* #ADF-166 - improved permission check

* #ADF-166 - added test for menu action permission check

* #ADF-166 upgraded disabled attribute to match all the browsers

* #ADF-166 added peer review changes

* #ADF-166 added some little code improvements
This commit is contained in:
Vito
2017-04-24 04:15:54 -07:00
committed by Mario Romano
parent dad7a575f7
commit 93b8d3742f
8 changed files with 266 additions and 47 deletions

View File

@@ -783,6 +783,7 @@ DocumentList emits the following events:
| `nodeDblClick` | emitted when user double-clicks list node |
| `folderChange` | emitted once current display folder has changed |
| `preview` | emitted when user acts upon files with either single or double click (depends on `navigation-mode`), recommended for Viewer components integration |
| `permissionError` | emitted when user is attempting to create a folder via action menu but it doesn't have the permission to do it |
## Advanced usage and customization

View File

@@ -2,7 +2,8 @@
*ngIf="creationMenuActions"
[folderId]="currentFolderId"
(success)="onActionMenuSuccess($event)"
(error)="onActionMenuError($event)">
(error)="onActionMenuError($event)"
(permissionErrorEvent)="onPermissionError($event)">
</alfresco-document-menu-action>
<alfresco-datatable
[data]="data"

View File

@@ -22,7 +22,14 @@ import {
import { Subject } from 'rxjs/Rx';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core';
import { DataRowEvent, DataTableComponent, ObjectDataColumn, DataCellEvent, DataRowActionEvent, DataColumn } from 'ng2-alfresco-datatable';
import {
DataRowEvent,
DataTableComponent,
ObjectDataColumn,
DataCellEvent,
DataRowActionEvent,
DataColumn
} from 'ng2-alfresco-datatable';
import { DocumentListService } from './../services/document-list.service';
import { ContentActionModel } from './../models/content-action.model';
import { ShareDataTableAdapter, ShareDataRow, RowFilter, ImageResolver } from './../data/share-datatable-adapter';
@@ -132,6 +139,9 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Output()
error: EventEmitter<any> = new EventEmitter();
@Output()
permissionError: EventEmitter<any> = new EventEmitter();
@ViewChild(DataTableComponent)
dataTable: DataTableComponent;
@@ -328,11 +338,11 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
// gets folder node and its content
loadFolderByNodeId(nodeId: string) {
this.documentListService.getFolderNode(nodeId).then(node => {
this.folderNode = node;
this.currentFolderId = node.id;
this.skipCount = 0;
this.loadFolderNodesByFolderNodeId(node.id, this.pageSize, this.skipCount).catch(err => this.error.emit(err));
})
this.folderNode = node;
this.currentFolderId = node.id;
this.skipCount = 0;
this.loadFolderNodesByFolderNodeId(node.id, this.pageSize, this.skipCount).catch(err => this.error.emit(err));
})
.catch(err => this.error.emit(err));
}
@@ -488,4 +498,8 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
this.skipCount = event.skipCount;
this.reload();
}
onPermissionError(event) {
this.permissionError.emit(event);
}
}

View File

@@ -1,5 +1,5 @@
<div class="container">
<button md-button [mdMenuTriggerFor]="menu">
<button id="folder-create-button" md-button [mdMenuTriggerFor]="menu" [disabled]="isButtonDisabled()">
<md-icon>add</md-icon>
<span>{{ 'ALFRESCO_DOCUMENT_LIST.BUTTON.ACTION_CREATE' | translate }}</span>
</button>

View File

@@ -16,9 +16,11 @@
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoTranslationService,
AlfrescoApiService,
CoreModule,
LogService
@@ -28,6 +30,56 @@ import { DocumentMenuActionComponent } from './document-menu-action.component';
declare let jasmine: any;
let exampleFolderWithCreate = {
'entry': {
'aspectNames': ['cm:auditable'],
'allowableOperations': ['create'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
let exampleFolderWithPermissions = {
'entry': {
'aspectNames': ['cm:auditable'],
'allowableOperations': ['check'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
let exampleFolderWithNoOperations = {
'entry': {
'aspectNames': ['cm:auditable'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
describe('Document menu action', () => {
let component: DocumentMenuActionComponent;
@@ -50,6 +102,9 @@ describe('Document menu action', () => {
});
TestBed.compileComponents();
let translateService = TestBed.get(AlfrescoTranslationService);
spyOn(translateService, 'get').and.returnValue({ value: 'fake translated message' });
}));
beforeEach(() => {
@@ -69,7 +124,7 @@ describe('Document menu action', () => {
describe('Folder creation', () => {
it('should createFolder fire a success event if the folder has been created', (done) => {
component.allowableOperations = ['create'];
component.showDialog();
component.createFolder('test-folder');
@@ -81,26 +136,12 @@ describe('Document menu action', () => {
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({
'entry': {
'aspectNames': ['cm:auditable'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': {'id': 'admin', 'displayName': 'Administrator'},
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': {'id': 'admin', 'displayName': 'Administrator'},
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
})
responseText: JSON.stringify(exampleFolderWithCreate)
});
});
it('should createFolder fire an error event if the folder has not been created', (done) => {
component.allowableOperations = ['create'];
component.showDialog();
component.createFolder('test-folder');
@@ -113,5 +154,118 @@ describe('Document menu action', () => {
status: 403
});
});
it('should createFolder fire an error when folder already exists', (done) => {
component.allowableOperations = ['create'];
component.showDialog();
component.createFolder('test-folder');
component.error.subscribe((err) => {
expect(err.message).toEqual('fake translated message');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403,
responseText: JSON.stringify({ message: 'Fake folder exists', error: { statusCode: 409 } })
});
});
});
describe('Check Permissions', () => {
it('should get the folder permission when folderId is changed', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithCreate)
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(component.allowableOperations).toBeDefined();
expect(component.allowableOperations).not.toBeNull();
expect(createButton.disabled).toBeFalsy();
});
}));
it('should disable the create button if folder does not have any allowable operations', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeTruthy();
});
}));
it('should disable the create button if folder does not have create permission', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithPermissions)
});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeTruthy();
});
}));
it('should not disable the option when disableWithNoPermission is false', async(() => {
component.disableWithNoPermission = false;
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeFalsy();
});
}));
it('should emit permission event error when user does not have create permission', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
component.permissionErrorEvent.subscribe((error) => {
expect(error.type).toEqual('folder');
expect(error.action).toEqual('create');
});
component.showDialog();
component.createFolder('not-allowed');
}));
});
});

View File

@@ -15,12 +15,13 @@
* limitations under the License.
*/
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, Input, Output, EventEmitter, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { DocumentListService } from './../services/document-list.service';
import { ContentActionModel } from './../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
declare let dialogPolyfill: any;
@@ -32,17 +33,23 @@ const ERROR_FOLDER_ALREADY_EXIST = 409;
styleUrls: ['./document-menu-action.component.css'],
templateUrl: './document-menu-action.component.html'
})
export class DocumentMenuActionComponent {
export class DocumentMenuActionComponent implements OnChanges {
@Input()
folderId: string;
@Input()
disableWithNoPermission: boolean = true;
@Output()
success = new EventEmitter();
@Output()
error = new EventEmitter();
@Output()
permissionErrorEvent = new EventEmitter();
@ViewChild('dialog')
dialog: any;
@@ -52,6 +59,8 @@ export class DocumentMenuActionComponent {
folderName: string = '';
allowableOperations: string[];
constructor(private documentListService: DocumentListService,
private translateService: AlfrescoTranslationService,
private logService: LogService) {
@@ -61,27 +70,43 @@ export class DocumentMenuActionComponent {
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes && changes['folderId']) {
if (changes['folderId'].currentValue !== changes['folderId'].previousValue) {
this.loadCurrentNodePermissions(changes['folderId'].currentValue);
}
}
}
public createFolder(name: string) {
this.cancel();
this.documentListService.createFolder(name, this.folderId)
.subscribe(
(res: MinimalNodeEntity) => {
this.folderName = '';
this.logService.info(res.entry);
this.success.emit({node: res.entry});
},
error => {
if (error.response) {
let errorMessagePlaceholder = this.getErrorMessage(error.response);
this.message = this.formatString(errorMessagePlaceholder, [name]);
this.error.emit({message: this.message});
this.logService.error(this.message);
} else {
this.error.emit(error);
this.logService.error(error);
if (this.hasCreatePermission()) {
this.documentListService.createFolder(name, this.folderId)
.subscribe(
(res: MinimalNodeEntity) => {
this.folderName = '';
this.logService.info(res.entry);
this.success.emit({ node: res.entry });
},
error => {
if (error.response) {
let errorMessagePlaceholder = this.getErrorMessage(error.response);
this.message = this.formatString(errorMessagePlaceholder, [name]);
this.error.emit({ message: this.message });
this.logService.error(this.message);
} else {
this.error.emit(error);
this.logService.error(error);
}
}
}
);
);
} else {
this.permissionErrorEvent.emit(new PermissionModel({
type: 'folder',
action: 'create',
permission: 'create'
}));
}
}
public showDialog() {
@@ -127,4 +152,27 @@ export class DocumentMenuActionComponent {
isFolderNameEmpty() {
return this.folderName === '' ? true : false;
}
isButtonDisabled(): boolean {
return !this.hasCreatePermission() && this.disableWithNoPermission ? true : undefined;
}
hasPermission(permission: string): boolean {
let hasPermission: boolean = false;
if (this.allowableOperations) {
let permFound = this.allowableOperations.find(element => element === permission);
hasPermission = permFound ? true : false;
}
return hasPermission;
}
hasCreatePermission() {
return this.hasPermission('create');
}
loadCurrentNodePermissions(nodeId: string) {
this.documentListService.getFolderNode(nodeId).then(node => {
this.allowableOperations = node ? node['allowableOperations'] : null;
}).catch(err => this.error.emit(err));
}
}