From 873b0fec90bc9c4ad0b7c443effa260425a248b0 Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Thu, 8 Aug 2019 19:12:44 +0200 Subject: [PATCH 1/3] Make the examples more readable Drop class components in favor of function components. Add comments to help understanding what's going on. Add an helper hook useSelectableRows to ease working with selectable rows. --- .../react-table/src/components/Table/Table.md | 423 +++++++++++------- .../src/components/Table/hooks/index.ts | 1 + .../Table/hooks/useSelectableRows.ts | 83 ++++ 3 files changed, 356 insertions(+), 151 deletions(-) create mode 100644 packages/patternfly-4/react-table/src/components/Table/hooks/index.ts create mode 100644 packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.md b/packages/patternfly-4/react-table/src/components/Table/Table.md index 030c9ddca5a..e8b87e8d15a 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.md +++ b/packages/patternfly-4/react-table/src/components/Table/Table.md @@ -41,70 +41,175 @@ import { Table, TableHeader, TableBody, - sortable, - SortByDirection, - headerCol, - TableVariant, - expandable, + textCenter, +} from '@patternfly/react-table'; + +function SimpleTable() { + const cells = [ + 'Repositories', + 'Branches', + 'Pull requests', + 'Workspaces', + 'Last Commit' + ]; + const rows = [ + ['Foo', 2, 0, 6, 1533635470], + ['Bar', 1, 11, 2, 1564566670], + ['Baz', 6, 4, 99, 1565167870], + ]; + + return ( + + + +
+ ); +} +``` + +## Cell/row options + +```js +import React from 'react'; +import { + Table, + TableHeader, + TableBody, cellWidth, textCenter, } from '@patternfly/react-table'; -class SimpleTable extends React.Component { - constructor(props) { - super(props); - this.state = { - columns: [ - { title: 'Repositories' }, - 'Branches', - { title: 'Pull requests' }, - 'Workspaces', - { - title: 'Last Commit', - transforms: [textCenter], - cellTransforms: [textCenter] - } - ], - rows: [ - { - cells: ['one', 'two', 'three', 'four', 'five'] - }, - { - cells: [ - { - title:
one - 2
, - props: { title: 'hover title', colSpan: 3 } - }, - 'four - 2', - 'five - 2' - ] - }, - { - cells: [ - 'one - 3', - 'two - 3', - 'three - 3', - 'four - 3', - { - title: 'five - 3 (not centered)', - props: { textCenter: false } - } - ] - } - ] - }; - } +function SimpleCustomTable() { + const cells = [ + // Equivalent to just passing the string + { title: 'Repositories' }, + 'Branches', + { title: 'Pull requests' }, + 'Workspaces', + // Will run the `transform` functions on the header, and the + // `cellTransforms` on the cells. Transformers are used to inject + // extra props to the the cell component. In this case we use the + // builtin `textCenter` transformer to instruct the cell to apply + // the required stylings to center the cell content. + { + title: 'Last Commit', + transforms: [textCenter], + cellTransforms: [textCenter] + } + ]; + const rows = [ + ['Foo', 2, 0, 6, 1533635470], + [ + // Cell content can also be defined as a JSX element. + // Extra props can be passed to the cell (the `td` element). + { + title:
Bar 👾
, + props: { title: 'hover title', colSpan: 3 } + }, + 2, + { + title:
1564566670
+ } + ], + [ + 'Baz', + 6, + 4, + 99, + // In this example, we disable the default cellTransform for this specific cell, setting the `textCenter` prop to `false`. + { + title: '1565167870 (not centered)', + props: { textCenter: false } + } + ] + ]; + + return ( + + + +
+ ); +} +``` - render() { - const { columns, rows } = this.state; +## Cell formatters +```js +import React from 'react'; +import { + Table, + TableHeader, + TableBody, + textCenter +} from '@patternfly/react-table'; +import { + CheckIcon, + TimesIcon +} from '@patternfly/react-icons'; + +function CellFormatters() { + const rows = [ + ['Foo', 1533635470, 1], + ['Bar', 1564566670, 0], + ['Baz', 1565167870, 1], + ]; + + // A component that displays a CI build status. + // *Heads-up* - this component definition should *not* stay inside + // the main component function in a real world application; it's + // done this way in this example as a work-around against the + // limitation of one component per example of the documentation + // system in use. + const CIStatusIcon = ({ passing }) => { + const styles = { + color: passing + ? 'var(--pf-global--success-color--200)' + : 'var(--pf-global--danger-color--100)' + }; + const icon = passing ? : ; + const text = passing ? 'Passing' : 'Failing'; return ( - - - -
+
+ {icon} {text} +
); + }; + + // Last commit comes as a unix timestamp (in seconds). We specify + // a formatter that convert it to something readable by humans. + const lastCommitTimestampToLocalHuman = (value, extraProps) => { + return new Date(value * 1000).toLocaleString(); } + + // CI Status comes as a boolean value. We specify a formatter that + // passes the value to the `CIStatusIcon` we wrote. + const statusToIcon = (value, extraProps) => { + return ; + } + + const cells = [ + 'Repositories', + { + title: 'Last Commit', + cellFormatters: [lastCommitTimestampToLocalHuman] + }, + { + title: 'CI Status', + cellFormatters: [statusToIcon], + // We apply some transforms to both the header and the cell, to make + // it centered. + transforms: [textCenter], + cellTransforms: [textCenter] + } + ]; + + return ( + + + +
+ ); } ``` @@ -117,51 +222,43 @@ import { TableHeader, TableBody, sortable, - SortByDirection, - headerCol, - TableVariant, - expandable, - cellWidth } from '@patternfly/react-table'; -class SortableTable extends React.Component { - constructor(props) { - super(props); - this.state = { - columns: [ - { title: 'Repositories', transforms: [sortable] }, - 'Branches', - { title: 'Pull requests', transforms: [sortable] }, - 'Workspaces', - 'Last Commit' - ], - rows: [['one', 'two', 'a', 'four', 'five'], ['a', 'two', 'k', 'four', 'five'], ['p', 'two', 'b', 'four', 'five']], - sortBy: {} - }; - this.onSort = this.onSort.bind(this); - } - - onSort(_event, index, direction) { - const sortedRows = this.state.rows.sort((a, b) => (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)); - this.setState({ - sortBy: { - index, - direction - }, - rows: direction === SortByDirection.asc ? sortedRows : sortedRows.reverse() +function SortableTable () { + const rows = [ + ['Foo', 2, 0, 6, 1533635470], + ['Bar', 1, 11, 2, 1564566670], + ['Baz', 6, 4, 99, 1565167870], + ]; + + const cells = [ + { title: 'Repositories', transforms: [sortable] }, + { title: 'Branches', transforms: [sortable] }, + { title: 'Pull requests', transforms: [sortable] }, + { title: 'Workspaces', transforms: [sortable] }, + { title: 'Last Commit', transforms: [sortable] } + ]; + + const [sortBy, setSortBy] = React.useState({}); + const onSort = (_event, index, direction) => { + setSortBy({ + index, + direction }); - } - - render() { - const { columns, rows, sortBy } = this.state; - - return ( - - - -
- ); - } + }; + + const sortedRows = !sortBy.direction ? rows : rows.sort( + (a, b) => (a[sortBy.index] < b[sortBy.index] ? -1 : a[sortBy.index] > b[sortBy.index] ? 1 : 0) + * + (sortBy.direction === 'desc' ? -1 : 1) + ); + + return ( + + + +
+ ); } ``` @@ -173,66 +270,90 @@ import { Table, TableHeader, TableBody, - sortable, - SortByDirection, headerCol, - TableVariant, - expandable, - cellWidth + sortable, + useSelectableRows } from '@patternfly/react-table'; -class SelectableTable extends React.Component { - constructor(props) { - super(props); - this.state = { - columns: [ - { title: 'Repositories', cellTransforms: [headerCol()] }, - 'Branches', - { title: 'Pull requests' }, - 'Workspaces', - 'Last Commit' - ], - rows: [ - { - cells: ['one', 'two', 'a', 'four', 'five'] - }, - { - cells: ['a', 'two', 'k', 'four', 'five'] - }, - { - cells: ['p', 'two', 'b', 'four', 'five'] - } - ] - }; - this.onSelect = this.onSelect.bind(this); - } +function SelectableTable () { + const rows = [ + ['Foo', 2, 0, 6, 1533635470], + ['Bar', 1, 11, 2, 1564566670], + ['Baz', 6, 4, 99, 1565167870], + ]; + + const cells = [ + { title: 'Repositories', cellTransforms: [headerCol()] }, + 'Branches', + 'Pull requests', + 'Workspaces', + 'Last Commit', + ]; + + const [selectedRows, onSelect] = useSelectableRows(rows); + + return ( + + + +
+ ); +} +``` - onSelect(event, isSelected, rowId) { - let rows; - if (rowId === -1) { - rows = this.state.rows.map(oneRow => { - oneRow.selected = isSelected; - return oneRow; - }); - } else { - rows = [...this.state.rows]; - rows[rowId].selected = isSelected; - } - this.setState({ - rows - }); - } +## Sortable and selectable table - render() { - const { columns, rows } = this.state; +```js +import React from 'react'; +import { + Table, + TableHeader, + TableBody, + headerCol, + useSelectableRows +} from '@patternfly/react-table'; - return ( - - - -
- ); - } +function SortableAndSelectableTable () { + const rows = [ + ['Foo', 2, 0, 6, 1533635470], + ['Bar', 1, 11, 2, 1564566670], + ['Baz', 6, 4, 99, 1565167870], + ]; + + const cells = [ + { title: 'Repositories', transforms: [sortable], cellTransforms: [headerCol()] }, + { title: 'Branches', transforms: [sortable] }, + { title: 'Pull requests', transforms: [sortable] }, + { title: 'Workspaces', transforms: [sortable] }, + { title: 'Last Commit', transforms: [sortable] } + ]; + + const [sortBy, setSortBy] = React.useState({}); + const onSort = (_event, index, direction) => { + setSortBy({ + index, + direction + }); + }; + + const sortedRows = !sortBy.direction ? rows : rows.sort( + (a, b) => (a[sortBy.index] < b[sortBy.index] ? -1 : a[sortBy.index] > b[sortBy.index] ? 1 : 0) + * + (sortBy.direction === 'desc' ? -1 : 1) + ); + + // we need to specify the getRowKey callback to use unique ids to identify the + // selected rows. We use the repository name in this example. + const [sortedAndSelectedRows, onSelect] = useSelectableRows(sortedRows, { + getRowKey: (rowData, rowIndex) => rowData[0] + }); + + return ( + + + +
+ ); } ``` diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts new file mode 100644 index 00000000000..a5d48af51f3 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts @@ -0,0 +1 @@ +export * from './useSelectableRows'; \ No newline at end of file diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts new file mode 100644 index 00000000000..bd55a1923ed --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts @@ -0,0 +1,83 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; +import { IRows, ISelectableArray, OnSelectCallback } from '../Table'; + +const defaultOptions = { + getRowKey: (rowData, rowIndex) => rowIndex +}; + +/** + * Returns the onSelect callback required by the Table component to allow for + * selecting rows, and the updated `rows` with the right internal flags to tell + * the Table component which rows are selected and which are not. + * + * @example + * const [selectedRows, selectedRows] = useSelectableRows(rows); + * + * @param rows + * @param getRowKey - optional, a function to return an unique key for a row. By + * default the row's index is used. For the table to be also sortable while being + * selectable, a key that uniquely identify the row among its siblings is required. + */ +export function useSelectableRows(rows: IRows, { getRowKey } = defaultOptions) { + // Selected rows's keys will be saved in the component's state + const [selectedKeys, setSelectedKeys] = useState>([]); + + // When selecting/deselecting all lines, or when transitioning from an all rows + // selected state, we need to compute the new keys based on the full list of keys + // available in the original rows array. + // Since that array can be composed of many entries, we cache the value so we don't + // pay the cost of the map when the user is not changing the original data. + const allKeys = useMemo(() => rows.map((r, idx) => getRowKey(r, idx)), [rows, getRowKey]); + + // Since the Table component will not re-render rows if unchanged (because of the + // BodyRow:shouldComponentUpdate method), we need to have a reference to an alway + // up to date value of the selected keys in the callback we pass to the selectable + // cell. This way we can ensure that that callback will not run against stale state + // data. + const latestSelectedKeys = useRef(selectedKeys); + + // The callback that should be passed to the Table's onSelect property. + // It will update the list of selected keys based on the user action. + // Note that the user could be interacting with the select/deselect all button + // in the header: that case is identified by the rowIndex value passed as -1 + // by the Table component. + const onSelect = useCallback( + (event, isSelected, rowIndex, rowData, extraData) => { + const latestIndexes = latestSelectedKeys.current; + let updatedIndexes = selectedKeys; + // A rowIndex -1 indicates that the user clicked on the select all checkbox. + if (rowIndex === -1) { + updatedIndexes = isSelected ? allKeys : []; + } else { + // A specific row has been selected/deselected + const rowKey = getRowKey(rowData, rowIndex); + updatedIndexes = isSelected + ? Array.from(new Set([...latestIndexes, rowKey])) + : latestIndexes.filter(index => index !== rowKey); + } + // Here we make sure that other onSelect callbacks will work against the latest + // set of selected keys. + latestSelectedKeys.current = updatedIndexes; + // We still have to save the selected keys in the state, to trigger a re-render + // of the component so that selected rows will actually be displayed as + // selected. + setSelectedKeys(updatedIndexes); + }, + [setSelectedKeys, latestSelectedKeys, allKeys, getRowKey] + ); + + const selectedRows = rows.map((row, index) => { + const isRowSelected = selectedKeys.includes(getRowKey(row, index)); + if (Array.isArray(row)) { + const updatedRow = [...row] as typeof row; + updatedRow.selected = isRowSelected; + return updatedRow; + } + + const updatedRow = { ...row }; + updatedRow.selected = isRowSelected; + return updatedRow; + }); + + return [selectedRows, onSelect]; +} From 1ecbde3229295c9a9a1dee22d0414aa123dbd7f7 Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Fri, 9 Aug 2019 13:09:54 +0200 Subject: [PATCH 2/3] Add a useSortableRows hook This should ease working with sortable tables, fixes a bug with the sortable cell not calculating the right column index, and makes it possible to mix sortable and selectable rows hooks. --- .../react-table/src/components/Table/Table.md | 63 +++++++------------ .../src/components/Table/hooks/index.ts | 3 +- .../components/Table/hooks/useSortableRows.ts | 50 +++++++++++++++ .../Table/utils/decorators/sortable.tsx | 13 ++-- 4 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.md b/packages/patternfly-4/react-table/src/components/Table/Table.md index e8b87e8d15a..021701a4d5d 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.md +++ b/packages/patternfly-4/react-table/src/components/Table/Table.md @@ -222,39 +222,36 @@ import { TableHeader, TableBody, sortable, + SortHelpers, + useSortableRows } from '@patternfly/react-table'; function SortableTable () { const rows = [ ['Foo', 2, 0, 6, 1533635470], - ['Bar', 1, 11, 2, 1564566670], + ['Bar', 1, 11, 2, { title:
⏰ 1564566670
, value: 1564566670 }], ['Baz', 6, 4, 99, 1565167870], + ['Qux', undefined, 4, undefined, 1565167870], ]; + + const sortLastCommit = (a, b, aObj, bObj) => { + a = aObj.value || a; + b = bObj.value || b; + return SortHelpers.numbers(a, b); + }; const cells = [ - { title: 'Repositories', transforms: [sortable] }, - { title: 'Branches', transforms: [sortable] }, - { title: 'Pull requests', transforms: [sortable] }, - { title: 'Workspaces', transforms: [sortable] }, - { title: 'Last Commit', transforms: [sortable] } + { title: 'Repositories', transforms: [sortable(SortHelpers.strings)] }, + { title: 'Branches', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Pull requests', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Workspaces', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Last Commit', transforms: [sortable(sortLastCommit)] } ]; - const [sortBy, setSortBy] = React.useState({}); - const onSort = (_event, index, direction) => { - setSortBy({ - index, - direction - }); - }; - - const sortedRows = !sortBy.direction ? rows : rows.sort( - (a, b) => (a[sortBy.index] < b[sortBy.index] ? -1 : a[sortBy.index] > b[sortBy.index] ? 1 : 0) - * - (sortBy.direction === 'desc' ? -1 : 1) - ); + const [sortedRows, onSort, sortBy] = useSortableRows(rows); return ( - +
@@ -321,26 +318,14 @@ function SortableAndSelectableTable () { ]; const cells = [ - { title: 'Repositories', transforms: [sortable], cellTransforms: [headerCol()] }, - { title: 'Branches', transforms: [sortable] }, - { title: 'Pull requests', transforms: [sortable] }, - { title: 'Workspaces', transforms: [sortable] }, - { title: 'Last Commit', transforms: [sortable] } + { title: 'Repositories', transforms: [sortable(SortHelpers.strings)], cellTransforms: [headerCol()] }, + { title: 'Branches', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Pull requests', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Workspaces', transforms: [sortable(SortHelpers.numbers)] }, + { title: 'Last Commit', transforms: [sortable(SortHelpers.numbers)] } ]; - const [sortBy, setSortBy] = React.useState({}); - const onSort = (_event, index, direction) => { - setSortBy({ - index, - direction - }); - }; - - const sortedRows = !sortBy.direction ? rows : rows.sort( - (a, b) => (a[sortBy.index] < b[sortBy.index] ? -1 : a[sortBy.index] > b[sortBy.index] ? 1 : 0) - * - (sortBy.direction === 'desc' ? -1 : 1) - ); + const [sortedRows, onSort, sortBy] = useSortableRows(rows, cells); // we need to specify the getRowKey callback to use unique ids to identify the // selected rows. We use the repository name in this example. @@ -349,7 +334,7 @@ function SortableAndSelectableTable () { }); return ( - +
diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts index a5d48af51f3..cd628f7d4a8 100644 --- a/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/index.ts @@ -1 +1,2 @@ -export * from './useSelectableRows'; \ No newline at end of file +export * from './useSelectableRows'; +export * from './useSortableRows'; diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts new file mode 100644 index 00000000000..e80969ca117 --- /dev/null +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts @@ -0,0 +1,50 @@ +import { useMemo, useState } from 'react'; +import { IExtraColumnData, IRows, OnSort, OnSortCallback, OnSortDirection } from '../Table'; + +export const SortHelpers = { + numbers(a: number, b: number) { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + return 0; + }, + + booleans(a: boolean, b: boolean) { + const toNumber = (v: boolean) => (v ? 1 : 0); + return SortHelpers.numbers(toNumber(a), toNumber(b)); + }, + + strings(a: string, b: string) { + return a.localeCompare(b); + } +}; + +export function useSortableRows(rows: IRows) { + const [sortBy, setSortBy] = useState< + | { index: number; direction: OnSortDirection; columnData: IExtraColumnData; sortCallback: OnSortCallback } + | undefined + >(); + + const onSort: OnSort = (_event, index, direction, columnData, sortCallback) => { + setSortBy({ + index, + direction, + columnData, + sortCallback + }); + }; + + const sortCb = (rowA, rowB) => { + const [a, b] = + sortBy.direction === 'desc' ? [rowA[sortBy.index], rowB[sortBy.index]] : [rowB[sortBy.index], rowA[sortBy.index]]; + const aValue = typeof a === 'object' && a.title ? a.title : a; + const bValue = typeof b === 'object' && b.title ? b.title : b; + return sortBy.sortCallback(aValue, bValue, a, b); + }; + + const sortedRows = useMemo(() => (!sortBy ? rows : rows.sort(sortCb)), [sortBy, rows, sortCb]); + + return [sortedRows, onSort, sortBy]; +} diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx b/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx index abcc2f7ad4b..a0a4f6b374b 100644 --- a/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx +++ b/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx @@ -7,15 +7,20 @@ import { SortColumn } from '../../SortColumn'; export const sortable = (label: IFormatterValueType, { columnIndex, column, property }: IExtra) => { const { - extraParams: { sortBy, onSort } + extraParams: { sortBy, onSort, firstUserColumnIndex } } = column; + + // correct the column index based on the presence of extra columns added on the + // left of the user provided ones + const correctedColumnIndex = columnIndex - firstUserColumnIndex; + const extraData = { - columnIndex, + columnIndex: correctedColumnIndex, column, property }; - const isSortedBy = sortBy && columnIndex === sortBy.index; + const isSortedBy = sortBy && correctedColumnIndex === sortBy.index; function sortClicked(event: React.MouseEvent) { let reversedDirection; if (!isSortedBy) { @@ -24,7 +29,7 @@ export const sortable = (label: IFormatterValueType, { columnIndex, column, prop reversedDirection = sortBy.direction === SortByDirection.asc ? SortByDirection.desc : SortByDirection.asc; } // tslint:disable-next-line:no-unused-expression - onSort && onSort(event, columnIndex, reversedDirection, extraData); + onSort && onSort(event, correctedColumnIndex, reversedDirection, extraData); } return { From d47a3933185c9ec40ad69ee95e8021e48d9d3b5a Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Wed, 4 Sep 2019 13:06:12 +0200 Subject: [PATCH 3/3] Rebased on master --- .../react-table/src/components/Table/Table.md | 34 ++++++++------- .../src/components/Table/Table.tsx | 16 +++++--- .../Table/hooks/useSelectableRows.ts | 17 ++++---- .../components/Table/hooks/useSortableRows.ts | 26 ++---------- .../react-table/src/components/Table/index.ts | 1 + .../Table/utils/decorators/sortable.tsx | 41 +++++++++++++++++-- .../components/Table/utils/transformers.tsx | 2 +- 7 files changed, 81 insertions(+), 56 deletions(-) diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.md b/packages/patternfly-4/react-table/src/components/Table/Table.md index 021701a4d5d..bbaa4a18f5c 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.md +++ b/packages/patternfly-4/react-table/src/components/Table/Table.md @@ -13,6 +13,7 @@ import { TableHeader, TableBody, sortable, + SortHelpers, SortByDirection, headerCol, TableVariant, @@ -22,13 +23,16 @@ import { textCenter, wrappable, classNames, - Visibility + Visibility, + useSortableRows, + useSelectableRows } from '@patternfly/react-table'; - import { CodeBranchIcon, CodeIcon, - CubeIcon + CubeIcon, + CheckIcon, + TimesIcon } from '@patternfly/react-icons'; import DemoSortableTable from './demo/DemoSortableTable'; @@ -241,11 +245,11 @@ function SortableTable () { }; const cells = [ - { title: 'Repositories', transforms: [sortable(SortHelpers.strings)] }, - { title: 'Branches', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Pull requests', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Workspaces', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Last Commit', transforms: [sortable(sortLastCommit)] } + { title: 'Repositories', transforms: [sortable] }, + { title: 'Branches', transforms: [sortable.numbers] }, + { title: 'Pull requests', transforms: [sortable.numbers] }, + { title: 'Workspaces', transforms: [sortable.numbers] }, + { title: 'Last Commit', transforms: [sortable.custom(sortLastCommit)] } ]; const [sortedRows, onSort, sortBy] = useSortableRows(rows); @@ -268,7 +272,6 @@ import { TableHeader, TableBody, headerCol, - sortable, useSelectableRows } from '@patternfly/react-table'; @@ -307,6 +310,7 @@ import { TableHeader, TableBody, headerCol, + sortable, useSelectableRows } from '@patternfly/react-table'; @@ -318,11 +322,11 @@ function SortableAndSelectableTable () { ]; const cells = [ - { title: 'Repositories', transforms: [sortable(SortHelpers.strings)], cellTransforms: [headerCol()] }, - { title: 'Branches', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Pull requests', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Workspaces', transforms: [sortable(SortHelpers.numbers)] }, - { title: 'Last Commit', transforms: [sortable(SortHelpers.numbers)] } + { title: 'Repositories', transforms: [sortable], cellTransforms: [headerCol()] }, + { title: 'Branches', transforms: [sortable.numbers] }, + { title: 'Pull requests', transforms: [sortable.numbers] }, + { title: 'Workspaces', transforms: [sortable.numbers] }, + { title: 'Last Commit', transforms: [sortable.numbers] } ]; const [sortedRows, onSort, sortBy] = useSortableRows(rows, cells); @@ -404,7 +408,7 @@ class SimpleActionsTable extends React.Component { render() { const { columns, rows, actions } = this.state; return ( - +
diff --git a/packages/patternfly-4/react-table/src/components/Table/Table.tsx b/packages/patternfly-4/react-table/src/components/Table/Table.tsx index f22fa20fea9..681c0b5ff05 100644 --- a/packages/patternfly-4/react-table/src/components/Table/Table.tsx +++ b/packages/patternfly-4/react-table/src/components/Table/Table.tsx @@ -8,7 +8,7 @@ import { BodyCell } from './BodyCell'; import { HeaderCell } from './HeaderCell'; import { RowWrapper } from './RowWrapper'; import { BodyWrapper } from './BodyWrapper'; -import { calculateColumns } from './utils/headerUtils'; +import { calculateColumns } from './utils'; import { formatterValueType, ColumnType, RowType, RowKeyType, ColumnsType } from './base'; export enum TableGridBreakpoint { @@ -23,8 +23,8 @@ export enum TableGridBreakpoint { export enum TableVariant { compact = 'compact' } - -export type OnSort = (event: React.MouseEvent, columnIndex: number, sortByDirection: SortByDirection, extraData: IExtraColumnData) => void; +export type OnSortCallback = (aValue: any, bValue: any, aObject: IRow | string, bObject: IRow | string) => number; +export type OnSort = (event: React.MouseEvent, columnIndex: number, sortByDirection: SortByDirection, extraData: IExtraColumnData, sortCallback: OnSortCallback) => void; export type OnCollapse = (event: React.MouseEvent, rowIndex: number, isOpen: boolean, rowData: IRowData, extraData: IExtraData) => void; export type OnExpand = (event: React.MouseEvent, rowIndex: number, colIndex: number, isOpen: boolean, rowData: IRowData, extraData: IExtraData) => void; export type OnSelect = (event: React.MouseEvent, isSelected: boolean, rowIndex: number, rowData: IRowData, extraData: IExtraData) => void; @@ -45,6 +45,7 @@ export interface IColumn { extraParams: { sortBy?: ISortBy; onSort?: OnSort; + sortCallback?: OnSortCallback; onCollapse?: OnCollapse; onExpand?: OnExpand; onSelect?: OnSelect; @@ -54,6 +55,7 @@ export interface IColumn { dropdownPosition?: DropdownPosition; dropdownDirection?: DropdownDirection; allRowsSelected?: boolean; + firstUserColumnIndex?: number; }; } @@ -106,6 +108,8 @@ export interface IDecorator extends React.HTMLProps { children?: React.ReactNode; } +export type ICells = (ICell | string)[]; + export interface ICell { title?: string; transforms?: ((...args: any) => any)[]; @@ -124,6 +128,8 @@ export interface IRowCell { props?: any; } +export type IRows = (((string | number | IRowCell)[]) | IRow)[]; + export interface IRow extends RowType { cells?: (React.ReactNode | IRowCell)[]; isOpen?: boolean; @@ -161,8 +167,8 @@ export interface TableProps { contentId?: string; dropdownPosition?: 'right' | 'left'; dropdownDirection?: 'up' | 'down'; - rows: (IRow | string[])[]; - cells: (ICell | string)[]; + rows: IRows; + cells: ICells; bodyWrapper?: Function; rowWrapper?: Function; role?: string; diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts index bd55a1923ed..c97f80f815c 100644 --- a/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/useSelectableRows.ts @@ -1,8 +1,8 @@ import { useCallback, useMemo, useRef, useState } from 'react'; -import { IRows, ISelectableArray, OnSelectCallback } from '../Table'; +import { IRowData, IRows, OnSelect } from '../Table'; const defaultOptions = { - getRowKey: (rowData, rowIndex) => rowIndex + getRowKey: (rowData: IRowData, rowIndex: number) => rowIndex }; /** @@ -20,7 +20,7 @@ const defaultOptions = { */ export function useSelectableRows(rows: IRows, { getRowKey } = defaultOptions) { // Selected rows's keys will be saved in the component's state - const [selectedKeys, setSelectedKeys] = useState>([]); + const [selectedKeys, setSelectedKeys] = useState[]>([]); // When selecting/deselecting all lines, or when transitioning from an all rows // selected state, we need to compute the new keys based on the full list of keys @@ -41,7 +41,7 @@ export function useSelectableRows(rows: IRows, { getRowKey } = defaultOptions) { // Note that the user could be interacting with the select/deselect all button // in the header: that case is identified by the rowIndex value passed as -1 // by the Table component. - const onSelect = useCallback( + const onSelect = useCallback( (event, isSelected, rowIndex, rowData, extraData) => { const latestIndexes = latestSelectedKeys.current; let updatedIndexes = selectedKeys; @@ -70,13 +70,14 @@ export function useSelectableRows(rows: IRows, { getRowKey } = defaultOptions) { const isRowSelected = selectedKeys.includes(getRowKey(row, index)); if (Array.isArray(row)) { const updatedRow = [...row] as typeof row; + // cast required to work with primitive types + (updatedRow as any).selected = isRowSelected; + return updatedRow; + } else { + const updatedRow = {...row}; updatedRow.selected = isRowSelected; return updatedRow; } - - const updatedRow = { ...row }; - updatedRow.selected = isRowSelected; - return updatedRow; }); return [selectedRows, onSelect]; diff --git a/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts b/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts index e80969ca117..e1021f40ce2 100644 --- a/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts +++ b/packages/patternfly-4/react-table/src/components/Table/hooks/useSortableRows.ts @@ -1,29 +1,9 @@ import { useMemo, useState } from 'react'; -import { IExtraColumnData, IRows, OnSort, OnSortCallback, OnSortDirection } from '../Table'; - -export const SortHelpers = { - numbers(a: number, b: number) { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } - return 0; - }, - - booleans(a: boolean, b: boolean) { - const toNumber = (v: boolean) => (v ? 1 : 0); - return SortHelpers.numbers(toNumber(a), toNumber(b)); - }, - - strings(a: string, b: string) { - return a.localeCompare(b); - } -}; +import { IExtraColumnData, IRows, OnSort, OnSortCallback, SortByDirection } from '../Table'; export function useSortableRows(rows: IRows) { const [sortBy, setSortBy] = useState< - | { index: number; direction: OnSortDirection; columnData: IExtraColumnData; sortCallback: OnSortCallback } + | { index: number; direction: SortByDirection; columnData: IExtraColumnData; sortCallback: OnSortCallback } | undefined >(); @@ -36,7 +16,7 @@ export function useSortableRows(rows: IRows) { }); }; - const sortCb = (rowA, rowB) => { + const sortCb = (rowA: any, rowB: any) => { const [a, b] = sortBy.direction === 'desc' ? [rowA[sortBy.index], rowB[sortBy.index]] : [rowB[sortBy.index], rowA[sortBy.index]]; const aValue = typeof a === 'object' && a.title ? a.title : a; diff --git a/packages/patternfly-4/react-table/src/components/Table/index.ts b/packages/patternfly-4/react-table/src/components/Table/index.ts index e8197c40dce..4f6069fe9b4 100644 --- a/packages/patternfly-4/react-table/src/components/Table/index.ts +++ b/packages/patternfly-4/react-table/src/components/Table/index.ts @@ -11,3 +11,4 @@ export * from './RowWrapper'; export * from './SelectColumn'; export * from './SortColumn'; export * from './utils'; +export * from './hooks'; diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx b/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx index a0a4f6b374b..f08a36eaea7 100644 --- a/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx +++ b/packages/patternfly-4/react-table/src/components/Table/utils/decorators/sortable.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Table/table'; import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; -import { SortByDirection, IExtra, IFormatterValueType } from '../../Table'; +import { SortByDirection, IExtra, IFormatterValueType, OnSortCallback } from '../../Table'; import { SortColumn } from '../../SortColumn'; -export const sortable = (label: IFormatterValueType, { columnIndex, column, property }: IExtra) => { +const sortableFn = (sortCallback: OnSortCallback, label: IFormatterValueType, { columnIndex, column, property }: IExtra) => { const { - extraParams: { sortBy, onSort, firstUserColumnIndex } + extraParams: { sortBy, onSort, firstUserColumnIndex = 0 } } = column; // correct the column index based on the presence of extra columns added on the @@ -29,7 +29,7 @@ export const sortable = (label: IFormatterValueType, { columnIndex, column, prop reversedDirection = sortBy.direction === SortByDirection.asc ? SortByDirection.desc : SortByDirection.asc; } // tslint:disable-next-line:no-unused-expression - onSort && onSort(event, correctedColumnIndex, reversedDirection, extraData); + onSort && onSort(event, correctedColumnIndex, reversedDirection, extraData, sortCallback); } return { @@ -47,3 +47,36 @@ export const sortable = (label: IFormatterValueType, { columnIndex, column, prop ) }; }; + +const SortHelpers = { + numbers(a: number, b: number) { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + return 0; + }, + + booleans(a: boolean, b: boolean) { + const toNumber = (v: boolean) => (v ? 1 : 0); + return SortHelpers.numbers(toNumber(a), toNumber(b)); + }, + + strings(a: string, b: string) { + return a.localeCompare(b); + } +}; + +const partialOnSort = (fn: OnSortCallback) => sortableFn.bind(null, fn); +const defaultSortable = partialOnSort(SortHelpers.strings); +const sortableFunctions = { + custom: partialOnSort, + numbers: partialOnSort(SortHelpers.numbers), + booleans: partialOnSort(SortHelpers.booleans), + strings: partialOnSort(SortHelpers.strings), +}; + +const sortable = Object.assign(defaultSortable, sortableFunctions); + +export { sortable, SortHelpers }; diff --git a/packages/patternfly-4/react-table/src/components/Table/utils/transformers.tsx b/packages/patternfly-4/react-table/src/components/Table/utils/transformers.tsx index 3e1c2ba28de..a5b12cc5612 100644 --- a/packages/patternfly-4/react-table/src/components/Table/utils/transformers.tsx +++ b/packages/patternfly-4/react-table/src/components/Table/utils/transformers.tsx @@ -1,5 +1,5 @@ export { selectable } from './decorators/selectable'; -export { sortable } from './decorators/sortable'; +export { sortable, SortHelpers } from './decorators/sortable'; export { cellActions } from './decorators/cellActions'; export { cellWidth } from './decorators/cellWidth'; export { wrappable } from './decorators/wrappable';