import { TablePagination, TablePaginationBaseProps, Theme, Toolbar, useMediaQuery, useTheme } from '@mui/material'; import PropTypes from 'prop-types'; import { ComponentPropType, ListPaginationContextValue, sanitizeListRestProps, useListPaginationContext, useResourceDefinition, useTranslate } from 'ra-core'; import { FC, ReactElement, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { debounce } from 'lodash'; import { PaginationActions, PaginationActionsProps } from './PaginationActions'; const Pagination: FC = memo((props) => { const { rowsPerPageOptions = DefaultRowsPerPageOptions, actions, limit = null, ...rest } = props; const { isLoading, hasNextPage, page, perPage, total, setPage, setPerPage } = useListPaginationContext(props); const translate = useTranslate(); const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); const [currentPage, setCurrentPage] = useState(page - 1); const { hasCreate } = useResourceDefinition(props); const [isSelectedPage, setIsSelectedPage] = useState(false); const theme = useTheme(); useEffect(() => { if (page !== currentPage + 1 && !isSelectedPage) { setCurrentPage(page - 1); } }, [currentPage, isSelectedPage, page]); useEffect(() => { setIsSelectedPage(false); }, [page]); const totalPages = useMemo(() => { return total != null ? Math.ceil(total / perPage) : undefined; }, [perPage, total]); // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedPageChange = useCallback( debounce((page) => { setPage(page + 1); }, 500), [setPage] ); /** * Warning: Material UI's page is 0-based */ const handlePageChange = useCallback( (event: React.MouseEvent | null, page: number) => { if (!event) { return; } event.preventDefault(); if (page < 0 || (totalPages !== undefined && page > totalPages - 1)) { throw new Error( translate('ra.navigation.page_out_of_boundaries', { page: page + 1 }) ); } const arrowSelected = ((event.target as HTMLElement).dataset?.testid || (event.target as HTMLElement).classList?.value) ?? ''; // check if user is clicking on the arrows or on the numbers const isArrowClick = arrowSelected.includes('MuiPaginationItem-previousNext') || arrowSelected.includes('NavigateBeforeIcon') || arrowSelected.includes('NavigateNextIcon') || arrowSelected.includes('KeyboardArrowLeftIcon') || arrowSelected.includes('KeyboardArrowRightIcon'); setCurrentPage(page); setIsSelectedPage(true); if (isArrowClick) { // apply debounced API call for arrows clicks debouncedPageChange(page); } else { // apply immediate API call for number clicks setPage(page + 1); } }, [debouncedPageChange, setPage, translate, totalPages, setIsSelectedPage] ); const handlePerPageChange = useCallback( (event: React.ChangeEvent) => { setPerPage(Number(event.target.value)); }, [setPerPage] ); const labelDisplayedRows = useCallback( ({ from, to, count }: { from: number; to: number; count: number }) => count === -1 && hasNextPage ? translate('ra.navigation.partial_page_range_info', { offsetBegin: from, offsetEnd: to, _: `%{from}-%{to} of more than %{to}` }) : translate('ra.navigation.page_range_info', { offsetBegin: from, offsetEnd: to, total: count === -1 ? to : count, _: `%{from}-%{to} of %{count === -1 ? to : count}` }), [translate, hasNextPage] ); const labelItem = useCallback( (type: string) => translate(`ra.navigation.${type}`, { _: `Go to ${type} page` }), [translate] ); if (isLoading) { return ; } // Avoid rendering TablePagination if "page" value is invalid if (total === 0 || page < 1 || (totalPages !== undefined && page > totalPages)) { if (limit != null && process.env.NODE_ENV === 'development') { console.warn( 'The Pagination limit prop is deprecated. Empty state should be handled by the component displaying data (Datagrid, SimpleList).' ); } return null; } if (isSmall) { return ( ); } const ActionsComponent = actions ? actions // overridden by caller : !isLoading && total != null ? PaginationActions // regular navigation : undefined; // partial navigation (uses default TablePaginationActions) return ( ); }); Pagination.propTypes = { actions: ComponentPropType, limit: PropTypes.element, rowsPerPageOptions: PropTypes.arrayOf( PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ label: PropTypes.string.isRequired, value: PropTypes.number.isRequired }) ]) ) as PropTypes.Validator<(number | { label: string; value: number })[] | null | undefined> }; const DefaultRowsPerPageOptions = [5, 10, 25, 50]; const emptyArray: any[] = []; interface PaginationProps extends TablePaginationBaseProps, Partial { rowsPerPageOptions?: Array; actions?: FC; limit?: ReactElement; displayStyle?: 'text' | 'outlined'; } export { Pagination }; export type { PaginationProps };