diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index 64c820e1c397..935d9560a69a 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 117d8792a96d..28197aed4c87 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 cf369037fa5f..04caad836070 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 5aff7d9779f2..d54e3e806c7d 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 32c24513f995..000000000000 --- 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 0bcb826f4034..6b142879e4f9 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 000000000000..6191eb8232a8 --- /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 000000000000..82b7e4ec6ffe --- /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 000000000000..2de106edc5d3 --- /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 000000000000..bb35ce14cebd --- /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 000000000000..5c6f399b3aa0 --- /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 a3af0fb6ac26..d23efc50aa1f 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 56215024f027..aed156c95bc5 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 }" } }, @@ -405,6 +406,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -526,6 +534,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 2de559b31451..6294f0ec4cdc 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 }" } }, @@ -355,6 +356,13 @@ "describedArgs": ["params", "event", "details"] } }, + "onPaginationMetaChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(paginationMeta: GridPaginationMeta) => void", + "describedArgs": ["paginationMeta"] + } + }, "onPaginationModelChange": { "type": { "name": "func" }, "signature": { @@ -469,6 +477,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 4ca488287bf5..b48c3937323c 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 4acb3f3986bb..a4fc87914cd1 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 b782ce762515..47f7684b511e 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 1140b1bf1d94..e10bb19d717b 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 e4650258c745..fbe690842af4 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." }, @@ -437,6 +440,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": { @@ -559,6 +566,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." }, @@ -578,7 +588,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 44babc11d794..113220858b36 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." }, @@ -390,6 +393,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": { @@ -505,6 +512,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." }, @@ -524,7 +534,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 957595b1a9a8..c4924176b374 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 ba27c6f79b3a..02e89d3757ea 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 c000f6cfab76..55adae6cb6a7 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 1f3caece0f9a..1b67680f5fec 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. @@ -716,6 +722,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. @@ -850,6 +861,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. @@ -896,6 +914,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 1b9dce1dbcf4..49ecc70449ee 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. @@ -624,6 +630,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. @@ -752,6 +763,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. @@ -798,6 +816,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 091f51a8697c..7ad8ff9e7d65 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, }, @@ -125,6 +126,7 @@ describe(' - State persistence', () => { filterModel: getDefaultGridFilterModel(), }, pagination: { + meta: {}, paginationModel: { page: 0, pageSize: 100 }, rowCount: 6, }, @@ -190,6 +192,7 @@ describe(' - State persistence', () => { }} paginationMode="server" rowCount={FULL_INITIAL_STATE.pagination?.rowCount} + paginationMeta={FULL_INITIAL_STATE.pagination?.meta} pinnedColumns={FULL_INITIAL_STATE.pinnedColumns} // Some portable states don't have a controllable model initialState={{ diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 8742dd514ba6..56a9868fa182 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 9fe8e5cf2f93..8a5c5fd0e763 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 6313a66231b5..2657dac0cd89 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 310c659870fa..a47c14ea4454 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 bc0b8ef20d2e..786ba8881b45 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 c68819a261ac..61c43af8df96 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 9abbeab5301c..50524ac4e656 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 000000000000..c87aa8ea6322 --- /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 579eacf8cfd8..c482999e44ac 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 0e8e76533dbc..39f950032ade 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 a5337ea3c74c..859167dcfb24 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 427a2db0692e..ee4d70e09949 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 05cf1bd6bda7..1e59d123c8a5 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 b25858988c0a..97735a52eb0f 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 16cae502d7ec..38a051fc1f05 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 5f29dceffb45..0ffc296f66fd 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 543323ac86b1..0e9395f39a9f 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 47619a4c8db1..170ee2e261e1 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 { @@ -411,8 +411,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 8f60c3d159cc..d2ba0c6e840e 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 8bc8e2f22c61..e28c5c454327 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 b82e069ad3be..9723176f8754 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 676755c5d1b3..314428c421b6 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 16c223ee5de3..16a74ef16850 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" },