From face919cdbc7967a21f804f069e3e16dc506525f Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Apr 2024 17:10:44 +0500 Subject: [PATCH] [DataGrid] Support advanced server-side pagination use cases (#12474) --- docs/data/data-grid/events/events.json | 7 + .../data-grid/localization/localization.md | 2 +- .../pagination/CursorPaginationGrid.js | 115 ++++++++++--- .../pagination/CursorPaginationGrid.tsx | 124 +++++++++++--- .../CursorPaginationGrid.tsx.preview | 10 -- .../pagination/ServerPaginationGrid.js | 22 ++- .../pagination/ServerPaginationGrid.tsx | 22 ++- .../ServerPaginationGrid.tsx.preview | 2 +- .../ServerPaginationGridEstimated.js | 58 +++++++ .../ServerPaginationGridEstimated.tsx | 58 +++++++ .../ServerPaginationGridEstimated.tsx.preview | 16 ++ .../ServerPaginationGridNoRowCount.js | 55 ++++++ .../ServerPaginationGridNoRowCount.tsx | 55 ++++++ ...ServerPaginationGridNoRowCount.tsx.preview | 12 ++ docs/data/data-grid/pagination/pagination.md | 156 ++++++++++++++++-- .../x/api/data-grid/data-grid-premium.json | 9 + docs/pages/x/api/data-grid/data-grid-pro.json | 9 + docs/pages/x/api/data-grid/data-grid.json | 9 + docs/pages/x/api/data-grid/grid-api.json | 4 + .../x/api/data-grid/grid-pagination-api.json | 5 + docs/pages/x/api/data-grid/selectors.json | 7 + .../data-grid-premium/data-grid-premium.json | 12 +- .../data-grid-pro/data-grid-pro.json | 12 +- .../data-grid/data-grid/data-grid.json | 12 +- .../api-docs/data-grid/grid-api.json | 1 + .../src/hooks/useQuery.ts | 7 +- .../src/DataGridPremium/DataGridPremium.tsx | 19 +++ .../src/DataGridPro/DataGridPro.tsx | 19 +++ .../statePersistence.DataGridPro.test.tsx | 3 + .../x-data-grid/src/DataGrid/DataGrid.tsx | 19 +++ .../src/components/GridPagination.tsx | 63 ++++++- packages/x-data-grid/src/components/index.ts | 2 +- .../pagination/gridPaginationInterfaces.ts | 20 ++- .../pagination/gridPaginationSelector.ts | 14 +- .../pagination/gridPaginationUtils.ts | 16 +- .../features/pagination/useGridPagination.ts | 11 +- .../pagination/useGridPaginationMeta.ts | 113 +++++++++++++ .../pagination/useGridPaginationModel.ts | 3 +- .../features/pagination/useGridRowCount.ts | 68 +++++--- .../src/hooks/utils/useGridSelector.ts | 2 +- .../src/internals/utils/propValidation.ts | 21 ++- .../src/models/api/gridApiCommon.ts | 2 +- .../src/models/api/gridLocaleTextApi.ts | 17 +- packages/x-data-grid/src/models/api/index.ts | 2 +- .../src/models/events/gridEventLookup.ts | 6 +- .../src/models/gridPaginationProps.ts | 4 + .../src/models/props/DataGridProps.ts | 19 ++- .../src/tests/pagination.DataGrid.test.tsx | 67 ++++++-- .../src/utils/getGridLocalization.ts | 9 +- scripts/x-data-grid-premium.exports.json | 2 + scripts/x-data-grid-pro.exports.json | 2 + scripts/x-data-grid.exports.json | 2 + 52 files changed, 1145 insertions(+), 181 deletions(-) delete mode 100644 docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridEstimated.js create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx create mode 100644 docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview create mode 100644 packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index 64c820e1c397d..935d9560a69a8 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -254,6 +254,13 @@ "event": "MuiEvent<{}>", "componentProp": "onMenuOpen" }, + { + "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], + "name": "paginationMetaChange", + "description": "Fired when the pagination meta change.", + "params": "GridPaginationMeta", + "event": "MuiEvent<{}>" + }, { "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], "name": "paginationModelChange", diff --git a/docs/data/data-grid/localization/localization.md b/docs/data/data-grid/localization/localization.md index 117d8792a96d3..28197aed4c870 100644 --- a/docs/data/data-grid/localization/localization.md +++ b/docs/data/data-grid/localization/localization.md @@ -24,7 +24,7 @@ One example is the table pagination component used in the Data Grid footer when localeText={{ MuiTablePagination: { labelDisplayedRows: ({ from, to, count }) => - `${from} - ${to} of more than ${count}`, + `${from} - ${to} of ${count === -1 ? `more than ${to}` : count}`, }, }} /> diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.js b/docs/data/data-grid/pagination/CursorPaginationGrid.js index cf369037fa5fa..04caad836070a 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.js +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.js @@ -1,5 +1,10 @@ import * as React from 'react'; import { DataGrid } from '@mui/x-data-grid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; import { createFakeServer } from '@mui/x-data-grid-generator'; const PAGE_SIZE = 5; @@ -11,6 +16,8 @@ const SERVER_OPTIONS = { const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); export default function CursorPaginationGrid() { + const [rowCountType, setRowCountType] = React.useState('known'); + const mapPageToNextCursor = React.useRef({}); const [paginationModel, setPaginationModel] = React.useState({ @@ -25,7 +32,11 @@ export default function CursorPaginationGrid() { }), [paginationModel], ); - const { isLoading, rows, pageInfo } = useQuery(queryOptions); + const { + isLoading, + rows, + pageInfo: { hasNextPage, nextCursor, totalRowCount }, + } = useQuery(queryOptions); const handlePaginationModelChange = (newPaginationModel) => { // We have the cursor, we can allow the page transition. @@ -37,38 +48,94 @@ export default function CursorPaginationGrid() { } }; + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + React.useEffect(() => { - if (!isLoading && pageInfo?.nextCursor) { + if (!isLoading && nextCursor) { // We add nextCursor when available - mapPageToNextCursor.current[paginationModel.page] = pageInfo?.nextCursor; + mapPageToNextCursor.current[paginationModel.page] = nextCursor; } - }, [paginationModel.page, isLoading, pageInfo?.nextCursor]); + }, [paginationModel.page, isLoading, nextCursor]); // Some API clients return undefined while loading // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); + const [rowCountState, setRowCountState] = React.useState(totalRowCount || 0); React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + if (rowCountType === 'known') { + setRowCountState((prevRowCountState) => + totalRowCount !== undefined ? totalRowCount : prevRowCountState, + ); + } + if ( + (rowCountType === 'unknown' || rowCountType === 'estimated') && + paginationMeta?.hasNextPage !== false + ) { + setRowCountState(-1); + } + }, [paginationMeta?.hasNextPage, rowCountType, totalRowCount]); + + const prevEstimatedRowCount = React.useRef(undefined); + const estimatedRowCount = React.useMemo(() => { + if (rowCountType === 'estimated') { + if (totalRowCount !== undefined) { + if (prevEstimatedRowCount.current === undefined) { + prevEstimatedRowCount.current = totalRowCount / 2; + } + return totalRowCount / 2; + } + return prevEstimatedRowCount.current; + } + return undefined; + }, [rowCountType, totalRowCount]); return ( -
- +
+ + + Row count + + setRowCountType(e.target.value)} + > + } label="Known" /> + } label="Unknown" /> + } + label="Estimated" + /> + + +
+ setRowCountState(newRowCount)} + estimatedRowCount={estimatedRowCount} + paginationMeta={paginationMeta} + paginationMode="server" + onPaginationModelChange={handlePaginationModelChange} + paginationModel={paginationModel} + loading={isLoading} + /> +
); } diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx index 5aff7d9779f20..d54e3e806c7d1 100644 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx +++ b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx @@ -1,5 +1,15 @@ import * as React from 'react'; -import { DataGrid, GridRowId, GridPaginationModel } from '@mui/x-data-grid'; +import { + DataGrid, + GridRowId, + GridPaginationModel, + GridPaginationMeta, +} from '@mui/x-data-grid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; import { createFakeServer } from '@mui/x-data-grid-generator'; const PAGE_SIZE = 5; @@ -10,7 +20,11 @@ const SERVER_OPTIONS = { const { useQuery, ...data } = createFakeServer({}, SERVER_OPTIONS); +type RowCountType = 'known' | 'unknown' | 'estimated'; + export default function CursorPaginationGrid() { + const [rowCountType, setRowCountType] = React.useState('known'); + const mapPageToNextCursor = React.useRef<{ [page: number]: GridRowId }>({}); const [paginationModel, setPaginationModel] = React.useState({ @@ -25,7 +39,11 @@ export default function CursorPaginationGrid() { }), [paginationModel], ); - const { isLoading, rows, pageInfo } = useQuery(queryOptions); + const { + isLoading, + rows, + pageInfo: { hasNextPage, nextCursor, totalRowCount }, + } = useQuery(queryOptions); const handlePaginationModelChange = (newPaginationModel: GridPaginationModel) => { // We have the cursor, we can allow the page transition. @@ -37,38 +55,94 @@ export default function CursorPaginationGrid() { } }; + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + React.useEffect(() => { - if (!isLoading && pageInfo?.nextCursor) { + if (!isLoading && nextCursor) { // We add nextCursor when available - mapPageToNextCursor.current[paginationModel.page] = pageInfo?.nextCursor; + mapPageToNextCursor.current[paginationModel.page] = nextCursor; } - }, [paginationModel.page, isLoading, pageInfo?.nextCursor]); + }, [paginationModel.page, isLoading, nextCursor]); // Some API clients return undefined while loading // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); + const [rowCountState, setRowCountState] = React.useState(totalRowCount || 0); React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + if (rowCountType === 'known') { + setRowCountState((prevRowCountState) => + totalRowCount !== undefined ? totalRowCount : prevRowCountState, + ); + } + if ( + (rowCountType === 'unknown' || rowCountType === 'estimated') && + paginationMeta?.hasNextPage !== false + ) { + setRowCountState(-1); + } + }, [paginationMeta?.hasNextPage, rowCountType, totalRowCount]); + + const prevEstimatedRowCount = React.useRef(undefined); + const estimatedRowCount = React.useMemo(() => { + if (rowCountType === 'estimated') { + if (totalRowCount !== undefined) { + if (prevEstimatedRowCount.current === undefined) { + prevEstimatedRowCount.current = totalRowCount / 2; + } + return totalRowCount / 2; + } + return prevEstimatedRowCount.current; + } + return undefined; + }, [rowCountType, totalRowCount]); return ( -
- +
+ + + Row count + + setRowCountType(e.target.value as RowCountType)} + > + } label="Known" /> + } label="Unknown" /> + } + label="Estimated" + /> + + +
+ setRowCountState(newRowCount)} + estimatedRowCount={estimatedRowCount} + paginationMeta={paginationMeta} + paginationMode="server" + onPaginationModelChange={handlePaginationModelChange} + paginationModel={paginationModel} + loading={isLoading} + /> +
); } diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview deleted file mode 100644 index 32c24513f9959..0000000000000 --- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx.preview +++ /dev/null @@ -1,10 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/data-grid/pagination/ServerPaginationGrid.js b/docs/data/data-grid/pagination/ServerPaginationGrid.js index 0bcb826f4034b..6b142879e4f95 100644 --- a/docs/data/data-grid/pagination/ServerPaginationGrid.js +++ b/docs/data/data-grid/pagination/ServerPaginationGrid.js @@ -17,24 +17,22 @@ export default function ServerPaginationGrid() { const { isLoading, rows, pageInfo } = useQuery(paginationModel); // Some API clients return undefined while loading - // Following lines are here to prevent `rowCountState` from being undefined during the loading - const [rowCountState, setRowCountState] = React.useState( - pageInfo?.totalRowCount || 0, - ); - React.useEffect(() => { - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + // Following lines are here to prevent `rowCount` from being undefined during the loading + const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + + const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; + }, [pageInfo?.totalRowCount]); return (
{ - setRowCountState((prevRowCountState) => - pageInfo?.totalRowCount !== undefined - ? pageInfo?.totalRowCount - : prevRowCountState, - ); - }, [pageInfo?.totalRowCount, setRowCountState]); + // Following lines are here to prevent `rowCount` from being undefined during the loading + const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + + const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; + }, [pageInfo?.totalRowCount]); return (
{ + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx new file mode 100644 index 0000000000000..6191eb8232a84 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; +import type { GridPaginationMeta } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const { useQuery, ...data } = createFakeServer({ rowLength: 1000 }, SERVER_OPTIONS); + +export default function ServerPaginationGridEstimated() { + const apiRef = useGridApiRef(); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 50, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const paginationMetaRef = React.useRef({}); + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview new file mode 100644 index 0000000000000..82b7e4ec6ffe3 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridEstimated.tsx.preview @@ -0,0 +1,16 @@ + +
+ +
\ No newline at end of file diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js new file mode 100644 index 0000000000000..2de106edc5d3e --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { DataGrid, useGridApiRef } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const rowLength = 98; + +const { useQuery, ...data } = createFakeServer({ rowLength }, SERVER_OPTIONS); + +export default function ServerPaginationGridNoRowCount() { + const apiRef = useGridApiRef(); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 5, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx new file mode 100644 index 0000000000000..bb35ce14cebd5 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { DataGrid, GridPaginationMeta, useGridApiRef } from '@mui/x-data-grid'; +import { createFakeServer } from '@mui/x-data-grid-generator'; + +const SERVER_OPTIONS = { + useCursorPagination: false, +}; + +const rowLength = 98; + +const { useQuery, ...data } = createFakeServer({ rowLength }, SERVER_OPTIONS); + +export default function ServerPaginationGridNoRowCount() { + const apiRef = useGridApiRef(); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 5, + }); + + const { + isLoading, + rows, + pageInfo: { hasNextPage }, + } = useQuery(paginationModel); + + const paginationMetaRef = React.useRef(); + + // Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch + const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; + }, [hasNextPage]); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview new file mode 100644 index 0000000000000..5c6f399b3aa01 --- /dev/null +++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx.preview @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/pagination/pagination.md b/docs/data/data-grid/pagination/pagination.md index a3af0fb6ac263..d23efc50aa1fa 100644 --- a/docs/data/data-grid/pagination/pagination.md +++ b/docs/data/data-grid/pagination/pagination.md @@ -94,40 +94,160 @@ const [paginationModel, setPaginationModel] = React.useState({ By default, the pagination is handled on the client. This means you have to give the rows of all pages to the data grid. -If your dataset is too big, and you only want to fetch the current page, you can use server-side pagination. +If your dataset is too big, and you want to fetch the pages on demand, you can use server-side pagination. + +In general, the server-side pagination could be categorized into two types: + +- Index-based pagination +- Cursor-based pagination :::info Check out [Selection—Usage with server-side pagination](/x/react-data-grid/row-selection/#usage-with-server-side-pagination) for more details. ::: -### Basic implementation +### Index-based pagination -- Set the prop `paginationMode` to `server` -- Provide a `rowCount` prop to let the data grid know how many pages there are -- Use the `onPaginationModelChange` prop callback to load the rows when the page changes +The index-based pagination uses the `page` and `pageSize` to fetch the data from the server page by page. -Since the `rowCount` prop is used to compute the number of available pages, switching it to `undefined` during loading resets the page to zero. -To avoid this problem, you can keep the previous value of `rowCount` while loading as follows: +To enable server-side pagination, you need to: -```jsx -const [rowCountState, setRowCountState] = React.useState(rowCount); -React.useEffect(() => { - setRowCountState((prevRowCountState) => - rowCount !== undefined ? rowCount : prevRowCountState, - ); -}, [rowCount, setRowCountState]); - -; -``` +- Set the `paginationMode` prop to `server` +- Use the `onPaginationModelChange` prop to react to the page changes and load the data from the server + +The server-side pagination can be further categorized into sub-types based on the availability of the total number of rows or `rowCount`. + +The Data Grid uses the `rowCount` to calculate the number of pages and to show the information about the current state of the pagination in the footer. +You can provide the `rowCount` in one of the following ways: + +- **Initialize.** + Use the `initialState.pagination.rowCount` prop to initialize the `rowCount`. +- **Control.** + Use the `rowCount` prop along with `onRowCountChange` to control the `rowCount` and reflect the changes when the row count is updated. +- **Set using the API.** + Use the `apiRef.current.setRowCount` method to set the `rowCount` after the Grid is initialized. + +There can be three different possibilities regarding the availability of the `rowCount` on the server-side: + +1. Row count is available (known) +2. Row count is not available (unknown) +3. Row count is available but is not accurate and may update later on (estimated) :::warning The `rowCount` prop is used in server-side pagination mode to inform the DataGrid about the total number of rows in your dataset. This prop is ignored when the `paginationMode` is set to `client`, i.e. when the pagination is handled on the client-side. ::: +You can configure `rowCount`, `paginationMeta.hasNextPage`, and `estimatedRowCount` props to handle the above scenarios. + +| | `rowCount` | `paginationMeta.hasNextPage` | `estimatedRowCount` | +| :------------------ | :--------- | :--------------------------- | :------------------ | +| Known row count | `number` | — | — | +| Unknown row count | `-1` | `boolean` | — | +| Estimated row count | `-1` | `boolean` | `number` | + +#### Known row count + +Pass the props to the Data Grid as explained in the table above to handle the case when the actual row count is known, as the following example demonstrates. + {{"demo": "ServerPaginationGrid.js", "bg": "inline"}} -### Cursor implementation +:::warning +If the value `rowCount` becomes `undefined` during loading, it will reset the page to zero. +To avoid this issue, you can memoize the `rowCount` value to ensure it doesn't change during loading: + +```jsx +const rowCountRef = React.useRef(pageInfo?.totalRowCount || 0); + +const rowCount = React.useMemo(() => { + if (pageInfo?.totalRowCount !== undefined) { + rowCountRef.current = pageInfo.totalRowCount; + } + return rowCountRef.current; +}, [pageInfo?.totalRowCount]); + +; +``` + +::: + +#### Unknown row count + +Pass the props to the Data Grid as explained in the table above to handle the case when the actual row count is unknown, as the following example demonstrates. + +{{"demo": "ServerPaginationGridNoRowCount.js", "bg": "inline"}} + +:::warning +The value of the `hasNextPage` variable might become `undefined` during loading if it's handled by some external fetching hook resulting in unwanted computations, one possible solution could be to memoize the `paginationMeta`: + +```tsx +const paginationMetaRef = React.useRef(); + +const paginationMeta = React.useMemo(() => { + if ( + hasNextPage !== undefined && + paginationMetaRef.current?.hasNextPage !== hasNextPage + ) { + paginationMetaRef.current = { hasNextPage }; + } + return paginationMetaRef.current; +}, [hasNextPage]); +``` + +::: + +#### Estimated row count + +Estimated row count could be considered a hybrid approach that switches between the "Known row count" and "Unknown row count" use cases. + +Initially, when an `estimatedRowCount` is set and `rowCount={-1}`, the Data Grid behaves as in the "Unknown row count" use case, but with the `estimatedRowCount` value shown in the pagination footer. + +If the number of rows loaded exceeds the `estimatedRowCount`, the Data Grid ignores the `estimatedRowCount` and the behavior is identical to the "Unknown row count" use case. + +When the `hasNextPage` returns `false` or `rowCount` is set to a positive number, the Data Grid switches to the "Known row count" behavior. + +In the following example, the actual row count is `1000` but the Data Grid is initially provided with `estimatedRowCount={100}`. +You can set the `rowCount` to the actual row count by pressing the "Set Row Count" button. + +{{"demo": "ServerPaginationGridEstimated.js", "bg": "inline"}} + +:::warning +The `hasNextPage` must not be set to `false` until there are _actually_ no records left to fetch, because when `hasNextPage` becomes `false`, the Grid considers this as the last page and tries to set the `rowCount` value to a finite value. + +If an external data fetching library sets the values to undefined during loading, you can memoize the `paginationMeta` value to ensure it doesn't change during loading as shown in the "Unknown row count" section. +::: + +:::info + +🌍 **Localization of the estimated row count** + +The Data Grid uses the [Table Pagination](/material-ui/api/table-pagination/) component from the Material UI library which doesn't support `estimated` row count. Until this is supported natively by the Table Pagination component, a workaround to make the localization work is to provide the `labelDisplayedRows` function to the `localeText.MuiTablePagination` property as per the locale you are interested in. + +The Grid injects an additional variable `estimated` to the `labelDisplayedRows` function which you can use to accomodate the estimated row count. +The following example demonstrates how to show the estimated row count in the pagination footer in the Croatian (hr-HR) language. + +```jsx +const labelDisplayedRows = ({ from, to, count, estimated }) => { + if (!estimated) { + return `${from}–${to} od ${count !== -1 ? count : `više nego ${to}`}`, + } + return `${from}–${to} od ${count !== -1 ? count : `više nego ${estimated > to ? estimated : to}`}`; +} + + +``` + +For more information, see [Translation keys](/x/react-data-grid/localization/#translation-keys) section of the localization documentation. + +::: + +### Cursor-based pagination You can also handle servers with cursor-based pagination. To do so, you just have to keep track of the next cursor associated with each page you fetched. diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 5d72c3f2e9fc7..5800b14cc52fe 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -73,6 +73,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -406,6 +407,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -527,6 +535,7 @@ "default": "[25, 50, 100]" }, "pagination": { "type": { "name": "bool" }, "default": "false" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 732f9d1513e9e..5e96f8adbe84c 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -59,6 +59,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -356,6 +357,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -470,6 +478,7 @@ "default": "[25, 50, 100]" }, "pagination": { "type": { "name": "bool" }, "default": "false" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 4b7fc7836b78f..8fad1a4c40630 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -48,6 +48,7 @@ "type": { "name": "enum", "description": "'cell'
| 'row'" }, "default": "\"cell\"" }, + "estimatedRowCount": { "type": { "name": "number" } }, "experimentalFeatures": { "type": { "name": "shape", "description": "{ warnIfFocusStateIsNotSynced?: bool }" } }, @@ -301,6 +302,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -393,6 +401,7 @@ }, "default": "[25, 50, 100]" }, + "paginationMeta": { "type": { "name": "shape", "description": "{ hasNextPage?: bool }" } }, "paginationMode": { "type": { "name": "enum", "description": "'client'
| 'server'" }, "default": "\"client\"" diff --git a/docs/pages/x/api/data-grid/grid-api.json b/docs/pages/x/api/data-grid/grid-api.json index 4acb3f3986bb8..a4fc87914cd13 100644 --- a/docs/pages/x/api/data-grid/grid-api.json +++ b/docs/pages/x/api/data-grid/grid-api.json @@ -346,6 +346,10 @@ }, "setPage": { "type": { "description": "(page: number) => void" }, "required": true }, "setPageSize": { "type": { "description": "(pageSize: number) => void" }, "required": true }, + "setPaginationMeta": { + "type": { "description": "(paginationMeta: GridPaginationMeta) => void" }, + "required": true + }, "setPaginationModel": { "type": { "description": "(model: GridPaginationModel) => void" }, "required": true diff --git a/docs/pages/x/api/data-grid/grid-pagination-api.json b/docs/pages/x/api/data-grid/grid-pagination-api.json index b782ce762515a..47f7684b511e0 100644 --- a/docs/pages/x/api/data-grid/grid-pagination-api.json +++ b/docs/pages/x/api/data-grid/grid-pagination-api.json @@ -12,6 +12,11 @@ "description": "Sets the number of displayed rows to the value given by pageSize.", "type": "(pageSize: number) => void" }, + { + "name": "setPaginationMeta", + "description": "Sets the paginationMeta to a new value.", + "type": "(paginationMeta: GridPaginationMeta) => void" + }, { "name": "setPaginationModel", "description": "Sets the paginationModel to a new value.", diff --git a/docs/pages/x/api/data-grid/selectors.json b/docs/pages/x/api/data-grid/selectors.json index 1140b1bf1d946..e10bb19d717b2 100644 --- a/docs/pages/x/api/data-grid/selectors.json +++ b/docs/pages/x/api/data-grid/selectors.json @@ -311,6 +311,13 @@ "description": "Get the id of each row to include in the current page if the pagination is enabled.", "supportsApiRef": true }, + { + "name": "gridPaginationMetaSelector", + "returnType": "GridPaginationMeta", + "category": "Pagination", + "description": "Get the pagination meta", + "supportsApiRef": true + }, { "name": "gridPaginationModelSelector", "returnType": "GridPaginationModel", diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index db2671217faba..dc1028649c3e2 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -104,6 +104,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, then the feature is fully disabled, and neither property nor method calls will have any effect." }, @@ -438,6 +441,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -560,6 +567,9 @@ }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, "pagination": { "description": "If true, pagination is enabled." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -579,7 +589,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowGroupingColumnMode": { "description": "If single, all the columns that are grouped are represented in the same grid column. If multiple, each column that is grouped is represented in its own grid column." diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index d95a4ffd05e32..3bc27f5060e0e 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -92,6 +92,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, @@ -391,6 +394,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -506,6 +513,9 @@ }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, "pagination": { "description": "If true, pagination is enabled." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -525,7 +535,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index 4f472991918d5..abe89e1a60738 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -67,6 +67,9 @@ "description": "If true, the virtualization is disabled." }, "editMode": { "description": "Controls whether to use the cell or row editing." }, + "estimatedRowCount": { + "description": "Use if the actual rowCount is not known upfront, but an estimation is available. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Applicable only with paginationMode="server" and when rowCount="-1"" + }, "experimentalFeatures": { "description": "Unstable features, breaking changes might be introduced. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect." }, @@ -319,6 +322,10 @@ "details": "Additional details for this callback." } }, + "onPaginationMetaChange": { + "description": "Callback fired when the pagination meta has changed.", + "typeDescriptions": { "paginationMeta": "Updated pagination meta." } + }, "onPaginationModelChange": { "description": "Callback fired when the pagination model has changed.", "typeDescriptions": { @@ -410,6 +417,9 @@ } }, "pageSizeOptions": { "description": "Select the pageSize dynamically using the component UI." }, + "paginationMeta": { + "description": "The extra information about the pagination state of the Data Grid. Only applicable with paginationMode="server"." + }, "paginationMode": { "description": "Pagination can be processed on the server or client-side. Set it to 'client' if you would like to handle the pagination on the client-side. Set it to 'server' if you would like to handle the pagination on the server-side." }, @@ -427,7 +437,7 @@ "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { - "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows." + "description": "Set the total number of rows, if it is different from the length of the value rows prop. If some rows have children (for instance in the tree data), this number represents the amount of top level rows. Only works with paginationMode="server", ignored when paginationMode="client"." }, "rowHeight": { "description": "Sets the height in pixel of a row in the Data Grid." }, "rowModesModel": { "description": "Controls the modes of the rows." }, diff --git a/docs/translations/api-docs/data-grid/grid-api.json b/docs/translations/api-docs/data-grid/grid-api.json index ba27c6f79b3ac..02e89d3757eaa 100644 --- a/docs/translations/api-docs/data-grid/grid-api.json +++ b/docs/translations/api-docs/data-grid/grid-api.json @@ -184,6 +184,7 @@ "setPageSize": { "description": "Sets the number of displayed rows to the value given by pageSize." }, + "setPaginationMeta": { "description": "Sets the paginationMeta to a new value." }, "setPaginationModel": { "description": "Sets the paginationModel to a new value." }, diff --git a/packages/x-data-grid-generator/src/hooks/useQuery.ts b/packages/x-data-grid-generator/src/hooks/useQuery.ts index c000f6cfab762..55adae6cb6a7a 100644 --- a/packages/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/x-data-grid-generator/src/hooks/useQuery.ts @@ -146,9 +146,11 @@ export const loadServerRows = ( firstRowIndex = page * pageSize; lastRowIndex = (page + 1) * pageSize; } + const hasNextPage = lastRowIndex < filteredRows.length - 1; const response: FakeServerResponse = { returnedRows: filteredRows.slice(firstRowIndex, lastRowIndex), nextCursor, + hasNextPage, totalRowCount, }; @@ -162,12 +164,14 @@ export const loadServerRows = ( interface FakeServerResponse { returnedRows: GridRowModel[]; nextCursor?: string; + hasNextPage: boolean; totalRowCount: number; } interface PageInfo { totalRowCount?: number; nextCursor?: string; + hasNextPage?: boolean; pageSize?: number; } @@ -249,7 +253,7 @@ export const createFakeServer = ( ); (async function fetchData() { - const { returnedRows, nextCursor, totalRowCount } = await loadServerRows( + const { returnedRows, nextCursor, totalRowCount, hasNextPage } = await loadServerRows( rows, queryOptions, serverOptionsWithDefault, @@ -263,6 +267,7 @@ export const createFakeServer = ( pageInfo: { totalRowCount, nextCursor, + hasNextPage, pageSize: returnedRows.length, }, }; diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index ed9e568c6f465..9e3f04214546f 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -313,6 +313,12 @@ DataGridPremiumRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, then the feature is fully disabled, and neither property nor method calls will have any effect. @@ -720,6 +726,11 @@ DataGridPremiumRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -854,6 +865,13 @@ DataGridPremiumRaw.propTypes = { * @default false */ pagination: PropTypes.bool, + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -900,6 +918,7 @@ DataGridPremiumRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 272316b357e7e..cd6c469747b2f 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -261,6 +261,12 @@ DataGridProRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. @@ -628,6 +634,11 @@ DataGridProRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -756,6 +767,13 @@ DataGridProRaw.propTypes = { * @default false */ pagination: PropTypes.bool, + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -802,6 +820,7 @@ DataGridProRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx index 85880a295f635..6472bd21f8dfd 100644 --- a/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx @@ -63,6 +63,7 @@ const FULL_INITIAL_STATE: GridInitialState = { }, }, pagination: { + meta: {}, paginationModel: { page: 1, pageSize: 2 }, rowCount: 6, }, @@ -126,6 +127,7 @@ describe(' - State persistence', () => { filterModel: getDefaultGridFilterModel(), }, pagination: { + meta: {}, paginationModel: { page: 0, pageSize: 100 }, rowCount: 6, }, @@ -193,6 +195,7 @@ describe(' - State persistence', () => { }} paginationMode="server" rowCount={FULL_INITIAL_STATE.pagination?.rowCount} + paginationMeta={FULL_INITIAL_STATE.pagination?.meta} pinnedColumns={FULL_INITIAL_STATE.pinnedColumns} density={FULL_INITIAL_STATE.density} // Some portable states don't have a controllable model diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 8742dd514ba65..56a9868fa1826 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -220,6 +220,12 @@ DataGridRaw.propTypes = { * @default "cell" */ editMode: PropTypes.oneOf(['cell', 'row']), + /** + * Use if the actual rowCount is not known upfront, but an estimation is available. + * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Applicable only with `paginationMode="server"` and when `rowCount="-1"` + */ + estimatedRowCount: PropTypes.number, /** * Unstable features, breaking changes might be introduced. * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. @@ -526,6 +532,11 @@ DataGridRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onMenuOpen: PropTypes.func, + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange: PropTypes.func, /** * Callback fired when the pagination model has changed. * @param {GridPaginationModel} model Updated pagination model. @@ -630,6 +641,13 @@ DataGridRaw.propTypes = { ]).isRequired, ), pagination: PropTypes.oneOf([true]), + /** + * The extra information about the pagination state of the Data Grid. + * Only applicable with `paginationMode="server"`. + */ + paginationMeta: PropTypes.shape({ + hasNextPage: PropTypes.bool, + }), /** * Pagination can be processed on the server or client-side. * Set it to 'client' if you would like to handle the pagination on the client-side. @@ -665,6 +683,7 @@ DataGridRaw.propTypes = { /** * Set the total number of rows, if it is different from the length of the value `rows` prop. * If some rows have children (for instance in the tree data), this number represents the amount of top level rows. + * Only works with `paginationMode="server"`, ignored when `paginationMode="client"`. */ rowCount: PropTypes.number, /** diff --git a/packages/x-data-grid/src/components/GridPagination.tsx b/packages/x-data-grid/src/components/GridPagination.tsx index 9fe8e5cf2f931..8a5c5fd0e7630 100644 --- a/packages/x-data-grid/src/components/GridPagination.tsx +++ b/packages/x-data-grid/src/components/GridPagination.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; +import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import TablePagination, { tablePaginationClasses, TablePaginationProps, + LabelDisplayedRowsArgs, } from '@mui/material/TablePagination'; -import { styled } from '@mui/material/styles'; import { useGridSelector } from '../hooks/utils/useGridSelector'; import { useGridApiContext } from '../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; import { gridPaginationModelSelector, gridPaginationRowCountSelector, + gridPageCountSelector, } from '../hooks/features/pagination/gridPaginationSelector'; const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ @@ -28,6 +30,25 @@ const GridPaginationRoot = styled(TablePagination)(({ theme }) => ({ }, })) as typeof TablePagination; +export type WrappedLabelDisplayedRows = ( + args: LabelDisplayedRowsArgs & { estimated?: number }, +) => React.ReactNode; + +const wrapLabelDisplayedRows = ( + labelDisplayedRows: WrappedLabelDisplayedRows, + estimated?: number, +): TablePaginationProps['labelDisplayedRows'] => { + return ({ from, to, count, page }: LabelDisplayedRowsArgs) => + labelDisplayedRows({ from, to, count, page, estimated }); +}; + +const defaultLabelDisplayedRows: WrappedLabelDisplayedRows = ({ from, to, count, estimated }) => { + if (!estimated) { + return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`; + } + return `${from}–${to} of ${count !== -1 ? count : `more than ${estimated > to ? estimated : to}`}`; +}; + // A mutable version of a readonly array. type MutableArray = A extends readonly (infer T)[] ? T[] : never; @@ -48,11 +69,29 @@ const GridPagination = React.forwardRef< const rootProps = useGridRootProps(); const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); const rowCount = useGridSelector(apiRef, gridPaginationRowCountSelector); + const pageCount = useGridSelector(apiRef, gridPageCountSelector); + + const { paginationMode, loading, estimatedRowCount } = rootProps; + + const computedProps: Partial = React.useMemo(() => { + if (rowCount === -1 && paginationMode === 'server' && loading) { + return { + backIconButtonProps: { disabled: true }, + nextIconButtonProps: { disabled: true }, + }; + } + + return {}; + }, [loading, paginationMode, rowCount]); + + const lastPage = React.useMemo(() => Math.max(0, pageCount - 1), [pageCount]); - const lastPage = React.useMemo(() => { - const calculatedValue = Math.ceil(rowCount / (paginationModel.pageSize || 1)) - 1; - return Math.max(0, calculatedValue); - }, [rowCount, paginationModel.pageSize]); + const computedPage = React.useMemo(() => { + if (rowCount === -1) { + return paginationModel.page; + } + return paginationModel.page <= lastPage ? paginationModel.page : lastPage; + }, [lastPage, paginationModel.page, rowCount]); const handlePageSizeChange = React.useCallback( (event: React.ChangeEvent) => { @@ -95,7 +134,7 @@ const GridPagination = React.forwardRef< ) { console.warn( [ - `MUI X: The page size \`${paginationModel.pageSize}\` is not preset in the \`pageSizeOptions\`.`, + `MUI X: The page size \`${paginationModel.pageSize}\` is not present in the \`pageSizeOptions\`.`, `Add it to show the pagination select.`, ].join('\n'), ); @@ -108,12 +147,18 @@ const GridPagination = React.forwardRef< ? rootProps.pageSizeOptions : []; + const locales = apiRef.current.getLocaleText('MuiTablePagination'); + const wrappedLabelDisplayedRows = wrapLabelDisplayedRows( + locales.labelDisplayedRows || defaultLabelDisplayedRows, + estimatedRowCount, + ); + return ( ); diff --git a/packages/x-data-grid/src/components/index.ts b/packages/x-data-grid/src/components/index.ts index 6313a66231b5c..2657dac0cd890 100644 --- a/packages/x-data-grid/src/components/index.ts +++ b/packages/x-data-grid/src/components/index.ts @@ -14,7 +14,7 @@ export * from './GridFooter'; export * from './GridHeader'; export * from './GridLoadingOverlay'; export * from './GridNoRowsOverlay'; -export * from './GridPagination'; +export { GridPagination } from './GridPagination'; export * from './GridRowCount'; export * from './GridRow'; export * from './GridSelectedRowCount'; diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts index 310c659870fae..a47c14ea44544 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationInterfaces.ts @@ -1,13 +1,15 @@ -import { GridPaginationModel } from '../../../models/gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../../../models/gridPaginationProps'; export interface GridPaginationState { paginationModel: GridPaginationModel; rowCount: number; + meta: GridPaginationMeta; } export interface GridPaginationInitialState { paginationModel?: Partial; rowCount?: number; + meta?: GridPaginationMeta; } /** @@ -42,7 +44,21 @@ export interface GridPaginationRowCountApi { setRowCount: (rowCount: number) => void; } +/** + * The pagination meta API interface that is available in the grid `apiRef`. + */ +export interface GridPaginationMetaApi { + /** + * Sets the `paginationMeta` to a new value. + * @param {GridPaginationMeta} paginationMeta The new pagination meta value. + */ + setPaginationMeta: (paginationMeta: GridPaginationMeta) => void; +} + /** * The pagination API interface that is available in the grid `apiRef`. */ -export interface GridPaginationApi extends GridPaginationModelApi, GridPaginationRowCountApi {} +export interface GridPaginationApi + extends GridPaginationModelApi, + GridPaginationRowCountApi, + GridPaginationMetaApi {} diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts index bc0b8ef20d2e3..786ba8881b453 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationSelector.ts @@ -32,6 +32,15 @@ export const gridPaginationRowCountSelector = createSelector( (pagination) => pagination.rowCount, ); +/** + * Get the pagination meta + * @category Pagination + */ +export const gridPaginationMetaSelector = createSelector( + gridPaginationSelector, + (pagination) => pagination.meta, +); + /** * Get the index of the page to render if the pagination is enabled * @category Pagination @@ -55,9 +64,10 @@ export const gridPageSizeSelector = createSelector( * @category Pagination */ export const gridPageCountSelector = createSelector( - gridPageSizeSelector, + gridPaginationModelSelector, gridPaginationRowCountSelector, - (pageSize, rowCount) => getPageCount(rowCount, pageSize), + (paginationModel, rowCount) => + getPageCount(rowCount, paginationModel.pageSize, paginationModel.page), ); /** diff --git a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts index c68819a261aca..61c43af8df961 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/gridPaginationUtils.ts @@ -1,27 +1,23 @@ import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { buildWarning } from '../../../utils/warning'; import { GridSignature } from '../../utils'; const MAX_PAGE_SIZE = 100; export const defaultPageSize = (autoPageSize: boolean) => (autoPageSize ? 0 : 100); -export const getPageCount = (rowCount: number, pageSize: number): number => { +export const getPageCount = (rowCount: number, pageSize: number, page: number): number => { if (pageSize > 0 && rowCount > 0) { return Math.ceil(rowCount / pageSize); } + if (rowCount === -1) { + // With unknown row-count, we can assume a page after the current one + return page + 2; + } + return 0; }; -export const noRowCountInServerMode = buildWarning( - [ - "MUI X: the 'rowCount' prop is undefined while using paginationMode='server'", - 'For more detail, see http://mui.com/components/data-grid/pagination/#basic-implementation', - ], - 'error', -); - export const getDefaultGridPaginationModel = (autoPageSize: boolean) => ({ page: 0, pageSize: autoPageSize ? 0 : 100, diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts index 9abbeab5301c4..50524ac4e656e 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPagination.ts @@ -9,11 +9,17 @@ import { } from './gridPaginationUtils'; import { useGridPaginationModel } from './useGridPaginationModel'; import { useGridRowCount } from './useGridRowCount'; +import { useGridPaginationMeta } from './useGridPaginationMeta'; export const paginationStateInitializer: GridStateInitializer< Pick< DataGridProcessedProps, - 'paginationModel' | 'rowCount' | 'initialState' | 'autoPageSize' | 'signature' + | 'paginationModel' + | 'rowCount' + | 'initialState' + | 'autoPageSize' + | 'signature' + | 'paginationMeta' > > = (state, props) => { const paginationModel = { @@ -24,11 +30,13 @@ export const paginationStateInitializer: GridStateInitializer< throwIfPageSizeExceedsTheLimit(paginationModel.pageSize, props.signature); const rowCount = props.rowCount ?? props.initialState?.pagination?.rowCount; + const meta = props.paginationMeta ?? props.initialState?.pagination?.meta ?? {}; return { ...state, pagination: { paginationModel, rowCount, + meta, }, }; }; @@ -41,6 +49,7 @@ export const useGridPagination = ( apiRef: React.MutableRefObject, props: DataGridProcessedProps, ) => { + useGridPaginationMeta(apiRef, props); useGridPaginationModel(apiRef, props); useGridRowCount(apiRef, props); }; diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts new file mode 100644 index 0000000000000..c87aa8ea63228 --- /dev/null +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationMeta.ts @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; +import { GridPaginationMetaApi } from './gridPaginationInterfaces'; +import { useGridLogger, useGridSelector, useGridApiMethod } from '../../utils'; +import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; +import { gridPaginationMetaSelector } from './gridPaginationSelector'; + +export const useGridPaginationMeta = ( + apiRef: React.MutableRefObject, + props: Pick< + DataGridProcessedProps, + 'paginationMeta' | 'initialState' | 'paginationMode' | 'onPaginationMetaChange' + >, +) => { + const logger = useGridLogger(apiRef, 'useGridPaginationMeta'); + + const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); + + apiRef.current.registerControlState({ + stateId: 'paginationMeta', + propModel: props.paginationMeta, + propOnChange: props.onPaginationMetaChange, + stateSelector: gridPaginationMetaSelector, + changeEvent: 'paginationMetaChange', + }); + + /** + * API METHODS + */ + const setPaginationMeta = React.useCallback( + (newPaginationMeta) => { + if (paginationMeta === newPaginationMeta) { + return; + } + logger.debug("Setting 'paginationMeta' to", newPaginationMeta); + + apiRef.current.setState((state) => ({ + ...state, + pagination: { + ...state.pagination, + meta: newPaginationMeta, + }, + })); + }, + [apiRef, logger, paginationMeta], + ); + + const paginationMetaApi: GridPaginationMetaApi = { + setPaginationMeta, + }; + + useGridApiMethod(apiRef, paginationMetaApi, 'public'); + + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState, context) => { + const exportedPaginationMeta = gridPaginationMetaSelector(apiRef); + + const shouldExportRowCount = + // Always export if the `exportOnlyDirtyModels` property is not activated + !context.exportOnlyDirtyModels || + // Always export if the `paginationMeta` is controlled + props.paginationMeta != null || + // Always export if the `paginationMeta` has been initialized + props.initialState?.pagination?.meta != null; + + if (!shouldExportRowCount) { + return prevState; + } + + return { + ...prevState, + pagination: { + ...prevState.pagination, + meta: exportedPaginationMeta, + }, + }; + }, + [apiRef, props.paginationMeta, props.initialState?.pagination?.meta], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const restoredPaginationMeta = context.stateToRestore.pagination?.meta + ? context.stateToRestore.pagination.meta + : gridPaginationMetaSelector(apiRef); + apiRef.current.setState((state) => ({ + ...state, + pagination: { + ...state.pagination, + meta: restoredPaginationMeta, + }, + })); + return params; + }, + [apiRef], + ); + + useGridRegisterPipeProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + + /** + * EFFECTS + */ + React.useEffect(() => { + if (props.paginationMeta) { + apiRef.current.setPaginationMeta(props.paginationMeta); + } + }, [apiRef, props.paginationMeta]); +}; diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts index 579eacf8cfd89..c482999e44acc 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridPaginationModel.ts @@ -29,7 +29,8 @@ export const getDerivedPaginationModel = ( let paginationModel = paginationState.paginationModel; const rowCount = paginationState.rowCount; const pageSize = paginationModelProp?.pageSize ?? paginationModel.pageSize; - const pageCount = getPageCount(rowCount, pageSize); + const page = paginationModelProp?.page ?? paginationModel.page; + const pageCount = getPageCount(rowCount, pageSize, page); if ( paginationModelProp && diff --git a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts index 0e8e76533dbce..39f950032ade2 100644 --- a/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts +++ b/packages/x-data-grid/src/hooks/features/pagination/useGridRowCount.ts @@ -1,17 +1,22 @@ import * as React from 'react'; +import useLazyRef from '@mui/utils/useLazyRef'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; -import { GridPaginationRowCountApi } from './gridPaginationInterfaces'; +import { GridPaginationRowCountApi, GridPaginationState } from './gridPaginationInterfaces'; import { gridFilteredTopLevelRowCountSelector } from '../filter'; -import { useGridLogger, useGridSelector, useGridApiMethod } from '../../utils'; +import { + useGridLogger, + useGridSelector, + useGridApiMethod, + useGridApiEventHandler, +} from '../../utils'; import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing'; -import { gridPaginationRowCountSelector } from './gridPaginationSelector'; -import { noRowCountInServerMode } from './gridPaginationUtils'; +import { + gridPaginationRowCountSelector, + gridPaginationMetaSelector, + gridPaginationModelSelector, +} from './gridPaginationSelector'; -/** - * @requires useGridFilter (state) - * @requires useGridDimensions (event) - can be after - */ export const useGridRowCount = ( apiRef: React.MutableRefObject, props: Pick< @@ -22,7 +27,10 @@ export const useGridRowCount = ( const logger = useGridLogger(apiRef, 'useGridRowCount'); const visibleTopLevelRowCount = useGridSelector(apiRef, gridFilteredTopLevelRowCountSelector); - const rowCount = useGridSelector(apiRef, gridPaginationRowCountSelector); + const rowCountState = useGridSelector(apiRef, gridPaginationRowCountSelector); + const paginationMeta = useGridSelector(apiRef, gridPaginationMetaSelector); + const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector); + const previousPageSize = useLazyRef(() => gridPaginationModelSelector(apiRef).pageSize); apiRef.current.registerControlState({ stateId: 'paginationRowCount', @@ -37,7 +45,7 @@ export const useGridRowCount = ( */ const setRowCount = React.useCallback( (newRowCount) => { - if (rowCount === newRowCount) { + if (rowCountState === newRowCount) { return; } logger.debug("Setting 'rowCount' to", newRowCount); @@ -50,7 +58,7 @@ export const useGridRowCount = ( }, })); }, - [apiRef, logger, rowCount], + [apiRef, logger, rowCountState], ); const paginationRowCountApi: GridPaginationRowCountApi = { @@ -110,21 +118,43 @@ export const useGridRowCount = ( useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); /** - * EFFECTS + * EVENTS */ - React.useEffect(() => { - if (process.env.NODE_ENV !== 'production') { - if (props.paginationMode === 'server' && props.rowCount == null) { - noRowCountInServerMode(); + const handlePaginationModelChange = React.useCallback( + (model: GridPaginationState['paginationModel']) => { + if (props.paginationMode === 'client' || !previousPageSize.current) { + return; } - } - }, [props.rowCount, props.paginationMode]); + if (model.pageSize !== previousPageSize.current) { + previousPageSize.current = model.pageSize; + if (rowCountState === -1) { + // Row count unknown and page size changed, reset the page + apiRef.current.setPage(0); + } + } + }, + [props.paginationMode, previousPageSize, rowCountState, apiRef], + ); + useGridApiEventHandler(apiRef, 'paginationModelChange', handlePaginationModelChange); + + /** + * EFFECTS + */ React.useEffect(() => { if (props.paginationMode === 'client') { apiRef.current.setRowCount(visibleTopLevelRowCount); } else if (props.rowCount != null) { apiRef.current.setRowCount(props.rowCount); } - }, [apiRef, visibleTopLevelRowCount, props.paginationMode, props.rowCount]); + }, [apiRef, props.paginationMode, visibleTopLevelRowCount, props.rowCount]); + + const isLastPage = paginationMeta.hasNextPage === false; + React.useEffect(() => { + if (isLastPage && rowCountState === -1) { + apiRef.current.setRowCount( + paginationModel.pageSize * paginationModel.page + visibleTopLevelRowCount, + ); + } + }, [apiRef, visibleTopLevelRowCount, isLastPage, rowCountState, paginationModel]); }; diff --git a/packages/x-data-grid/src/hooks/utils/useGridSelector.ts b/packages/x-data-grid/src/hooks/utils/useGridSelector.ts index a5337ea3c74cc..859167dcfb242 100644 --- a/packages/x-data-grid/src/hooks/utils/useGridSelector.ts +++ b/packages/x-data-grid/src/hooks/utils/useGridSelector.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GridApiCommon } from '../../models/api/gridApiCommon'; +import type { GridApiCommon } from '../../models/api/gridApiCommon'; import { OutputSelector } from '../../utils/createSelector'; import { useLazyRef } from './useLazyRef'; import { useOnMount } from './useOnMount'; diff --git a/packages/x-data-grid/src/internals/utils/propValidation.ts b/packages/x-data-grid/src/internals/utils/propValidation.ts index 427a2db0692e9..ee4d70e099492 100644 --- a/packages/x-data-grid/src/internals/utils/propValidation.ts +++ b/packages/x-data-grid/src/internals/utils/propValidation.ts @@ -15,11 +15,30 @@ export const propValidatorsDataGrid: PropValidator[] = [ 'Please remove one of these two props.', ].join('\n')) || undefined, + (props) => + (props.paginationMode === 'client' && + props.paginationMeta != null && + [ + 'MUI X: Usage of the `paginationMeta` prop with client-side pagination (`paginationMode="client"`) has no effect.', + '`paginationMeta` is only meant to be used with `paginationMode="server"`.', + ].join('\n')) || + undefined, (props) => (props.signature === GridSignature.DataGrid && props.paginationMode === 'client' && isNumber(props.rowCount) && - 'MUI X: Usage of the `rowCount` prop with client side pagination (`paginationMode="client"`) has no effect. `rowCount` is only meant to be used with `paginationMode="server"`.') || + [ + 'MUI X: Usage of the `rowCount` prop with client side pagination (`paginationMode="client"`) has no effect.', + '`rowCount` is only meant to be used with `paginationMode="server"`.', + ].join('\n')) || + undefined, + (props) => + (props.paginationMode === 'server' && + props.rowCount == null && + [ + "MUI X: The `rowCount` prop must be passed using `paginationMode='server'`", + 'For more detail, see http://mui.com/components/data-grid/pagination/#index-based-pagination', + ].join('\n')) || undefined, ]; diff --git a/packages/x-data-grid/src/models/api/gridApiCommon.ts b/packages/x-data-grid/src/models/api/gridApiCommon.ts index 05cf1bd6bda7e..1e59d123c8a5b 100644 --- a/packages/x-data-grid/src/models/api/gridApiCommon.ts +++ b/packages/x-data-grid/src/models/api/gridApiCommon.ts @@ -6,7 +6,7 @@ import { GridDensityApi } from './gridDensityApi'; import { GridEditingApi, GridEditingPrivateApi } from './gridEditingApi'; import type { GridFilterApi } from './gridFilterApi'; import { GridFocusApi, GridFocusPrivateApi } from './gridFocusApi'; -import { GridLocaleTextApi } from './gridLocaleTextApi'; +import type { GridLocaleTextApi } from './gridLocaleTextApi'; import type { GridParamsApi } from './gridParamsApi'; import { GridPreferencesPanelApi } from './gridPreferencesPanelApi'; import { GridPrintExportApi } from './gridPrintExportApi'; diff --git a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts index b25858988c0a4..97735a52eb0f3 100644 --- a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts +++ b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts @@ -1,6 +1,14 @@ import * as React from 'react'; -import { ComponentsPropsList } from '@mui/material/styles'; -import { GridColDef } from '../colDef'; +import type { ComponentsPropsList } from '@mui/material/styles'; +import type { WrappedLabelDisplayedRows } from '../../components/GridPagination'; +import type { GridColDef } from '../colDef'; + +export type MuiTablePaginationLocalizedProps = Omit< + ComponentsPropsList['MuiTablePagination'], + 'page' | 'count' | 'onChangePage' | 'rowsPerPage' | 'onPageChange' | 'labelDisplayedRows' +> & { + labelDisplayedRows?: WrappedLabelDisplayedRows; +}; /** * Set the types of the texts in the grid. @@ -174,10 +182,7 @@ export interface GridLocaleText { aggregationFunctionLabelSize: string; // Used core components translation keys - MuiTablePagination: Omit< - ComponentsPropsList['MuiTablePagination'], - 'page' | 'count' | 'onChangePage' | 'rowsPerPage' | 'onPageChange' - >; + MuiTablePagination: MuiTablePaginationLocalizedProps; } export type GridTranslationKeys = keyof GridLocaleText; diff --git a/packages/x-data-grid/src/models/api/index.ts b/packages/x-data-grid/src/models/api/index.ts index 16cae502d7ec8..38a051fc1f055 100644 --- a/packages/x-data-grid/src/models/api/index.ts +++ b/packages/x-data-grid/src/models/api/index.ts @@ -9,7 +9,7 @@ export type { GridRowsMetaApi } from './gridRowsMetaApi'; export * from './gridRowSelectionApi'; export * from './gridSortApi'; export type { GridStateApi } from './gridStateApi'; -export * from './gridLocaleTextApi'; +export type { GridLocaleText, GridLocaleTextApi, GridTranslationKeys } from './gridLocaleTextApi'; export * from './gridCsvExportApi'; export type { GridFocusApi } from './gridFocusApi'; export * from './gridFilterApi'; diff --git a/packages/x-data-grid/src/models/events/gridEventLookup.ts b/packages/x-data-grid/src/models/events/gridEventLookup.ts index 5f29dceffb45d..0ffc296f66fdb 100644 --- a/packages/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/x-data-grid/src/models/events/gridEventLookup.ts @@ -24,7 +24,7 @@ import type { GridColumnVisibilityModel } from '../../hooks/features/columns'; import type { GridStrategyProcessorName } from '../../hooks/core/strategyProcessing'; import { GridRowEditStartParams, GridRowEditStopParams } from '../params/gridRowParams'; import { GridCellModesModel, GridRowModesModel } from '../api/gridEditingApi'; -import { GridPaginationModel } from '../gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps'; import { GridDensity } from '../gridDensity'; export interface GridRowEventLookup { @@ -367,6 +367,10 @@ export interface GridControlledStateEventLookup { * Fired when the density changes. */ densityChange: { params: GridDensity }; + /** + * Fired when the pagination meta change. + */ + paginationMetaChange: { params: GridPaginationMeta }; } export interface GridControlledStateReasonLookup { diff --git a/packages/x-data-grid/src/models/gridPaginationProps.ts b/packages/x-data-grid/src/models/gridPaginationProps.ts index 543323ac86b14..0e9395f39a9f2 100644 --- a/packages/x-data-grid/src/models/gridPaginationProps.ts +++ b/packages/x-data-grid/src/models/gridPaginationProps.ts @@ -11,3 +11,7 @@ export interface GridPaginationModel { */ page: number; } + +export interface GridPaginationMeta { + hasNextPage?: boolean; +} diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 84f1c3f9456a1..3d80dcdb101c4 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -30,7 +30,7 @@ import { GridSlotsComponentsProps } from '../gridSlotsComponentsProps'; import { GridColumnVisibilityModel } from '../../hooks/features/columns/gridColumnsInterfaces'; import { GridCellModesModel, GridRowModesModel } from '../api/gridEditingApi'; import { GridColumnGroupingModel } from '../gridColumnGrouping'; -import { GridPaginationModel } from '../gridPaginationProps'; +import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps'; import type { GridAutosizeOptions } from '../../hooks/features/columnResize'; export interface GridExperimentalFeatures { @@ -403,8 +403,15 @@ export interface DataGridPropsWithoutDefaultValue void; + /** + * Callback fired when the pagination meta has changed. + * @param {GridPaginationMeta} paginationMeta Updated pagination meta. + */ + onPaginationMetaChange?: (paginationMeta: GridPaginationMeta) => void; /** * Callback fired when the preferences panel is closed. * @param {GridPreferencePanelParams} params With all properties from [[GridPreferencePanelParams]]. diff --git a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx index 8f60c3d159cc2..d2ba0c6e840e6 100644 --- a/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/pagination.DataGrid.test.tsx @@ -322,8 +322,8 @@ describe(' - Pagination', () => { />, ); }).toWarnDev([ - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, ]); }); @@ -350,8 +350,8 @@ describe(' - Pagination', () => { expect(() => { render(); }).toWarnDev([ - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`${pageSize}\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`${pageSize}\` is not present in the \`pageSizeOptions\``, ]); }); @@ -359,8 +359,8 @@ describe(' - Pagination', () => { expect(() => { render(); }).toWarnDev([ - `MUI X: The page size \`100\` is not preset in the \`pageSizeOptions\``, - `MUI X: The page size \`100\` is not preset in the \`pageSizeOptions\``, + `MUI X: The page size \`100\` is not present in the \`pageSizeOptions\``, + `MUI X: The page size \`100\` is not present in the \`pageSizeOptions\``, ]); }); @@ -538,8 +538,8 @@ describe(' - Pagination', () => { expect(document.querySelector('.MuiTablePagination-root')).to.have.text('1–1 of 21'); }); - it('should support server side pagination', () => { - function ServerPaginationGrid() { + describe('server-side pagination', () => { + function ServerPaginationGrid(props: Partial) { const [rows, setRows] = React.useState([]); const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 1 }); @@ -576,20 +576,52 @@ describe(' - Pagination', () => {
); } - render(); - expect(getColumnValues(0)).to.deep.equal(['0']); - fireEvent.click(screen.getByRole('button', { name: /next page/i })); - expect(getColumnValues(0)).to.deep.equal(['1']); + it('should support server side pagination with known row count', () => { + render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + }); + + it('should support server side pagination with unknown row count', () => { + const { setProps } = render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + expect(screen.getByText('1–1 of more than 1')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + expect(screen.getByText('2–2 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + setProps({ rowCount: 3 }); + expect(getColumnValues(0)).to.deep.equal(['2']); + expect(screen.getByText('3–3 of 3')).to.not.equal(null); + }); + + it('should support server side pagination with estimated row count', () => { + const { setProps } = render(); + expect(getColumnValues(0)).to.deep.equal(['0']); + expect(screen.getByText('1–1 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['1']); + expect(screen.getByText('2–2 of more than 2')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + expect(getColumnValues(0)).to.deep.equal(['2']); + expect(screen.getByText('3–3 of more than 3')).to.not.equal(null); + fireEvent.click(screen.getByRole('button', { name: /next page/i })); + setProps({ rowCount: 4 }); + expect(getColumnValues(0)).to.deep.equal(['3']); + expect(screen.getByText('4–4 of 4')).to.not.equal(null); + }); }); it('should make the first cell focusable after changing the page', () => { @@ -712,8 +744,11 @@ describe(' - Pagination', () => { it('should log an error if rowCount is used with client-side pagination', () => { expect(() => { render(); - }).toErrorDev([ - 'MUI X: Usage of the `rowCount` prop with client side pagination (`paginationMode="client"`) has no effect. `rowCount` is only meant to be used with `paginationMode="server"`.', - ]); + }).toErrorDev( + [ + 'MUI X: Usage of the `rowCount` prop with client side pagination (`paginationMode="client"`) has no effect.', + '`rowCount` is only meant to be used with `paginationMode="server"`.', + ].join('\n'), + ); }); }); diff --git a/packages/x-data-grid/src/utils/getGridLocalization.ts b/packages/x-data-grid/src/utils/getGridLocalization.ts index 8bc8e2f22c61b..e28c5c454327b 100644 --- a/packages/x-data-grid/src/utils/getGridLocalization.ts +++ b/packages/x-data-grid/src/utils/getGridLocalization.ts @@ -1,5 +1,8 @@ import { Localization as CoreLocalization } from '@mui/material/locale'; -import { GridLocaleText } from '../models/api/gridLocaleTextApi'; +import type { + GridLocaleText, + MuiTablePaginationLocalizedProps, +} from '../models/api/gridLocaleTextApi'; export interface Localization { components: { @@ -20,7 +23,9 @@ export const getGridLocalization = ( defaultProps: { localeText: { ...gridTranslations, - MuiTablePagination: coreTranslations?.components?.MuiTablePagination?.defaultProps || {}, + MuiTablePagination: + (coreTranslations?.components?.MuiTablePagination + ?.defaultProps as MuiTablePaginationLocalizedProps) || {}, }, }, }, diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index b82e069ad3be2..9723176f87546 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -409,6 +409,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 676755c5d1b36..314428c421b6a 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -373,6 +373,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index 16c223ee5de3e..16a74ef16850e 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -344,6 +344,8 @@ { "name": "GridPagination", "kind": "Variable" }, { "name": "GridPaginationApi", "kind": "Interface" }, { "name": "GridPaginationInitialState", "kind": "Interface" }, + { "name": "GridPaginationMeta", "kind": "Interface" }, + { "name": "gridPaginationMetaSelector", "kind": "Variable" }, { "name": "GridPaginationModel", "kind": "Interface" }, { "name": "gridPaginationModelSelector", "kind": "Variable" }, { "name": "gridPaginationRowCountSelector", "kind": "Variable" },