diff --git a/src/components/MainTable/MainTable.tsx b/src/components/MainTable/MainTable.tsx
index b3fc4ed1c..e2aa71d55 100644
--- a/src/components/MainTable/MainTable.tsx
+++ b/src/components/MainTable/MainTable.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import type { HTMLProps, ReactNode } from "react";
import type { ClassName, PropsWithSpread, SortDirection } from "types";
@@ -9,6 +9,7 @@ import TableRow from "../TableRow";
import TableHeader from "../TableHeader";
import TableCell from "../TableCell";
import type { TableCellProps } from "../TableCell";
+import { usePagination } from "hooks";
export type MainTableHeader = PropsWithSpread<
{
@@ -190,60 +191,19 @@ const generateHeaders = (
);
};
-const generateRows = (
- currentSortDirection: SortDirection,
- currentSortKey: MainTableHeader["sortKey"],
- emptyStateMsg: ReactNode,
- expanding: Props["expanding"],
- responsive: Props["responsive"],
- headers: Props["headers"],
- paginate: Props["paginate"],
- rows: Props["rows"],
- currentPage: number,
- setCurrentPage: (page: number) => void,
- sortable: Props["sortable"],
- sortFunction: Props["sortFunction"]
-) => {
- // If the table has no rows, return empty state message
- if (Object.entries(rows).length === 0 && emptyStateMsg) {
- return
{emptyStateMsg};
- }
- // Clone the rows so we can restore the original order.
- const sortedRows = [...rows];
- if (sortable && currentSortKey) {
- if (!sortFunction) {
- sortFunction = (a, b) => {
- if (!a.sortData || !b.sortData) {
- return 0;
- }
- if (a.sortData[currentSortKey] > b.sortData[currentSortKey]) {
- return currentSortDirection === "ascending" ? 1 : -1;
- } else if (a.sortData[currentSortKey] < b.sortData[currentSortKey]) {
- return currentSortDirection === "ascending" ? -1 : 1;
- }
- return 0;
- };
- }
- sortedRows.sort((a, b) =>
- sortFunction(a, b, currentSortDirection, currentSortKey)
- );
- }
- let slicedRows = sortedRows;
- if (paginate) {
- const startIndex = (currentPage - 1) * paginate;
- if (startIndex > rows.length) {
- // If the rows have changed e.g. when filtering and the user is on a page
- // that no longer exists then send them to the start.
- setCurrentPage(1);
- }
- slicedRows = sortedRows.slice(startIndex, startIndex + paginate);
- }
- const rowItems = slicedRows.map(
+const generateRows = ({
+ rows,
+ headers,
+ responsive,
+ expanding,
+}: Required> &
+ Pick) =>
+ rows.map(
(
{ columns, expanded, expandedContent, key, sortData, ...rowProps },
index
) => {
- const cellItems = columns.map(({ content, ...cellProps }, index) => {
+ const cellItems = columns?.map(({ content, ...cellProps }, index) => {
const headerContent = headers && headers[index]["content"];
const headerReplacement = headers && headers[index]["heading"];
@@ -279,7 +239,41 @@ const generateRows = (
);
}
);
- return {rowItems};
+
+const sortRows = ({
+ currentSortDirection,
+ currentSortKey,
+ rows,
+ sortable,
+ sortFunction,
+}: Pick & {
+ currentSortDirection: Props["defaultSortDirection"];
+ currentSortKey: Props["defaultSort"];
+}): MainTableRow[] => {
+ if (!rows) {
+ return [];
+ }
+ // Clone the rows so we can restore the original order.
+ const sortedRows = [...rows];
+ if (sortable && currentSortKey) {
+ if (!sortFunction) {
+ sortFunction = (a, b) => {
+ if (!a.sortData || !b.sortData) {
+ return 0;
+ }
+ if (a.sortData[currentSortKey] > b.sortData[currentSortKey]) {
+ return currentSortDirection === "ascending" ? 1 : -1;
+ } else if (a.sortData[currentSortKey] < b.sortData[currentSortKey]) {
+ return currentSortDirection === "ascending" ? -1 : 1;
+ }
+ return 0;
+ };
+ }
+ sortedRows.sort((a, b) =>
+ sortFunction(a, b, currentSortDirection, currentSortKey)
+ );
+ }
+ return sortedRows;
};
const MainTable = ({
@@ -299,7 +293,6 @@ const MainTable = ({
const [currentSortKey, setSortKey] = useState(defaultSort);
const [currentSortDirection, setSortDirection] =
useState(defaultSortDirection);
- const [currentPage, setCurrentPage] = useState(1);
// Update the current sort state if the prop changes.
useEffect(() => {
@@ -316,6 +309,27 @@ const MainTable = ({
onUpdateSort && onUpdateSort(newSort);
};
+ const sortedRows = useMemo(
+ () =>
+ sortRows({
+ currentSortDirection,
+ currentSortKey,
+ rows,
+ sortable,
+ sortFunction,
+ }),
+ [currentSortDirection, currentSortKey, rows, sortable, sortFunction]
+ );
+
+ const {
+ pageData: finalRows,
+ currentPage,
+ paginate: setCurrentPage,
+ } = usePagination(sortedRows, {
+ itemsPerPage: paginate,
+ autoResetPage: true,
+ });
+
return (
<>
@@ -329,21 +343,21 @@ const MainTable = ({
updateSort,
setSortDirection
)}
- {!!rows &&
- generateRows(
- currentSortDirection,
- currentSortKey,
- emptyStateMsg,
- expanding,
- responsive,
- headers,
- paginate,
- rows,
- currentPage,
- setCurrentPage,
- sortable,
- sortFunction
- )}
+ {
+ // If the table has no rows, return empty state message
+ Object.entries(finalRows).length === 0 && emptyStateMsg ? (
+ {emptyStateMsg}
+ ) : (
+
+ {generateRows({
+ rows: finalRows,
+ headers,
+ responsive,
+ expanding,
+ })}
+
+ )
+ }
{paginate && rows && rows.length > 0 && (
{
+ const { result } = renderHook(() =>
+ usePagination([1, 2, 3], { itemsPerPage: 2 })
+ );
+ const { pageData, currentPage, paginate, itemsPerPage, totalItems } =
+ result.current;
+ expect(currentPage).toBe(1);
+ expect(pageData).toEqual([1, 2]);
+ expect(paginate).toBeInstanceOf(Function);
+ expect(itemsPerPage).toBe(2);
+ expect(totalItems).toBe(3);
+});
+
+it("correctly sets the initial page", () => {
+ const { result } = renderHook(() =>
+ usePagination([1, 2], { itemsPerPage: 1, initialPage: 2 })
+ );
+ const { pageData, currentPage } = result.current;
+ expect(currentPage).toBe(2);
+ expect(pageData).toEqual([2]);
+});
+
+it("goes to the last available page if the current page is out of bounds", () => {
+ const options = { itemsPerPage: 1, initialPage: 3 };
+ const { result, rerender } = renderHook(
+ ({ data, options }) => usePagination(data, options),
+ {
+ initialProps: {
+ data: [1, 2, 3],
+ options,
+ },
+ }
+ );
+ expect(result.current.currentPage).toBe(3);
+ expect(result.current.pageData).toEqual([3]);
+ rerender({ data: [1, 2], options });
+ expect(result.current.currentPage).toBe(2);
+ expect(result.current.pageData).toEqual([2]);
+});
+
+it("go to the initial page if autoResetPage is true", () => {
+ const options = { itemsPerPage: 1, initialPage: 3, autoResetPage: true };
+ const { result, rerender } = renderHook(
+ ({ data, options }) => usePagination(data, options),
+ {
+ initialProps: {
+ data: [1, 2, 3],
+ options,
+ },
+ }
+ );
+ expect(result.current.currentPage).toBe(3);
+ expect(result.current.pageData).toEqual([3]);
+ rerender({ data: [1, 2], options });
+ expect(result.current.currentPage).toBe(1);
+ expect(result.current.pageData).toEqual([1]);
+});
diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts
new file mode 100644
index 000000000..67a482d9c
--- /dev/null
+++ b/src/hooks/usePagination.ts
@@ -0,0 +1,73 @@
+import { useMemo, useEffect, useState } from "react";
+
+/**
+ * A hook that handles pagination.
+ * @param data - The data array to paginate.
+ * @param {Object} options
+ * @param {number} [options.itemsPerPage] - Number of items per page. Returns all items if no value has been provided.
+ * @param {number} [options.initialPage=1] - Initial page number. Defaults to 1.
+ * @param {boolean} [options.autoResetPage=false] - Whether to reset the page number to 1 when the data changes.
+ */
+
+export function usePagination(
+ data: Array,
+ options?: {
+ itemsPerPage: I;
+ initialPage?: number;
+ autoResetPage?: boolean;
+ }
+): {
+ pageData: Array;
+ currentPage: number;
+ paginate: (pageNumber: number) => void;
+ itemsPerPage: I;
+ totalItems: number;
+} {
+ const defaultOptions = {
+ initialPage: 1,
+ autoResetPage: false,
+ };
+ const { itemsPerPage, initialPage, autoResetPage } = Object.assign(
+ defaultOptions,
+ options
+ );
+ const totalItems = data?.length ?? 0;
+ const initialPageIndex = initialPage > 0 ? initialPage - 1 : 0;
+ const [pageIndex, setPageIndex] = useState(initialPageIndex);
+ const startIndex =
+ typeof itemsPerPage === "number" ? pageIndex * itemsPerPage : 0;
+ const paginate = (pageNumber: number) => setPageIndex(pageNumber - 1);
+
+ useEffect(() => {
+ if (typeof itemsPerPage === "number" && startIndex >= totalItems) {
+ !autoResetPage && Math.floor(totalItems / itemsPerPage) > 0
+ ? // go to the last available page if the current page is out of bounds
+ setPageIndex(Math.floor(totalItems / itemsPerPage) - 1)
+ : // go to the initial page if autoResetPage is true
+ setPageIndex(0);
+ }
+ }, [
+ pageIndex,
+ startIndex,
+ setPageIndex,
+ totalItems,
+ itemsPerPage,
+ autoResetPage,
+ ]);
+
+ const pageData = useMemo(
+ () =>
+ typeof itemsPerPage === "number"
+ ? data?.slice(startIndex, startIndex + itemsPerPage)
+ : data,
+ [startIndex, data, itemsPerPage]
+ );
+
+ return {
+ pageData,
+ currentPage: pageIndex + 1,
+ paginate,
+ itemsPerPage,
+ totalItems,
+ };
+}