Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added functionality for selecting all rows in multi-page table #2127

30 changes: 30 additions & 0 deletions src/components/Table/components/SelectAllRowsCallout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";

import Button from "../../Button";
import Callout from "../../Callout";
import Typography from "../../Typography";

const SelectAllRowsCallout = ({
calloutProps,
onBulkSelectAllRows,
selectAllRowButtonLabel,
selectAllRowMessage,
}) => (
<Callout
className="my-2"
{...calloutProps}
data-testid="select-all-rows-callout"
>
<div className="flex space-x-3">
<Typography style="body2">{selectAllRowMessage}</Typography>
<Button
data-testid="select-all-rows-button"
label={selectAllRowButtonLabel}
style="link"
onClick={onBulkSelectAllRows}
/>
</div>
</Callout>
);

export default SelectAllRowsCallout;
62 changes: 54 additions & 8 deletions src/components/Table/index.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";

import { Table as AntTable, ConfigProvider } from "antd";
import classnames from "classnames";
import { dynamicArray, modifyBy, snakeToCamelCase } from "neetocist";
import { Left, Right, MenuHorizontal } from "neetoicons";
import PropTypes from "prop-types";
import { assoc, isEmpty, mergeLeft } from "ramda";
import { assoc, isEmpty, mergeLeft, pluck } from "ramda";
import ReactDragListView from "react-drag-listview";
import { useHistory } from "react-router-dom";

import { useQueryParams, useTimeout } from "hooks";
import { ANT_DESIGN_GLOBAL_TOKEN_OVERRIDES, buildUrl, noop } from "utils";

import SelectAllRowsCallout from "./components/SelectAllRowsCallout";
import { TABLE_SORT_ORDERS } from "./constants";
import useColumns from "./hooks/useColumns";
import useTableSort from "./hooks/useTableSort";
import { getHeaderCell } from "./utils";
import { getHeaderCell, isIncludedIn } from "./utils";

import Button from "../Button";
import Typography from "../Typography";
Expand All @@ -39,9 +46,10 @@ const Table = ({
onRowSelect,
rowData = [],
totalCount = 0,
selectedRowKeys = [],
selectedRowKeys: initialSelectedRowKeys = [],
fixedHeight = false,
paginationProps = {},
rowKey = "id",
scroll,
rowSelection,
shouldDynamicallyRenderRowSize = false,
Expand All @@ -53,17 +61,22 @@ const Table = ({
onColumnDelete,
onChange,
onMoreActionClick,
bulkSelectAllRowsProps,
...otherProps
}) => {
const [containerHeight, setContainerHeight] = useState(null);
const [headerHeight, setHeaderHeight] = useState(TABLE_DEFAULT_HEADER_HEIGHT);
const [columns, setColumns] = useState(columnData);
const [bulkSelectedAllRows, setBulkSelectedAllRows] = useState(false);
const {
handleTableChange: handleTableSortChange,
sortedInfo,
setSortedInfo,
} = useTableSort();

const { setBulkSelectedAllRows: handleSetBulkSelectedAllRows } =
bulkSelectAllRowsProps ?? {};

const isDefaultPageChangeHandler = handlePageChange === noop;

const history = useHistory();
Expand Down Expand Up @@ -143,13 +156,30 @@ const Table = ({
}),
}));

const selectedRowKeys = bulkSelectedAllRows
? pluck(rowKey, rowData)
: initialSelectedRowKeys;

const showBulkSelectionCallout = useMemo(
() =>
isIncludedIn(selectedRowKeys, pluck(rowKey, rowData)) &&
selectedRowKeys.length !== totalCount &&
!bulkSelectedAllRows,
[selectedRowKeys, rowKey, rowData, totalCount, bulkSelectedAllRows]
);

const handleRowChange = (selectedRowKeys, selectedRows) => {
selectedRowKeys.length !== defaultPageSize && setBulkSelectedAllRows(false);
handleSetBulkSelectedAllRows && handleSetBulkSelectedAllRows(false);
onRowSelect && onRowSelect(selectedRowKeys, selectedRows);
};

