[AAE-7817] Show hide columns on datatable (#7580)

* [AAE-7817]  Show hide columns for data-table

* Update

* update tests and uses material harness

* added pipes test

* update

* update

* added tests for datatable

* update

* Added documentation

* Fix for drop column header

* lint fix

* fix lint
This commit is contained in:
Bartosz Sekuła
2022-05-10 19:09:26 +02:00
committed by GitHub
parent 3a5daf960c
commit 7629797408
25 changed files with 862 additions and 34 deletions

View File

@@ -39,6 +39,17 @@
</data-column>
</data-columns>
-->
<adf-main-menu-datatable-template>
<ng-template let-mainMenuTrigger>
<adf-datatable-column-selector
[columns]="data.getColumns()"
[mainMenuTrigger]="mainMenuTrigger"
(submitColumnsVisibility)="onColumnsVisibilityChange($event)">
</adf-datatable-column-selector>
</ng-template>
</adf-main-menu-datatable-template>
</adf-datatable>
<ng-template #customColumnHeaderTemplate>

View File

@@ -326,6 +326,10 @@ export class DataTableComponent {
];
}
onColumnsVisibilityChange(columns: DataColumn[]): void {
this.data.setColumns(columns);
}
onExecuteRowAction(event: DataRowActionEvent) {
const args = event.value;
window.alert(`My custom action: ${args.action.title}`);

View File

@@ -53,6 +53,7 @@ Defines column properties for DataTable, Tasklist, Document List and other compo
| key | `string` | | Data source key. Can be either a column/property key like `title` or a property path like `createdBy.name`. |
| sortable | `boolean` | true | Toggles ability to sort by this column, for example by clicking the column header. |
| draggable | `boolean` | false | Toggles drag and drop for header column. |
| isHidden | `boolean` | false | Hides columns |
| sortingKey | `string` | | When using server side sorting the column used by the api call where the sorting will be performed |
| srTitle | `string` | | Title to be used for screen readers. |
| title | `string` | "" | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically. |

View File

@@ -257,7 +257,7 @@ You can add [Data column component](data-column.component.md) instances to defin
<!--Add your custom empty template here-->
<ng-template>
<div></div>
<span> My custom value </spam>
<span> My custom value </span>
</ng-template>
</data-column>
</adf-datatable>
@@ -312,6 +312,27 @@ while the data for the table is loading:
}
```
You can also show main menu for datatable using `<adf-main-menu-datatable-template>`
```html
<adf-datatable ...>
<adf-main-menu-datatable-template>
<ng-template let-mainMenuTrigger>
<!--Add your custom main menu template here-->
<adf-datatable-column-selector
[columns]="data.getColumns()"
[mainMenuTrigger]="mainMenuTrigger"
(submitColumnsVisibility)="onColumnsVisibilityChange($event)">
</adf-datatable-column-selector>
</ng-template>
</adf-main-menu-datatable-template>
</adf-datatable>
```
Provided template receives `let-mainMenuTrigger`, so you can programaticaly work with the menu (please see [MatMenuTrigger](https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically)).
For convenience, you can use `<adf-datatable-column-selector>` which will allow you to change column visibility.
\###Styling transcluded content
When adding your custom templates you can style them as you like. However, for an out of the box experience, if you want to apply datatable styles to your column you will need to follow this structure:
@@ -375,6 +396,8 @@ Learm more about styling your datatable: [Customizing the component's styles](#c
| rowDblClick | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataRowEvent`](../../../lib/core/datatable/data/data-row-event.model.ts)`>` | Emitted when the user double-clicks a row. |
| showRowActionsMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/datatable/components/data-cell.event.ts)`>` | Emitted before the actions menu is displayed for a row. |
| showRowContextMenu | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataCellEvent`](../../../lib/core/datatable/components/data-cell.event.ts)`>` | Emitted before the context menu is displayed for a row. |
| columnOrderChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`DataColumn`](../../../lib/core/datatable/components/data-cell.event.ts)`>` | Emitted after dragging and dropping column header. |
## Details
@@ -440,7 +463,7 @@ Given that DataTable raises bubbling DOM events, you can handle drop behavior fr
(header-drop)="onDrop($event)"
(cell-dragover)="onDragOver($event)"
(cell-drop)="onDrop($event)">
<adf-datatable [data]="data">
</adf-datatable>
</div>

View File

@@ -0,0 +1,52 @@
<div class="adf-columns-selector" (click)="$event.stopPropagation();">
<div class="adf-columns-selector-header">
<span class="adf-columns-selector-header-label">
{{"ADF-DATATABLE.COLUMNS_SELECTOR.COLUMNS" | translate}}
</span>
<button mat-icon-button (click)="closeMenu()">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider class="adf-columns-selector-divider"></mat-divider>
<div class="adf-columns-selector-search-input-container">
<mat-icon
class="adf-columns-selector-search-input-icon">
search
</mat-icon>
<input
[formControl]="searchInputControl"
class="adf-columns-selector-search-input"
type="text"
[placeholder]='"ADF-DATATABLE.COLUMNS_SELECTOR.SEARCH" | translate'>
</div>
<ng-container *ngFor="let column of columnItems">
<div
*ngIf="(column.title | translate | filterString:searchQuery) as translatedTitle"
class="adf-columns-selector-list-item-container">
<mat-checkbox
color="primary"
class="adf-columns-selector-column-checkbox"
[attr.data-automation-id]="'adf-columns-selector-column-checkbox-' + column.title"
[checked]="!column.isHidden"
(change)="changeColumnVisibility(column)">
<div class="adf-columns-selector-list-content">{{translatedTitle}}</div>
</mat-checkbox>
</div>
</ng-container>
<mat-divider class="adf-columns-selector-divider"></mat-divider>
<div class="adf-columns-selector-footer">
<button
mat-flat-button
color="primary"
(click)="apply()">
{{"ADF-DATATABLE.COLUMNS_SELECTOR.APPLY" | translate}}
</button>
</div>
</div>

View File

@@ -0,0 +1,87 @@
$adf-columns-selector-space: 12px;
@mixin adf-columns-selector-side-padding {
padding: 0 $adf-columns-selector-space;
}
@mixin adf-columns-selector-top-bottom-padding {
padding: $adf-columns-selector-space 0;
}
.adf-columns-selector {
@include adf-columns-selector-top-bottom-padding;
min-width: 277px;
&-header {
@include adf-columns-selector-side-padding;
display: flex;
justify-content: space-between;
align-items: center;
}
&-header-label {
font-size: var(--theme-body-1-font-size);
}
&-list-item-container {
margin-top: 10px;
&:hover {
background-color: var(--theme-bg-hover-color);
}
}
&-list-content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
width: 210px;
}
&-column-checkbox {
padding: 0 20px;
}
&-footer {
@include adf-columns-selector-side-padding;
display: flex;
justify-content: flex-end;
}
&-divider {
margin: 16px 0;
}
&-search-input-container {
@include adf-columns-selector-side-padding;
position: relative;
display: flex;
align-items: center;
margin-bottom: 15px;
}
&-search-input {
padding: 10px 10px 10px 29px;
width: 100%;
outline: 0;
border-radius: 6px;
border: 1px solid var(--theme-background-color);
background: var(--theme-background-color);
:focus {
outline: none !important;
}
}
&-search-input-icon {
position: absolute;
left: 17px;
top: 10px;
font-size: var(--theme-adf-icon-1-font-size);
}
}

View File

@@ -0,0 +1,162 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ColumnsSelectorComponent } from './columns-selector.component';
import { DataColumn } from '../../data/data-column.model';
import { Observable, Subject } from 'rxjs';
import { MatMenuTrigger } from '@angular/material/menu';
import { CoreTestingModule } from 'core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import { MatCheckboxHarness } from '@angular/material/checkbox/testing';
describe('ColumnsSelectorComponent', () => {
let fixture: ComponentFixture<ColumnsSelectorComponent>;
let loader: HarnessLoader;
let component: ColumnsSelectorComponent;
let inputColumns: DataColumn[] = [];
const menuOpenedTrigger = new Subject<void>();
const menuClosedTrigger = new Subject<void>();
let mainMenuTrigger: { menuOpened: Observable<void>; menuClosed: Observable<void> };
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
declarations: [ColumnsSelectorComponent]
}).compileComponents();
fixture = TestBed.createComponent(ColumnsSelectorComponent);
loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
inputColumns = [{
id: 'id0',
key: 'key0',
title: 'title0',
type: 'text'
}, {
id: 'id1',
key: 'key1',
title: 'title1',
type: 'text'
}, {
id: 'id2',
key: 'key2',
title: 'title2',
type: 'text'
}, {
id: 'id3',
key: 'NoTitle',
type: 'text'
}, {
id: 'id4',
key: 'IsHidden',
type: 'text',
title: 'title4',
isHidden: true
}];
mainMenuTrigger = {
menuOpened: menuOpenedTrigger.asObservable(),
menuClosed: menuClosedTrigger.asObservable()
};
component.columns = inputColumns;
component.mainMenuTrigger = mainMenuTrigger as MatMenuTrigger;
fixture.detectChanges();
});
it('should clear search after closing menu', fakeAsync(() => {
menuOpenedTrigger.next();
fixture.detectChanges();
let searchInput = fixture.debugElement.query(By.css('.adf-columns-selector-search-input')).nativeElement;
searchInput.value = 'TEST';
searchInput.dispatchEvent(new Event('input'));
tick(300);
expect(searchInput.value).toBe('TEST');
menuClosedTrigger.next();
tick(300);
searchInput = fixture.debugElement.query(By.css('.adf-columns-selector-search-input')).nativeElement;
expect(searchInput.value).toBe('');
}));
it('should list only columns with title', async () => {
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(checkboxes.length).toBe(4);
expect(await checkboxes[0].getLabelText()).toBe(inputColumns[0].title);
expect(await checkboxes[1].getLabelText()).toBe(inputColumns[1].title);
expect(await checkboxes[2].getLabelText()).toBe(inputColumns[2].title);
expect(await checkboxes[3].getLabelText()).toBe(inputColumns[4].title);
});
it('should filter columns by search text', fakeAsync(async () => {
fixture.detectChanges();
menuOpenedTrigger.next();
const searchInput = fixture.debugElement.query(By.css('.adf-columns-selector-search-input')).nativeElement;
searchInput.value = inputColumns[0].title;
searchInput.dispatchEvent(new Event('input'));
tick(400);
fixture.detectChanges();
const columnCheckboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(columnCheckboxes.length).toBe(1);
expect(await columnCheckboxes[0].getLabelText()).toBe(inputColumns[0].title);
}));
it('should change column visibility', async () => {
menuOpenedTrigger.next();
fixture.detectChanges();
const firstColumnCheckbox = await loader.getHarness(MatCheckboxHarness);
await firstColumnCheckbox.toggle();
expect(component.columnItems[0].isHidden).toBe(true);
});
it('should set proper default state for checkboxes', async () => {
menuOpenedTrigger.next();
fixture.detectChanges();
const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness);
expect(await checkboxes[0].isChecked()).toBe(true);
expect(await checkboxes[1].isChecked()).toBe(true);
expect(await checkboxes[2].isChecked()).toBe(true);
expect(await checkboxes[3].isChecked()).toBe(false);
});
});

View File

@@ -0,0 +1,83 @@
/*!
* @license
* Copyright 2019 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, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatMenuTrigger } from '@angular/material/menu';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { DataColumn } from '../../data/data-column.model';
@Component({
selector: 'adf-datatable-column-selector',
templateUrl: './columns-selector.component.html',
styleUrls: ['./columns-selector.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ColumnsSelectorComponent implements OnInit, OnDestroy {
@Input()
columns: DataColumn[] = [];
@Input()
mainMenuTrigger: MatMenuTrigger;
@Output()
submitColumnsVisibility = new EventEmitter<DataColumn[]>();
onDestroy$ = new Subject();
columnItems: DataColumn[] = [];
searchInputControl = new FormControl('');
searchQuery = '';
ngOnInit(): void {
this.mainMenuTrigger.menuOpened.pipe(
takeUntil(this.onDestroy$)
).subscribe(() => {
this.columnItems = this.columns.map(column => ({...column}));
});
this.mainMenuTrigger.menuClosed.pipe(
takeUntil(this.onDestroy$)
).subscribe(() => {
this.searchInputControl.setValue('');
});
this.searchInputControl.valueChanges.pipe(
debounceTime(300),
takeUntil(this.onDestroy$)
).subscribe((searchQuery) => {
this.searchQuery = searchQuery;
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
closeMenu(): void {
this.mainMenuTrigger.closeMenu();
}
changeColumnVisibility(column: DataColumn): void {
column.isHidden = !column.isHidden;
}
apply(): void {
this.submitColumnsVisibility.emit(this.columnItems);
this.closeMenu();
}
}

View File

@@ -27,7 +27,9 @@
</div>
<div class="adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}} adf-datatable-cell-header"
*ngFor="let col of data.getColumns(); let columnIndex = index"
*ngFor="
let col of (data.getColumns() | filterOutEvery:'isHidden':true);
let columnIndex = index"
[class.adf-sortable]="col.sortable"
[attr.data-automation-id]="'auto_id_' + col.key"
[class.adf-datatable__header--sorted-asc]="isColumnSorted(col, 'asc')"
@@ -93,10 +95,31 @@
<div class="adf-drop-header-cell-placeholder" *cdkDragPlaceholder></div>
</div>
<!-- Actions (right) -->
<div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell">
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
<!-- Header actions (right) -->
<div
*ngIf="actions && actionsPosition === 'right' || mainActionTemplate"
class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell"
>
<ng-container *ngIf="mainActionTemplate">
<button
mat-icon-button
#mainMenuTrigger="matMenuTrigger"
[matMenuTriggerFor]="mainMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #mainMenu>
<ng-container
[ngTemplateOutlet]="mainActionTemplate"
[ngTemplateOutletContext]="{
$implicit: mainMenuTrigger
}">
</ng-container>
</mat-menu>
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
</ng-container>
</div>
</adf-datatable-row>
<mat-form-field *ngIf="display === 'gallery' && isHeaderVisible()">
<mat-select [value]="getSortingKey()" [attr.data-automation-id]="'grid-view-sorting'">
@@ -156,7 +179,7 @@
{{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate }}
</mat-checkbox>
</div>
<div *ngFor="let col of data.getColumns()"
<div *ngFor="let col of (data.getColumns() | filterOutEvery:'isHidden':true);"
role="gridcell"
class="adf-datatable-cell adf-datatable-cell--{{col.type || 'text'}} {{col.cssClass}}"
[attr.title]="col.title | translate"
@@ -262,28 +285,31 @@
</div>
</div>
<!-- Actions (right) -->
<div *ngIf="actions && actionsPosition === 'right'"
<!-- Row actions (right) -->
<div *ngIf="(actions && actionsPosition === 'right') || mainActionTemplate"
role="gridcell"
class="adf-datatable-cell adf-datatable__actions-cell adf-datatable-center-actions-column-ie">
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.ROW_OPTION_BUTTON' | translate"
[title]="'ADF-DATATABLE.CONTENT-ACTIONS.TOOLTIP' | translate"
[attr.id]="'action_menu_right_' + idx"
[attr.data-automation-id]="'action_menu_' + idx">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let action of getRowActions(row)"
[attr.data-automation-id]="action.title"
[attr.aria-label]="action.title | translate"
[disabled]="action.disabled"
(click)="onExecuteRowAction(row, action)">
<mat-icon *ngIf="action.icon">{{ action.icon }}</mat-icon>
<span>{{ action.title | translate }}</span>
<ng-container *ngIf="(actions && actionsPosition === 'right')">
<button mat-icon-button [matMenuTriggerFor]="menu" #actionsMenuTrigger="matMenuTrigger"
[ngClass]="getHideActionsWithoutHoverClass(actionsMenuTrigger)"
[attr.aria-label]="'ADF-DATATABLE.ACCESSIBILITY.ROW_OPTION_BUTTON' | translate"
[title]="'ADF-DATATABLE.CONTENT-ACTIONS.TOOLTIP' | translate"
[attr.id]="'action_menu_right_' + idx"
[attr.data-automation-id]="'action_menu_' + idx">
<mat-icon>more_vert</mat-icon>
</button>
</mat-menu>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let action of getRowActions(row)"
[attr.data-automation-id]="action.title"
[attr.aria-label]="action.title | translate"
[disabled]="action.disabled"
(click)="onExecuteRowAction(row, action)">
<mat-icon *ngIf="action.icon">{{ action.icon }}</mat-icon>
<span>{{ action.title | translate }}</span>
</button>
</mat-menu>
</ng-container>
</div>
</adf-datatable-row>
<div *ngIf="isEmpty()"

View File

@@ -1720,3 +1720,70 @@ describe('Drag&Drop column header', () => {
expect(headerCells[1].innerText).toBe(dataTableSchema[0].title);
});
});
describe('Show/hide columns', () => {
let fixture: ComponentFixture<DataTableComponent>;
let dataTable: DataTableComponent;
let data: DataColumn[] = [];
let dataTableSchema: DataColumn[] = [];
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
declarations: [CustomColumnTemplateComponent],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance;
data = [
{ id: '1', title: 'name1', key: 'key', type: 'text' },
{ id: '2', title: 'name1', key: 'key', type: 'text' },
{ id: '3', title: 'name1', key: 'key', type: 'text' }
];
dataTableSchema = [
new ObjectDataColumn({ key: 'id', title: 'ID' }),
new ObjectDataColumn({ key: 'name', title: 'Name'}),
new ObjectDataColumn({ key: 'status', title: 'status', isHidden: true })
];
dataTable.data = new ObjectDataTableAdapter(
[...data],
[...dataTableSchema]
);
fixture.detectChanges();
});
it('should hide columns with isHidden prop', () => {
const headerCells = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell--text.adf-datatable-cell-header');
expect(headerCells.length).toBe(2);
});
it('should reload columns after changing columns visibility', () => {
const columns = [
new ObjectDataColumn({ key: 'id', title: 'ID' }),
new ObjectDataColumn({ key: 'name', title: 'Name', isHidden: true }),
new ObjectDataColumn({ key: 'status', title: 'status', isHidden: true })
];
dataTable.ngOnChanges({
columns: {
previousValue: undefined,
currentValue: columns,
firstChange: false,
isFirstChange: () => false
}
});
fixture.detectChanges();
const headerCells = fixture.debugElement.nativeElement.querySelectorAll('.adf-datatable-cell--text.adf-datatable-cell-header');
expect(headerCells.length).toBe(1);
});
});

View File

@@ -104,6 +104,10 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
@Input()
multiselect: boolean = false;
/** Toggles main data table action column. */
@Input()
mainTableAction: boolean = true;
/** Toggles the data actions column. */
@Input()
actions: boolean = false;
@@ -200,6 +204,7 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
noContentTemplate: TemplateRef<any>;
noPermissionTemplate: TemplateRef<any>;
loadingTemplate: TemplateRef<any>;
mainActionTemplate: TemplateRef<any>;
isSelectAllIndeterminate: boolean = false;
isSelectAllChecked: boolean = false;
@@ -313,10 +318,16 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges,
}
onDropHeaderColumn(event: CdkDragDrop<unknown>): void {
const columns = this.data.getColumns();
moveItemInArray(columns, event.previousIndex, event.currentIndex);
const allColumns = this.data.getColumns();
const shownColumns = allColumns.filter(column => !column.isHidden);
const hiddenColumns = allColumns.filter(column => column.isHidden);
moveItemInArray(shownColumns, event.previousIndex, event.currentIndex);
const allColumnsWithNewOrder = [...shownColumns, ...hiddenColumns];
this.setTableColumns(allColumnsWithNewOrder);
this.columnOrderChanged.emit(allColumnsWithNewOrder);
this.columnOrderChanged.emit(columns);
this.isDraggingHeaderColumn = false;
}

View File

@@ -46,4 +46,5 @@ export interface DataColumn {
sortingKey?: string;
header?: TemplateRef<any>;
draggable?: boolean;
isHidden?: boolean;
}

View File

@@ -38,6 +38,9 @@ export abstract class DataTableSchema {
protected columnsOrder: string[] | undefined;
protected columnsOrderedByKey: string = 'id';
protected hiddenColumns: string[] | undefined;
protected hiddenColumnsKey: string = 'id';
private layoutPresets = {};
private columnsSchemaSubject$ = new ReplaySubject<boolean>();
@@ -59,7 +62,8 @@ export abstract class DataTableSchema {
}
public createColumns(): void {
const columns = this.mergeJsonAndHtmlSchema();
const allColumns = this.mergeJsonAndHtmlSchema();
const columns = this.setHiddenColumns(allColumns);
this.columns = this.sortColumnsByKey(columns);
}
@@ -127,4 +131,19 @@ export abstract class DataTableSchema {
return [...columnsWithProperOrder, ...defaultColumns];
}
private setHiddenColumns(columns: DataColumn[]): DataColumn[] {
if (this.hiddenColumns) {
return columns.map(column => {
const columnShouldBeHidden = this.hiddenColumns.includes(column[this.hiddenColumnsKey]);
return {
...column,
isHidden: columnShouldBeHidden
};
});
}
return columns;
}
}

View File

@@ -34,6 +34,7 @@ export class ObjectDataColumn implements DataColumn {
sortingKey?: string;
header?: TemplateRef<any>;
draggable: boolean;
isHidden: boolean;
constructor(input: any) {
this.id = input.id ?? '';
@@ -50,5 +51,6 @@ export class ObjectDataColumn implements DataColumn {
this.sortingKey = input.sortingKey;
this.header = input.header;
this.draggable = input.draggable ?? false;
this.isHidden = input.isHidden ?? false;
}
}

View File

@@ -29,6 +29,7 @@ import { DataTableCellComponent } from './components/datatable-cell/datatable-ce
import { DataTableRowComponent } from './components/datatable-row/datatable-row.component';
import { DataTableComponent } from './components/datatable/datatable.component';
import { DateCellComponent } from './components/date-cell/date-cell.component';
import { ColumnsSelectorComponent } from './components/columns-selector/columns-selector.component';
import { EmptyListBodyDirective,
EmptyListComponent,
EmptyListFooterDirective,
@@ -42,12 +43,14 @@ import { HeaderFilterTemplateDirective } from './directives/header-filter-templa
import { CustomEmptyContentTemplateDirective } from './directives/custom-empty-content-template.directive';
import { CustomLoadingContentTemplateDirective } from './directives/custom-loading-template.directive';
import { CustomNoPermissionTemplateDirective } from './directives/custom-no-permission-template.directive';
import { MainMenuDataTableTemplateDirective } from './directives/main-data-table-action-template.directive';
import { JsonCellComponent } from './components/json-cell/json-cell.component';
import { ClipboardModule } from '../clipboard/clipboard.module';
import { DropZoneDirective } from './directives/drop-zone.directive';
import { DataColumnModule } from '../data-column/data-column.module';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { IconModule } from '../icon/icon.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
@@ -61,7 +64,9 @@ import { IconModule } from '../icon/icon.module';
DirectiveModule,
ClipboardModule,
DragDropModule,
IconModule
IconModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
DataTableComponent,
@@ -75,6 +80,7 @@ import { IconModule } from '../icon/icon.module';
FileSizeCellComponent,
LocationCellComponent,
JsonCellComponent,
ColumnsSelectorComponent,
NoContentTemplateDirective,
NoPermissionTemplateDirective,
LoadingContentTemplateDirective,
@@ -82,6 +88,7 @@ import { IconModule } from '../icon/icon.module';
CustomEmptyContentTemplateDirective,
CustomLoadingContentTemplateDirective,
CustomNoPermissionTemplateDirective,
MainMenuDataTableTemplateDirective,
DropZoneDirective
],
exports: [
@@ -93,6 +100,7 @@ import { IconModule } from '../icon/icon.module';
DataTableCellComponent,
DataTableRowComponent,
DateCellComponent,
ColumnsSelectorComponent,
FileSizeCellComponent,
LocationCellComponent,
JsonCellComponent,
@@ -103,6 +111,7 @@ import { IconModule } from '../icon/icon.module';
CustomEmptyContentTemplateDirective,
CustomLoadingContentTemplateDirective,
CustomNoPermissionTemplateDirective,
MainMenuDataTableTemplateDirective,
DropZoneDirective
]
})

View File

@@ -0,0 +1,44 @@
/*!
* @license
* Copyright 2019 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 { TestBed, ComponentFixture } from '@angular/core/testing';
import { DataTableComponent } from '../components/datatable/datatable.component';
import { MainMenuDataTableTemplateDirective } from './main-data-table-action-template.directive';
describe('MainMenuDataTableTemplateDirective', () => {
let fixture: ComponentFixture<DataTableComponent>;
let dataTable: DataTableComponent;
let directive: MainMenuDataTableTemplateDirective;
beforeEach(() => {
fixture = TestBed.createComponent(DataTableComponent);
dataTable = fixture.componentInstance;
directive = new MainMenuDataTableTemplateDirective(dataTable);
});
afterEach(() => {
fixture.destroy();
});
it('applies template to the datatable', () => {
const template: any = 'test template';
directive.template = template;
directive.ngAfterContentInit();
expect(dataTable.mainActionTemplate).toBe(template);
});
});

View File

@@ -0,0 +1,36 @@
/*!
* @license
* Copyright 2019 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 { AfterContentInit, ContentChild, Directive, TemplateRef } from '@angular/core';
import { DataTableComponent } from '../components/datatable/datatable.component';
@Directive({
selector: 'adf-main-menu-datatable-template'
})
export class MainMenuDataTableTemplateDirective implements AfterContentInit {
@ContentChild(TemplateRef)
template: any;
constructor(private dataTable: DataTableComponent) {}
ngAfterContentInit() {
if (this.dataTable) {
this.dataTable.mainActionTemplate = this.template;
}
}
}

View File

@@ -38,6 +38,7 @@ export * from './components/empty-list/empty-list.component';
export * from './components/filesize-cell/filesize-cell.component';
export * from './components/json-cell/json-cell.component';
export * from './components/location-cell/location-cell.component';
export * from './components/columns-selector/columns-selector.component';
export * from './data/data-table.schema';
export * from './directives/loading-template.directive';

View File

@@ -350,6 +350,11 @@
"PRESENTATION": "Presentation",
"SPREADSHEET": "Spreadsheet",
"MISCELLANEOUS": "Miscellaneous"
},
"COLUMNS_SELECTOR": {
"COLUMNS": "Columns",
"SEARCH": "Search",
"APPLY": "Apply"
}
},
"USER_PROFILE": {

View File

@@ -0,0 +1,75 @@
/*!
* @license
* Copyright 2019 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 { FilterOutArrayObjectsByPropPipe } from './filter-out-every-object-by-prop.pipe';
describe('FilterOutArrayObjectsByPropPipe', () => {
let pipe: FilterOutArrayObjectsByPropPipe<any>;
beforeEach(() => {
pipe = new FilterOutArrayObjectsByPropPipe();
});
it('should filter out object', () => {
const testArray = [{
id: 1
}, {
id: 2
}, {
id: 3
}];
const result = pipe.transform(testArray, 'id', 3);
expect(result.length).toBe(testArray.length - 1);
expect(result[0]).toEqual(testArray[0]);
expect(result[1]).toEqual(testArray[1]);
});
it('should filter out multiple objects', () => {
const testArray = [{
isHidden: true
}, {
isHidden: true
}, {
isHidden: true
}, {
isHidden: false
}];
const result = pipe.transform(testArray, 'isHidden', true);
expect(result.length).toBe(1);
expect(result[0]).toEqual(testArray[3]);
});
it('should work with empty array', () => {
const testArray = [];
const result = pipe.transform(testArray, 'prop', true);
expect(result.length).toBe(0);
});
it('should work with non existing prop', () => {
const testArray = [{ prop: 1 }];
const result = pipe.transform(testArray, 'nonExistionProp', 1);
expect(result.length).toBe(1);
});
});

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'filterOutEvery' })
export class FilterOutArrayObjectsByPropPipe<T> implements PipeTransform {
transform(values: T[], filterKey: string, filterValue: any): T[] {
return (values ?? []).filter(value => value[filterKey] !== filterValue);
}
}

View File

@@ -0,0 +1,44 @@
/*!
* @license
* Copyright 2019 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 { FilterStringPipe } from './filter-string.pipe';
describe('FilterStringPipe', () => {
let pipe: FilterStringPipe;
beforeEach(() => {
pipe = new FilterStringPipe();
});
it('should left string', () => {
const result = pipe.transform('ABC', 'B');
expect(result).toBe('ABC');
});
it('should filter out string', () => {
const result = pipe.transform('ABC', 'D');
expect(result).toBe('');
});
it('should left string when no query string is passed', () => {
const result = pipe.transform('ABC');
expect(result).toBe('ABC');
});
});

View File

@@ -0,0 +1,30 @@
/*!
* @license
* Copyright 2019 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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'filterString' })
export class FilterStringPipe implements PipeTransform {
transform(value: string = '', filterBy: string = ''): string {
const testResult = filterBy ?
value.toLowerCase().indexOf(filterBy.toLowerCase()) > -1 :
true;
return testResult ? value : '';
}
}

View File

@@ -34,6 +34,8 @@ import { LocalizedRolePipe } from './localized-role.pipe';
import { TranslateModule } from '@ngx-translate/core';
import { MomentDatePipe } from './moment-date.pipe';
import { MomentDateTimePipe } from './moment-datetime.pipe';
import { FilterStringPipe } from './filter-string.pipe';
import { FilterOutArrayObjectsByPropPipe } from './filter-out-every-object-by-prop.pipe';
@NgModule({
imports: [
@@ -55,7 +57,9 @@ import { MomentDateTimePipe } from './moment-datetime.pipe';
DecimalNumberPipe,
LocalizedRolePipe,
MomentDatePipe,
MomentDateTimePipe
MomentDateTimePipe,
FilterStringPipe,
FilterOutArrayObjectsByPropPipe
],
providers: [
FileSizePipe,
@@ -71,7 +75,9 @@ import { MomentDateTimePipe } from './moment-datetime.pipe';
DecimalNumberPipe,
LocalizedRolePipe,
MomentDatePipe,
MomentDateTimePipe
MomentDateTimePipe,
FilterStringPipe,
FilterOutArrayObjectsByPropPipe
],
exports: [
FileSizePipe,
@@ -88,7 +94,9 @@ import { MomentDateTimePipe } from './moment-datetime.pipe';
DecimalNumberPipe,
LocalizedRolePipe,
MomentDatePipe,
MomentDateTimePipe
MomentDateTimePipe,
FilterStringPipe,
FilterOutArrayObjectsByPropPipe
]
})
export class PipeModule {

View File

@@ -31,3 +31,5 @@ export * from './localized-role.pipe';
export * from './pipe.module';
export * from './moment-date.pipe';
export * from './moment-datetime.pipe';
export * from './filter-string.pipe';
export * from './filter-out-every-object-by-prop.pipe';