diff --git a/examples/sorting-client-side/src/App.js b/examples/sorting-client-side/src/App.js index e9cfee6..13ec4bd 100644 --- a/examples/sorting-client-side/src/App.js +++ b/examples/sorting-client-side/src/App.js @@ -58,26 +58,17 @@ function Table({ columns, data }) { {headerGroups.map(headerGroup => ( - {headerGroup.headers.map( - column => - console.log(column) || ( - // Add the sorting props to control sorting. For this example - // we can add them into the header props - - {column.render('Header')} - {/* Add a sort direction indicator */} - - {column.sorted - ? column.sortedDesc - ? ' 🔽' - : ' 🔼' - : ''} - - - ) - )} + {headerGroup.headers.map(column => ( + // Add the sorting props to control sorting. For this example + // we can add them into the header props + + {column.render('Header')} + {/* Add a sort direction indicator */} + + {column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''} + + + ))} ))} diff --git a/src/filterTypes.js b/src/filterTypes.js index cfa1ec2..ad785b5 100644 --- a/src/filterTypes.js +++ b/src/filterTypes.js @@ -1,12 +1,11 @@ export const text = (rows, id, filterValue) => { - return rows.filter(row => { + rows = rows.filter(row => { const rowValue = row.values[id] - return rowValue !== undefined - ? String(rowValue) - .toLowerCase() - .includes(String(filterValue).toLowerCase()) - : true + return String(rowValue) + .toLowerCase() + .includes(String(filterValue).toLowerCase()) }) + return rows } text.autoRemove = val => !val diff --git a/src/hooks/tests/useTable.test.js b/src/hooks/tests/useTable.test.js index 9f7b879..2c8841d 100644 --- a/src/hooks/tests/useTable.test.js +++ b/src/hooks/tests/useTable.test.js @@ -1,6 +1,5 @@ import '@testing-library/react/cleanup-after-each' import '@testing-library/jest-dom/extend-expect' -// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required import React from 'react' import { render } from '@testing-library/react' diff --git a/src/hooks/useTable.js b/src/hooks/useTable.js index 839cd80..e34673f 100755 --- a/src/hooks/useTable.js +++ b/src/hooks/useTable.js @@ -63,6 +63,7 @@ export const useTable = (props, ...plugins) => { plugins, hooks: { columnsBeforeHeaderGroups: [], + columnsBeforeHeaderGroupsDeps: [], useMain: [], useColumns: [], useHeaders: [], @@ -84,45 +85,57 @@ export const useTable = (props, ...plugins) => { }) if (debug) console.timeEnd('plugins') - // Compute columns, headerGroups and headers - const columnInfo = React.useMemo( + if (debug) console.info('buildColumns/headerGroup/headers') + // Decorate All the columns + let columnTree = React.useMemo( + () => decorateColumnTree(userColumns, defaultColumn), + [defaultColumn, userColumns] + ) + + // Get the flat list of all columns + let columns = React.useMemo(() => flattenBy(columnTree, 'columns'), [ + columnTree, + ]) + + // Allow hooks to decorate columns (and trigger this memoization via deps) + columns = React.useMemo( () => { - if (debug) console.info('buildColumns/headerGroup/headers') - // Decorate All the columns - let columnTree = decorateColumnTree(userColumns, defaultColumn) - - // Get the flat list of all columns - let columns = flattenBy(columnTree, 'columns') - - // Allow hooks to decorate columns if (debug) console.time('hooks.columnsBeforeHeaderGroups') - columns = applyHooks( + const newColumns = applyHooks( instanceRef.current.hooks.columnsBeforeHeaderGroups, columns, instanceRef.current ) if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups') - - // Make the headerGroups - const headerGroups = makeHeaderGroups( - columns, - findMaxDepth(columnTree), - defaultColumn - ) - - const headers = flattenBy(headerGroups, 'headers') - - return { - columns, - headerGroups, - headers, - } + return newColumns }, - [debug, defaultColumn, userColumns] + [ + columns, + debug, + // eslint-disable-next-line react-hooks/exhaustive-deps + ...applyHooks( + instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps, + [], + instanceRef.current + ), + ] ) - // Place the columns, headerGroups and headers on the api - Object.assign(instanceRef.current, columnInfo) + // Make the headerGroups + const headerGroups = React.useMemo( + () => makeHeaderGroups(columns, findMaxDepth(columnTree), defaultColumn), + [columnTree, columns, defaultColumn] + ) + + const headers = React.useMemo(() => flattenBy(headerGroups, 'headers'), [ + headerGroups, + ]) + + Object.assign(instanceRef.current, { + columns, + headerGroups, + headers, + }) // Access the row model instanceRef.current.rows = React.useMemo( diff --git a/src/plugin-hooks/tests/__snapshots__/useFilters.test.js.snap b/src/plugin-hooks/tests/__snapshots__/useFilters.test.js.snap index 792cada..9a77135 100644 --- a/src/plugin-hooks/tests/__snapshots__/useFilters.test.js.snap +++ b/src/plugin-hooks/tests/__snapshots__/useFilters.test.js.snap @@ -1,11 +1,203 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders a sortable table 1`] = ` +exports[`renders a filterable table 1`] = ` "Snapshot Diff: -Compared values have no visual difference." +- First value ++ Second value + +@@ -37,11 +37,11 @@ + colspan=\\"1\\" + > + Last Name + + + + + progress: 50 +- +- +- +- +- firstName: derek +- +- +- lastName: perkins +- +- +- age: 40 +- +- +- visits: 40 +- +- +- status: Single +- +- +- progress: 80 +- +- +- +- +- firstName: joe +- +- +- lastName: bergevin +- +- +- age: 45 +- +- +- visits: 20 +- +- +- status: Complicated +- +- +- progress: 10 + + + " `; -exports[`renders a sortable table 2`] = ` +exports[`renders a filterable table 2`] = ` "Snapshot Diff: -Compared values have no visual difference." +- First value ++ Second value + +@@ -37,11 +37,11 @@ + colspan=\\"1\\" + > + Last Name + + + + +- firstName: tanner ++ firstName: derek + + +- lastName: linsley ++ lastName: perkins + + +- age: 29 ++ age: 40 + + +- visits: 100 ++ visits: 40 + + +- status: In Relationship ++ status: Single + + +- progress: 50 ++ progress: 80 + + + + +- firstName: jaylen ++ firstName: joe + + +- lastName: linsley ++ lastName: bergevin + + +- age: 26 ++ age: 45 + + +- visits: 99 ++ visits: 20 + + +- status: In Relationship ++ status: Complicated + + +- progress: 70 ++ progress: 10 + + + + + " `; diff --git a/src/plugin-hooks/tests/__snapshots__/useGroupBy.test.js.snap b/src/plugin-hooks/tests/__snapshots__/useGroupBy.test.js.snap new file mode 100644 index 0000000..416f162 --- /dev/null +++ b/src/plugin-hooks/tests/__snapshots__/useGroupBy.test.js.snap @@ -0,0 +1,371 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a filterable table 1`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -29,13 +29,13 @@ + +- 👊 ++ 🛑 + +- First Name ++ Last Name + + +@@ -44,11 +44,11 @@ + style=\\"cursor: pointer;\\" + title=\\"Toggle GroupBy\\" + > + 👊 + +- Last Name ++ First Name + + +@@ -107,138 +107,119 @@ + class=\\"\\" + > + +- firstName: tanner +- +- +- lastName: linsley +- +- +- age: 29 +- +- +- visits: 100 +- +- +- status: In Relationship ++ 👉 ++ ++ lastName: linsley (2) + + +- progress: 50 +- +- +- +- +- firstName: derek +- +- +- lastName: perkins ++ 2 Names + + +- age: 40 ++ 27.5 (avg) + + +- visits: 40 ++ 199 (total) + + +- status: Single ++ status: null + + +- progress: 80 ++ 60 (med) + + + + +- firstName: joe ++ ++ 👉 ++ ++ lastName: perkins (1) + + +- lastName: bergevin ++ 1 Names + + +- age: 45 ++ 40 (avg) + + +- visits: 20 ++ 40 (total) + + +- status: Complicated ++ status: null + + +- progress: 10 ++ 80 (med) + + + + +- firstName: jaylen ++ ++ 👉 ++ ++ lastName: bergevin (1) + + +- lastName: linsley ++ 1 Names + + +- age: 26 ++ 45 (avg) + + +- visits: 99 ++ 20 (total) + + +- status: In Relationship ++ status: null + + +- progress: 70 ++ 10 (med) + + + + + " +`; + +exports[`renders a filterable table 2`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -6,17 +6,29 @@ + + + Name + + ++ Info ++ ++ ++ Name ++ ++ + Info + + + +- 👊 ++ 🛑 + +- First Name ++ Visits + + +@@ -57,11 +69,11 @@ + style=\\"cursor: pointer;\\" + title=\\"Toggle GroupBy\\" + > + 👊 + +- Age ++ First Name + + +@@ -70,11 +82,11 @@ + style=\\"cursor: pointer;\\" + title=\\"Toggle GroupBy\\" + > + 👊 + +- Visits ++ Age + + +@@ -114,10 +126,13 @@ + > + 👉 + + lastName: linsley (2) + ++ + + 2 Names + +@@ -127,15 +142,10 @@ + 27.5 (avg) + + +- 199 (total) +- +- + status: null + + +@@ -155,22 +165,20 @@ + + lastName: perkins (1) + + +- 1 Names +- ++ /> + +- 40 (avg) ++ 1 Names + + +- 40 (total) ++ 40 (avg) + + + status: null +@@ -194,22 +202,20 @@ + + lastName: bergevin (1) + + +- 1 Names +- ++ /> + +- 45 (avg) ++ 1 Names + + +- 20 (total) ++ 45 (avg) + + + status: null" +`; diff --git a/src/plugin-hooks/tests/useFilters.test.js b/src/plugin-hooks/tests/useFilters.test.js index 87a1c95..d2551cb 100644 --- a/src/plugin-hooks/tests/useFilters.test.js +++ b/src/plugin-hooks/tests/useFilters.test.js @@ -1,6 +1,5 @@ import '@testing-library/react/cleanup-after-each' import '@testing-library/jest-dom/extend-expect' -// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required import React from 'react' import { render, fireEvent } from '@testing-library/react' @@ -73,7 +72,7 @@ function Table({ columns, data }) { {headerGroup.headers.map(column => ( {column.render('Header')} - {column.render('Filter')} + {column.canFilter ? column.render('Filter') : null} ))} @@ -139,19 +138,21 @@ function App() { return } -test('renders a sortable table', () => { - const { getByText, asFragment } = render() +test('renders a filterable table', () => { + const { getAllByPlaceholderText, asFragment } = render() - const beforeSort = asFragment() + const filterInputs = getAllByPlaceholderText('Search...') - fireEvent.click(getByText('First Name')) + const beforeFilter = asFragment() - const afterSort1 = asFragment() + fireEvent.change(filterInputs[1], { target: { value: 'l' } }) - fireEvent.click(getByText('First Name')) + const afterFilter1 = asFragment() - const afterSort2 = asFragment() + fireEvent.change(filterInputs[1], { target: { value: 'er' } }) - expect(beforeSort).toMatchDiffSnapshot(afterSort1) - expect(afterSort1).toMatchDiffSnapshot(afterSort2) + const afterFilter2 = asFragment() + + expect(beforeFilter).toMatchDiffSnapshot(afterFilter1) + expect(afterFilter1).toMatchDiffSnapshot(afterFilter2) }) diff --git a/src/plugin-hooks/tests/useGroupBy.test.js b/src/plugin-hooks/tests/useGroupBy.test.js new file mode 100644 index 0000000..4bb7508 --- /dev/null +++ b/src/plugin-hooks/tests/useGroupBy.test.js @@ -0,0 +1,207 @@ +import '@testing-library/react/cleanup-after-each' +import '@testing-library/jest-dom/extend-expect' + +import React from 'react' +import { render, fireEvent } from '@testing-library/react' +import { useTable } from '../../hooks/useTable' +import { useGroupBy } from '../useGroupBy' +import { useExpanded } from '../useExpanded' + +const data = [ + { + firstName: 'tanner', + lastName: 'linsley', + age: 29, + visits: 100, + status: 'In Relationship', + progress: 50, + }, + { + firstName: 'derek', + lastName: 'perkins', + age: 40, + visits: 40, + status: 'Single', + progress: 80, + }, + { + firstName: 'joe', + lastName: 'bergevin', + age: 45, + visits: 20, + status: 'Complicated', + progress: 10, + }, + { + firstName: 'jaylen', + lastName: 'linsley', + age: 26, + visits: 99, + status: 'In Relationship', + progress: 70, + }, +] + +const defaultColumn = { + Cell: ({ value, column: { id } }) => `${id}: ${value}`, + Filter: ({ filterValue, setFilter }) => ( + { + setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely + }} + placeholder="Search..." + /> + ), +} + +function Table({ columns, data }) { + const { getTableProps, headerGroups, rows, prepareRow } = useTable( + { + columns, + data, + defaultColumn, + }, + useGroupBy, + useExpanded + ) + + return ( +
+ + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => ( + + ))} + + ))} + + + {rows.map( + (row, i) => + prepareRow(row) || ( + + {row.cells.map(cell => { + return ( + + ) + })} + + ) + )} + +
+ {column.canGroupBy ? ( + // If the column can be grouped, let's add a toggle + + {column.grouped ? '🛑' : '👊'} + + ) : null} + {column.render('Header')} +
+ {cell.grouped ? ( + <> + row.toggleExpanded()} + > + {row.isExpanded ? '👇' : '👉'} + + {cell.render('Cell')} ({row.subRows.length}) + + ) : cell.aggregated ? ( + cell.render('Aggregated') + ) : cell.repeatedValue ? null : ( + cell.render('Cell') + )} +
+ ) +} + +function roundedMedian(values) { + let min = values[0] || '' + let max = values[0] || '' + + values.forEach(value => { + min = Math.min(min, value) + max = Math.max(max, value) + }) + + return Math.round((min + max) / 2) +} + +function App() { + const columns = React.useMemo( + () => [ + { + Header: 'Name', + columns: [ + { + Header: 'First Name', + accessor: 'firstName', + aggregate: ['sum', 'count'], + Aggregated: ({ value }) => `${value} Names`, + }, + { + Header: 'Last Name', + accessor: 'lastName', + aggregate: ['sum', 'uniqueCount'], + Aggregated: ({ value }) => `${value} Unique Names`, + }, + ], + }, + { + Header: 'Info', + columns: [ + { + Header: 'Age', + accessor: 'age', + aggregate: 'average', + Aggregated: ({ value }) => `${value} (avg)`, + }, + { + Header: 'Visits', + accessor: 'visits', + aggregate: 'sum', + Aggregated: ({ value }) => `${value} (total)`, + }, + { + Header: 'Status', + accessor: 'status', + }, + { + Header: 'Profile Progress', + accessor: 'progress', + aggregate: roundedMedian, + Aggregated: ({ value }) => `${value} (med)`, + }, + ], + }, + ], + [] + ) + + return +} + +test('renders a filterable table', () => { + const { getAllByText, asFragment } = render() + + const groupByButtons = getAllByText('👊') + + const beforeGrouping = asFragment() + + fireEvent.click(groupByButtons[1]) + + const afterGrouping1 = asFragment() + + fireEvent.click(groupByButtons[3]) + + const afterGrouping2 = asFragment() + + expect(beforeGrouping).toMatchDiffSnapshot(afterGrouping1) + expect(afterGrouping1).toMatchDiffSnapshot(afterGrouping2) +}) diff --git a/src/plugin-hooks/tests/useSortBy.test.js b/src/plugin-hooks/tests/useSortBy.test.js index 3ce5464..2b1ce11 100644 --- a/src/plugin-hooks/tests/useSortBy.test.js +++ b/src/plugin-hooks/tests/useSortBy.test.js @@ -1,6 +1,5 @@ import '@testing-library/react/cleanup-after-each' import '@testing-library/jest-dom/extend-expect' -// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required import React from 'react' import { render, fireEvent } from '@testing-library/react' diff --git a/src/plugin-hooks/useFilters.js b/src/plugin-hooks/useFilters.js index d4b1197..8a13530 100755 --- a/src/plugin-hooks/useFilters.js +++ b/src/plugin-hooks/useFilters.js @@ -181,14 +181,6 @@ function useMain(instance) { } }) - // then filter any rows without subcolumns because it would be strange to show - filteredRows = filteredRows.filter(row => { - if (!row.subRows) { - return true - } - return row.subRows.length > 0 - }) - return filteredRows } diff --git a/src/plugin-hooks/useGroupBy.js b/src/plugin-hooks/useGroupBy.js index d592aa2..5ada90b 100755 --- a/src/plugin-hooks/useGroupBy.js +++ b/src/plugin-hooks/useGroupBy.js @@ -38,6 +38,10 @@ const propTypes = { export const useGroupBy = hooks => { hooks.columnsBeforeHeaderGroups.push(columnsBeforeHeaderGroups) + hooks.columnsBeforeHeaderGroupsDeps.push((deps, instance) => { + deps.push(instance.state[0].groupBy) + return deps + }) hooks.useMain.push(useMain) }