Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
422 changes: 266 additions & 156 deletions packages/patternfly-4/react-table/src/components/Table/Table.md

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions packages/patternfly-4/react-table/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -45,6 +45,7 @@ export interface IColumn {
extraParams: {
sortBy?: ISortBy;
onSort?: OnSort;
sortCallback?: OnSortCallback;
onCollapse?: OnCollapse;
onExpand?: OnExpand;
onSelect?: OnSelect;
Expand All @@ -54,6 +55,7 @@ export interface IColumn {
dropdownPosition?: DropdownPosition;
dropdownDirection?: DropdownDirection;
allRowsSelected?: boolean;
firstUserColumnIndex?: number;
};
}

Expand Down Expand Up @@ -106,6 +108,8 @@ export interface IDecorator extends React.HTMLProps<HTMLElement> {
children?: React.ReactNode;
}

export type ICells = (ICell | string)[];

export interface ICell {
title?: string;
transforms?: ((...args: any) => any)[];
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useSelectableRows';
export * from './useSortableRows';
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { IRowData, IRows, OnSelect } from '../Table';

const defaultOptions = {
getRowKey: (rowData: IRowData, rowIndex: number) => 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<ReturnType<typeof getRowKey>[]>([]);

// 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<typeof selectedKeys>(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<OnSelect>(
(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;
// cast required to work with primitive types
(updatedRow as any).selected = isRowSelected;
return updatedRow;
} else {
const updatedRow = {...row};
updatedRow.selected = isRowSelected;
return updatedRow;
}
});

return [selectedRows, onSelect];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useMemo, useState } from 'react';
import { IExtraColumnData, IRows, OnSort, OnSortCallback, SortByDirection } from '../Table';

export function useSortableRows(rows: IRows) {
const [sortBy, setSortBy] = useState<
| { index: number; direction: SortByDirection; columnData: IExtraColumnData; sortCallback: OnSortCallback }
| undefined
>();

const onSort: OnSort = (_event, index, direction, columnData, sortCallback) => {
setSortBy({
index,
direction,
columnData,
sortCallback
});
};

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;
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];
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './RowWrapper';
export * from './SelectColumn';
export * from './SortColumn';
export * from './utils';
export * from './hooks';
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ 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 }
extraParams: { sortBy, onSort, firstUserColumnIndex = 0 }
} = 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) {
Expand All @@ -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, sortCallback);
}

return {
Expand All @@ -42,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 };
Original file line number Diff line number Diff line change
@@ -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';
Expand Down