Skip to content

Commit

Permalink
feat: add sorting and filtering support for nested objects (#206)
Browse files Browse the repository at this point in the history
Co-authored-by: jwthewes <jwthewes@amazon.de>
  • Loading branch information
JWThewes and jwthewes committed Jan 30, 2024
1 parent a17a003 commit dd15ba8
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-phones-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-bs-datatable': minor
---

Add support for sorting and filtering in cunjunction with nested objects
89 changes: 84 additions & 5 deletions src/__stories__/00-Uncontrolled.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1095,10 +1095,14 @@ describe('composed table rows', () => {

describe('Nested object', () => {
test('nested object table renders', () => {
const { getByRole } = render(<NestedObjectTable />);
const { getByRole, getByText } = render(<NestedObjectTable />);

const tableElement = getByRole('table');

let nameTh = getByText('Name', { selector: 'th' });
fireEvent.click(nameTh);
fireEvent.click(nameTh);

const allTableRows = tableElement
.querySelector('tbody')
?.querySelectorAll('tr');
Expand All @@ -1121,20 +1125,95 @@ describe('Nested object', () => {
expect(firstRowFourthColumn.textContent).toBe('F-1');

const lastRowFirstColumn = allTableRows
?.item(1)
?.item(2)
.children.item(0) as HTMLElement;
expect(lastRowFirstColumn.textContent).toBe('Wileen');
const lastRowSecondColumn = allTableRows
?.item(1)
?.item(2)
.children.item(1) as HTMLElement;
expect(lastRowSecondColumn.textContent).toBe('');
const lastRowThirdColumn = allTableRows
?.item(1)
?.item(2)
.children.item(2) as HTMLElement;
expect(lastRowThirdColumn.textContent).toBe('');
const lastRowFourthColumn = allTableRows
?.item(1)
?.item(2)
.children.item(3) as HTMLElement;
expect(lastRowFourthColumn.textContent).toBe('');
});

test('sorting nested object', () => {
const { getByText, getByRole } = render(<NestedObjectTable />);

const tableElement = getByRole('table');
let tableRows = tableElement.querySelector('tbody')?.querySelectorAll('tr');
expect(tableRows).toBeDefined();

let nameTh = getByText('Rocket name', { selector: 'th' });
fireEvent.click(nameTh);

let firstRowSecondColumn = tableRows
?.item(0)
.children.item(1) as HTMLElement;

expect(firstRowSecondColumn?.textContent).toBe('Ariane 5');
expect(nameTh.getAttribute('data-sort-order')).toBe('asc');

firstRowSecondColumn = tableRows?.item(0).children.item(1) as HTMLElement;

fireEvent.click(nameTh);
expect(firstRowSecondColumn?.textContent).toBe('Saturn V');
expect(nameTh.getAttribute('data-sort-order')).toBe('desc');

let rocketEngineCompanyTh = getByText('Rocket engine company', {
selector: 'th'
});
fireEvent.click(rocketEngineCompanyTh);

let firstRowFourthColumn = tableRows
?.item(0)
.children.item(4) as HTMLElement;

expect(firstRowFourthColumn?.textContent).toBe('NASA');
expect(nameTh.getAttribute('data-sort-order')).toBe(null);
expect(rocketEngineCompanyTh.getAttribute('data-sort-order')).toBe('asc');

fireEvent.click(rocketEngineCompanyTh);

firstRowFourthColumn = tableRows?.item(0).children.item(4) as HTMLElement;

expect(firstRowFourthColumn?.textContent).toBe('Safran Aircraft Engines');
expect(rocketEngineCompanyTh.getAttribute('data-sort-order')).toBe('desc');
});

test('filtering nested object', () => {
const { getByRole, getByPlaceholderText } = render(<NestedObjectTable />);

const tableElement = getByRole('table');
let tableRows = tableElement.querySelector('tbody')?.querySelectorAll('tr');
expect(tableRows).toBeDefined();

let filterElement = getByPlaceholderText('Enter text...');
fireEvent.change(filterElement, { target: { value: 'vulcain' } });

let firstRowSecondColumn = tableRows
?.item(0)
.children.item(1) as HTMLElement;

expect(
tableElement.querySelector('tbody')?.querySelectorAll('tr').length
).toBe(1);

expect(firstRowSecondColumn?.textContent).toBe('Ariane 5');

fireEvent.change(filterElement, { target: { value: 'f-1' } });

firstRowSecondColumn = tableRows?.item(0).children.item(1) as HTMLElement;
expect(firstRowSecondColumn?.textContent).toBe('Saturn V');

fireEvent.change(filterElement, { target: { value: '' } });
expect(
tableElement.querySelector('tbody')?.querySelectorAll('tr').length
).toBe(3);
});
});
31 changes: 27 additions & 4 deletions src/__stories__/00-Uncontrolled/08-NestedObject.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import React from 'react';
import { Table } from 'react-bootstrap';
import { Col, Row, Table } from 'react-bootstrap';
import { DatatableWrapper } from '../../components/DatatableWrapper';
import { TableColumnType } from '../../helpers/types';
import { StoryColumnType } from '../resources/types';
import NESTED_TABLE_DATA from '../resources/nested-story-data.json';
import { TableHeader } from '../../components/TableHeader';
import { TableBody } from '../../components/TableBody';
import { Filter } from '../../components/Filter';

// @@@SNIPSTART NestedObject
export function NestedObjectComponent() {
const headers: TableColumnType<StoryColumnType>[] = [
{
prop: 'name',
title: 'Name'
title: 'Name',
isSortable: true
},
{
title: 'Rocket name',
prop: 'rocket.name',
isSortable: true,
isFilterable: true
},
{
title: 'Rocket company',
prop: 'rocket.company'
prop: 'rocket.company',
isSortable: true,
isFilterable: true
},
{
title: 'Rocket engine',
prop: 'rocket.engine.name'
prop: 'rocket.engine.name',
isSortable: true,
isFilterable: true
},
{
title: 'Rocket engine company',
prop: 'rocket.engine.company',
isSortable: true,
isFilterable: true
}
];

return (
<DatatableWrapper body={NESTED_TABLE_DATA} headers={headers}>
<Row className="mb-4">
<Col
xs={12}
lg={4}
className="d-flex flex-col justify-content-end align-items-end"
>
<Filter />
</Col>
</Row>
<Table>
<TableHeader />
<TableBody />
Expand Down
16 changes: 16 additions & 0 deletions src/__stories__/resources/nested-story-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
}
}
},
{
"name": "Adriana",
"username": "adriana-3",
"date": "February 04, 2022",
"score": 97,
"location": "Earth",
"status": "Inactive",
"rocket": {
"name": "Ariane 5",
"company": "ESA",
"engine": {
"name": "Vulcain",
"company": "Safran Aircraft Engines"
}
}
},
{
"name": "Wileen",
"username": "wileen-55",
Expand Down
8 changes: 2 additions & 6 deletions src/components/TableBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useControlledStateSetter,
useCreateCheckboxHandlers
} from '../helpers/hooks';
import { extractValueFromObject } from '../helpers/data';

const VALID_TAGS_FOR_ROW_ONCLICK = ['TD', 'TR'];

Expand Down Expand Up @@ -286,12 +287,7 @@ export function TableRow<TTableRowType extends TableRowType>({
} else {
// Render normally.
if (cell === undefined) {
value = prop.split('.').reduce((a, b) => {
if (a) {
return a[b];
}
return undefined;
}, rowData);
value = extractValueFromObject(prop, rowData);
} else {
value = cell(rowData);
}
Expand Down
15 changes: 12 additions & 3 deletions src/helpers/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export function sortData<TTableRowType extends TableRowType>(
: undefined;

sortedData.sort((a, b) => {
let quantifiedValue1 = a[prop];
let quantifiedValue2 = b[prop];
let quantifiedValue1 = extractValueFromObject(prop, a);
let quantifiedValue2 = extractValueFromObject(prop, b);

if (sortFnFromObject) {
quantifiedValue1 = sortFnFromObject(quantifiedValue1);
Expand All @@ -53,6 +53,15 @@ export function sortData<TTableRowType extends TableRowType>(
return sortedData;
}

export function extractValueFromObject(prop: string, rowData: any) {
return prop.split('.').reduce((a, b) => {
if (a) {
return a[b];
}
return undefined;
}, rowData);
}

/**
* @internal
*
Expand All @@ -76,7 +85,7 @@ export function filterData<TTableRowType extends TableRowType>(
const header = headers[key];

if (header.isFilterable) {
let columnValue = element[header.prop];
let columnValue = extractValueFromObject(header.prop, element);

// Only process non-null values.
if (columnValue !== null && columnValue !== undefined) {
Expand Down

0 comments on commit dd15ba8

Please sign in to comment.