Skip to content

Commit

Permalink
feat: table row selection (#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 authored Feb 2, 2024
1 parent f63aada commit f420cdd
Show file tree
Hide file tree
Showing 13 changed files with 670 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
[rows]="pageSize"
[rowsPerPageOptions]="pageSizes"
id="dataTable_{{name}}"
(selectionChange)="onSelectionChange($event)"
[selection]="(selectedRows$ | async) || []"
>
<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 @@ -66,6 +71,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,26 +1,239 @@
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 { ColumnType } from '../../../model/column-type.model';
import { PortalCoreModule } from '../../portal-core.module';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { DataTableHarness, PTableCheckboxHarness } from '../../../../../testing';

describe('DataTableComponent', () => {
let fixture: ComponentFixture<DataTableComponent>
let component: DataTableComponent
let dataTable: DataTableHarness
let unselectedCheckBoxes: PTableCheckboxHarness[];
let selectedCheckBoxes: PTableCheckboxHarness[];
const mockData = [
{
version: 0,
creationDate: '2023-09-12T09:34:11.997048Z',
creationUser: 'creation user',
modificationDate: '2023-09-12T09:34:11.997048Z',
modificationUser: '',
id: '195ee34e-41c6-47b7-8fc4-3f245dee7651',
name: 'some name',
description: '',
status: 'some status',
responsible: 'someone responsible',
endDate: '2023-09-14T09:34:09Z',
startDate: '2023-09-13T09:34:05Z',
imagePath: '/path/to/image',
testNumber: '1',
},
{
version: 0,
creationDate: '2023-09-12T09:33:58.544494Z',
creationUser: '',
modificationDate: '2023-09-12T09:33:58.544494Z',
modificationUser: '',
id: '5f8bb05b-d089-485e-a234-0bb6ff25234e',
name: 'example',
description: 'example description',
status: 'status example',
responsible: '',
endDate: '2023-09-13T09:33:55Z',
startDate: '2023-09-12T09:33:53Z',
imagePath: '',
testNumber: '3.141',
},
{
version: 0,
creationDate: '2023-09-12T09:34:27.184086Z',
creationUser: '',
modificationDate: '2023-09-12T09:34:27.184086Z',
modificationUser: '',
id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064',
name: 'name 1',
description: '',
status: 'status name 1',
responsible: '',
endDate: '2023-09-15T09:34:24Z',
startDate: '2023-09-14T09:34:22Z',
imagePath: '',
testNumber: '123456789',
},
{
version: 0,
creationDate: '2023-09-12T09:34:27.184086Z',
creationUser: '',
modificationDate: '2023-09-12T09:34:27.184086Z',
modificationUser: '',
id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064',
name: 'name 2',
description: '',
status: 'status name 2',
responsible: '',
endDate: '2023-09-15T09:34:24Z',
startDate: '2023-09-14T09:34:22Z',
imagePath: '',
testNumber: '12345.6789',
},
{
version: 0,
creationDate: '2023-09-12T09:34:27.184086Z',
creationUser: '',
modificationDate: '2023-09-12T09:34:27.184086Z',
modificationUser: '',
id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064',
name: 'name 3',
description: '',
status: 'status name 3',
responsible: '',
endDate: '2023-09-15T09:34:24Z',
startDate: '2023-09-14T09:34:22Z',
imagePath: '',
testNumber: '7.1',
},
]
const mockColumns = [
{
columnType: ColumnType.STRING,
id: 'name',
nameKey: 'COLUMN_HEADER_NAME.NAME',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.STRING,
id: 'description',
nameKey: 'COLUMN_HEADER_NAME.DESCRIPTION',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.DATE,
id: 'startDate',
nameKey: 'COLUMN_HEADER_NAME.START_DATE',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.DATE,
id: 'endDate',
nameKey: 'COLUMN_HEADER_NAME.END_DATE',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.TRANSLATION_KEY,
id: 'status',
nameKey: 'COLUMN_HEADER_NAME.STATUS',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.STRING,
id: 'responsible',
nameKey: 'COLUMN_HEADER_NAME.RESPONSIBLE',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.RELATIVE_DATE,
id: 'modificationDate',
nameKey: 'COLUMN_HEADER_NAME.MODIFICATION_DATE',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.STRING,
id: 'creationUser',
nameKey: 'COLUMN_HEADER_NAME.CREATION_USER',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'],
},
{
columnType: ColumnType.NUMBER,
id: 'testNumber',
nameKey: 'COLUMN_HEADER_NAME.TEST_NUMBER',
filterable: true,
sortable: true,
predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'],
},
]

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DataTableComponent],
imports: [PrimeNgModule, BrowserAnimationsModule, TranslateModule.forRoot()],
imports: [PrimeNgModule, BrowserAnimationsModule, TranslateModule.forRoot(), PortalCoreModule],
}).compileComponents()

fixture = TestBed.createComponent(DataTableComponent)
component = fixture.componentInstance
component.rows = mockData
component.columns = mockColumns
TestBed.inject(TranslateService).use('en')
fixture.detectChanges()
dataTable = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness)
})

it('should create', () => {
expect(component).toBeDefined()
})

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;
Loading

0 comments on commit f420cdd

Please sign in to comment.