diff --git a/demo-shell/src/app/components/datatable/datatable.component.html b/demo-shell/src/app/components/datatable/datatable.component.html index 1fe4a8fbc4..f143ffde1c 100644 --- a/demo-shell/src/app/components/datatable/datatable.component.html +++ b/demo-shell/src/app/components/datatable/datatable.component.html @@ -39,6 +39,17 @@ --> + + + + + + + + diff --git a/demo-shell/src/app/components/datatable/datatable.component.ts b/demo-shell/src/app/components/datatable/datatable.component.ts index 58b193c38c..f65d888c03 100644 --- a/demo-shell/src/app/components/datatable/datatable.component.ts +++ b/demo-shell/src/app/components/datatable/datatable.component.ts @@ -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}`); diff --git a/docs/core/components/data-column.component.md b/docs/core/components/data-column.component.md index b0259688b0..6ab5c1c342 100644 --- a/docs/core/components/data-column.component.md +++ b/docs/core/components/data-column.component.md @@ -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. | diff --git a/docs/core/components/datatable.component.md b/docs/core/components/datatable.component.md index 088f6ea5e2..faa23e04ba 100644 --- a/docs/core/components/datatable.component.md +++ b/docs/core/components/datatable.component.md @@ -257,7 +257,7 @@ You can add [Data column component](data-column.component.md) instances to defin
- My custom value + My custom value
@@ -312,6 +312,27 @@ while the data for the table is loading: } ``` +You can also show main menu for datatable using `` + +```html + + + + + + + + + +``` + +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 `` 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)"> - + diff --git a/lib/core/datatable/components/columns-selector/columns-selector.component.html b/lib/core/datatable/components/columns-selector/columns-selector.component.html new file mode 100644 index 0000000000..17bfb9f804 --- /dev/null +++ b/lib/core/datatable/components/columns-selector/columns-selector.component.html @@ -0,0 +1,52 @@ +
+
+ + {{"ADF-DATATABLE.COLUMNS_SELECTOR.COLUMNS" | translate}} + + + +
+ + + +
+ + search + + + +
+ + +
+ +
{{translatedTitle}}
+
+
+
+ + + + +
diff --git a/lib/core/datatable/components/columns-selector/columns-selector.component.scss b/lib/core/datatable/components/columns-selector/columns-selector.component.scss new file mode 100644 index 0000000000..1757a5c192 --- /dev/null +++ b/lib/core/datatable/components/columns-selector/columns-selector.component.scss @@ -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); + } +} diff --git a/lib/core/datatable/components/columns-selector/columns-selector.component.spec.ts b/lib/core/datatable/components/columns-selector/columns-selector.component.spec.ts new file mode 100644 index 0000000000..a42fc77d28 --- /dev/null +++ b/lib/core/datatable/components/columns-selector/columns-selector.component.spec.ts @@ -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; + let loader: HarnessLoader; + + let component: ColumnsSelectorComponent; + let inputColumns: DataColumn[] = []; + + const menuOpenedTrigger = new Subject(); + const menuClosedTrigger = new Subject(); + + let mainMenuTrigger: { menuOpened: Observable; menuClosed: Observable }; + + 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); + }); +}); diff --git a/lib/core/datatable/components/columns-selector/columns-selector.component.ts b/lib/core/datatable/components/columns-selector/columns-selector.component.ts new file mode 100644 index 0000000000..251527985d --- /dev/null +++ b/lib/core/datatable/components/columns-selector/columns-selector.component.ts @@ -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(); + + 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(); + } +} diff --git a/lib/core/datatable/components/datatable/datatable.component.html b/lib/core/datatable/components/datatable/datatable.component.html index 88a3423321..2c042abb21 100644 --- a/lib/core/datatable/components/datatable/datatable.component.html +++ b/lib/core/datatable/components/datatable/datatable.component.html @@ -27,7 +27,9 @@
- -
- {{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }} + +
+ + + + + + + + {{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }} +
+ @@ -156,7 +179,7 @@ {{ 'ADF-DATATABLE.ACCESSIBILITY.SELECT_FILE' | translate }}
-
- -
+
- - - - + + + +
{ expect(headerCells[1].innerText).toBe(dataTableSchema[0].title); }); }); + +describe('Show/hide columns', () => { + let fixture: ComponentFixture; + 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); + }); +}); diff --git a/lib/core/datatable/components/datatable/datatable.component.ts b/lib/core/datatable/components/datatable/datatable.component.ts index 461407709d..dc45e39595 100644 --- a/lib/core/datatable/components/datatable/datatable.component.ts +++ b/lib/core/datatable/components/datatable/datatable.component.ts @@ -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; noPermissionTemplate: TemplateRef; loadingTemplate: TemplateRef; + mainActionTemplate: TemplateRef; isSelectAllIndeterminate: boolean = false; isSelectAllChecked: boolean = false; @@ -313,10 +318,16 @@ export class DataTableComponent implements OnInit, AfterContentInit, OnChanges, } onDropHeaderColumn(event: CdkDragDrop): 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; } diff --git a/lib/core/datatable/data/data-column.model.ts b/lib/core/datatable/data/data-column.model.ts index 3eff6ed68a..cbbbe3ee3b 100644 --- a/lib/core/datatable/data/data-column.model.ts +++ b/lib/core/datatable/data/data-column.model.ts @@ -46,4 +46,5 @@ export interface DataColumn { sortingKey?: string; header?: TemplateRef; draggable?: boolean; + isHidden?: boolean; } diff --git a/lib/core/datatable/data/data-table.schema.ts b/lib/core/datatable/data/data-table.schema.ts index 7a5d4d1ca0..bdae7d78de 100644 --- a/lib/core/datatable/data/data-table.schema.ts +++ b/lib/core/datatable/data/data-table.schema.ts @@ -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(); @@ -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; + } } diff --git a/lib/core/datatable/data/object-datacolumn.model.ts b/lib/core/datatable/data/object-datacolumn.model.ts index 075c2a3cb1..b4d6d9151d 100644 --- a/lib/core/datatable/data/object-datacolumn.model.ts +++ b/lib/core/datatable/data/object-datacolumn.model.ts @@ -34,6 +34,7 @@ export class ObjectDataColumn implements DataColumn { sortingKey?: string; header?: TemplateRef; 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; } } diff --git a/lib/core/datatable/datatable.module.ts b/lib/core/datatable/datatable.module.ts index d4a9e2f43a..1d70aacd78 100644 --- a/lib/core/datatable/datatable.module.ts +++ b/lib/core/datatable/datatable.module.ts @@ -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 ] }) diff --git a/lib/core/datatable/directives/main-data-table-action-template.directive.spec.ts b/lib/core/datatable/directives/main-data-table-action-template.directive.spec.ts new file mode 100644 index 0000000000..44c9b468a1 --- /dev/null +++ b/lib/core/datatable/directives/main-data-table-action-template.directive.spec.ts @@ -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; + 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); + }); +}); diff --git a/lib/core/datatable/directives/main-data-table-action-template.directive.ts b/lib/core/datatable/directives/main-data-table-action-template.directive.ts new file mode 100644 index 0000000000..1e5a97b333 --- /dev/null +++ b/lib/core/datatable/directives/main-data-table-action-template.directive.ts @@ -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; + } + } +} diff --git a/lib/core/datatable/public-api.ts b/lib/core/datatable/public-api.ts index 533de02367..0ea838113b 100644 --- a/lib/core/datatable/public-api.ts +++ b/lib/core/datatable/public-api.ts @@ -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'; diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index 276e84da08..de999306a7 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -350,6 +350,11 @@ "PRESENTATION": "Presentation", "SPREADSHEET": "Spreadsheet", "MISCELLANEOUS": "Miscellaneous" + }, + "COLUMNS_SELECTOR": { + "COLUMNS": "Columns", + "SEARCH": "Search", + "APPLY": "Apply" } }, "USER_PROFILE": { diff --git a/lib/core/pipes/filter-out-every-object-by-prop.pipe.spec.ts b/lib/core/pipes/filter-out-every-object-by-prop.pipe.spec.ts new file mode 100644 index 0000000000..ca7bfd7247 --- /dev/null +++ b/lib/core/pipes/filter-out-every-object-by-prop.pipe.spec.ts @@ -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; + + 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); + }); +}); diff --git a/lib/core/pipes/filter-out-every-object-by-prop.pipe.ts b/lib/core/pipes/filter-out-every-object-by-prop.pipe.ts new file mode 100644 index 0000000000..2154a90711 --- /dev/null +++ b/lib/core/pipes/filter-out-every-object-by-prop.pipe.ts @@ -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 implements PipeTransform { + transform(values: T[], filterKey: string, filterValue: any): T[] { + return (values ?? []).filter(value => value[filterKey] !== filterValue); + } +} diff --git a/lib/core/pipes/filter-string.pipe.spec.ts b/lib/core/pipes/filter-string.pipe.spec.ts new file mode 100644 index 0000000000..380a79afbd --- /dev/null +++ b/lib/core/pipes/filter-string.pipe.spec.ts @@ -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'); + }); +}); diff --git a/lib/core/pipes/filter-string.pipe.ts b/lib/core/pipes/filter-string.pipe.ts new file mode 100644 index 0000000000..8b2725a50f --- /dev/null +++ b/lib/core/pipes/filter-string.pipe.ts @@ -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 : ''; + } +} diff --git a/lib/core/pipes/pipe.module.ts b/lib/core/pipes/pipe.module.ts index 0ac38f822e..de11fb9731 100644 --- a/lib/core/pipes/pipe.module.ts +++ b/lib/core/pipes/pipe.module.ts @@ -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 { diff --git a/lib/core/pipes/public-api.ts b/lib/core/pipes/public-api.ts index 1fd17a9f0e..449abb0baa 100644 --- a/lib/core/pipes/public-api.ts +++ b/lib/core/pipes/public-api.ts @@ -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';