const rowSelectionProps = rowSelection
? {
type: "checkbox",
preserveSelectedRowKeys: true,
...rowSelection,
onChange: (selectedRowKeys, selectedRows) =>
onRowSelect && onRowSelect(selectedRowKeys, selectedRows),
onChange: handleRowChange,
selectedRowKeys,
}
: false;
Expand Down Expand Up @@ -287,13 +317,21 @@ const Table = ({
},
}}
>
{bulkSelectAllRowsProps && showBulkSelectionCallout && (
<SelectAllRowsCallout
{...bulkSelectAllRowsProps}
onBulkSelectAllRows={() => {
setBulkSelectedAllRows(true);
handleSetBulkSelectedAllRows && handleSetBulkSelectedAllRows(true);
}}
/>
)}
<AntTable
{...{ bordered, loading, locale }}
{...{ bordered, loading, locale, rowKey }}
columns={sortedColumnsWithAlignment}
components={componentOverrides}
dataSource={rowData}
ref={tableRef}
rowKey="id"
rowSelection={rowSelectionProps}
showSorterTooltip={false}
pagination={{
Expand Down Expand Up @@ -451,6 +489,14 @@ Table.propTypes = {
* Make sure to pass `id` in `rowData` for this to work.
*/
rowSelection: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
/**
* Props for adding `Select all rows` option for multi-page table.
*/
bulkSelectAllRowsProps: PropTypes.shape({
selectAllRowMessage: PropTypes.string.isRequired,
selectAllRowButtonLabel: PropTypes.string.isRequired,
setBulkSelectedAllRows: PropTypes.func.isRequired,
}),
};

export default Table;
5 changes: 5 additions & 0 deletions src/components/Table/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { __, all, includes } from "ramda";

import {
CellContent,
HeaderCell,
Expand All @@ -14,3 +16,6 @@ export const getHeaderCell = ({ enableColumnResize, enableColumnReorder }) => {

return { cell: CellContent };
};

export const isIncludedIn = (array1, array2) =>
all(includes(__, array1), array2);
50 changes: 50 additions & 0 deletions stories/Components/Table.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,55 @@ TableWithSelectedRowKeys.parameters = {
},
};

const TableWithBulkSelectAllRowsOption = ({
selectedRowKeys: selectedRowKeysProp,
defaultPageSize,
...args
}) => {
const [pageNumber, setPageNumber] = useState(1);
const [selectedRowKeys, setSelectedRowKeys] = useState(selectedRowKeysProp);
const [bulkSelectedAllRows, setBulkSelectedAllRows] = useState(false);

const rowData = TABLE_DATA.slice(
(pageNumber - 1) * defaultPageSize,
pageNumber * defaultPageSize
);

return (
<>
<Typography className="mb-2" style="h4">
Selected{" "}
{bulkSelectedAllRows ? TABLE_DATA.length : selectedRowKeys.length} of{" "}
{TABLE_DATA.length} users
</Typography>
<Table
columnData={getColumns()}
currentPageNumber={pageNumber}
// Mimicking data-source coming from api call and at a time only defaultPageSize rows are present
handlePageChange={page => setPageNumber(page)}
{...{ ...args, defaultPageSize, rowData, selectedRowKeys }}
rowSelection
bulkSelectAllRowsProps={{
setBulkSelectedAllRows,
selectAllRowMessage: `All ${rowData.length} users on this page are selected`,
selectAllRowButtonLabel: `Select all ${TABLE_DATA.length} users`,
}}
onRowSelect={selectedRowKeys => setSelectedRowKeys(selectedRowKeys)}
/>
</>
);
};

TableWithBulkSelectAllRowsOption.storyName =
"Table with bulk select all rows option";

TableWithBulkSelectAllRowsOption.args = {
defaultPageSize: 15,
selectedRowKeys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
bulkSelectAllRowsProps: {},
totalCount: TABLE_DATA.length,
};

const TableWithSorting = args => (
<Table
columnData={getColumns()}
Expand Down Expand Up @@ -587,6 +636,7 @@ export {
TableWithTooltipsOnHeader,
TableWithFixedRightColumn,
TableWithSelectedRowKeys,
TableWithBulkSelectAllRowsOption,
TableWithSorting,
TableWithFixedHeight,
TableWithoutCheckbox,
Expand Down
67 changes: 66 additions & 1 deletion tests/Table.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";

import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
Expand Down Expand Up @@ -358,4 +358,69 @@ describe("Table", () => {
await userEvent.click(screen.getByText("Action 1"));
expect(onMoreActionClick).toBeCalledWith("action1", columnData[5]);
});

it("should have select all callout when all rows are selected and bulkSelectAllRowsProps are passed in multipage table", () => {
render(
<NeetoUITable
{...{ columnData }}
rowSelection
defaultPageSize={2}
rowData={[rowData[0], rowData[1]]}
selectedRowKeys={[rowData[0].id, rowData[1].id]}
totalCount={rowData.length}
bulkSelectAllRowsProps={{
setBulkSelectedAllRows: () => {},
selectAllRowButtonLabel: "Select all",
selectAllRowMessage: "Selected 2 rows in this page",
}}
/>
);
const selectAllCallout = screen.getByTestId("select-all-rows-callout");
expect(selectAllCallout).toBeInTheDocument();
});

it("should select all rows of all pages and call the callback when the select all button is clicked from the callout", async () => {
const setBulkSelectedAllRows = jest.fn();
const NeetoUITableWithWrapper = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([
rowData[0].id,
rowData[1].id,
]);
const [page, setPage] = useState(1);
const PAGE_SIZE = 2;

return (
<NeetoUITable
{...{ columnData, selectedRowKeys }}
rowSelection
currentPageNumber={page}
defaultPageSize={2}
handlePageChange={(page, _) => setPage(page)}
rowData={rowData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)}
totalCount={rowData.length}
bulkSelectAllRowsProps={{
setBulkSelectedAllRows,
selectAllRowButtonLabel: "Select all rows",
selectAllRowMessage: "Selected 2 rows in this page",
}}
onRowSelect={setSelectedRowKeys}
/>
);
};

render(<NeetoUITableWithWrapper />);
const selectAllRowsBulkButton = screen.getByTestId(
"select-all-rows-button"
);
await userEvent.click(selectAllRowsBulkButton);
const pages = screen.getAllByRole("listitem");
await userEvent.click(pages[2]);
const checkboxes = screen.getAllByRole("checkbox");

checkboxes.forEach(checkbox => {
expect(checkbox).toBeChecked();
});

expect(setBulkSelectedAllRows).toBeCalledTimes(1);
});
});
Loading