From 84f3af3c45c311bbb7d3cc5103568927773f3160 Mon Sep 17 00:00:00 2001 From: Jordan Koschei <91091570+jordankoschei-okta@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:42:31 -0400 Subject: [PATCH] Data Table legacy improvements (#2294) OKTA-753577 feat: add max pages and max rows per page to pagination feat: add manual pagination next disabling feat: reset pagination if search or filters changes refactor: alphabetize props feat: fixing pagination bug refactor: improve pagination calculation refactor: update currentRowsCount to be non-breaking --- .../src/DataTable/DataTable.tsx | 28 ++++ .../src/Pagination/Pagination.tsx | 128 +++++++++++------- .../src/Pagination/usePagination.ts | 32 ++--- .../src/labs/DataComponents/DataStack.tsx | 6 + .../src/labs/DataComponents/DataTable.tsx | 6 + .../src/labs/DataComponents/DataView.tsx | 16 +++ .../src/labs/DataComponents/componentTypes.ts | 3 + .../DataComponents/DataComponents.stories.tsx | 11 ++ .../DataTable/DataTable.stories.tsx | 35 ++++- .../Pagination/Pagination.stories.tsx | 41 ++++++ 10 files changed, 239 insertions(+), 67 deletions(-) diff --git a/packages/odyssey-react-mui/src/DataTable/DataTable.tsx b/packages/odyssey-react-mui/src/DataTable/DataTable.tsx index 7dfefe2d66..ec80de53ee 100644 --- a/packages/odyssey-react-mui/src/DataTable/DataTable.tsx +++ b/packages/odyssey-react-mui/src/DataTable/DataTable.tsx @@ -195,6 +195,10 @@ export type DataTableProps = { * The initial search value */ initialSearchValue?: string; + /** + * Is the next or show-more button disabled + */ + isPaginationMoreDisabled?: boolean; /** * The component to display when the query returns no results */ @@ -240,6 +244,15 @@ export type DataTableProps = { * to calculate. Used in table pagination to know when to disable the "next"/"more" button. */ totalRows?: number; + /** + * The largest number of rows allowed to be shown per page. This only affects the row input + * in pagination. + */ + maxResultsPerPage?: number; + /** + * The highest page number allowed to be manually input in pagination + */ + maxPages?: number; }; const displayColumnDefOptions = { @@ -376,12 +389,15 @@ const DataTable = ({ hasSorting, initialDensity = densityValues[0], initialSearchValue = "", + isPaginationMoreDisabled, noResultsPlaceholder, onChangeRowSelection, onReorderRows, paginationType = "paged", renderDetailPanel, resultsPerPage = 20, + maxResultsPerPage, + maxPages, rowActionButtons, rowActionMenuItems, searchDelayTime, @@ -809,11 +825,19 @@ const DataTable = ({ resultsPerPage, ]); + useEffect(() => { + setPagination((prev) => ({ + pageIndex: 1, + pageSize: prev.pageSize, + })); + }, [filters, search]); + useEffect(() => { onChangeRowSelection?.(rowSelection); }, [rowSelection, onChangeRowSelection]); const { lastRow } = usePagination({ + currentRowsCount: data.length, pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, totalRows, @@ -873,10 +897,14 @@ const DataTable = ({ void; + isDisabled?: boolean; + /** + * If true, the next or Show More button will be disabled + */ + isMoreDisabled?: boolean; /** * The current page last row index */ lastRow: number; /** - * Total rows count + * If the pagination is of "loadMore" variant, then this is the the load more label */ - totalRows?: number; + loadMoreLabel: string; /** - * If true, the pagination controls will be disabled + * The max page */ - isDisabled?: boolean; + maxPageIndex?: number; /** - * The type of pagination controls shown. Defaults to next/prev buttons, but can be - * set to a simple "Load more" button by setting to "loadMore". + * The max rows per page */ - variant?: (typeof paginationTypeValues)[number]; + maxPageSize?: number; /** - * The label that shows how many results are rendered per page + * The label for the next control */ - rowsPerPageLabel: string; + nextLabel: string; /** - * The labeled rendered for the current page index + * Page index and page size setter */ - currentPageLabel: string; + onPaginationChange: ({ + pageIndex, + pageSize, + }: { + pageIndex: number; + pageSize: number; + }) => void; + /** + * The current page index + */ + pageIndex: number; + /** + * The current page size + */ + pageSize: number; /** * The label for the previous control */ previousLabel: string; /** - * The label for the next control + * The label that shows how many results are rendered per page */ - nextLabel: string; + rowsPerPageLabel: string; /** - * If the pagination is of "loadMore" variant, then this is the the load more label + * Total rows count */ - loadMoreLabel: string; + totalRows?: number; + /** + * The type of pagination controls shown. Defaults to next/prev buttons, but can be + * set to a simple "Load more" button by setting to "loadMore". + */ + variant?: (typeof paginationTypeValues)[number]; }; const Pagination = ({ + currentPageLabel, + isDisabled, + isMoreDisabled, + lastRow, + loadMoreLabel, + maxPageIndex, + maxPageSize, + nextLabel, + onPaginationChange, pageIndex, pageSize, - onPaginationChange, - lastRow, + previousLabel, + rowsPerPageLabel, totalRows, - isDisabled, + currentRowsCount, variant, - rowsPerPageLabel, - currentPageLabel, - previousLabel, - nextLabel, - loadMoreLabel, }: PaginationProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); @@ -147,7 +167,12 @@ const Pagination = ({ setRowsPerPage(pageSize); }, [pageIndex, pageSize]); - const { totalRowsLabel } = usePagination({ pageIndex, pageSize, totalRows }); + const { totalRowsLabel } = usePagination({ + pageIndex, + pageSize, + currentRowsCount: currentRowsCount || pageSize, + totalRows, + }); const handlePaginationChange = useCallback(() => { const updatedPage = @@ -192,16 +217,22 @@ const Pagination = ({ const setPageFromEvent = useCallback( (event: React.ChangeEvent) => { - setPage(parseInt(event.target.value)); + const value = maxPageIndex + ? Math.min(parseInt(event.target.value), maxPageIndex) + : parseInt(event.target.value); + setPage(value); }, - [setPage], + [setPage, maxPageIndex], ); const setRowsPerPageFromEvent = useCallback( (event: React.ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value)); + const value = maxPageSize + ? Math.min(parseInt(event.target.value), maxPageSize) + : parseInt(event.target.value); + setRowsPerPage(value); }, - [setRowsPerPage], + [setRowsPerPage, maxPageSize], ); const handleLoadMore = useCallback(() => { @@ -220,12 +251,15 @@ const Pagination = ({ }, [onPaginationChange, page, rowsPerPage]); const loadMoreIsDisabled = useMemo(() => { - return totalRows ? rowsPerPage >= totalRows : false; - }, [rowsPerPage, totalRows]); + return isMoreDisabled || (totalRows ? rowsPerPage >= totalRows : false); + }, [isMoreDisabled, rowsPerPage, totalRows]); const nextButtonDisabled = useMemo( - () => (totalRows ? lastRow >= totalRows : false) || isDisabled, - [totalRows, lastRow, isDisabled], + () => + isMoreDisabled || + (totalRows ? lastRow >= totalRows : false) || + isDisabled, + [isMoreDisabled, totalRows, lastRow, isDisabled], ); const previousButtonDisabled = useMemo( @@ -236,15 +270,17 @@ const Pagination = ({ const rowsPerPageInputProps = useMemo( () => ({ "aria-label": rowsPerPageLabel, + max: maxPageSize || totalRows, }), - [rowsPerPageLabel], + [maxPageSize, rowsPerPageLabel, totalRows], ); const currentPageInputProps = useMemo( () => ({ "aria-label": currentPageLabel, + max: maxPageIndex, }), - [currentPageLabel], + [currentPageLabel, maxPageIndex], ); return variant === "paged" ? ( diff --git a/packages/odyssey-react-mui/src/Pagination/usePagination.ts b/packages/odyssey-react-mui/src/Pagination/usePagination.ts index 5f90643861..4b4f8dd02f 100644 --- a/packages/odyssey-react-mui/src/Pagination/usePagination.ts +++ b/packages/odyssey-react-mui/src/Pagination/usePagination.ts @@ -11,39 +11,35 @@ */ import { useTranslation } from "react-i18next"; +import { useMemo } from "react"; type UsePaginationType = { + currentRowsCount: number; pageIndex: number; pageSize: number; totalRows?: number; }; export const usePagination = ({ + currentRowsCount, pageIndex, pageSize, totalRows, }: UsePaginationType) => { const { t } = useTranslation(); - const firstRow = pageSize * (pageIndex - 1) + 1; - const lastRow = firstRow + (pageSize - 1); - if (totalRows && lastRow > totalRows) { - // If the last eligible row is greater than the number of total rows, - // show the number of total rows instead (ie, if we're showing rows - // 180-200 but there are only 190 rows, show 180-190 instead) + return useMemo(() => { + const firstRow = pageSize * (pageIndex - 1) + 1; + const lastRow = firstRow + (currentRowsCount - 1); + + const totalRowsLabel = totalRows + ? t("pagination.rowswithtotal", { firstRow, lastRow, totalRows }) + : t("pagination.rowswithouttotal", { firstRow, lastRow }); + return { firstRow, - lastRow: totalRows, - totalRowsLabel: totalRows - ? t("pagination.rowswithtotal", { firstRow, lastRow, totalRows }) - : t("pagination.rowswithouttotal", { firstRow, lastRow }), + lastRow, + totalRowsLabel, }; - } - return { - firstRow, - lastRow, - totalRowsLabel: totalRows - ? t("pagination.rowswithtotal", { firstRow, lastRow, totalRows }) - : t("pagination.rowswithouttotal", { firstRow, lastRow }), - }; + }, [currentRowsCount, pageIndex, pageSize, totalRows, t]); }; diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx index 2bfd3359d7..36d3aa9aae 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx +++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx @@ -44,8 +44,11 @@ const DataStack = ({ isEmpty, isLoading, isNoResults, + isPaginationMoreDisabled, isRowReorderingDisabled, maxGridColumns, + maxPages, + maxResultsPerPage, noResultsPlaceholder, onChangeRowSelection, paginationType, @@ -81,7 +84,10 @@ const DataStack = ({ isEmpty={isEmpty} isLoading={isLoading} isNoResults={isNoResults} + isPaginationMoreDisabled={isPaginationMoreDisabled} isRowReorderingDisabled={isRowReorderingDisabled} + maxPages={maxPages} + maxResultsPerPage={maxResultsPerPage} noResultsPlaceholder={noResultsPlaceholder} onChangeRowSelection={onChangeRowSelection} paginationType={paginationType} diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx index 707c3bc6b5..23ed110667 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx +++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx @@ -39,7 +39,10 @@ const DataTable = ({ isLoading, isEmpty, isNoResults, + isPaginationMoreDisabled, isRowReorderingDisabled, + maxResultsPerPage, + maxPages, noResultsPlaceholder, onChangeRowSelection, paginationType, @@ -93,7 +96,10 @@ const DataTable = ({ isEmpty={isEmpty} isLoading={isLoading} isNoResults={isNoResults} + isPaginationMoreDisabled={isPaginationMoreDisabled} isRowReorderingDisabled={isRowReorderingDisabled} + maxPages={maxPages} + maxResultsPerPage={maxResultsPerPage} noResultsPlaceholder={noResultsPlaceholder} onChangeRowSelection={onChangeRowSelection} paginationType={paginationType} diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx index 32b4a9389d..8b60dd44c4 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx +++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx @@ -86,6 +86,7 @@ const DataView = ({ isEmpty: isEmptyProp, isLoading: isLoadingProp, isNoResults: isNoResultsProp, + isPaginationMoreDisabled, isRowReorderingDisabled, noResultsPlaceholder, onChangeRowSelection, @@ -96,6 +97,8 @@ const DataView = ({ stackOptions, tableOptions, totalRows, + maxPages, + maxResultsPerPage, }: DataViewProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); @@ -179,6 +182,14 @@ const DataView = ({ }); }, [currentPage, resultsPerPage]); + // Reset pagination if search or filters change + useEffect(() => { + setPagination((prev) => ({ + pageIndex: 1, + pageSize: prev.pageSize, + })); + }, [filters, search]); + // Retrieve the data useEffect(() => { fetchData({ @@ -266,6 +277,7 @@ const DataView = ({ ); const { lastRow: lastRowOnPage } = usePagination({ + currentRowsCount: data.length, pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, totalRows, @@ -372,8 +384,11 @@ const DataView = ({ )} diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts index eb038a9a4c..c6a4af900d 100644 --- a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts +++ b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts @@ -68,7 +68,10 @@ export type UniversalProps = { isEmpty?: boolean; isLoading?: boolean; isNoResults?: boolean; + isPaginationMoreDisabled?: boolean; isRowReorderingDisabled?: boolean; + maxPages?: number; + maxResultsPerPage?: number; noResultsPlaceholder?: ReactNode; onChangeRowSelection?: (rowSelection: DataRowSelectionState) => void; onReorderRows?: ({ rowId, newRowIndex }: DataOnReorderRowsType) => void; diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx index 41647a3a5b..10e8a033db 100644 --- a/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx @@ -216,6 +216,16 @@ const storybookMeta: Meta = { isNoResults: { control: "boolean", }, + isPaginationMoreDisabled: { + control: "boolean", + description: + "If true, the pagination next or show more button will be disabled.", + table: { + type: { + summary: "boolean", + }, + }, + }, hasCustomEmptyPlaceholder: { control: "boolean", name: "[STORY ONLY] Has custom empty placeholder?", @@ -375,6 +385,7 @@ const BaseStory: StoryObj = { hasFilters={args.hasFilters} hasSearch={args.hasSearch} hasSearchSubmitButton={args.hasSearchSubmitButton} + isPaginationMoreDisabled={args.isPaginationMoreDisabled} searchDelayTime={args.searchDelayTime} errorMessage={args.errorMessage} initialLayout={args.initialLayout} diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx index bfcf9b6ac5..ddfc0bdd12 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx @@ -18,7 +18,7 @@ import { import { Box, Button, - // DataTable, + DataTable, EmptyState, DataTableGetDataType, DataTableOnReorderRowsType, @@ -26,9 +26,8 @@ import { DataTableRenderDetailPanelType, DataTableRowSelectionState, MenuItem, - // densityValues, + densityValues, } from "@okta/odyssey-react-mui"; -import { DataTable, densityValues } from "@okta/odyssey-react-mui/labs"; import { MuiThemeDecorator } from "../../../../.storybook/components"; import { Planet, @@ -247,6 +246,36 @@ const storybookMeta: Meta = { }, }, }, + isPaginationMoreDisabled: { + control: "boolean", + description: + "If true, the pagination next or show more button will be disabled.", + table: { + type: { + summary: "boolean", + }, + }, + }, + maxPages: { + control: "number", + description: + "The highest page number allowed to be manually input in pagination.", + table: { + type: { + summary: "number", + }, + }, + }, + maxResultsPerPage: { + control: "number", + description: + "The largest number of rows allowed to be shown per page. This only affects the row input in pagination.", + table: { + type: { + summary: "number", + }, + }, + }, paginationType: { options: paginationTypeValues, control: { type: "radio" }, diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Pagination/Pagination.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Pagination/Pagination.stories.tsx index e2cdc3e083..a014eb4aeb 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Pagination/Pagination.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Pagination/Pagination.stories.tsx @@ -87,6 +87,16 @@ const storyBookMeta: Meta = { }, }, + isMoreDisabled: { + control: "boolean", + description: + "If true, the pagination next/show more button will be disabled", + type: { + required: true, + name: "boolean", + }, + }, + variant: { control: { type: "radio" }, options: paginationTypeValues, @@ -147,6 +157,36 @@ const storyBookMeta: Meta = { name: "string", }, }, + + maxPageIndex: { + control: "number", + description: + "The highest page number allowed to be manually input in pagination.", + table: { + type: { + summary: "number", + }, + }, + }, + maxPageSize: { + control: "number", + description: + "The largest number of rows allowed to be shown per page. This only affects the row input in pagination.", + table: { + type: { + summary: "number", + }, + }, + }, + currentRowsCount: { + control: "number", + description: "The number of items currently visible on the page", + table: { + type: { + summary: "number", + }, + }, + }, }, decorators: [MuiThemeDecorator], @@ -160,6 +200,7 @@ export const Default: StoryObj = { pageIndex: 1, pageSize: 20, lastRow: 20, + currentRowsCount: 20, isDisabled: false, variant: "paged", rowsPerPageLabel: "Rows per page",