Skip to content

Commit

Permalink
feat: table row selection (onecx#82)
Browse files Browse the repository at this point in the history
* feat: create DataTableComponent storybook entry

* feat: basic selection feature for data-table component

* feat: connect selection feature to parent components

* test: add smoke tests to interactive-data-view

* test: add smoke tests to data-view

* test: add tests to data-table

* refactor: improve data-table tests & harness

* fix: use combineLatest instead of withLatestFrom

* fix: fix harness loading

---------

Co-authored-by: Bastian Jakobi <[email protected]>
  • Loading branch information
bastianjakobi and Bastian Jakobi committed Feb 5, 2024
1 parent 301273a commit 10bd6eb
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
currentPageReportTemplate="{{ (totalRecordsOnServer ? currentPageShowingWithTotalOnServerKey : currentPageShowingKey) | translate:params }}"
[rowsPerPageOptions]="[10, 25, 50, rows?.length]"
id="dataTable_{{name}}"
(selectionChange)="onSelectionChange($event)"
[selection]="(selectedRows$ | async) || []"
>
<ng-template let-item pTemplate="paginatordropdownitem">
{{ item.value === rows?.length ? ("OCX_DATA_TABLE.ALL" | translate) : item.value }}
</ng-template>
<ng-template pTemplate="header">
<tr>
<th style="width: 4rem" scope="col" *ngIf="selectionChangedObserved">
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
</th>
<ng-container *ngFor="let column of columns">
<th *ngIf="column.sortable || column.filterable; else simpleHeader" scope="col">
<div class="table-header-wrapper">
Expand Down Expand Up @@ -71,6 +76,9 @@
</ng-template>
<ng-template pTemplate="body" let-rowObject>
<tr>
<td *ngIf="selectionChangedObserved">
<p-tableCheckbox [value]="rowObject"></p-tableCheckbox>
</td>
<td *ngFor="let column of columns">
<ng-container
[ngTemplateOutlet]="
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { TranslateModule, TranslateService } from '@ngx-translate/core'
import { DataTableComponent } from './data-table.component'
import { DataTableComponent, Row } from './data-table.component'
import { PrimeNgModule } from '../../primeng.module';
import { TranslateTestingModule } from 'ngx-translate-testing';
import { ColumnType } from '../../../model/column-type.model';
import { PortalCoreModule } from '../../portal-core.module';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { DataTableHarness } from '../../../../../../../libs/portal-integration-angular/testing';
import { DataTableHarness, PTableCheckboxHarness } from '../../../../../testing';

describe('DataTableComponent', () => {
let fixture: ComponentFixture<DataTableComponent>
let component: DataTableComponent
let translateService: TranslateService
let dataTable: DataTableHarness
let unselectedCheckBoxes: PTableCheckboxHarness[];
let selectedCheckBoxes: PTableCheckboxHarness[];

const ENGLISH_LANGUAGE = 'en';
const ENGLISH_TRANSLATIONS = {
Expand Down Expand Up @@ -207,6 +210,7 @@ describe('DataTableComponent', () => {
translateService = TestBed.inject(TranslateService)
translateService.use('en')
fixture.detectChanges()
dataTable = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness)
})

it('should create the data table component', () => {
Expand Down Expand Up @@ -278,4 +282,50 @@ describe('DataTableComponent', () => {
})
})

describe('Table row selection', () => {
it('should initially show a table without selection checkboxes', async () => {
expect(dataTable).toBeTruthy()
expect(await dataTable.rowSelectionIsEnabled()).toEqual(false)
})

it('should show a table with selection checkboxes if the parent binds to the event emitter',async () => {
expect(await dataTable.rowSelectionIsEnabled()).toEqual(false)
component.selectionChanged.subscribe()
expect(await dataTable.rowSelectionIsEnabled()).toEqual(true)
})

it('should pre-select rows given through selectedRows input', async () => {
component.selectionChanged.subscribe()

unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked')
selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked')
expect(unselectedCheckBoxes.length).toBe(5)
expect(selectedCheckBoxes.length).toBe(0)
component.selectedRows = mockData.slice(0,2)

unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked')
selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked')
expect(selectedCheckBoxes.length).toBe(2)
expect(unselectedCheckBoxes.length).toBe(3)
})
})

it('should emit all selected elements when checkbox is clicked', async () => {
let selectionChangedEvent: Row[] | undefined;

component.selectionChanged.subscribe((event) => (selectionChangedEvent = event))
unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked')
selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked')
expect(unselectedCheckBoxes.length).toBe(5)
expect(selectedCheckBoxes.length).toBe(0)
expect(selectionChangedEvent).toBeUndefined()

const firstRowCheckBox = unselectedCheckBoxes[0]
await firstRowCheckBox.checkBox()
unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked')
selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked')
expect(unselectedCheckBoxes.length).toBe(4)
expect(selectedCheckBoxes.length).toBe(1)
expect(selectionChangedEvent).toEqual([mockData[0]])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Meta, moduleMetadata, applicationConfig, StoryFn } from '@storybook/angular';
import { DataTableComponent } from "./data-table.component";
import { TableModule } from 'primeng/table';
import { TranslateModule } from '@ngx-translate/core';
import { ButtonModule } from 'primeng/button';
import { MultiSelectModule } from 'primeng/multiselect';
import { importProvidersFrom } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ColumnType } from '../../../model/column-type.model';
type DataTableInputTypes = Pick<DataTableComponent, 'rows' | 'columns' | 'emptyResultsMessage' | 'selectedRows'>
const DataTableComponentSBConfig: Meta<DataTableComponent> = {
title: 'DataTableComponent',
component: DataTableComponent,
decorators: [
applicationConfig({
providers: [
importProvidersFrom(BrowserModule),
importProvidersFrom(BrowserAnimationsModule),
importProvidersFrom(TranslateModule.forRoot({})),
]
}),
moduleMetadata({
declarations: [DataTableComponent],
imports: [
TableModule,
TranslateModule,
ButtonModule,
MultiSelectModule
]
})
]
}
const Template: StoryFn = (args) => ({
props: args,
})

const defaultComponentArgs: DataTableInputTypes = {
columns: [
{
id: "product",
columnType: ColumnType.STRING,
nameKey: "Product",
sortable: false
},
{
id: "amount",
columnType: ColumnType.NUMBER,
nameKey: "Amount",
sortable: true
}
],
rows: [
{
id: 1,
product: "Apples",
amount: 2
},
{
id: 2,
product: "Bananas",
amount: 10
},
{
id: 3,
product: "Strawberries",
amount: 5
}
],
emptyResultsMessage: "No results",
selectedRows: []
}

export const WithMockData = {
render: Template,
args: defaultComponentArgs,
}

export const NoData = {
render: Template,
args: {
...defaultComponentArgs,
rows: [],
}
}

export const WithRowSelection = {
argTypes: {
selectionChanged: {action: 'selectionChanged'}
},
render: Template,
args: {
...defaultComponentArgs,
selectionChanged: ($event: any) => console.log("Selection changed ", $event)
}
}

export const WithRowSelectionAndDefaultSelection = {
argTypes: {
selectionChanged: {action: 'selectionChanged'}
},
render: Template,
args: {
...defaultComponentArgs,
selectionChanged: ($event: any) => console.log("Selection changed ", $event),
selectedRows: [
{
id: 1
}
]
}
}

export default DataTableComponentSBConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export class DataTableComponent extends DataSortBase implements OnInit {
set rows(value: Row[]) {
this._rows$.next(value)
}
_selection$ = new BehaviorSubject<Row[]>([])
@Input()
get selectedRows(): Row[] {
return this._selection$.getValue()
}
set selectedRows(value: Row[]) {
this._selection$.next(value)
}
_filters$ = new BehaviorSubject<Filter[]>([])
@Input()
get filters(): Filter[] {
Expand Down Expand Up @@ -128,8 +136,10 @@ export class DataTableComponent extends DataSortBase implements OnInit {
@Output() viewTableRow = new EventEmitter<Row>()
@Output() editTableRow = new EventEmitter<Row>()
@Output() deleteTableRow = new EventEmitter<Row>()
@Output() selectionChanged = new EventEmitter<Row[]>()

displayedRows$: Observable<unknown[]> | undefined
selectedRows$: Observable<unknown[]> | undefined

currentFilterColumn$ = new BehaviorSubject<DataTableColumn | null>(null)
currentFilterOptions$: Observable<SelectItem[]> | undefined
Expand All @@ -154,6 +164,11 @@ export class DataTableComponent extends DataSortBase implements OnInit {
return dv?.deleteItemObserved || dv?.deleteItem.observed || this.deleteTableRow.observed
}

get selectionChangedObserved(): boolean {
const dv = this.injector.get('DataViewComponent', null)
return dv?.selectionChangedObserved || dv?.selectionChanged.observed || this.selectionChanged.observed
}

constructor(@Inject(LOCALE_ID) locale: string, translateService: TranslateService, private router: Router, private injector: Injector) {
super(locale, translateService)
this.name = this.name || this.router.url.replace(/[^A-Za-z0-9]/, '_')
Expand Down Expand Up @@ -209,6 +224,7 @@ export class DataTableComponent extends DataSortBase implements OnInit {
),
map((amounts) => Object.fromEntries(amounts))
)
this.mapSelectionToRows()
}

onSortColumnClick(sortColumn: string) {
Expand Down Expand Up @@ -268,4 +284,18 @@ export class DataTableComponent extends DataSortBase implements OnInit {
return 'OCX_LIST_GRID_SORT.TOGGLE_BUTTON.DEFAULT_TITLE'
}
}
}

mapSelectionToRows() {
this.selectedRows$ = combineLatest([this._selection$, this._rows$]).pipe(
map(([selectedRows, rows]) => {
return selectedRows.map((row) => {
return rows.find((r) => r.id === row.id)
})
})
)
}

onSelectionChange(event: Row[]) {
this.selectionChanged.emit(event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
[pageSizes]="pageSizes"
[pageSize]="pageSize"
[paginator]="tablePaginator"
[selectedRows]="selectedRows"
[emptyResultsMessage]="emptyResultsMessage"
[name]="name"
[deletePermission]="deletePermission"
Expand Down
Loading

0 comments on commit 10bd6eb

Please sign in to comment.