Skip to content

Commit

Permalink
feat: maximum selectable rows on the Datatable (#2451)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnvente authored and viktorrusakov committed Aug 23, 2023
1 parent 822260b commit 5aa0bf1
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 4 deletions.
98 changes: 98 additions & 0 deletions src/DataTable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1569,4 +1569,102 @@ You can create your own cell content by passing the `Cell` property to a specifi
</DataTable>
);
}

```
## maxSelectedRows and onMaxSelectedRows props

These props will allow us to handle the maximum number of selectable rows, which is necessary for validation. Using the `maxSelectedRows` prop, the implementation process can be simplified. You only need to pass the maximum number of rows you want to have selected.

After selecting the maximum possible number of rows, you can display an error message or perform other actions. This is achieved through the `onMaxSelectedRows` callback. In the example below, the callback will be executed when the table has 3 rows selected.

```jsx live
() => {
return (
<DataTable
isPaginated
isSelectable
maxSelectedRows={3}
onMaxSelectedRows={() => console.log('this is the last row allowed')}
itemCount={7}
data={[
{
name: 'Lil Bub',
color: 'brown tabby',
famous_for: 'weird tongue',
},
{
name: 'Grumpy Cat',
color: 'siamese',
famous_for: 'serving moods',
},
{
name: 'Smoothie',
color: 'orange tabby',
famous_for: 'modeling',
},
{
name: 'Maru',
color: 'brown tabby',
famous_for: 'being a lovable oaf',
},
{
name: 'Keyboard Cat',
color: 'orange tabby',
famous_for: 'piano virtuoso',
},
{
name: 'Long Cat',
color: 'russian white',
famous_for: 'being looooooooooooooooooooooooooong',
},
{
name: 'Zeno',
color: 'brown tabby',
famous_for: 'getting halfway there',
},
]}
columns={[
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Famous For',
accessor: 'famous_for',
},
{
Header: 'Coat Color',
accessor: 'color',
filter: 'includesValue',
filterChoices: [
{
name: 'russian white',
number: 1,
value: 'russian white',
},
{
name: 'orange tabby',
number: 2,
value: 'orange tabby',
},
{
name: 'brown tabby',
number: 3,
value: 'brown tabby',
},
{
name: 'siamese',
number: 1,
value: 'siamese',
},
],
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No results found" />
<DataTable.TableFooter />
</DataTable>
);
}
37 changes: 35 additions & 2 deletions src/DataTable/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ function DataTable({
isLoading,
children,
onSelectedRowsChanged,
maxSelectedRows,
onMaxSelectedRows,
...props
}) {
const defaultColumn = useMemo(
Expand All @@ -60,7 +62,7 @@ function DataTable({
);
const tableOptions = useMemo(() => {
const updatedTableOptions = {
stateReducer: (newState, action) => {
stateReducer: (newState, action, previousState) => {
switch (action.type) {
// Note: we override the `toggleAllRowsSelected` action
// from react-table because it only clears the selections on the
Expand All @@ -77,6 +79,28 @@ function DataTable({
selectedRowIds: {},
};
}
/* Note: We override the `toggleRowSelected` action from react-table
because we need to preserve the order of the selected rows.
While `selectedRowIds` is an object that contains the selected rows as key-value pairs,
it does not maintain the order of selection. Therefore, we have added the `selectedRowsOrdered` property
to keep track of the order in which the rows were selected.
*/
case 'toggleRowSelected': {
const rowIndex = parseInt(action.id, 10);
const { selectedRowsOrdered = [] } = previousState;

let newSelectedRowsOrdered;
if (action.value) {
newSelectedRowsOrdered = [...selectedRowsOrdered, rowIndex];
} else {
newSelectedRowsOrdered = selectedRowsOrdered.filter((item) => item !== rowIndex);
}

return {
...newState,
selectedRowsOrdered: newSelectedRowsOrdered,
};
}
default:
return newState;
}
Expand All @@ -93,7 +117,7 @@ function DataTable({
initialState,
...updatedTableOptions,
};
}, [columns, data, defaultColumn, manualFilters, manualPagination, initialState, initialTableOptions, manualSortBy]);
}, [initialTableOptions, columns, data, defaultColumn, manualFilters, manualPagination, manualSortBy, initialState]);

const [selections, selectionsDispatch] = useReducer(selectionsReducer, initialSelectionsState);

Expand Down Expand Up @@ -176,6 +200,8 @@ function DataTable({
isSelectable,
isPaginated,
manualSelectColumn,
maxSelectedRows,
onMaxSelectedRows,
...selectionProps,
...selectionActions,
...props,
Expand Down Expand Up @@ -236,6 +262,8 @@ DataTable.defaultProps = {
isExpandable: false,
isLoading: false,
onSelectedRowsChanged: undefined,
maxSelectedRows: undefined,
onMaxSelectedRows: undefined,
};

DataTable.propTypes = {
Expand Down Expand Up @@ -308,6 +336,7 @@ DataTable.propTypes = {
filters: requiredWhen(PropTypes.arrayOf(PropTypes.shape()), 'manualFilters'),
sortBy: requiredWhen(PropTypes.arrayOf(PropTypes.shape()), 'manualSortBy'),
selectedRowIds: PropTypes.shape(),
selectedRowsOrdered: PropTypes.arrayOf(PropTypes.number),
}),
/** Table options passed to react-table's useTable hook. Will override some options passed in to DataTable, such
as: data, columns, defaultColumn, manualFilters, manualPagination, manualSortBy, and initialState */
Expand Down Expand Up @@ -405,6 +434,10 @@ DataTable.propTypes = {
isLoading: PropTypes.bool,
/** Callback function called when row selections change. */
onSelectedRowsChanged: PropTypes.func,
/** Indicates the max of rows selectable in the table. Requires isSelectable prop */
maxSelectedRows: PropTypes.number,
/** Callback after selected max rows. Requires isSelectable and maxSelectedRows props */
onMaxSelectedRows: PropTypes.func,
};

DataTable.BulkActions = BulkActions;
Expand Down
4 changes: 3 additions & 1 deletion src/DataTable/selection/BaseSelectionStatus.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ function BaseSelectionStatus({
}) {
const {
itemCount, filteredRows, isPaginated, state,
isSelectable, maxSelectedRows,
} = useContext(DataTableContext);
const hasAppliedFilters = state?.filters?.length > 0;
const isAllRowsSelected = numSelectedRows === itemCount;
const filteredItems = filteredRows?.length || itemCount;
const hasMaxSelectedRows = isSelectable && maxSelectedRows;

const intlAllSelectedText = allSelectedText || (
<FormattedMessage
Expand Down Expand Up @@ -57,7 +59,7 @@ function BaseSelectionStatus({
return (
<div className={className}>
<span>{isAllRowsSelected ? intlAllSelectedText : intlSelectedText}</span>
{!isAllRowsSelected && (
{!isAllRowsSelected && !hasMaxSelectedRows && (
<Button
className={SELECT_ALL_TEST_ID}
variant="link"
Expand Down
1 change: 1 addition & 0 deletions src/DataTable/tests/CardView.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DataTableContext from '../DataTableContext';

const instance = {
isSelectable: false,
state: { selectedRowIds: {} },
rows: [
{
id: '1',
Expand Down
26 changes: 25 additions & 1 deletion src/DataTable/utils/getVisibleColumns.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import React, { useMemo, useContext, useEffect } from 'react';

import { CheckboxControl } from '../../Form';
import DataTableContext from '../DataTableContext';
import useConvertIndeterminateProp from './useConvertIndeterminateProp';

export const selectColumn = {
Expand All @@ -11,6 +12,7 @@ export const selectColumn = {
// Proptypes disabled as these props are passed in separately
/* eslint-disable-next-line react/prop-types */
Header: ({ getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps, page }) => {
const { isSelectable, maxSelectedRows } = useContext(DataTableContext);
const toggleRowsSelectedProps = useMemo(
() => {
// determine if this selection is for an individual page or the entire table
Expand All @@ -20,6 +22,11 @@ export const selectColumn = {
[getToggleAllPageRowsSelectedProps, getToggleAllRowsSelectedProps, page],
);
const updatedProps = useConvertIndeterminateProp(toggleRowsSelectedProps);
const formatMaxSelectedRows = Math.max(0, maxSelectedRows);

if (isSelectable && formatMaxSelectedRows >= 0) {
return null;
}

return (
<div className="pgn__data-table__controlled-select">
Expand All @@ -35,12 +42,29 @@ export const selectColumn = {
// Proptypes disabled as this prop is passed in separately
/* eslint-disable react/prop-types */
Cell: ({ row }) => {
const {
isSelectable, maxSelectedRows, onMaxSelectedRows, state: { selectedRowIds, selectedRowsOrdered },
} = useContext(DataTableContext);
const updatedProps = useConvertIndeterminateProp(row.getToggleRowSelectedProps());
const { index } = row;
const isRowSelected = index in selectedRowIds;
const selectedRowsLength = Object.keys(selectedRowIds).length;
const formatMaxSelectedRows = Math.max(0, maxSelectedRows);
const hasMaxSelectedRows = formatMaxSelectedRows === selectedRowsLength;
const disableCheck = isSelectable && hasMaxSelectedRows && !isRowSelected;
const lastRowSelected = selectedRowsOrdered?.[selectedRowsOrdered.length - 1] ?? null;

useEffect(() => {
if (hasMaxSelectedRows && lastRowSelected === index) {
onMaxSelectedRows?.();
}
}, [hasMaxSelectedRows, index, isRowSelected, lastRowSelected, onMaxSelectedRows, selectedRowIds]);

return (
<div className="pgn__data-table__controlled-select">
<CheckboxControl
{...updatedProps}
disabled={disableCheck}
data-testid="datatable-select-column-checkbox-cell"
/>
</div>
Expand Down
Loading

0 comments on commit 5aa0bf1

Please sign in to comment.