[ADF-551] sorting for document list (#1891)

* table cell component

- table cell component with performance improvements

* permissions and sorting fixes

- fixed ability to set default sorting
- fixed permission evaluation
- new: context menu now also works with permissions
- disabled tags column in demo shell due to performance implications

* fix unit tests

* fix tsconfig for unit testing
This commit is contained in:
Denys Vuika
2017-05-22 15:46:54 +01:00
committed by Eugenio Romano
parent ef5bb6333c
commit da18a21e7c
32 changed files with 112 additions and 92 deletions

View File

@@ -45,6 +45,8 @@
--> -->
</data-column> </data-column>
<!-- Notes: has performance problems due to multiple files/folders causing separate HTTP calls to get tags -->
<!--
<data-column <data-column
title="{{'DOCUMENT_LIST.COLUMNS.TAG' | translate}}" title="{{'DOCUMENT_LIST.COLUMNS.TAG' | translate}}"
key="id" key="id"
@@ -53,6 +55,7 @@
<alfresco-tag-node-list [nodeId]="entry.data.getValue(entry.row, entry.col)"></alfresco-tag-node-list> <alfresco-tag-node-list [nodeId]="entry.data.getValue(entry.row, entry.col)"></alfresco-tag-node-list>
</template> </template>
</data-column> </data-column>
-->
<data-column <data-column
title="{{'DOCUMENT_LIST.COLUMNS.CREATED_BY' | translate}}" title="{{'DOCUMENT_LIST.COLUMNS.CREATED_BY' | translate}}"
key="createdByUser.displayName" key="createdByUser.displayName"

View File

@@ -21,9 +21,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -52,7 +52,10 @@ import { ContextMenuService } from './context-menu.service';
template: ` template: `
<div [ngStyle]="locationCss" class="menu-container"> <div [ngStyle]="locationCss" class="menu-container">
<ul class="context-menu"> <ul class="context-menu">
<li (click)="link.subject.next(link)" class="mdl-menu__item link" *ngFor="let link of links"> <li *ngFor="let link of links"
class="mdl-menu__item link"
(click)="onMenuItemClick($event, link)"
[attr.disabled]="link.model?.disabled || undefined">
{{link.title || link.model?.title}} {{link.title || link.model?.title}}
</li> </li>
</ul> </ul>
@@ -82,6 +85,15 @@ export class ContextMenuHolderComponent {
this.isShown = false; this.isShown = false;
} }
onMenuItemClick(event: Event, menuItem: any): void {
if (menuItem && menuItem.model && menuItem.model.disabled) {
event.preventDefault();
event.stopImmediatePropagation();
return;
}
menuItem.subject.next(menuItem);
}
showMenu(e, links) { showMenu(e, links) {
this.isShown = true; this.isShown = true;
this.links = links; this.links = links;

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -27,9 +27,11 @@ export * from './src/components/datatable/data-row-action.event';
import { DataTableComponent } from './src/components/datatable/datatable.component'; import { DataTableComponent } from './src/components/datatable/datatable.component';
import { NoContentTemplateComponent } from './src/components/datatable/no-content-template.component'; import { NoContentTemplateComponent } from './src/components/datatable/no-content-template.component';
import { PaginationComponent } from './src/components/pagination/pagination.component'; import { PaginationComponent } from './src/components/pagination/pagination.component';
import { DataTableCellComponent } from './src/components/datatable/datatable-cell.component';
export const ALFRESCO_DATATABLE_DIRECTIVES: [any] = [ export const ALFRESCO_DATATABLE_DIRECTIVES: [any] = [
DataTableComponent, DataTableComponent,
DataTableCellComponent,
NoContentTemplateComponent, NoContentTemplateComponent,
PaginationComponent PaginationComponent
]; ];

View File

@@ -0,0 +1,48 @@
/*!
* @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, Input, ChangeDetectionStrategy } from '@angular/core';
import { DataTableAdapter, DataColumn, DataRow } from '../../data/index';
@Component({
selector: 'alfresco-datatable-cell',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-container>{{value}}</ng-container>'
})
export class DataTableCellComponent {
@Input()
data: DataTableAdapter;
@Input()
column: DataColumn;
@Input()
row: DataRow;
@Input()
value: any;
constructor() { }
ngOnInit() {
if (this.column && this.column.key && this.row && this.data) {
this.value = this.data.getValue(this.row, this.column);
}
}
}

View File

@@ -70,10 +70,10 @@
(error)="onImageLoadingError($event)"> (error)="onImageLoadingError($event)">
</div> </div>
<div *ngSwitchCase="'date'" class="cell-value" [attr.data-automation-id]="'date_' + data.getValue(row, col)"> <div *ngSwitchCase="'date'" class="cell-value" [attr.data-automation-id]="'date_' + data.getValue(row, col)">
{{data.getValue(row, col)}} <alfresco-datatable-cell [data]="data" [column]="col" [row]="row"></alfresco-datatable-cell>
</div> </div>
<div *ngSwitchCase="'text'" class="cell-value" [attr.data-automation-id]="'text_' + data.getValue(row, col)"> <div *ngSwitchCase="'text'" class="cell-value" [attr.data-automation-id]="'text_' + data.getValue(row, col)">
{{data.getValue(row, col)}} <alfresco-datatable-cell [data]="data" [column]="col" [row]="row"></alfresco-datatable-cell>
</div> </div>
<span *ngSwitchDefault class="cell-value"> <span *ngSwitchDefault class="cell-value">
<!-- empty cell for unknown column type --> <!-- empty cell for unknown column type -->
@@ -95,7 +95,7 @@
</button> </button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-right" <ul alfresco-mdl-menu class="mdl-menu--bottom-right"
[attr.for]="'action_menu_' + idx"> [attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item" [attr.disabled]="action.disabled" <li class="mdl-menu__item" [attr.disabled]="action.disabled || undefined"
[attr.data-automation-id]="action.title" [attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)" *ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)"> (click)="onExecuteRowAction(row, action)">

View File

@@ -20,6 +20,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core'; import { CoreModule } from 'ng2-alfresco-core';
import { MdCheckboxChange } from '@angular/material'; import { MdCheckboxChange } from '@angular/material';
import { DataTableComponent } from './datatable.component'; import { DataTableComponent } from './datatable.component';
import { DataTableCellComponent } from './datatable-cell.component';
import { import {
DataRow, DataRow,
DataColumn, DataColumn,
@@ -40,7 +41,10 @@ describe('DataTable', () => {
imports: [ imports: [
CoreModule.forRoot() CoreModule.forRoot()
], ],
declarations: [DataTableComponent] declarations: [
DataTableCellComponent,
DataTableComponent
]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -258,7 +258,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
} }
onExecuteRowAction(row: DataRow, action: any) { onExecuteRowAction(row: DataRow, action: any) {
if (action.disabled) { if (action.disabled || action.disabled) {
event.stopPropagation(); event.stopPropagation();
} else { } else {
this.executeRowAction.emit(new DataRowActionEvent(row, action)); this.executeRowAction.emit(new DataRowActionEvent(row, action));

View File

@@ -17,3 +17,4 @@
export * from './datatable.component'; export * from './datatable.component';
export * from './no-content-template.component'; export * from './no-content-template.component';
export * from './datatable-cell.component';

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -47,6 +47,9 @@ export class ContentActionComponent implements OnInit, OnChanges {
@Input() @Input()
disableWithNoPermission: boolean; disableWithNoPermission: boolean;
@Input()
disabled: boolean = false;
@Output() @Output()
execute = new EventEmitter(); execute = new EventEmitter();
@@ -68,7 +71,8 @@ export class ContentActionComponent implements OnInit, OnChanges {
icon: this.icon, icon: this.icon,
permission: this.permission, permission: this.permission,
disableWithNoPermission: this.disableWithNoPermission, disableWithNoPermission: this.disableWithNoPermission,
target: this.target target: this.target,
disabled: this.disabled
}); });
if (this.handler) { if (this.handler) {

View File

@@ -30,6 +30,8 @@ describe('ContentColumnList', () => {
let service = new DocumentListServiceMock(); let service = new DocumentListServiceMock();
documentList = new DocumentListComponent(service, null, null, null); documentList = new DocumentListComponent(service, null, null, null);
columnList = new ContentColumnListComponent(documentList); columnList = new ContentColumnListComponent(documentList);
documentList.ngOnInit();
}); });
it('should register column within parent document list', () => { it('should register column within parent document list', () => {

View File

@@ -29,6 +29,8 @@ describe('ContentColumn', () => {
let service = new DocumentListServiceMock(); let service = new DocumentListServiceMock();
documentList = new DocumentListComponent(service, null, null, null); documentList = new DocumentListComponent(service, null, null, null);
columnList = new ContentColumnListComponent(documentList); columnList = new ContentColumnListComponent(documentList);
documentList.ngOnInit();
}); });
it('should register model within parent column list', () => { it('should register model within parent column list', () => {

View File

@@ -157,11 +157,11 @@ describe('DocumentList', () => {
let actions = documentList.getNodeActions(new FolderNode()); let actions = documentList.getNodeActions(new FolderNode());
expect(actions.length).toBe(1); expect(actions.length).toBe(1);
expect(actions[0]).toBe(folderMenu); expect(actions[0].target).toBe(folderMenu.target);
actions = documentList.getNodeActions(new FileNode()); actions = documentList.getNodeActions(new FileNode());
expect(actions.length).toBe(1); expect(actions.length).toBe(1);
expect(actions[0]).toBe(documentMenu); expect(actions[0].target).toBe(documentMenu.target);
}); });
it('should disable the action if there is no permission for the file and disableWithNoPermission true', () => { it('should disable the action if there is no permission for the file and disableWithNoPermission true', () => {

View File

@@ -28,7 +28,8 @@ import {
ObjectDataColumn, ObjectDataColumn,
DataCellEvent, DataCellEvent,
DataRowActionEvent, DataRowActionEvent,
DataColumn DataColumn,
DataSorting
} from 'ng2-alfresco-datatable'; } from 'ng2-alfresco-datatable';
import { DocumentListService } from './../services/document-list.service'; import { DocumentListService } from './../services/document-list.service';
import { ContentActionModel } from './../models/content-action.model'; import { ContentActionModel } from './../models/content-action.model';
@@ -156,8 +157,6 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
private translateService: AlfrescoTranslationService, private translateService: AlfrescoTranslationService,
private el: ElementRef) { private el: ElementRef) {
this.data = new ShareDataTableAdapter(this.documentListService);
if (translateService) { if (translateService) {
translateService.addTranslationFolder('ng2-alfresco-documentlist', 'node_modules/ng2-alfresco-documentlist/src'); translateService.addTranslationFolder('ng2-alfresco-documentlist', 'node_modules/ng2-alfresco-documentlist/src');
} }
@@ -186,6 +185,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
} }
ngOnInit() { ngOnInit() {
this.data = new ShareDataTableAdapter(this.documentListService, null, this.getDefaultSorting());
this.data.thumbnails = this.thumbnails; this.data.thumbnails = this.thumbnails;
this.contextActionHandler.subscribe(val => this.contextActionCallback(val)); this.contextActionHandler.subscribe(val => this.contextActionCallback(val));
@@ -200,7 +200,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
} }
if (!this.data) { if (!this.data) {
this.data = new ShareDataTableAdapter(this.documentListService, schema); this.data = new ShareDataTableAdapter(this.documentListService, schema, this.getDefaultSorting());
} else if (schema && schema.length > 0) { } else if (schema && schema.length > 0) {
this.data.setColumns(schema); this.data.setColumns(schema);
} }
@@ -209,18 +209,6 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
if (!columns || columns.length === 0) { if (!columns || columns.length === 0) {
this.setupDefaultColumns(); this.setupDefaultColumns();
} }
// TODO: commented out as Permissions feature (Context Menus and Row Actions) breaks all component functionality
/*
if (this.sorting) {
const [ key, direction ] = this.sorting;
this.data.setSorting({
key,
direction: direction || 'asc'
});
}
*/
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@@ -294,28 +282,21 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
if (target) { if (target) {
let ltarget = target.toLowerCase(); let ltarget = target.toLowerCase();
let actionWithPermission = this.checkPermissions(node); let actionsByTarget = this.actions.filter(entry => {
let actionsByTarget = actionWithPermission.filter(entry => {
return entry.target.toLowerCase() === ltarget; return entry.target.toLowerCase() === ltarget;
}).map(action => new ContentActionModel(action));
actionsByTarget.forEach((action) => {
this.checkPermission(node, action);
}); });
let cloneActions = Object.create(actionsByTarget); return actionsByTarget;
return cloneActions;
} }
} }
return []; return [];
} }
checkPermissions(node: MinimalNodeEntity): ContentActionModel[] {
let actionsPermission: ContentActionModel[] = [];
this.actions.forEach((action) => {
actionsPermission.push(this.checkPermission(node, action));
});
return actionsPermission;
}
checkPermission(node: any, action: ContentActionModel): ContentActionModel { checkPermission(node: any, action: ContentActionModel): ContentActionModel {
if (action.permission) { if (action.permission) {
if (this.hasPermissions(node)) { if (this.hasPermissions(node)) {
@@ -562,4 +543,13 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
this.navigationMode = DocumentListComponent.SINGLE_CLICK_NAVIGATION; this.navigationMode = DocumentListComponent.SINGLE_CLICK_NAVIGATION;
} }
} }
private getDefaultSorting(): DataSorting {
let defaultSorting: DataSorting;
if (this.sorting) {
const [ key, direction ] = this.sorting;
defaultSorting = new DataSorting(key, direction);
}
return defaultSorting;
}
} }

