[ADF-1404] Data Column enhancements for Document List (#2220)

* support 'timeAgo' format for data-column

* file size column type and bug fixes

* readme updates

* location column type

* readme fixes

* update unit tests

* file size pipe tests
This commit is contained in:
Denys Vuika 2017-08-16 09:53:39 +01:00 committed by Mario Romano
parent 9e5b19e34c
commit 06e03ad1e9
14 changed files with 295 additions and 50 deletions

View File

@ -99,11 +99,25 @@
class="image-table-cell">
</data-column>
<data-column
title="{{'DOCUMENT_LIST.COLUMNS.DISPLAY_NAME' | translate}}"
key="name"
title="{{'DOCUMENT_LIST.COLUMNS.DISPLAY_NAME' | translate}}"
[formatTooltip]="getNodeNameTooltip"
class="full-width ellipsis-cell">
</data-column>
<!-- Location column demo -->
<!--
<data-column
key="path"
type="location"
format="/files"
title="Location">
</data-column>
-->
<data-column
key="content.sizeInBytes"
title="Size"
type="fileSize">
</data-column>
<!-- Notes: has performance overhead due to multiple files/folders causing separate HTTP calls to get tags -->
<!--
<data-column
@ -124,11 +138,8 @@
title="{{'DOCUMENT_LIST.COLUMNS.CREATED' | translate}}"
key="createdAt"
type="date"
format="medium"
format="timeAgo"
class="desktop-only">
<ng-template let-value="value">
<span title="{{ value }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
</data-columns>

View File

@ -126,6 +126,7 @@ export { ContextMenuModule } from './src/components/context-menu/context-menu.mo
export { CardViewModule } from './src/components/view/card-view.module';
export { CollapsableModule } from './src/components/collapsable/collapsable.module';
export { CardViewItem } from './src/interface/card-view-item.interface';
export { TimeAgoPipe } from './src/pipes/time-ago.pipe';
export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component';

View File

@ -0,0 +1,75 @@
/*!
* @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 { FileSizePipe } from './file-size.pipe';
describe('FileSizePipe', () => {
let pipe: FileSizePipe;
beforeEach(() => {
pipe = new FileSizePipe();
});
it('returns empty string with invalid input', () => {
expect(pipe.transform(null)).toBe('');
expect(pipe.transform(undefined)).toBe('');
});
it('should convert value to Bytes', () => {
expect(pipe.transform(0)).toBe('0 Bytes');
expect(pipe.transform(1023)).toBe('1023 Bytes');
});
it('should convert value to KB', () => {
expect(pipe.transform(1024)).toBe('1 KB');
expect(pipe.transform(1048575)).toBe('1024 KB');
});
it('should convert value to MB', () => {
expect(pipe.transform(1048576)).toBe('1 MB');
expect(pipe.transform(1073741823)).toBe('1024 MB');
});
it('should convert value to GB', () => {
expect(pipe.transform(1073741824)).toBe('1 GB');
expect(pipe.transform(1099511627775)).toBe('1024 GB');
});
it('should convert value to TB and PB', () => {
expect(pipe.transform(1099511627776)).toBe('1 TB');
expect(pipe.transform(1125899906842623)).toBe('1 PB');
});
it('should convert value with custom precision', () => {
const tests = [
{ size: 10, precision: 2, expectancy: '10 Bytes'},
{ size: 1023, precision: 1, expectancy: '1023 Bytes'},
{ size: 1025, precision: 2, expectancy: '1 KB'},
{ size: 1499, precision: 0, expectancy: '1.46 KB'},
{ size: 1999, precision: 0, expectancy: '1.95 KB'},
{ size: 2000, precision: 2, expectancy: '1.95 KB'},
{ size: 5000000, precision: 4, expectancy: '4.7684 MB'},
{ size: 12345678901234, precision: 3, expectancy: '11.228 TB'}
];
tests.forEach(({ size, precision, expectancy }) => {
expect(pipe.transform(size, precision)).toBe(expectancy);
});
});
});

View File

@ -18,19 +18,25 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'adfFileSize'
name: 'adfFileSize'
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number = 0, decimals: number = 2): string {
if (bytes === 0) {
return '0 Bytes';
transform(bytes: number, decimals: number = 2): string {
if (bytes == null || bytes === undefined) {
return '';
}
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024,
dm = decimals || 2,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
const k = 1024,
dm = decimals || 2,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}

View File

@ -16,6 +16,7 @@
*/
import { ModuleWithProviders, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CoreModule, TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
import { MaterialModule } from './src/material.module';
@ -30,6 +31,7 @@ export { DataRowActionEvent, DataRowActionModel } from './src/components/datatab
import { DataTableCellComponent } from './src/components/datatable/datatable-cell.component';
import { DataTableComponent } from './src/components/datatable/datatable.component';
import { EmptyListComponent } from './src/components/datatable/empty-list.component';
import { LocationCellComponent } from './src/components/datatable/location-cell.component';
import { LoadingContentTemplateDirective } from './src/directives/loading-template.directive';
import { NoContentTemplateDirective } from './src/directives/no-content-template.directive';
@ -38,6 +40,7 @@ export function directives() {
DataTableComponent,
EmptyListComponent,
DataTableCellComponent,
LocationCellComponent,
NoContentTemplateDirective,
LoadingContentTemplateDirective
];
@ -45,6 +48,7 @@ export function directives() {
@NgModule({
imports: [
RouterModule,
CoreModule,
MaterialModule
],
@ -61,7 +65,8 @@ export function directives() {
],
exports: [
...directives(),
MaterialModule
MaterialModule,
RouterModule
]
})
export class DataTableModule {

View File

@ -37,10 +37,8 @@ export class DataTableCellComponent implements OnInit {
@Input()
value: any;
constructor() { }
ngOnInit() {
if (this.column && this.column.key && this.row && this.data) {
if (!this.value && this.column && this.column.key && this.row && this.data) {
this.value = this.data.getValue(this.row, this.column);
}
}

View File

@ -84,6 +84,16 @@
[attr.data-automation-id]="'date_' + data.getValue(row, col)">
<adf-datatable-cell [data]="data" [column]="col" [row]="row"></adf-datatable-cell>
</div>
<div *ngSwitchCase="'location'" class="cell-value"
[mdTooltip]="getCellTooltip(row, col)"
[attr.data-automation-id]="'location' + data.getValue(row, col)">
<adf-location-cell [data]="data" [column]="col" [row]="row"></adf-location-cell>
</div>
<div *ngSwitchCase="'fileSize'" class="cell-value"
[mdTooltip]="getCellTooltip(row, col)"
[attr.data-automation-id]="'fileSize_' + data.getValue(row, col)">
<adf-datatable-cell [value]="data.getValue(row, col) | adfFileSize"></adf-datatable-cell>
</div>
<div *ngSwitchCase="'text'" class="cell-value"
[mdTooltip]="getCellTooltip(row, col)"
[attr.data-automation-id]="'text_' + data.getValue(row, col)">

View File

@ -18,6 +18,7 @@
import { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MdCheckboxChange } from '@angular/material';
import { RouterTestingModule } from '@angular/router/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { MaterialModule } from '../../material.module';
import {
@ -29,6 +30,7 @@ import {
} from './../../data/index';
import { DataTableCellComponent } from './datatable-cell.component';
import { DataTableComponent } from './datatable.component';
import { LocationCellComponent } from './location-cell.component';
describe('DataTable', () => {
@ -40,11 +42,13 @@ describe('DataTable', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot(),
RouterTestingModule,
CoreModule,
MaterialModule
],
declarations: [
DataTableCellComponent,
LocationCellComponent,
DataTableComponent
]
}).compileComponents();

View File

@ -0,0 +1,60 @@
/*!
* @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 { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { PathInfoEntity } from 'alfresco-js-api';
import { DataTableCellComponent } from './datatable-cell.component';
@Component({
selector: 'adf-location-cell',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ng-container>
<a href="" [title]="tooltip" [routerLink]="link">
{{ displayText }}
</a>
</ng-container>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-location-cell' }
})
export class LocationCellComponent extends DataTableCellComponent implements OnInit {
@Input()
tooltip: string = '';
@Input()
link: any[];
@Input()
displayText: string = '';
/** @override */
ngOnInit() {
if (!this.value && this.column && this.column.key && this.row && this.data) {
const path: PathInfoEntity = this.data.getValue(this.row, this.column);
if (path) {
this.value = path;
this.displayText = path.name.split('/').pop();
this.tooltip = path.name;
const parent = path.elements[path.elements.length - 1];
this.link = [ this.column.format, parent.id ];
}
}
}
}

View File

@ -17,7 +17,8 @@
import { DatePipe } from '@angular/common';
import { TemplateRef } from '@angular/core';
import { ObjectUtils } from 'ng2-alfresco-core';
import { ObjectUtils, TimeAgoPipe } from 'ng2-alfresco-core';
import { DataColumn, DataRow, DataSorting, DataTableAdapter } from './datatable-adapter';
// Simple implementation of the DataTableAdapter interface.
@ -103,12 +104,10 @@ export class ObjectDataTableAdapter implements DataTableAdapter {
let value = row.getValue(col.key);
if (col.type === 'date') {
let datePipe = new DatePipe('en-US');
let format = col.format || 'medium';
try {
return datePipe.transform(value, format);
return this.formatDate(col, value);
} catch (err) {
console.error(`DocumentList: error parsing date ${value} to format ${format}`);
console.error(`Error parsing date ${value} to format ${col.format}`);
}
}
@ -120,6 +119,21 @@ export class ObjectDataTableAdapter implements DataTableAdapter {
return value;
}
formatDate(col: DataColumn, value: any): string {
if (col.type === 'date') {
const format = col.format || 'medium';
if (format === 'timeAgo') {
const timeAgoPipe = new TimeAgoPipe();
return timeAgoPipe.transform(value);
} else {
const datePipe = new DatePipe('en-US');
return datePipe.transform(value, format);
}
}
return value;
}
getSorting(): DataSorting {
return this._sorting;
}

View File

@ -16,9 +16,11 @@
*/
import { async, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { DataTableCellComponent } from '../components/datatable/datatable-cell.component';
import { DataTableComponent } from '../components/datatable/datatable.component';
import { LocationCellComponent } from '../components/datatable/location-cell.component';
import { MaterialModule } from '../material.module';
import { LoadingContentTemplateDirective } from './loading-template.directive';
@ -30,12 +32,14 @@ describe('LoadingContentTemplateDirective', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
MaterialModule,
CoreModule.forRoot()
CoreModule
],
declarations: [
DataTableComponent,
DataTableCellComponent,
LocationCellComponent,
LoadingContentTemplateDirective
]
}).compileComponents();

View File

@ -16,9 +16,11 @@
*/
import { async, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { DataTableCellComponent } from '../components/datatable/datatable-cell.component';
import { DataTableComponent } from '../components/datatable/datatable.component';
import { LocationCellComponent } from '../components/datatable/location-cell.component';
import { MaterialModule } from '../material.module';
import { NoContentTemplateDirective } from './no-content-template.directive';
@ -30,13 +32,15 @@ describe('NoContentTemplateDirective', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
MaterialModule,
CoreModule.forRoot()
CoreModule
],
declarations: [
DataTableComponent,
DataTableCellComponent,
NoContentTemplateDirective
NoContentTemplateDirective,
LocationCellComponent
]
}).compileComponents();
}));

View File

@ -441,7 +441,7 @@ Here's the list of available properties you can define for a Data Column definit
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| key | string | | Data source key, can be either column/property key like `title` or property path like `createdBy.name` |
| type | string (text\|image\|date) | text | Value type |
| type | string | text | Value type |
| format | string | | Value format (if supported by components), for example format of the date |
| sortable | boolean | true | Toggles ability to sort by this column, for example by clicking the column header |
| title | string | | Display title of the column, typically used for column headers |
@ -450,6 +450,18 @@ Here's the list of available properties you can define for a Data Column definit
| class | string | | Additional CSS class to be applied to column (header and cells) |
| formatTooltip | Function | | Custom tooltip formatter function. |
### Column Types
The DataColumn `type` property can take one of the following values:
- text
- image
- date
- fileSize
- location
### Underlying node object
DocumentList component assigns an instance of `MinimalNode` type (`alfresco-js-api`) as a data context of each row.
```js
@ -493,24 +505,54 @@ Here's a short example:
</adf-document-list>
```
## Column definition
Properties:
| Name | Type | Default | Description
| --- | --- | --- | --- |
| title | string | | Column title |
| sr-title | string | | Screen reader title, used only when `title` is empty |
| key | string | | Column source key, example: `createdByUser.displayName` |
| sortable | boolean | false | Toggle sorting ability via column header clicks |
| class | string | | CSS class list, example: `full-width ellipsis-cell` |
| type | string | text | Column type, text\|date\|number |
| format | string | | Value format pattern |
| template | `TemplateRef<any>` | | Column template |
### Date Column
For `date` column type the [DatePipe](https://angular.io/docs/ts/latest/api/common/DatePipe-class.html) formatting is used.
For a full list of available `format` values please refer to [DatePipe](https://angular.io/docs/ts/latest/api/common/DatePipe-class.html) documentation.
ADF also supports additional `timeAgo` value for the `format` property.
That triggers the date values to be rendered using popular ["Time from now"](https://momentjs.com/docs/#/displaying/fromnow/) format.
### Location Column
This column is used to display a clickable location link pointing to the parent path of the node.
You are going to use it with custom navigation or when displaying content from the sources like:
- Sites
- Shared Links
- Recent Files
- Favorites
- Trashcan
or any other location that needs nagivating to the node parent folder easily.
Note that the parent node is evaluated automatically,
the generated link will be pointing to URL based on the `format` property value with the node `id` value appended:
```text
/<format>/:id
```
For example:
```html
<data-column
key="path"
type="location"
format="/files"
title="Location">
</data-column>
```
All links rendered in the column above will have an address mapped to `/files`:
```text
/files/node-1-id
/files/node-2-id
...
```
### Column Template
It is possible to provide custom column/cell template that may contain other Angular components or HTML elements:

View File

@ -17,7 +17,7 @@
import { DatePipe } from '@angular/common';
import { MinimalNode, MinimalNodeEntity, NodePaging } from 'alfresco-js-api';
import { ObjectUtils } from 'ng2-alfresco-core';
import { ObjectUtils, TimeAgoPipe } from 'ng2-alfresco-core';
import { DataColumn, DataRow, DataSorting, DataTableAdapter } from 'ng2-alfresco-datatable';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { DocumentListService } from './../services/document-list.service';
@ -27,8 +27,6 @@ export class ShareDataTableAdapter implements DataTableAdapter {
ERR_ROW_NOT_FOUND: string = 'Row not found';
ERR_COL_NOT_FOUND: string = 'Column not found';
DEFAULT_DATE_FORMAT: string = 'medium';
private sorting: DataSorting;
private rows: DataRow[];
private columns: DataColumn[];
@ -81,13 +79,11 @@ export class ShareDataTableAdapter implements DataTableAdapter {
}
if (col.type === 'date') {
let datePipe = new DatePipe('en-US');
let format = col.format || this.DEFAULT_DATE_FORMAT;
try {
let result = datePipe.transform(value, format);
const result = this.formatDate(col, value);
return dataRow.cacheValue(col.key, result);
} catch (err) {
console.error(`Error parsing date ${value} to format ${format}`);
console.error(`Error parsing date ${value} to format ${col.format}`);
return 'Error';
}
}
@ -129,6 +125,21 @@ export class ShareDataTableAdapter implements DataTableAdapter {
return dataRow.cacheValue(col.key, value);
}
formatDate(col: DataColumn, value: any): string {
if (col.type === 'date') {
const format = col.format || 'medium';
if (format === 'timeAgo') {
const timeAgoPipe = new TimeAgoPipe();
return timeAgoPipe.transform(value);
} else {
const datePipe = new DatePipe('en-US');
return datePipe.transform(value, format);
}
}
return value;
}
getSorting(): DataSorting {
return this.sorting;
}