From 37e673e9db19a3c5b6b8798cd978087142f0c336 Mon Sep 17 00:00:00 2001 From: Shawn Zhu Date: Tue, 21 Mar 2023 14:04:21 -0400 Subject: [PATCH] feat(PHC-4380): add sorting #331 --- .../TableModule/ReactTableModule.stories.tsx | 66 ++++++++++++++++++- .../TableModule/ReactTableModule.tsx | 28 ++++++-- .../TableModule/TableHeaderCell.tsx | 34 +++++++--- src/components/TableModule/TableModuleRow.tsx | 9 ++- src/components/TableModule/types.ts | 7 +- src/components/TableModule/utils.ts | 24 +++++-- 6 files changed, 136 insertions(+), 32 deletions(-) diff --git a/src/components/TableModule/ReactTableModule.stories.tsx b/src/components/TableModule/ReactTableModule.stories.tsx index 9b67dff6..aad3c2d3 100644 --- a/src/components/TableModule/ReactTableModule.stories.tsx +++ b/src/components/TableModule/ReactTableModule.stories.tsx @@ -1,6 +1,6 @@ import React, { useRef } from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { createColumnHelper, Row } from '@tanstack/react-table'; +import { createColumnHelper, SortingState } from '@tanstack/react-table'; import { Checkbox } from '../Checkbox'; import { ReactTableModule } from './ReactTableModule'; @@ -150,6 +150,70 @@ Default.args = { config, }; +export const ManualSort: ComponentStory = (args) => { + const tableRef = useRef(null); + + const [sorting, setSorting] = React.useState([]); + const [sortedData, setSortedData] = React.useState(data); + + args.state = { sorting }; + + React.useEffect(() => { + const sort = sorting?.[0]; + const index = sort?.id.toLowerCase(); + console.log('sorting', sort); + if (sort && index) { + setSortedData( + [...data].sort((a: any, b: any) => { + if (!sort.desc) { + return a[index!] - b[index!]; + } + + if (sort.desc) { + return b[index!] - a[index!]; + } + + return 0; + }) + ); + } + }, [sorting, setSortedData]); + + return ( +
+ +
+ ); +}; + +export const DefaultSort: ComponentStory = (args) => { + const tableRef = useRef(null); + + const [sorting, setSorting] = React.useState([]); + + args.state = { sorting }; + + return ( +
+ +
+ ); +}; + export const RowSelection = Template.bind({}); const selectionColumn = { id: 'select', diff --git a/src/components/TableModule/ReactTableModule.tsx b/src/components/TableModule/ReactTableModule.tsx index 313dddf9..b1197bf2 100644 --- a/src/components/TableModule/ReactTableModule.tsx +++ b/src/components/TableModule/ReactTableModule.tsx @@ -3,10 +3,13 @@ import clsx from 'clsx'; import { getCoreRowModel, + getSortedRowModel, useReactTable, ColumnDef, OnChangeFn, TableState, + Updater, + SortingState, } from '@tanstack/react-table'; import { @@ -24,7 +27,10 @@ export interface ReactTableProps extends TableModuleProps { data: Array; columns?: ColumnDef[]; enableRowSelection?: boolean; + enableSorting?: boolean; onRowSelectionChange?: OnChangeFn; + manualSorting?: boolean; + onSortingChange?: OnChangeFn; state?: Partial; } @@ -37,9 +43,12 @@ export const ReactTableModule = React.memo( className, data, enableRowSelection, + enableSorting, isLoading = false, onRowSelectionChange, + onSortingChange, rowRole, + sortState = { sortKey: null, sortDirection: null }, maxCellWidth, rowClickLabel, state, @@ -49,19 +58,24 @@ export const ReactTableModule = React.memo( ) => { const classes = useStyles({}); - if (columns === undefined && !!config) { + // use legacy when using config of TableModule + if (config) { columns = config.map(mapTableConfigToColumnDef); } - - console.log('columns', columns); + // it does manual sorting in legacy mode + const manualSorting = !!config; const table = useReactTable({ data, columns, enableRowSelection, - getCoreRowModel: getCoreRowModel(), + enableSorting, onRowSelectionChange, + onSortingChange, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), state, + manualSorting, }); const headers = table.getHeaderGroups()[0].headers; @@ -85,7 +99,11 @@ export const ReactTableModule = React.memo( ))} diff --git a/src/components/TableModule/TableHeaderCell.tsx b/src/components/TableModule/TableHeaderCell.tsx index 5cd89417..a3f3873c 100644 --- a/src/components/TableModule/TableHeaderCell.tsx +++ b/src/components/TableModule/TableHeaderCell.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import * as React from 'react'; -import { flexRender } from '@tanstack/react-table'; +import { Header, flexRender } from '@tanstack/react-table'; import { ChevronDown } from '@lifeomic/chromicons'; import { makeStyles } from '../../styles/index'; import { GetClasses } from '../../typeUtils'; @@ -98,6 +98,7 @@ export type TableHeaderCellClasses = GetClasses; export interface TableHeaderCellProps extends TableSortDirection { isSorting?: boolean; header: TableHeader; + coreHeader?: Header; onClick?: (header: TableSortClickProps) => any; index: number; headingsCount: number; @@ -107,6 +108,7 @@ export interface TableHeaderCellProps extends TableSortDirection { export const TableHeaderCell: React.FC = ({ header, + coreHeader, isSorting = false, sortDirection, onClick, @@ -117,12 +119,21 @@ export const TableHeaderCell: React.FC = ({ ...rootProps }) => { const classes = useStyles({}); + // use column API when coreHeader is available + const isSorted = coreHeader ? coreHeader.column.getIsSorted() : isSorting; + // TODO combine these two props into one + const sortedDirection = coreHeader + ? coreHeader.column.getIsSorted() + : sortDirection; const handleClick = () => { onClick?.({ index, sortDirection, header }); }; - const canSort = onClick && header.onSort; + // use column API when coreHeader is available + const canSort = coreHeader + ? coreHeader.column.getCanSort() + : onClick && header.onSort; const Tag = !header?.content && !header.label ? 'td' : 'th'; @@ -130,7 +141,7 @@ export const TableHeaderCell: React.FC = ({ = ({ onClick={canSort ? handleClick : undefined} role="columnheader" aria-sort={ - !isSorting || !sortDirection + !isSorted || !sortedDirection ? 'none' - : sortDirection === 'asc' + : sortedDirection === 'asc' ? 'ascending' : 'descending' } style={{ left: left }} {...rootProps} > - {header.column - ? flexRender(header.column.columnDef.header, header.getContext()) + {coreHeader + ? flexRender( + coreHeader.column.columnDef.header, + coreHeader.getContext() + ) : header.content ? header.content(header) : header.label} {/* We aren't actively sorting this column, but we want to display a "peek" icon so they know they can sort it */} - {(!sortDirection || !isSorting) && canSort && ( + {(!sortedDirection || !isSorted) && canSort && ( = ({ /> )} {/* We have a sort active */} - {isSorting && sortDirection && ( + {isSorted && sortedDirection && ( ; + cells: Array | Cell[]; rowActions?: TableModuleProps['rowActions']; rowClickLabel?: TableModuleProps['rowClickLabel']; stickyCols?: Array; @@ -82,9 +83,7 @@ const TableModuleRow: React.FC = React.memo( // table-core API const cellContent = cell.column ? flexRender(cell.column.columnDef.cell, cell.getContext()) - : cell.content // private API - ? cell.content(row) - : cell.valuePath && row[cell.valuePath]; + : cellContentAccessor(cell)(row); return ( { +export interface TableHeader extends TableAlignOptions { label?: string; content?(header: TableHeader): any; onSort?(sort: TableSortClickProps): any; className?: string; } -export interface TableCell - extends TableAlignOptions, - Cell { +export interface TableCell extends TableAlignOptions { valuePath?: string; content?(cell: Item): any; className?: string; diff --git a/src/components/TableModule/utils.ts b/src/components/TableModule/utils.ts index 9fe93eaf..54e1d821 100644 --- a/src/components/TableModule/utils.ts +++ b/src/components/TableModule/utils.ts @@ -1,7 +1,5 @@ -import { createColumnHelper, ColumnDef } from '@tanstack/react-table'; -import { TableConfiguration } from './types'; - -const columnHelper = createColumnHelper(); +import { ColumnDef } from '@tanstack/react-table'; +import { TableConfiguration, TableCell } from './types'; /** * returns react table ColumnDef. @@ -11,8 +9,22 @@ export const mapTableConfigToColumnDef = ( config: TableConfiguration ): ColumnDef => { // TODO support accessor columnFn - return { + const columnDef: ColumnDef = { id: config.header.label || config.header.content(config.header), - accessorFn: config.cell.content, + accessorFn: cellContentAccessor(config.cell), }; + + return columnDef; +}; + +/** + * returns an accessorFn. + * @param cell + */ +export const cellContentAccessor = (cell: TableCell): ((row: any) => any) => { + return cell.content // private API + ? cell.content + : (row: any): any => { + return cell.valuePath && row[cell.valuePath]; + }; };