View File

@@ -41,9 +41,11 @@ export class ShareDataTableAdapter implements DataTableAdapter {
selectedRow: DataRow; selectedRow: DataRow;
constructor(private documentListService: DocumentListService, constructor(private documentListService: DocumentListService,
schema: DataColumn[] = []) { schema: DataColumn[] = [],
sorting?: DataSorting) {
this.rows = []; this.rows = [];
this.columns = schema || []; this.columns = schema || [];
this.sorting = sorting;
} }
getRows(): Array<DataRow> { getRows(): Array<DataRow> {

View File

@@ -21,8 +21,8 @@ export class ContentActionModel {
handler: ContentActionHandler; handler: ContentActionHandler;
target: string; target: string;
permission: string; permission: string;
disableWithNoPermission: boolean; disableWithNoPermission: boolean = false;
disabled: boolean; disabled: boolean = false;
constructor(obj?: any) { constructor(obj?: any) {
if (obj) { if (obj) {
@@ -32,6 +32,7 @@ export class ContentActionModel {
this.target = obj.target; this.target = obj.target;
this.permission = obj.permission; this.permission = obj.permission;
this.disableWithNoPermission = obj.disableWithNoPermission; this.disableWithNoPermission = obj.disableWithNoPermission;
this.disabled = obj.disabled;
} }
} }
} }

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -41,9 +41,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [

View File

@@ -21,9 +21,6 @@
"es2015", "es2015",
"dom" "dom"
], ],
"typeRoots": [
"./node_modules/@types"
],
"suppressImplicitAnyIndexErrors": true "suppressImplicitAnyIndexErrors": true
}, },
"exclude": [ "exclude": [