From 37977197c44600fd5fc4372b5aac5c1cb94d4770 Mon Sep 17 00:00:00 2001 From: atanasster Date: Fri, 26 Feb 2021 10:57:30 -0500 Subject: [PATCH] feat: add react-table pagination --- ui/components/src/Table/Table.stories.tsx | 26 ++- ui/components/src/Table/Table.tsx | 224 +++++++++++--------- ui/components/src/Table/TablePagination.tsx | 138 ++++++++++++ ui/components/src/ThemeContext/theme.ts | 43 ++++ 4 files changed, 334 insertions(+), 97 deletions(-) create mode 100644 ui/components/src/Table/TablePagination.tsx diff --git a/ui/components/src/Table/Table.stories.tsx b/ui/components/src/Table/Table.stories.tsx index 4d56a2d0a..47e24e8ee 100644 --- a/ui/components/src/Table/Table.stories.tsx +++ b/ui/components/src/Table/Table.stories.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useState, useEffect } from 'react'; import { Document, Example, faker } from '@component-controls/core'; import { Table, Column } from './Table'; +import { TablePaginationProps } from './TablePagination'; import { ThemeProvider } from '../ThemeContext'; export default { @@ -47,11 +48,11 @@ const columns: Column[] = [ accessor: 'address.zipcode' as any, }, ]; -const mockData = (): DataType[] => { +const mockDataGenerator = (count = 20): DataType[] => { let i = 10; faker.seed(123); // eslint-disable-next-line prefer-spread - return Array.apply(null, Array(20)).map(() => ({ + return Array.apply(null, Array(count)).map(() => ({ id: i++, ...faker.helpers.userCard(), age: faker.random.number({ min: 21, max: 25 }), @@ -59,6 +60,7 @@ const mockData = (): DataType[] => { })); }; +const mockData = () => mockDataGenerator(5); export const overview: Example = () => { const data = useMemo(mockData, []); return ( @@ -170,3 +172,23 @@ export const rowSelect: Example = () => { ); }; + +export const pagination: Example = props => { + const data = useMemo(mockDataGenerator, []); + return ( + + pagination={props} columns={columns} data={data} /> + + ); +}; + +pagination.controls = { + pageIndex: 0, + pageSize: 10, + pageTemplate: 'Page ${pageIndex} of ${pageLength}', + pageVisible: false, + pageSizeTemplate: '${pageSize} rows', + pageSizeVisible: false, + goToPageVisible: false, + goToPageTemplate: 'Go to page:', +}; diff --git a/ui/components/src/Table/Table.tsx b/ui/components/src/Table/Table.tsx index d73d2e48d..8f10fd6af 100644 --- a/ui/components/src/Table/Table.tsx +++ b/ui/components/src/Table/Table.tsx @@ -9,6 +9,7 @@ import { useGlobalFilter, useGroupBy, useExpanded, + usePagination, useRowSelect, Column, Cell, @@ -27,6 +28,7 @@ import { UseExpandedState, UseRowSelectState, UseSortByState, + UsePaginationState, SortingRule, UseGroupByState, TableState, @@ -36,6 +38,7 @@ import { GlobalFilter } from './TableFilter'; import { useExpanderColumn } from './TableGrouping'; import { useRowSelectionColumn } from './TableRowSelection'; import { useTableLayout } from './useTableLayout'; +import { TablePagination, TablePaginationProps } from './TablePagination'; const defaultColumn = memoize(() => ({ subRows: undefined, accessor: '', @@ -112,6 +115,11 @@ interface TableOwnProps { * initial sorting */ sortBy?: Array>; + + /** + * enable pagination + */ + pagination?: TablePaginationProps | boolean; } export type TableProps = TableOwnProps & @@ -137,6 +145,7 @@ export function Table({ onSelectRowsChange, rowSelect, sortBy, + pagination, ...rest }: TableProps): ReactElement | null { const plugins: PluginHook[] = [ @@ -145,14 +154,12 @@ export function Table({ useGroupBy, useSortBy, useExpanded, - useRowSelect, + useExpanderColumn(itemsLabel), ]; - if (rowSelect) { - plugins.push(useRowSelectionColumn); - } const initialState: Partial> & Partial>> & + Partial> & Partial> & Partial> & Partial>> = {}; @@ -168,6 +175,21 @@ export function Table({ if (typeof expanded === 'object') { initialState.expanded = expanded; } + if (pagination) { + plugins.push(usePagination); + if (typeof pagination === 'object') { + if (typeof pagination.pageIndex === 'number') { + initialState.pageIndex = pagination.pageIndex; + } + if (typeof pagination.pageSize === 'number') { + initialState.pageSize = pagination.pageSize; + } + } + } + if (rowSelect) { + plugins.push(useRowSelect); + plugins.push(useRowSelectionColumn); + } initialState.selectedRowIds = initialSelected; const options: TableOptions & UseFiltersOptions & @@ -195,12 +217,14 @@ export function Table({ getTableProps, getTableBodyProps, headerGroups, - rows, prepareRow, visibleColumns, preGlobalFilteredRows, setGlobalFilter, state, + rows, + page, // Instead of using 'rows', when using pagination + state: { pageIndex: statePageIndex, pageSize: statePageSize }, } = tableOptions; const { selectedRowIds } = state; useEffect(() => { @@ -209,104 +233,114 @@ export function Table({ } }, [selectedRowIds, onSelectRowsChange]); return ( - - {header && ( - - {headerGroups.map((headerGroup: any) => ( - - {headerGroup.headers.map((column: any) => ( + + + {header && ( + + {headerGroups.map((headerGroup: any) => ( + + {headerGroup.headers.map((column: any) => ( + + + {column.render('Header')} + {sorting && + column.isSorted && + (column.isSortedDesc ? ( + + ) : ( + + ))} + + + ))} + + ))} + {filtering && ( + - - {column.render('Header')} - {sorting && - column.isSorted && - (column.isSortedDesc ? ( - - ) : ( - - ))} - + - ))} - - ))} - {filtering && ( - - - - + )} + + )} + + {(page || rows).map( + ( + row: Row & + UseGroupByRowProps & { + isExpanded?: boolean; + }, + ) => { + prepareRow(row); + const { key, ...rowProps } = row.getRowProps(); + return ( + + + {row.isGrouped + ? row.cells[0].render('Aggregated') + : row.cells.map( + (cell: Cell & Partial>) => { + return ( + + {cell.render('Cell')} + + ); + }, + )} + + {row.isExpanded && ( + + + {renderRowSubComponent + ? renderRowSubComponent({ row }) + : null} + + + )} + + ); + }, )} - )} - - {rows.map( - ( - row: Row & - UseGroupByRowProps & { - isExpanded?: boolean; - }, - ) => { - prepareRow(row); - const { key, ...rowProps } = row.getRowProps(); - return ( - - - {row.isGrouped - ? row.cells[0].render('Aggregated') - : row.cells.map( - (cell: Cell & Partial>) => { - return ( - - {cell.render('Cell')} - - ); - }, - )} - - {row.isExpanded && ( - - - {renderRowSubComponent - ? renderRowSubComponent({ row }) - : null} - - - )} - - ); - }, - )} - + {pagination && ( + + )} + ); } diff --git a/ui/components/src/Table/TablePagination.tsx b/ui/components/src/Table/TablePagination.tsx new file mode 100644 index 000000000..380c43c80 --- /dev/null +++ b/ui/components/src/Table/TablePagination.tsx @@ -0,0 +1,138 @@ +/** @jsx jsx */ +import { FC } from 'react'; +import { jsx, Box, Select, Button, Input } from 'theme-ui'; +import { UsePaginationInstanceProps } from 'react-table'; + +const runtimeTemplate = (str: string, obj: Record) => + str.replace(/\${(.*?)}/g, (x, g) => obj[g]); + +export interface TablePaginationProps { + /** + * 'Page ${pageIndex} of ${pageLength}' template + */ + pageTemplate?: string; + + /** + * ability to hide the page xx of yy block + */ + pageVisible?: boolean; + /** + * string template for the page size selection + * '${pageSize} rows', + */ + pageSizeTemplate?: string; + + /** + * ability to hide the page size selector + */ + pageSizeVisible?: boolean; + /** + * string for the go to page label + * 'Go to page:' + */ + goToPageTemplate?: string; + /** + * ability to hide the go to page block + */ + goToPageVisible?: boolean; + /** + * initial page index when pagination is enabled + */ + pageIndex?: number; + + /** + * initial page size when pagination is enabled + */ + pageSize?: number; +} +export const TablePagination: FC & + TablePaginationProps> = props => { + const { + gotoPage, + canPreviousPage, + previousPage, + nextPage, + canNextPage, + pageCount, + setPageSize, + pageIndex = 0, + pageSize = 10, + pageOptions, + pageTemplate = 'Page ${pageIndex} of ${pageLength}', + pageVisible = false, + pageSizeTemplate = '${pageSize} rows', + pageSizeVisible = false, + goToPageVisible = false, + goToPageTemplate = 'Go to page:', + } = props; + const pageResolvedTemplate = runtimeTemplate(pageTemplate, { + pageIndex: pageIndex + 1, + pageLength: pageOptions.length, + }); + + return ( + + + + + + + + + + + + + + + + {pageVisible && ( + {pageResolvedTemplate} + )} + {goToPageVisible && ( + + {goToPageTemplate} + { + const page = e.target.value ? Number(e.target.value) - 1 : 0; + gotoPage(page); + }} + /> + + )} + {pageSizeVisible && ( + + + + )} + + ); +}; diff --git a/ui/components/src/ThemeContext/theme.ts b/ui/components/src/ThemeContext/theme.ts index d2f703188..a9058bf8d 100644 --- a/ui/components/src/ThemeContext/theme.ts +++ b/ui/components/src/ThemeContext/theme.ts @@ -52,6 +52,7 @@ export type ControlsTheme = { syntaxhighlight: Record; tabs: Record; tag: Record; + table: Record; titledimage: Record; value: Record; zoom: ThemeUIStyleObject; @@ -153,6 +154,9 @@ export const theme: ControlsTheme = { backgroundColor: '#f3f3f3', borderRadius: '5px', boxShadow: 'rgba(0, 0, 0, 0.1) 0px 0px 0px 1px inset', + ':disabled': { + color: '#aaa', + }, }, secondary: { backgroundColor: 'action', @@ -637,6 +641,45 @@ export const theme: ControlsTheme = { display: 'block', }, }, + table: { + pagination: { + container: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + backgroundColor: 'background', + pt: 2, + }, + navigation: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + px: 2, + button: { + width: '50px', + mx: '2px', + }, + }, + page: { + px: 2, + }, + pagesize: { + px: 2, + width: '140px', + }, + interactive: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + px: 2, + input: { + ml: 1, + width: '100px', + }, + }, + }, + }, tag: { default: { display: 'inline-block',