From 5fb9fbe12854c1359e4a90be29e3c2c3f1be3d27 Mon Sep 17 00:00:00 2001 From: Gabe Abud Date: Mon, 17 Aug 2020 14:00:51 -0700 Subject: [PATCH] feat(Add optional sort function to columns): Add optional sort function to columns The default sorting sorts by strings, however in the case where you have more complex data such as dates, you may need to specify your own sorting function --- package-lock.json | 12 +++++ package.json | 2 + src/hooks.tsx | 28 ++++++++---- src/test/makeData.tsx | 47 +++++++++++++++++-- src/test/selectionGlobalFiltering.spec.tsx | 31 ++++++------- src/test/sorting.spec.tsx | 53 ++++++++++++++++++++-- src/test/table.spec.tsx | 4 +- src/types.ts | 15 +++--- 8 files changed, 151 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index d93aa78..b630457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1839,6 +1839,12 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -4267,6 +4273,12 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index dbaebf7..94c6532 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,11 @@ "devDependencies": { "@testing-library/jest-dom": "^5.11.3", "@testing-library/react": "^10.4.8", + "@types/faker": "^4.1.12", "@types/react": "^16.9.46", "@types/react-dom": "^16.9.8", "codecov": "^3.7.2", + "faker": "^4.1.0", "husky": "^4.2.5", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/src/hooks.tsx b/src/hooks.tsx index 9fdc66a..053eaf8 100644 --- a/src/hooks.tsx +++ b/src/hooks.tsx @@ -9,6 +9,7 @@ import { DataType, UseTableReturnType, UseTableOptionsType, + RowType, } from './types'; import { byTextAscending, byTextDescending } from './utils'; @@ -24,9 +25,22 @@ const createReducer = () => ( let isAscending = null; + let sortedRows: RowType[] = []; + const columnCopy = state.columns.map(column => { if (action.columnName === column.name) { isAscending = column.sorted.asc; + if (column.sort) { + sortedRows = isAscending + ? state.rows.sort(column.sort) + : state.rows.sort(column.sort).reverse(); + } else { + sortedRows = state.rows.sort( + isAscending + ? byTextAscending(object => object.original[action.columnName]) + : byTextDescending(object => object.original[action.columnName]) + ); + } return { ...column, sorted: { @@ -47,11 +61,7 @@ const createReducer = () => ( return { ...state, columns: columnCopy, - rows: state.rows.sort( - isAscending - ? byTextAscending(object => object.original[action.columnName]) - : byTextDescending(object => object.original[action.columnName]) - ), + rows: sortedRows, columnsById: getColumnsById(columnCopy), }; case 'GLOBAL_FILTER': @@ -154,7 +164,7 @@ const createReducer = () => ( }; export const useTable = ( - columns: ColumnType[], + columns: ColumnType[], data: T[], options?: UseTableOptionsType ): UseTableReturnType => { @@ -245,7 +255,7 @@ const makeRender = ( const sortDataInOrder = ( data: T[], - columns: ColumnType[] + columns: ColumnType[] ): T[] => { return data.map((row: any) => { const newRow: any = {}; @@ -259,7 +269,9 @@ const sortDataInOrder = ( }); }; -const getColumnsById = (columns: ColumnType[]): ColumnByIdsType => { +const getColumnsById = ( + columns: ColumnType[] +): ColumnByIdsType => { const columnsById: ColumnByIdsType = {}; columns.forEach(column => { const col: any = { diff --git a/src/test/makeData.tsx b/src/test/makeData.tsx index 883543e..14297b9 100644 --- a/src/test/makeData.tsx +++ b/src/test/makeData.tsx @@ -1,4 +1,5 @@ -import { ColumnType } from 'types'; +import { ColumnType, DataType } from 'types'; +import { date } from 'faker'; // from json-generator.com const randomData = [ @@ -275,11 +276,51 @@ export type UserType = { address: string; }; -export const makeData = ( +export const makeData = ( rowNum: number -): { columns: ColumnType[]; data: UserType[] } => { +): { columns: ColumnType[]; data: UserType[] } => { return { columns, data: randomData.slice(0, rowNum), }; }; + +export const makeSimpleData = () => { + const columns: ColumnType[] = [ + { + name: 'firstName', + label: 'First Name', + }, + { + name: 'lastName', + label: 'Last Name', + }, + { + name: 'birthDate', + label: 'Birth Date', + }, + ]; + + const recentDate = date.recent(); + const pastDate = date.past(undefined, recentDate); + const oldestDate = date.past(100, pastDate); + + const data = [ + { + firstName: 'Samwise', + lastName: 'Gamgee', + birthDate: pastDate.toISOString(), + }, + { + firstName: 'Frodo', + lastName: 'Baggins', + birthDate: recentDate.toISOString(), // must be youngest for tests + }, + { + firstName: 'Bilbo', + lastName: 'Baggins', + birthDate: oldestDate.toISOString(), + }, + ]; + return { columns, data }; +}; diff --git a/src/test/selectionGlobalFiltering.spec.tsx b/src/test/selectionGlobalFiltering.spec.tsx index 04b7e19..452dcae 100644 --- a/src/test/selectionGlobalFiltering.spec.tsx +++ b/src/test/selectionGlobalFiltering.spec.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useState } from 'react'; import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { useTable } from '../hooks'; -import { ColumnType, RowType } from '../types'; -import { makeData, UserType } from './makeData'; +import { ColumnType, RowType, DataType } from '../types'; +import { makeData } from './makeData'; const columns = [ { @@ -27,17 +27,12 @@ const data = [ }, ]; -type TestDataType = { - firstName: string; - lastName: string; -}; - -const TableWithSelection = ({ +const TableWithSelection = ({ columns, data, }: { - columns: ColumnType[]; - data: Object[]; + columns: ColumnType[]; + data: T[]; }) => { const { headers, rows, selectRow, selectedRows, toggleAll } = useTable( columns, @@ -122,14 +117,14 @@ test('Should be able to select rows', async () => { expect(rtl.queryAllByTestId('selected-row')).toHaveLength(0); }); -const TableWithFilter = ({ +const TableWithFilter = ({ columns, data, filter, }: { - columns: ColumnType[]; - data: TestDataType[]; - filter: (row: RowType[]) => RowType[]; + columns: ColumnType[]; + data: T[]; + filter: (row: RowType[]) => RowType[]; }) => { const { headers, rows } = useTable(columns, data, { filter, @@ -171,12 +166,12 @@ test('Should be able to filter rows', () => { expect(rtl.getAllByTestId('table-row')).toHaveLength(1); }); -const TableWithSelectionAndFiltering = ({ +const TableWithSelectionAndFiltering = ({ columns, data, }: { - columns: ColumnType[]; - data: UserType[]; + columns: ColumnType[]; + data: T[]; }) => { const [searchString, setSearchString] = useState(''); const [filterOn, setFilterOn] = useState(false); @@ -184,7 +179,7 @@ const TableWithSelectionAndFiltering = ({ const { headers, rows, selectRow, selectedRows } = useTable(columns, data, { selectable: true, filter: useCallback( - (rows: RowType[]) => { + (rows: RowType[]) => { return rows.filter(row => { return ( row.cells.filter(cell => { diff --git a/src/test/sorting.spec.tsx b/src/test/sorting.spec.tsx index d5344cd..279cc64 100644 --- a/src/test/sorting.spec.tsx +++ b/src/test/sorting.spec.tsx @@ -4,14 +4,14 @@ import '@testing-library/jest-dom/extend-expect'; import { useTable } from '../hooks'; import { ColumnType } from '../types'; -import { makeData } from './makeData'; +import { makeData, makeSimpleData } from './makeData'; -const Table = ({ +const Table = ({ columns, data, }: { - columns: ColumnType[]; - data: Object[]; + columns: ColumnType[]; + data: T[]; }) => { const { headers, rows, toggleSort } = useTable(columns, data, { sortable: true, @@ -75,3 +75,48 @@ test('Should render a table with sorting enabled', () => { ({ getByText } = within(firstRow)); expect(getByText('Yesenia')).toBeInTheDocument(); }); + +test('Should sort by dates correctly', () => { + const { columns, data } = makeSimpleData<{ + firstName: string; + lastName: string; + birthDate: string; + }>(); + columns[2] = { + name: 'birthDate', + label: 'Birth Date', + sort: (objectA, objectB) => { + return ( + Number(new Date(objectA.original.birthDate)) - + Number(new Date(objectB.original.birthDate)) + ); + }, + }; + const rtl = render(); + + const dateColumn = rtl.getByTestId('column-birthDate'); + + // should be sorted in ascending order + fireEvent.click(dateColumn); + + expect(rtl.queryByTestId('sorted-birthDate')).toBeInTheDocument(); + + let firstRow = rtl.getByTestId('row-0'); + let lastRow = rtl.getByTestId('row-2'); + + let { getByText } = within(firstRow); + expect(getByText('Bilbo')).toBeInTheDocument(); + ({ getByText } = within(lastRow)); + expect(getByText('Frodo')).toBeInTheDocument(); + + // should be sorted in descending order + fireEvent.click(dateColumn); + + firstRow = rtl.getByTestId('row-0'); + lastRow = rtl.getByTestId('row-2'); + + ({ getByText } = within(firstRow)); + expect(getByText('Frodo')).toBeInTheDocument(); + ({ getByText } = within(lastRow)); + expect(getByText('Bilbo')).toBeInTheDocument(); +}); diff --git a/src/test/table.spec.tsx b/src/test/table.spec.tsx index 85a328e..151231b 100644 --- a/src/test/table.spec.tsx +++ b/src/test/table.spec.tsx @@ -30,7 +30,7 @@ const Table = ({ columns, data, }: { - columns: ColumnType[]; + columns: any[]; data: { firstName: string; lastName: string }[]; }) => { const { headers, rows } = useTable<{ firstName: string; lastName: string }>( @@ -87,7 +87,7 @@ test('Should be equal regardless of field order in data', () => { expect(normalTl.asFragment()).toEqual(reverseTl.asFragment()); }); -const columnsWithRender: ColumnType[] = [ +const columnsWithRender: ColumnType[] = [ { name: 'firstName', label: 'First Name', diff --git a/src/types.ts b/src/types.ts index 32240c8..6ca13cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,13 @@ -export type ColumnType = { +export type ColumnType = { name: string; label?: string; hidden?: boolean; + sort?: ((a: RowType, b: RowType) => number) | undefined; render?: (value: any) => React.ReactNode; }; -export type HeaderType = { +// this is the type saved as state and returned +export type HeaderType = { name: string; label?: string; hidden?: boolean; @@ -13,6 +15,7 @@ export type HeaderType = { on: boolean; asc: boolean; }; + sort?: ((a: RowType, b: RowType) => number) | undefined; render?: (value: any) => React.ReactNode; }; @@ -52,7 +55,7 @@ export type CellType = { }; export interface UseTableTypeParams { - columns: ColumnType[]; + columns: ColumnType[]; data: T[]; options?: { sortable?: boolean; @@ -63,7 +66,7 @@ export interface UseTableTypeParams { } export interface UseTablePropsType { - columns: ColumnType[]; + columns: ColumnType[]; data: T[]; options?: { sortable?: boolean; @@ -79,7 +82,7 @@ export interface UseTableOptionsType { } export interface UseTableReturnType { - headers: HeaderType[]; + headers: HeaderType[]; originalRows: RowType[]; rows: RowType[]; selectedRows: RowType[]; @@ -91,7 +94,7 @@ export interface UseTableReturnType { export type TableState = { columnsById: ColumnByIdsType; - columns: HeaderType[]; + columns: HeaderType[]; rows: RowType[]; originalRows: RowType[]; selectedRows: RowType[];