diff --git a/docs/pages/api-docs/data-grid/grid-api.md b/docs/pages/api-docs/data-grid/grid-api.md index 05e4df29b715..06667a0ba335 100644 --- a/docs/pages/api-docs/data-grid/grid-api.md +++ b/docs/pages/api-docs/data-grid/grid-api.md @@ -10,97 +10,99 @@ import { GridApi } from '@mui/x-data-grid-pro'; ## Properties -| Name | Type | Description | -| :--------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| addRowGroupingCriteria | (groupingCriteriaField: string, groupingIndex?: number) => void | Adds the field to the row grouping model. | -| applySorting | () => void | Applies the current sort model to the rows. | -| commitCellChange | (params: GridCommitCellChangeParams, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the field at the given id with the value stored in the edit row model. | -| commitRowChange | (id: GridRowId, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the row at the given id with the values stored in the edit row model. | -| deleteFilterItem | (item: GridFilterItem) => void | Deletes a [GridFilterItem](/api/data-grid/grid-filter-item/). | -| exportDataAsCsv | (options?: GridCsvExportOptions) => void | Downloads and exports a CSV of the grid's data. | -| exportDataAsPrint | (options?: GridPrintExportOptions) => void | Print the grid's data. | -| forceUpdate | () => void | Forces the grid to rerender. It's often used after a state update. | -| getAllColumns | () => GridStateColDef[] | Returns an array of [GridColDef](/api/data-grid/grid-col-def/) containing all the column definitions. | -| getAllRowIds | () => GridRowId[] | Gets the list of row ids. | -| getCellElement | (id: GridRowId, field: string) => HTMLDivElement \| null | Gets the underlying DOM element for a cell at the given `id` and `field`. | -| getCellMode | (id: GridRowId, field: string) => GridCellMode | Gets the mode of a cell. | -| getCellParams | (id: GridRowId, field: string) => GridCellParams | Gets the [GridCellParams](/api/data-grid/grid-cell-params/) object that is passed as argument in events. | -| getCellValue | (id: GridRowId, field: string) => GridCellValue | Gets the value of a cell at the given `id` and `field`. | -| getColumn | (field: string) => GridStateColDef | Returns the [GridColDef](/api/data-grid/grid-col-def/) for the given `field`. | -| getColumnHeaderElement | (field: string) => HTMLDivElement \| null | Gets the underlying DOM element for the column header with the given `field`. | -| getColumnHeaderParams | (field: string) => GridColumnHeaderParams | Gets the GridColumnHeaderParams object that is passed as argument in events. | -| getColumnIndex | (field: string, useVisibleColumns?: boolean) => number | Returns the index position of a column. By default, only the visible columns are considered.
Pass `false` to `useVisibleColumns` to consider all columns. | -| getColumnPosition | (field: string) => number | Returns the left-position of a column relative to the inner border of the grid. | -| getColumnsMeta | () => GridColumnsMeta | Returns the GridColumnsMeta for each column. | -| getDataAsCsv | (options?: GridCsvExportOptions) => string | Returns the grid data as a CSV string.
This method is used internally by `exportDataAsCsv`. | -| getEditRowsModel | () => GridEditRowsModel | Gets the edit rows model of the grid. | -| getLocaleText | <T extends GridTranslationKeys>(key: T) => GridLocaleText[T] | Returns the translation for the `key`. | -| getPinnedColumns | () => GridPinnedColumns | Returns which columns are pinned. | -| getRootDimensions | () => GridDimensions \| null | Returns the dimensions of the grid | -| getRow | (id: GridRowId) => GridRowModel \| null | Gets the row data with a given id. | -| getRowElement | (id: GridRowId) => HTMLDivElement \| null | Gets the underlying DOM element for a row at the given `id`. | -| getRowIdFromRowIndex | (index: number) => GridRowId | Gets the `GridRowId` of a row at a specific index.
The index is based on the sorted but unfiltered row list. | -| getRowIndex | (id: GridRowId) => number | Gets the row index of a row with a given id.
The index is based on the sorted but unfiltered row list. | -| getRowMode | (id: GridRowId) => GridRowMode | Gets the mode of a row. | -| getRowModels | () => Map<GridRowId, GridRowModel> | Gets the full set of rows as Map<GridRowId, GridRowModel>. | -| getRowNode | (id: GridRowId) => GridRowTreeNodeConfig \| null | Gets the row node from the internal tree structure. | -| getRowParams | (id: GridRowId) => GridRowParams | Gets the [GridRowParams](/api/data-grid/grid-row-params/) object that is passed as argument in events. | -| getRowsCount | () => number | Gets the total number of rows in the grid. | -| getScrollPosition | () => GridScrollParams | Returns the current scroll position. | -| getSelectedRows | () => Map<GridRowId, GridRowModel> | Returns an array of the selected rows. | -| getSortedRowIds | () => GridRowId[] | Returns all row ids sorted according to the active sort model. | -| getSortedRows | () => GridRowModel[] | Returns all rows sorted according to the active sort model. | -| getSortModel | () => GridSortModel | Returns the sort model currently applied to the grid. | -| getVisibleColumns | () => GridStateColDef[] | Returns the currently visible columns. | -| getVisibleRowModels | () => Map<GridRowId, GridRowModel> | Returns a sorted `Map` containing only the visible rows. | -| hideColumnMenu | () => void | Hides the column menu that is open. | -| hideFilterPanel | () => void | Hides the filter panel. | -| hidePreferences | () => void | Hides the preferences panel. | -| isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. | -| isColumnPinned | (field: string) => GridPinnedPosition \| false | Returns which side a column is pinned to. | -| isRowSelected | (id: GridRowId) => boolean | Determines if a row is selected or not. | -| pinColumn | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | -| publishEvent | GridEventPublisher | Emits an event. | -| removeRowGroupingCriteria | (groupingCriteriaField: string) => void | sRemove the field from the row grouping model. | -| resize | () => void | Triggers a resize of the component and recalculation of width and height. | -| scroll | (params: Partial<GridScrollParams>) => void | Triggers the viewport to scroll to the given positions (in pixels). | -| scrollToIndexes | (params: Partial<GridCellIndexCoordinates>) => boolean | Triggers the viewport to scroll to the cell at indexes given by `params`.
Returns `true` if the grid had to scroll to reach the target. | -| selectRow | (id: GridRowId, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of a row. | -| selectRowRange | (range: { startId: GridRowId; endId: GridRowId }, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of all the selectable rows in a range. | -| selectRows | (ids: GridRowId[], isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of multiple rows. | -| setCellFocus | (id: GridRowId, field: string) => void | Sets the focus to the cell at the given `id` and `field`. | -| setCellMode | (id: GridRowId, field: string, mode: GridCellMode) => void | Sets the mode of a cell. | -| setColumnHeaderFocus | (field: string, event?: MuiBaseEvent) => void | Sets the focus to the column header at the given `field`. | -| setColumnIndex | (field: string, targetIndexPosition: number) => void | Moves a column from its original position to the position given by `targetIndexPosition`. | -| setColumnVisibility | (field: string, isVisible: boolean) => void | Changes the visibility of the column referred by `field`. | -| setColumnVisibilityModel | (model: GridColumnVisibilityModel) => void | Sets the column visibility model to the one given by `model`. | -| setColumnWidth | (field: string, width: number) => void | Updates the width of a column. | -| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number) => void | Sets the density of the grid. | -| setEditCellValue | (params: GridEditCellValueParams, event?: MuiBaseEvent) => Promise<boolean> \| void | Sets the value of the edit cell.
Commonly used inside the edit cell component. | -| setEditRowsModel | (model: GridEditRowsModel) => void | Set the edit rows model of the grid. | -| setFilterLinkOperator | (operator: GridLinkOperator) => void | Changes the GridLinkOperator used to connect the filters. | -| setFilterModel | (model: GridFilterModel) => void | Sets the filter model to the one given by `model`. | -| setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | -| setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | -| setPinnedColumns | (pinnedColumns: GridPinnedColumns) => void | Changes the pinned columns. | -| setRowChildrenExpansion | (id: GridRowId, isExpanded: boolean) => void | Expand or collapse a row children. | -| setRowGroupingCriteriaIndex | (groupingCriteriaField: string, groupingIndex: number) => void | Sets the grouping index of a grouping criteria. | -| setRowGroupingModel | (model: GridRowGroupingModel) => void | Sets the columns to use as grouping criteria. | -| setRowMode | (id: GridRowId, mode: GridRowMode) => void | Sets the mode of a row. | -| setRows | (rows: GridRowModel[]) => void | Sets a new set of rows. | -| setSelectionModel | (rowIds: GridRowId[]) => void | Updates the selected rows to be those passed to the `rowIds` argument.
Any row already selected will be unselected. | -| setSortModel | (model: GridSortModel) => void | Updates the sort model and triggers the sorting of rows. | -| setState | (state: GridState \| ((previousState: GridState) => GridState)) => boolean | Sets the whole state of the grid. | -| showColumnMenu | (field: string) => void | Display the column menu under the `field` column. | -| showError | (props: any) => void | Displays the error overlay component. | -| showFilterPanel | (targetColumnField?: string) => void | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. | -| showPreferences | (newValue: GridPreferencePanelsValue) => void | Displays the preferences panel. The `newValue` argument controls the content of the panel. | -| sortColumn | (column: GridColDef, direction?: GridSortDirection, allowMultipleSorting?: boolean) => void | Sorts a column. | -| state | GridState | Property that contains the whole state of the grid. | -| subscribeEvent | <E extends GridEventsStr>(event: E, handler: GridEventListener<E>, options?: EventListenerOptions) => () => void | Registers a handler for an event. | -| toggleColumnMenu | (field: string) => void | Toggles the column menu under the `field` column. | -| unpinColumn | (field: string) => void | Unpins a column. | -| updateColumn | (col: GridColDef) => void | Updates the definition of a column. | -| updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. | -| updateRows | (updates: GridRowModelUpdate[]) => void | Allows to updates, insert and delete rows in a single call. | -| upsertFilterItem | (item: GridFilterItem) => void | Updates or inserts a [GridFilterItem](/api/data-grid/grid-filter-item/). | +| Name | Type | Description | +| :--------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| addRowGroupingCriteria | (groupingCriteriaField: string, groupingIndex?: number) => void | Adds the field to the row grouping model. | +| applySorting | () => void | Applies the current sort model to the rows. | +| commitCellChange | (params: GridCommitCellChangeParams, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the field at the given id with the value stored in the edit row model. | +| commitRowChange | (id: GridRowId, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the row at the given id with the values stored in the edit row model. | +| deleteFilterItem | (item: GridFilterItem) => void | Deletes a [GridFilterItem](/api/data-grid/grid-filter-item/). | +| exportDataAsCsv | (options?: GridCsvExportOptions) => void | Downloads and exports a CSV of the grid's data. | +| exportDataAsPrint | (options?: GridPrintExportOptions) => void | Print the grid's data. | +| exportState | () => GridInitialState | Generates a serializable object containing the exportable parts of the DataGrid state.
These values can then be passed to the `initialState` prop or injected using the `restoreState` method. | +| forceUpdate | () => void | Forces the grid to rerender. It's often used after a state update. | +| getAllColumns | () => GridStateColDef[] | Returns an array of [GridColDef](/api/data-grid/grid-col-def/) containing all the column definitions. | +| getAllRowIds | () => GridRowId[] | Gets the list of row ids. | +| getCellElement | (id: GridRowId, field: string) => HTMLDivElement \| null | Gets the underlying DOM element for a cell at the given `id` and `field`. | +| getCellMode | (id: GridRowId, field: string) => GridCellMode | Gets the mode of a cell. | +| getCellParams | (id: GridRowId, field: string) => GridCellParams | Gets the [GridCellParams](/api/data-grid/grid-cell-params/) object that is passed as argument in events. | +| getCellValue | (id: GridRowId, field: string) => GridCellValue | Gets the value of a cell at the given `id` and `field`. | +| getColumn | (field: string) => GridStateColDef | Returns the [GridColDef](/api/data-grid/grid-col-def/) for the given `field`. | +| getColumnHeaderElement | (field: string) => HTMLDivElement \| null | Gets the underlying DOM element for the column header with the given `field`. | +| getColumnHeaderParams | (field: string) => GridColumnHeaderParams | Gets the GridColumnHeaderParams object that is passed as argument in events. | +| getColumnIndex | (field: string, useVisibleColumns?: boolean) => number | Returns the index position of a column. By default, only the visible columns are considered.
Pass `false` to `useVisibleColumns` to consider all columns. | +| getColumnPosition | (field: string) => number | Returns the left-position of a column relative to the inner border of the grid. | +| getColumnsMeta | () => GridColumnsMeta | Returns the GridColumnsMeta for each column. | +| getDataAsCsv | (options?: GridCsvExportOptions) => string | Returns the grid data as a CSV string.
This method is used internally by `exportDataAsCsv`. | +| getEditRowsModel | () => GridEditRowsModel | Gets the edit rows model of the grid. | +| getLocaleText | <T extends GridTranslationKeys>(key: T) => GridLocaleText[T] | Returns the translation for the `key`. | +| getPinnedColumns | () => GridPinnedColumns | Returns which columns are pinned. | +| getRootDimensions | () => GridDimensions \| null | Returns the dimensions of the grid | +| getRow | (id: GridRowId) => GridRowModel \| null | Gets the row data with a given id. | +| getRowElement | (id: GridRowId) => HTMLDivElement \| null | Gets the underlying DOM element for a row at the given `id`. | +| getRowIdFromRowIndex | (index: number) => GridRowId | Gets the `GridRowId` of a row at a specific index.
The index is based on the sorted but unfiltered row list. | +| getRowIndex | (id: GridRowId) => number | Gets the row index of a row with a given id.
The index is based on the sorted but unfiltered row list. | +| getRowMode | (id: GridRowId) => GridRowMode | Gets the mode of a row. | +| getRowModels | () => Map<GridRowId, GridRowModel> | Gets the full set of rows as Map<GridRowId, GridRowModel>. | +| getRowNode | (id: GridRowId) => GridRowTreeNodeConfig \| null | Gets the row node from the internal tree structure. | +| getRowParams | (id: GridRowId) => GridRowParams | Gets the [GridRowParams](/api/data-grid/grid-row-params/) object that is passed as argument in events. | +| getRowsCount | () => number | Gets the total number of rows in the grid. | +| getScrollPosition | () => GridScrollParams | Returns the current scroll position. | +| getSelectedRows | () => Map<GridRowId, GridRowModel> | Returns an array of the selected rows. | +| getSortedRowIds | () => GridRowId[] | Returns all row ids sorted according to the active sort model. | +| getSortedRows | () => GridRowModel[] | Returns all rows sorted according to the active sort model. | +| getSortModel | () => GridSortModel | Returns the sort model currently applied to the grid. | +| getVisibleColumns | () => GridStateColDef[] | Returns the currently visible columns. | +| getVisibleRowModels | () => Map<GridRowId, GridRowModel> | Returns a sorted `Map` containing only the visible rows. | +| hideColumnMenu | () => void | Hides the column menu that is open. | +| hideFilterPanel | () => void | Hides the filter panel. | +| hidePreferences | () => void | Hides the preferences panel. | +| isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. | +| isColumnPinned | (field: string) => GridPinnedPosition \| false | Returns which side a column is pinned to. | +| isRowSelected | (id: GridRowId) => boolean | Determines if a row is selected or not. | +| pinColumn | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | +| publishEvent | GridEventPublisher | Emits an event. | +| removeRowGroupingCriteria | (groupingCriteriaField: string) => void | sRemove the field from the row grouping model. | +| resize | () => void | Triggers a resize of the component and recalculation of width and height. | +| restoreState | (stateToRestore: GridInitialState) => void | Inject the given values into the state of the DataGrid. | +| scroll | (params: Partial<GridScrollParams>) => void | Triggers the viewport to scroll to the given positions (in pixels). | +| scrollToIndexes | (params: Partial<GridCellIndexCoordinates>) => boolean | Triggers the viewport to scroll to the cell at indexes given by `params`.
Returns `true` if the grid had to scroll to reach the target. | +| selectRow | (id: GridRowId, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of a row. | +| selectRowRange | (range: { startId: GridRowId; endId: GridRowId }, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of all the selectable rows in a range. | +| selectRows | (ids: GridRowId[], isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of multiple rows. | +| setCellFocus | (id: GridRowId, field: string) => void | Sets the focus to the cell at the given `id` and `field`. | +| setCellMode | (id: GridRowId, field: string, mode: GridCellMode) => void | Sets the mode of a cell. | +| setColumnHeaderFocus | (field: string, event?: MuiBaseEvent) => void | Sets the focus to the column header at the given `field`. | +| setColumnIndex | (field: string, targetIndexPosition: number) => void | Moves a column from its original position to the position given by `targetIndexPosition`. | +| setColumnVisibility | (field: string, isVisible: boolean) => void | Changes the visibility of the column referred by `field`. | +| setColumnVisibilityModel | (model: GridColumnVisibilityModel) => void | Sets the column visibility model to the one given by `model`. | +| setColumnWidth | (field: string, width: number) => void | Updates the width of a column. | +| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number) => void | Sets the density of the grid. | +| setEditCellValue | (params: GridEditCellValueParams, event?: MuiBaseEvent) => Promise<boolean> \| void | Sets the value of the edit cell.
Commonly used inside the edit cell component. | +| setEditRowsModel | (model: GridEditRowsModel) => void | Set the edit rows model of the grid. | +| setFilterLinkOperator | (operator: GridLinkOperator) => void | Changes the GridLinkOperator used to connect the filters. | +| setFilterModel | (model: GridFilterModel) => void | Sets the filter model to the one given by `model`. | +| setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | +| setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | +| setPinnedColumns | (pinnedColumns: GridPinnedColumns) => void | Changes the pinned columns. | +| setRowChildrenExpansion | (id: GridRowId, isExpanded: boolean) => void | Expand or collapse a row children. | +| setRowGroupingCriteriaIndex | (groupingCriteriaField: string, groupingIndex: number) => void | Sets the grouping index of a grouping criteria. | +| setRowGroupingModel | (model: GridRowGroupingModel) => void | Sets the columns to use as grouping criteria. | +| setRowMode | (id: GridRowId, mode: GridRowMode) => void | Sets the mode of a row. | +| setRows | (rows: GridRowModel[]) => void | Sets a new set of rows. | +| setSelectionModel | (rowIds: GridRowId[]) => void | Updates the selected rows to be those passed to the `rowIds` argument.
Any row already selected will be unselected. | +| setSortModel | (model: GridSortModel) => void | Updates the sort model and triggers the sorting of rows. | +| setState | (state: GridState \| ((previousState: GridState) => GridState)) => boolean | Sets the whole state of the grid. | +| showColumnMenu | (field: string) => void | Display the column menu under the `field` column. | +| showError | (props: any) => void | Displays the error overlay component. | +| showFilterPanel | (targetColumnField?: string) => void | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. | +| showPreferences | (newValue: GridPreferencePanelsValue) => void | Displays the preferences panel. The `newValue` argument controls the content of the panel. | +| sortColumn | (column: GridColDef, direction?: GridSortDirection, allowMultipleSorting?: boolean) => void | Sorts a column. | +| state | GridState | Property that contains the whole state of the grid. | +| subscribeEvent | <E extends GridEventsStr>(event: E, handler: GridEventListener<E>, options?: EventListenerOptions) => () => void | Registers a handler for an event. | +| toggleColumnMenu | (field: string) => void | Toggles the column menu under the `field` column. | +| unpinColumn | (field: string) => void | Unpins a column. | +| updateColumn | (col: GridColDef) => void | Updates the definition of a column. | +| updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. | +| updateRows | (updates: GridRowModelUpdate[]) => void | Allows to updates, insert and delete rows in a single call. | +| upsertFilterItem | (item: GridFilterItem) => void | Updates or inserts a [GridFilterItem](/api/data-grid/grid-filter-item/). | diff --git a/docs/src/pages/components/data-grid/state/RestoreStateApiRef.js b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.js new file mode 100644 index 000000000000..0841c8558c51 --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.js @@ -0,0 +1,338 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DataGridPro, useGridApiContext, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import DoneIcon from '@mui/icons-material/Done'; +import Divider from '@mui/material/Divider'; +import Fade from '@mui/material/Fade'; +import Popper from '@mui/material/Popper'; + +const demoReducer = (state, action) => { + switch (action.type) { + case 'createView': { + const id = Math.random().toString(); + + return { + ...state, + activeViewId: id, + newViewLabel: '', + views: { + ...state.views, + [id]: { label: state.newViewLabel, value: action.value }, + }, + }; + } + + case 'deleteView': { + const views = Object.fromEntries( + Object.entries(state.views).filter(([id]) => id !== action.id), + ); + + let activeViewId; + if (state.activeViewId !== action.id) { + activeViewId = state.activeViewId; + } else { + const viewIds = Object.keys(state.views); + + if (viewIds.length === 0) { + activeViewId = null; + } else { + activeViewId = viewIds[0]; + } + } + + return { + ...state, + views, + activeViewId, + }; + } + + case 'setActiveView': { + return { + ...state, + activeViewId: action.id, + isMenuOpened: false, + }; + } + + case 'setNewViewLabel': { + return { + ...state, + newViewLabel: action.label, + }; + } + + case 'togglePopper': { + return { + ...state, + isMenuOpened: !state.isMenuOpened, + menuAnchorEl: action.element, + }; + } + + case 'closePopper': { + return { + ...state, + isMenuOpened: false, + }; + } + + default: { + return state; + } + } +}; + +const DEMO_INITIAL_STATE = { + views: {}, + newViewLabel: '', + isMenuOpened: false, + menuAnchorEl: null, + activeViewId: null, +}; + +const ViewListItem = (props) => { + const { view, viewId, selected, onDelete, onSelect } = props; + + return ( + onSelect(viewId)} + secondaryAction={ + { + event.stopPropagation(); + onDelete(viewId); + }} + > + + + } + > + + + + + ); +}; + +ViewListItem.propTypes = { + onDelete: PropTypes.func.isRequired, + onSelect: PropTypes.func.isRequired, + selected: PropTypes.bool.isRequired, + view: PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.shape({ + columns: PropTypes.shape({ + columnVisibilityModel: PropTypes.object, + }), + filter: PropTypes.shape({ + filterModel: PropTypes.object, + }), + pagination: PropTypes.shape({ + page: PropTypes.number, + pageSize: PropTypes.number, + }), + pinnedColumns: PropTypes.shape({ + left: PropTypes.arrayOf(PropTypes.string), + right: PropTypes.arrayOf(PropTypes.string), + }), + preferencePanel: PropTypes.shape({ + open: PropTypes.bool.isRequired, + openedPanelValue: PropTypes.oneOf(['columns', 'filters']), + }), + rowGrouping: PropTypes.shape({ + model: PropTypes.arrayOf(PropTypes.string), + }), + sorting: PropTypes.shape({ + sortModel: PropTypes.arrayOf(PropTypes.object), + }), + }).isRequired, + }).isRequired, + viewId: PropTypes.string.isRequired, +}; + +const NewViewListItem = (props) => { + const { label, onLabelChange, onSubmit, isValid } = props; + const [isAddingView, setIsAddingView] = React.useState(false); + + if (isAddingView) { + return ( + { + onSubmit(); + setIsAddingView(false); + }} + disabled={!isValid} + > + + + } + > + + + ); + } + + return ( + + + + ); +}; + +NewViewListItem.propTypes = { + isValid: PropTypes.bool.isRequired, + label: PropTypes.string.isRequired, + onLabelChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, +}; + +const CustomToolbar = () => { + const apiRef = useGridApiContext(); + const [state, dispatch] = React.useReducer(demoReducer, DEMO_INITIAL_STATE); + + const createNewView = () => { + dispatch({ + type: 'createView', + value: apiRef.current.exportState(), + }); + }; + + const handleNewViewLabelChange = (e) => { + dispatch({ type: 'setNewViewLabel', label: e.target.value }); + }; + + const handleDeleteView = React.useCallback((viewId) => { + dispatch({ type: 'deleteView', id: viewId }); + }, []); + + const handleSetActiveView = (viewId) => { + apiRef.current.restoreState(state.views[viewId].value); + dispatch({ type: 'setActiveView', id: viewId }); + }; + + const handlePopperAnchorClick = (event) => { + dispatch({ type: 'togglePopper', element: event.currentTarget }); + event.stopPropagation(); + }; + + const handleClosePopper = () => { + dispatch({ type: 'closePopper' }); + }; + + const isNewViewLabelValid = React.useMemo(() => { + if (state.newViewLabel.length === 0) { + return false; + } + + return Object.values(state.views).every( + (view) => view.label !== state.newViewLabel, + ); + }, [state.views, state.newViewLabel]); + + const canBeMenuOpened = state.isMenuOpened && Boolean(state.menuAnchorEl); + const popperId = canBeMenuOpened ? 'transition-popper' : undefined; + + return ( + + + + + {({ TransitionProps }) => ( + + + + {Object.entries(state.views).map(([viewId, view]) => ( + + ))} + + + + + + + )} + + + + ); +}; + +export default function RestoreStateApiRef() { + const apiRef = useGridApiRef(); + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 500, + }); + + return ( + + + + ); +} diff --git a/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx new file mode 100644 index 000000000000..2aadb298c734 --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx @@ -0,0 +1,334 @@ +import * as React from 'react'; +import { + DataGridPro, + GridInitialState, + useGridApiContext, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import DoneIcon from '@mui/icons-material/Done'; +import Divider from '@mui/material/Divider'; +import Fade from '@mui/material/Fade'; +import Popper from '@mui/material/Popper'; + +interface StateView { + label: string; + value: GridInitialState; +} + +interface DemoState { + views: { [id: string]: StateView }; + newViewLabel: string; + activeViewId: string | null; + isMenuOpened: boolean; + menuAnchorEl: HTMLElement | null; +} + +type DemoActions = + | { type: 'createView'; value: GridInitialState } + | { type: 'deleteView'; id: string } + | { type: 'setNewViewLabel'; label: string } + | { type: 'setActiveView'; id: string | null } + | { type: 'togglePopper'; element: HTMLElement } + | { type: 'closePopper' }; + +const demoReducer: React.Reducer = (state, action) => { + switch (action.type) { + case 'createView': { + const id = Math.random().toString(); + + return { + ...state, + activeViewId: id, + newViewLabel: '', + views: { + ...state.views, + [id]: { label: state.newViewLabel, value: action.value }, + }, + }; + } + + case 'deleteView': { + const views = Object.fromEntries( + Object.entries(state.views).filter(([id]) => id !== action.id), + ); + + let activeViewId: string | null; + if (state.activeViewId !== action.id) { + activeViewId = state.activeViewId; + } else { + const viewIds = Object.keys(state.views); + + if (viewIds.length === 0) { + activeViewId = null; + } else { + activeViewId = viewIds[0]; + } + } + + return { + ...state, + views, + activeViewId, + }; + } + + case 'setActiveView': { + return { + ...state, + activeViewId: action.id, + isMenuOpened: false, + }; + } + + case 'setNewViewLabel': { + return { + ...state, + newViewLabel: action.label, + }; + } + + case 'togglePopper': { + return { + ...state, + isMenuOpened: !state.isMenuOpened, + menuAnchorEl: action.element, + }; + } + + case 'closePopper': { + return { + ...state, + isMenuOpened: false, + }; + } + + default: { + return state; + } + } +}; + +const DEMO_INITIAL_STATE: DemoState = { + views: {}, + newViewLabel: '', + isMenuOpened: false, + menuAnchorEl: null, + activeViewId: null, +}; + +const ViewListItem = (props: { + view: StateView; + viewId: string; + selected: boolean; + onDelete: (viewId: string) => void; + onSelect: (viewId: string) => void; +}) => { + const { view, viewId, selected, onDelete, onSelect } = props; + + return ( + onSelect(viewId)} + secondaryAction={ + { + event.stopPropagation(); + onDelete(viewId); + }} + > + + + } + > + + + + + ); +}; + +const NewViewListItem = (props: { + label: string; + onLabelChange: ( + e: React.ChangeEvent, + ) => void; + onSubmit: () => void; + isValid: boolean; +}) => { + const { label, onLabelChange, onSubmit, isValid } = props; + const [isAddingView, setIsAddingView] = React.useState(false); + + if (isAddingView) { + return ( + { + onSubmit(); + setIsAddingView(false); + }} + disabled={!isValid} + > + + + } + > + + + ); + } + + return ( + + + + ); +}; + +const CustomToolbar = () => { + const apiRef = useGridApiContext(); + const [state, dispatch] = React.useReducer(demoReducer, DEMO_INITIAL_STATE); + + const createNewView = () => { + dispatch({ + type: 'createView', + value: apiRef.current.exportState(), + }); + }; + + const handleNewViewLabelChange = ( + e: React.ChangeEvent, + ) => { + dispatch({ type: 'setNewViewLabel', label: e.target.value }); + }; + + const handleDeleteView = React.useCallback((viewId: string) => { + dispatch({ type: 'deleteView', id: viewId }); + }, []); + + const handleSetActiveView = (viewId: string) => { + apiRef.current.restoreState(state.views[viewId].value); + dispatch({ type: 'setActiveView', id: viewId }); + }; + + const handlePopperAnchorClick = (event: React.MouseEvent) => { + dispatch({ type: 'togglePopper', element: event.currentTarget as HTMLElement }); + event.stopPropagation(); + }; + + const handleClosePopper = () => { + dispatch({ type: 'closePopper' }); + }; + + const isNewViewLabelValid = React.useMemo(() => { + if (state.newViewLabel.length === 0) { + return false; + } + + return Object.values(state.views).every( + (view) => view.label !== state.newViewLabel, + ); + }, [state.views, state.newViewLabel]); + + const canBeMenuOpened = state.isMenuOpened && Boolean(state.menuAnchorEl); + const popperId = canBeMenuOpened ? 'transition-popper' : undefined; + + return ( + + + + + {({ TransitionProps }) => ( + + + + {Object.entries(state.views).map(([viewId, view]) => ( + + ))} + + + + + + )} + + + + ); +}; + +export default function RestoreStateApiRef() { + const apiRef = useGridApiRef(); + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 500, + }); + + return ( + + + + ); +} diff --git a/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx.preview b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx.preview new file mode 100644 index 000000000000..aa54d80bcd0f --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateApiRef.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/state/RestoreStateInitialState.js b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.js new file mode 100644 index 000000000000..7be28245fad9 --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.js @@ -0,0 +1,95 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { + DataGridPro, + GridToolbarContainer, + GridToolbarDensitySelector, + GridToolbarFilterButton, + useGridApiContext, + useGridRootProps, +} from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import Alert from '@mui/material/Alert'; + +const GridCustomToolbar = ({ unMount }) => { + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + + return ( + + + + + + ); +}; + +GridCustomToolbar.propTypes = { + unMount: PropTypes.func.isRequired, +}; + +const WrappedDataGridPro = (props) => { + const { unMount, ...other } = props; + + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 500, + }); + + if (loading) { + return null; + } + + return ( + + ); +}; + +WrappedDataGridPro.propTypes = { + unMount: PropTypes.func.isRequired, +}; + +export default function RestoreStateInitialState() { + const [savedState, setSavedState] = React.useState(undefined); + + const [isMounted, setIsMounted] = React.useState(true); + + const unMountGrid = React.useCallback((stateToState) => { + setIsMounted(false); + setSavedState(stateToState); + }, []); + + const restoreGrid = () => setIsMounted(true); + + if (isMounted) { + return ( + + + + + {!!savedState && ( + + Initial state: {JSON.stringify(savedState)} + + )} + + ); + } + + return ; +} diff --git a/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx new file mode 100644 index 000000000000..d400bb118627 --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { + DataGridPro, + DataGridProProps, + GridInitialState, + GridToolbarContainer, + GridToolbarDensitySelector, + GridToolbarFilterButton, + useGridApiContext, + useGridRootProps, +} from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import Alert from '@mui/material/Alert'; + +const GridCustomToolbar = ({ + unMount, +}: { + unMount: (stateToSave: GridInitialState) => void; +}) => { + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + + return ( + + + + + + ); +}; + +interface WrappedDataGridProProps + extends Omit< + DataGridProProps, + 'columns' | 'rows' | 'loading' | 'apiRef' | 'components' | 'componentsProps' + > { + unMount: (stateToSave: GridInitialState) => void; +} + +const WrappedDataGridPro = (props: WrappedDataGridProProps) => { + const { unMount, ...other } = props; + + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 500, + }); + + if (loading) { + return null; + } + + return ( + + ); +}; + +export default function RestoreStateInitialState() { + const [savedState, setSavedState] = React.useState( + undefined, + ); + const [isMounted, setIsMounted] = React.useState(true); + + const unMountGrid = React.useCallback((stateToState: GridInitialState) => { + setIsMounted(false); + setSavedState(stateToState); + }, []); + + const restoreGrid = () => setIsMounted(true); + + if (isMounted) { + return ( + + + + + {!!savedState && ( + + Initial state: {JSON.stringify(savedState)} + + )} + + ); + } + + return ; +} diff --git a/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx.preview b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx.preview new file mode 100644 index 000000000000..03bd4701ede7 --- /dev/null +++ b/docs/src/pages/components/data-grid/state/RestoreStateInitialState.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/state/state.md b/docs/src/pages/components/data-grid/state/state.md index fa87ba6602ac..b7a8f444a582 100644 --- a/docs/src/pages/components/data-grid/state/state.md +++ b/docs/src/pages/components/data-grid/state/state.md @@ -9,6 +9,7 @@ title: Data Grid - State ## Initialize the state Some state keys can be initialized with the `initialState` prop. +This prop has the same format as the returned value of `apiRef.current.exportState()`. > ⚠️ The `initialState` can only be used to set the initial value of the state, the grid will not react if you change the `initialState` value later on. > @@ -61,6 +62,37 @@ Some selectors are yet to be documented. > > 👍 Upvote [issue #820](https://github.com/mui-org/material-ui-x/issues/820) if you want to see it land faster. +[//]: # 'The current state of the grid can be exported using `apiRef.current.exportState()`.' +[//]: # 'It can then be restored by either passing it to the `initialState` prop or to the `apiRef.current.restoreState()` method.' +[//]: # +[//]: # 'Watch out for controlled models and their callbacks (`onFilterModelChange` if you use `filterModel` for instance), the grid will call those callbacks when restoring the state.' +[//]: # 'But if the callback is not defined or if calling it does not update the prop value, then the restored value will not be applied.' +[//]: # +[//]: # '### Restore the state with `initialState`' +[//]: # +[//]: # '> ⚠️ If you restore the page using `initialState` before the data is fetched, the grid will automatically move to the 1st page.' +[//]: # +[//]: # '{{"demo": "pages/components/data-grid/state/RestoreStateInitialState.js", "bg": "inline", "defaultCodeOpen": false}}' +[//]: # +[//]: # '### Restore the state with `apiRef` [](https://mui.com/store/items/material-ui-pro/)' +[//]: # +[//]: # '{{"demo": "pages/components/data-grid/state/RestoreStateApiRef.js", "bg": "inline", "defaultCodeOpen": false}}' +[//]: # +[//]: # '#### Restore part of the state' +[//]: # +[//]: # 'It is possible to restore specific properties of the state using the `apiRef.current.restoreState()` method.' +[//]: # 'For instance, to only restore the pinned columns:' +[//]: # +[//]: # '```ts' +[//]: # 'apiRef.current.restoreState({' +[//]: # " pinnedColumns: ['brand']," +[//]: # '});' +[//]: # '```' +[//]: # +[//]: # '> ⚠️ Most of the state keys are not fully independent.' +[//]: # '>' +[//]: # '> Restoring the pagination without restoring the filters or the sorting will work, but the rows displayed after the re-import will not be the same as before the export.' + ## API - [DataGrid](/api/data-grid/data-grid/) diff --git a/packages/grid/_modules_/grid/hooks/core/preProcessing/gridPreProcessingApi.ts b/packages/grid/_modules_/grid/hooks/core/preProcessing/gridPreProcessingApi.ts index 9245d0ceb279..88645c196eab 100644 --- a/packages/grid/_modules_/grid/hooks/core/preProcessing/gridPreProcessingApi.ts +++ b/packages/grid/_modules_/grid/hooks/core/preProcessing/gridPreProcessingApi.ts @@ -1,4 +1,13 @@ -import { GridCellIndexCoordinates, GridColDef, GridScrollParams } from '../../../models'; +import { + GridCellIndexCoordinates, + GridColDef, + GridInitialState, + GridScrollParams, +} from '../../../models'; +import { + GridRestoreStatePreProcessingContext, + GridRestoreStatePreProcessingValue, +} from '../../features/statePersistence'; import { GridFilteringMethodCollection } from '../../features/filter/gridFilterState'; import { GridSortingMethodCollection } from '../../features/sorting/gridSortingState'; import { GridCanBeReorderedPreProcessingContext } from '../../features/columnReorder/columnReorderInterfaces'; @@ -23,6 +32,11 @@ interface GridPreProcessingGroupLookup { }; filteringMethod: { value: GridFilteringMethodCollection }; sortingMethod: { value: GridSortingMethodCollection }; + exportState: { value: GridInitialState }; + restoreState: { + value: GridRestoreStatePreProcessingValue; + context: GridRestoreStatePreProcessingContext; + }; } export type GridPreProcessor

= ( diff --git a/packages/grid/_modules_/grid/hooks/features/columnPinning/useGridColumnPinning.tsx b/packages/grid/_modules_/grid/hooks/features/columnPinning/useGridColumnPinning.tsx index e88737ff1531..6154efbfc7ad 100644 --- a/packages/grid/_modules_/grid/hooks/features/columnPinning/useGridColumnPinning.tsx +++ b/packages/grid/_modules_/grid/hooks/features/columnPinning/useGridColumnPinning.tsx @@ -14,16 +14,25 @@ import { gridClasses } from '../../../gridClasses'; import { useGridRegisterPreProcessor } from '../../core/preProcessing/useGridRegisterPreProcessor'; import { GridColumnPinningMenuItems } from '../../../components/menu/columnMenu/GridColumnPinningMenuItems'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; -import { GridColumnPinningApi, GridPinnedPosition } from '../../../models/api/gridColumnPinningApi'; +import { + GridColumnPinningApi, + GridPinnedColumns, + GridPinnedPosition, +} from '../../../models/api/gridColumnPinningApi'; import { gridPinnedColumnsSelector } from './columnPinningSelector'; import { useGridStateInit } from '../../utils/useGridStateInit'; import { useGridSelector } from '../../utils/useGridSelector'; import { filterColumns } from '../../../../../x-data-grid-pro/src/DataGridProVirtualScroller'; import { GridRowParams } from '../../../models/params/gridRowParams'; import { MuiEvent } from '../../../models/muiEvent'; +import { GridState } from '../../../models'; const Divider = () => event.stopPropagation()} />; +const mergeStateWithPinnedColumns = + (pinnedColumns: GridPinnedColumns) => + (state: GridState): GridState => ({ ...state, pinnedColumns }); + export const useGridColumnPinning = ( apiRef: GridApiRef, props: Pick< @@ -31,13 +40,23 @@ export const useGridColumnPinning = ( 'initialState' | 'disableColumnPinning' | 'pinnedColumns' | 'onPinnedColumnsChange' >, ): void => { - useGridStateInit(apiRef, (state) => ({ - ...state, - pinnedColumns: { - left: !props.disableColumnPinning ? props.initialState?.pinnedColumns?.left : undefined, - right: !props.disableColumnPinning ? props.initialState?.pinnedColumns?.right : undefined, - }, - })); + useGridStateInit(apiRef, (state) => { + let model: GridPinnedColumns; + if (props.disableColumnPinning) { + model = {}; + } else if (props.pinnedColumns) { + model = props.pinnedColumns; + } else if (props.initialState?.pinnedColumns) { + model = props.initialState?.pinnedColumns; + } else { + model = {}; + } + + return { + ...state, + pinnedColumns: model, + }; + }); const pinnedColumns = useGridSelector(apiRef, gridPinnedColumnsSelector); // Each visible row (not to be confused with a filter result) is composed of a central .MuiDataGrid-row element @@ -207,10 +226,42 @@ export const useGridColumnPinning = ( [apiRef, pinnedColumns], ); + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const pinnedColumnsToExport = gridPinnedColumnsSelector(apiRef.current.state); + if ( + (!pinnedColumnsToExport.left || pinnedColumnsToExport.left.length === 0) && + (!pinnedColumnsToExport.right || pinnedColumnsToExport.right.length === 0) + ) { + return prevState; + } + + return { + ...prevState, + pinnedColumns: pinnedColumnsToExport, + }; + }, + [apiRef], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const newPinnedColumns = context.stateToRestore.pinnedColumns; + if (newPinnedColumns != null) { + apiRef.current.setState(mergeStateWithPinnedColumns(newPinnedColumns)); + } + + return params; + }, + [apiRef], + ); + useGridRegisterPreProcessor(apiRef, 'scrollToIndexes', calculateScrollLeft); useGridRegisterPreProcessor(apiRef, 'columnMenu', addColumnMenuButtons); useGridRegisterPreProcessor(apiRef, 'hydrateColumns', reorderPinnedColumns); useGridRegisterPreProcessor(apiRef, 'canBeReordered', checkIfCanBeReordered); + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); apiRef.current.unstable_updateControlState({ stateId: 'pinnedColumns', @@ -278,7 +329,7 @@ export const useGridColumnPinning = ( const setPinnedColumns = React.useCallback( (newPinnedColumns) => { checkIfEnabled('setPinnedColumns'); - apiRef.current.setState((state) => ({ ...state, pinnedColumns: newPinnedColumns })); + apiRef.current.setState(mergeStateWithPinnedColumns(newPinnedColumns)); apiRef.current.forceUpdate(); }, [apiRef, checkIfEnabled], diff --git a/packages/grid/_modules_/grid/hooks/features/columnReorder/columnReorderState.ts b/packages/grid/_modules_/grid/hooks/features/columnReorder/columnReorderState.ts deleted file mode 100644 index b08d7f315276..000000000000 --- a/packages/grid/_modules_/grid/hooks/features/columnReorder/columnReorderState.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GridColumnReorderState { - dragCol: string; -} diff --git a/packages/grid/_modules_/grid/hooks/features/columns/gridColumnsUtils.ts b/packages/grid/_modules_/grid/hooks/features/columns/gridColumnsUtils.ts index aa662640349f..6f085d0450e2 100644 --- a/packages/grid/_modules_/grid/hooks/features/columns/gridColumnsUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/columns/gridColumnsUtils.ts @@ -11,6 +11,7 @@ import { GridColDef, GridColType, GridColumnTypesRecord, + GridState, GridStateColDef, } from '../../../models'; import { gridColumnsSelector, gridColumnVisibilityModelSelector } from './gridColumnsSelector'; @@ -208,3 +209,10 @@ export const createColumnsState = ({ apiRef.current.getRootDimensions?.()?.viewportInnerSize.width ?? 0, ); }; + +export const setColumnsState = + (columnsState: GridColumnsState) => + (state: GridState): GridState => ({ + ...state, + columns: columnsState, + }); diff --git a/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts b/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts index cc7bfe8473bb..215a3a9052b1 100644 --- a/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts +++ b/packages/grid/_modules_/grid/hooks/features/columns/useGridColumns.ts @@ -21,8 +21,14 @@ import { import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { useGridStateInit } from '../../utils/useGridStateInit'; import { GridColumnVisibilityChangeParams } from '../../../models'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; import { GridColumnsState, GridColumnVisibilityModel } from './gridColumnsInterfaces'; -import { hydrateColumnsWidth, computeColumnTypes, createColumnsState } from './gridColumnsUtils'; +import { + hydrateColumnsWidth, + computeColumnTypes, + createColumnsState, + setColumnsState, +} from './gridColumnsUtils'; /** * @requires useGridParamsApi (method) @@ -87,7 +93,7 @@ export function useGridColumns( (columnsState: GridColumnsState) => { logger.debug('Updating columns state.'); - apiRef.current.setState((state) => ({ ...state, columns: columnsState })); + apiRef.current.setState(setColumnsState(columnsState)); apiRef.current.forceUpdate(); apiRef.current.publishEvent(GridEvents.columnsChange, columnsState.all); }, @@ -268,6 +274,59 @@ export function useGridColumns( useGridApiMethod(apiRef, columnApi, 'GridColumnApi'); + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + if (!shouldUseVisibleColumnModel) { + return prevState; + } + + const columnVisibilityModelToExport = gridColumnVisibilityModelSelector(apiRef.current.state); + const hasHiddenColumns = Object.values(columnVisibilityModelToExport).some( + (value) => value === false, + ); + if (!hasHiddenColumns) { + return prevState; + } + + return { + ...prevState, + columns: { + columnVisibilityModel: columnVisibilityModelToExport, + }, + }; + }, + [apiRef, shouldUseVisibleColumnModel], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + if (!shouldUseVisibleColumnModel) { + return params; + } + + const columnVisibilityModel = context.stateToRestore.columns?.columnVisibilityModel; + if (columnVisibilityModel != null) { + const columnsState = createColumnsState({ + apiRef, + columnsTypes, + columnsToUpsert: [], + shouldRegenColumnVisibilityModelFromColumns: false, + currentColumnVisibilityModel: columnVisibilityModel, + reset: false, + }); + apiRef.current.setState(setColumnsState(columnsState)); + } + return params; + }, + [apiRef, shouldUseVisibleColumnModel, columnsTypes], + ); + + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + /** * EVENTS */ diff --git a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts index 53f1a9d5014a..a29c9b4e385d 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts @@ -4,6 +4,7 @@ import { GridFilterModel, GridLinkOperator, GridRowId, + GridState, } from '../../../models'; import { GridAggregatedFilterItemApplier } from './gridFilterState'; @@ -12,6 +13,24 @@ type GridFilterItemApplier = { item: GridFilterItem; }; +export const mergeStateWithFilterModel = ( + filterModel: GridFilterModel, + disableMultipleColumnsFiltering: boolean, +) => { + const cleanFilterModel = { ...filterModel }; + if (cleanFilterModel.items.length > 1 && disableMultipleColumnsFiltering) { + cleanFilterModel.items = [cleanFilterModel.items[0]]; + } + + return (state: GridState): GridState => ({ + ...state, + filter: { + ...state.filter, + filterModel, + }, + }); +}; + /** * Adds default values to the optional fields of a filter items. * @param {GridFilterItem} item The raw filter item. diff --git a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts index de7d1e7ef819..ef5c0a63f58c 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts @@ -21,8 +21,13 @@ import { gridFilterModelSelector, gridVisibleSortedRowEntriesSelector } from './ import { useGridStateInit } from '../../utils/useGridStateInit'; import { useFirstRender } from '../../utils/useFirstRender'; import { gridRowIdsSelector, gridRowGroupingNameSelector } from '../rows'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; import { useGridRegisterFilteringMethod } from './useGridRegisterFilteringMethod'; -import { buildAggregatedFilterApplier, cleanFilterItem } from './gridFilterUtils'; +import { + buildAggregatedFilterApplier, + cleanFilterItem, + mergeStateWithFilterModel, +} from './gridFilterUtils'; const checkFilterModelValidity = (model: GridFilterModel) => { if (model.items.length > 1) { @@ -203,18 +208,10 @@ export const useGridFilter = ( if (currentModel !== model) { checkFilterModelValidity(model); - if (model.items.length > 1 && props.disableMultipleColumnsFiltering) { - model.items = [model.items[0]]; - } - logger.debug('Setting filter model'); - apiRef.current.setState((state) => ({ - ...state, - filter: { - ...state.filter, - filterModel: model, - }, - })); + apiRef.current.setState( + mergeStateWithFilterModel(model, props.disableMultipleColumnsFiltering), + ); apiRef.current.unstable_applyFilters(); } }, @@ -242,6 +239,44 @@ export const useGridFilter = ( /** * PRE-PROCESSING */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const filterModelToExport = gridFilterModelSelector(apiRef.current.state); + if ( + filterModelToExport.items.length === 0 && + filterModelToExport.linkOperator === getDefaultGridFilterModel().linkOperator + ) { + return prevState; + } + + return { + ...prevState, + filter: { + filterModel: filterModelToExport, + }, + }; + }, + [apiRef], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const filterModel = context.stateToRestore.filter?.filterModel; + if (filterModel == null) { + return params; + } + apiRef.current.setState( + mergeStateWithFilterModel(filterModel, props.disableMultipleColumnsFiltering), + ); + + return { + ...params, + callbacks: [...params.callbacks, apiRef.current.unstable_applyFilters], + }; + }, + [apiRef, props.disableMultipleColumnsFiltering], + ); + const flatFilteringMethod = React.useCallback( (params) => { if (props.filterMode === GridFeatureModeConstant.client && params.isRowMatchingFilters) { @@ -268,6 +303,8 @@ export const useGridFilter = ( [apiRef, props.filterMode], ); + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); useGridRegisterFilteringMethod(apiRef, 'none', flatFilteringMethod); /** diff --git a/packages/grid/_modules_/grid/hooks/features/pagination/useGridPage.ts b/packages/grid/_modules_/grid/hooks/features/pagination/useGridPage.ts index aeef97fbe6da..22be7a94570f 100644 --- a/packages/grid/_modules_/grid/hooks/features/pagination/useGridPage.ts +++ b/packages/grid/_modules_/grid/hooks/features/pagination/useGridPage.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GridApiRef } from '../../../models'; +import { GridApiRef, GridState } from '../../../models'; import { useGridLogger, useGridSelector, @@ -12,6 +12,7 @@ import { GridPageApi, GridPaginationState } from './gridPaginationInterfaces'; import { gridVisibleTopLevelRowCountSelector } from '../filter'; import { useGridStateInit } from '../../utils/useGridStateInit'; import { gridPageSelector } from './gridPaginationSelector'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; const getPageCount = (rowCount: number, pageSize: number): number => { if (pageSize > 0 && rowCount > 0) { @@ -32,6 +33,16 @@ const applyValidPage = (paginationState: GridPaginationState): GridPaginationSta }; }; +const mergeStateWithPage = + (page: number) => + (state: GridState): GridState => ({ + ...state, + pagination: applyValidPage({ + ...state.pagination, + page, + }), + }); + /** * @requires useGridPageSize (state, event) * @requires useGridFilter (state) @@ -68,14 +79,7 @@ export const useGridPage = ( const setPage = React.useCallback( (page) => { logger.debug(`Setting page to ${page}`); - - apiRef.current.setState((state) => ({ - ...state, - pagination: applyValidPage({ - ...state.pagination, - page, - }), - })); + apiRef.current.setState(mergeStateWithPage(page)); apiRef.current.forceUpdate(); }, [apiRef, logger], @@ -87,6 +91,41 @@ export const useGridPage = ( useGridApiMethod(apiRef, pageApi, 'GridPageApi'); + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const pageToExport = gridPageSelector(apiRef.current.state); + if (pageToExport === 0) { + return prevState; + } + + return { + ...prevState, + pagination: { + ...prevState.pagination, + page: pageToExport, + }, + }; + }, + [apiRef], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + // We apply the constraint even if the page did not change in case the pageSize changed. + const page = + context.stateToRestore.pagination?.page ?? gridPageSelector(apiRef.current.state); + apiRef.current.setState(mergeStateWithPage(page)); + return params; + }, + [apiRef], + ); + + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + /** * EVENTS */ diff --git a/packages/grid/_modules_/grid/hooks/features/pagination/useGridPageSize.ts b/packages/grid/_modules_/grid/hooks/features/pagination/useGridPageSize.ts index 482eaee4b357..8fade2b2845b 100644 --- a/packages/grid/_modules_/grid/hooks/features/pagination/useGridPageSize.ts +++ b/packages/grid/_modules_/grid/hooks/features/pagination/useGridPageSize.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GridApiRef } from '../../../models'; +import { GridApiRef, GridState } from '../../../models'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridPageSizeApi } from './gridPaginationInterfaces'; import { GridEvents } from '../../../models/events'; @@ -12,6 +12,17 @@ import { import { useGridStateInit } from '../../utils/useGridStateInit'; import { gridPageSizeSelector } from './gridPaginationSelector'; import { gridDensityRowHeightSelector } from '../density'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; + +const mergeStateWithPageSize = + (pageSize: number) => + (state: GridState): GridState => ({ + ...state, + pagination: { + ...state.pagination, + pageSize, + }, + }); /** * @requires useGridDimensions (event) - can be after @@ -27,16 +38,16 @@ export const useGridPageSize = ( const logger = useGridLogger(apiRef, 'useGridPageSize'); const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector); + const defaultPageSize = props.autoPageSize ? 0 : 100; + useGridStateInit(apiRef, (state) => { let pageSize: number; if (props.pageSize != null) { pageSize = props.pageSize; } else if (props.initialState?.pagination?.pageSize != null) { pageSize = props.initialState.pagination.pageSize; - } else if (props.autoPageSize) { - pageSize = 0; } else { - pageSize = 100; + pageSize = defaultPageSize; } return { @@ -66,13 +77,7 @@ export const useGridPageSize = ( logger.debug(`Setting page size to ${pageSize}`); - apiRef.current.setState((state) => ({ - ...state, - pagination: { - ...state.pagination, - pageSize, - }, - })); + apiRef.current.setState(mergeStateWithPageSize(pageSize)); apiRef.current.forceUpdate(); }, [apiRef, logger], @@ -84,6 +89,44 @@ export const useGridPageSize = ( useGridApiMethod(apiRef, pageSizeApi, 'GridPageSizeApi'); + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const pageSizeToExport = gridPageSizeSelector(apiRef.current.state); + if (pageSizeToExport === defaultPageSize) { + return prevState; + } + + return { + ...prevState, + pagination: { + ...prevState.pagination, + pageSize: pageSizeToExport, + }, + }; + }, + [apiRef, defaultPageSize], + ); + + /** + * TODO: Add error if `prop.autoHeight = true` + */ + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const pageSize = context.stateToRestore.pagination?.pageSize; + if (pageSize != null) { + apiRef.current.setState(mergeStateWithPageSize(pageSize)); + } + return params; + }, + [apiRef], + ); + + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + /** * EVENTS */ diff --git a/packages/grid/_modules_/grid/hooks/features/preferencesPanel/useGridPreferencesPanel.ts b/packages/grid/_modules_/grid/hooks/features/preferencesPanel/useGridPreferencesPanel.ts index 779aa211b236..199ac272d933 100644 --- a/packages/grid/_modules_/grid/hooks/features/preferencesPanel/useGridPreferencesPanel.ts +++ b/packages/grid/_modules_/grid/hooks/features/preferencesPanel/useGridPreferencesPanel.ts @@ -5,7 +5,12 @@ import { useGridLogger } from '../../utils/useGridLogger'; import { GridPreferencePanelsValue } from './gridPreferencePanelsValue'; import { useGridStateInit } from '../../utils/useGridStateInit'; import { DataGridProcessedProps } from '../../../models/props/DataGridProps'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; +import { gridPreferencePanelStateSelector } from './gridPreferencePanelSelector'; +/** + * TODO: Add a single `setPreferencePanel` method to avoid multiple `setState` + */ export const useGridPreferencesPanel = ( apiRef: GridApiRef, props: Pick, @@ -19,6 +24,9 @@ export const useGridPreferencesPanel = ( const hideTimeout = React.useRef(); const immediateTimeout = React.useRef(); + /** + * API METHODS + */ const hidePreferences = React.useCallback(() => { logger.debug('Hiding Preferences Panel'); apiRef.current.setState((state) => ({ ...state, preferencePanel: { open: false } })); @@ -59,6 +67,45 @@ export const useGridPreferencesPanel = ( 'ColumnMenuApi', ); + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const preferencePanelToExport = gridPreferencePanelStateSelector(apiRef.current.state); + if (!preferencePanelToExport.open && !preferencePanelToExport.openedPanelValue) { + return prevState; + } + + return { + ...prevState, + preferencePanel: preferencePanelToExport, + }; + }, + [apiRef], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const preferencePanel = context.stateToRestore.preferencePanel; + if (preferencePanel != null) { + apiRef.current.setState((state) => ({ + ...state, + preferencePanel, + })); + } + + return params; + }, + [apiRef], + ); + + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + + /** + * EFFECTS + */ React.useEffect(() => { return () => { clearTimeout(hideTimeout.current); diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts index 9ac467ea6926..2951fea33f3d 100644 --- a/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts @@ -3,10 +3,12 @@ import { GridRowId, GridRowTreeConfig, GridRowTreeNodeConfig, + GridState, } from '../../../models'; import { GridFilterState } from '../filter'; import { DataGridProProcessedProps } from '../../../models/props/DataGridProProps'; import { GridAggregatedFilterItemApplier } from '../filter/gridFilterState'; +import { GridRowGroupingModel } from './gridRowGroupingInterfaces'; export const GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD = '__row_group_by_columns_group__'; @@ -146,3 +148,10 @@ export const getColDefOverrides = ( return groupingColDefProp; }; + +export const mergeStateWithRowGroupingModel = + (rowGroupingModel: GridRowGroupingModel) => + (state: GridState): GridState => ({ + ...state, + rowGrouping: { ...state.rowGrouping, model: rowGroupingModel }, + }); diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx b/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx index 7296fb4005cd..69b8c8efbabd 100644 --- a/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx @@ -25,13 +25,14 @@ import { getColDefOverrides, GROUPING_COLUMNS_FEATURE_NAME, isGroupingColumn, + mergeStateWithRowGroupingModel, } from './gridRowGroupingUtils'; import { createGroupingColDefForOneGroupingCriteria, createGroupingColDefForAllGroupingCriteria, } from './createGroupingColDef'; import { isDeepEqual } from '../../../utils/utils'; -import { useGridRegisterPreProcessor } from '../../core/preProcessing'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; import { GridColumnRawLookup, GridColumnsRawState } from '../columns/gridColumnsInterfaces'; import { useGridRegisterFilteringMethod } from '../filter/useGridRegisterFilteringMethod'; import { GridFilteringMethod } from '../filter/gridFilterState'; @@ -363,10 +364,7 @@ export const useGridRowGrouping = ( (model) => { const currentModel = gridRowGroupingModelSelector(apiRef.current.state); if (currentModel !== model) { - apiRef.current.setState((state) => ({ - ...state, - rowGrouping: { ...state.rowGrouping, model }, - })); + apiRef.current.setState(mergeStateWithRowGroupingModel(model)); updateRowGrouping(); apiRef.current.forceUpdate(); } @@ -437,6 +435,48 @@ export const useGridRowGrouping = ( 'GridRowGroupingApi', ); + /** + * PRE-PROCESSING + */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + if (props.disableRowGrouping) { + return prevState; + } + + const rowGroupingModelToExport = gridRowGroupingModelSelector(apiRef.current.state); + if (rowGroupingModelToExport.length === 0) { + return prevState; + } + + return { + ...prevState, + rowGrouping: { + model: rowGroupingModelToExport, + }, + }; + }, + [apiRef, props.disableRowGrouping], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + if (props.disableRowGrouping) { + return params; + } + + const rowGroupingModel = context.stateToRestore.rowGrouping?.model; + if (rowGroupingModel != null) { + apiRef.current.setState(mergeStateWithRowGroupingModel(rowGroupingModel)); + } + return params; + }, + [apiRef, props.disableRowGrouping], + ); + + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); + /** * EVENTS */ diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts index d04a9d4e5b18..767f7eb99456 100644 --- a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts @@ -9,6 +9,7 @@ import type { GridSortDirection, GridSortItem, GridSortModel, + GridState, } from '../../../models'; type GridSortingFieldComparator = { @@ -21,6 +22,16 @@ interface GridParsedSortItem { getSortCellParams: (id: GridRowId) => GridSortCellParams; } +export const mergeStateWithSortModel = + (sortModel: GridSortModel) => + (state: GridState): GridState => ({ + ...state, + sorting: { + ...state.sorting, + sortModel, + }, + }); + const isDesc = (direction: GridSortDirection) => direction === 'desc'; /** diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts index 82d01433d05b..b5b9e4e7ddf5 100644 --- a/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts +++ b/packages/grid/_modules_/grid/hooks/features/sorting/useGridSorting.ts @@ -20,7 +20,12 @@ import { gridRowIdsSelector, gridRowGroupingNameSelector, gridRowTreeSelector } import { useGridStateInit } from '../../utils/useGridStateInit'; import { useFirstRender } from '../../utils/useFirstRender'; import { GridSortingMethod, GridSortingMethodCollection } from './gridSortingState'; -import { buildAggregatedSortingApplier, getNextGridSortDirection } from './gridSortingUtils'; +import { + buildAggregatedSortingApplier, + mergeStateWithSortModel, + getNextGridSortDirection, +} from './gridSortingUtils'; +import { GridPreProcessor, useGridRegisterPreProcessor } from '../../core/preProcessing'; import { useGridRegisterSortingMethod } from './useGridRegisterSortingMethod'; /** @@ -142,10 +147,7 @@ export const useGridSorting = ( const currentModel = gridSortModelSelector(apiRef.current.state); if (currentModel !== model) { logger.debug(`Setting sort model`); - apiRef.current.setState((state) => ({ - ...state, - sorting: { ...state.sorting, sortModel: model }, - })); + apiRef.current.setState(mergeStateWithSortModel(model)); apiRef.current.forceUpdate(); apiRef.current.applySorting(); } @@ -210,6 +212,39 @@ export const useGridSorting = ( /** * PRE-PROCESSING */ + const stateExportPreProcessing = React.useCallback>( + (prevState) => { + const sortModelToExport = gridSortModelSelector(apiRef.current.state); + if (sortModelToExport.length === 0) { + return prevState; + } + + return { + ...prevState, + sorting: { + sortModel: sortModelToExport, + }, + }; + }, + [apiRef], + ); + + const stateRestorePreProcessing = React.useCallback>( + (params, context) => { + const sortModel = context.stateToRestore.sorting?.sortModel; + if (sortModel == null) { + return params; + } + apiRef.current.setState(mergeStateWithSortModel(sortModel)); + + return { + ...params, + callbacks: [...params.callbacks, apiRef.current.applySorting], + }; + }, + [apiRef], + ); + const flatSortingMethod = React.useCallback( (params) => { if (!params.sortRowList) { @@ -222,6 +257,8 @@ export const useGridSorting = ( [apiRef], ); + useGridRegisterPreProcessor(apiRef, 'exportState', stateExportPreProcessing); + useGridRegisterPreProcessor(apiRef, 'restoreState', stateRestorePreProcessing); useGridRegisterSortingMethod(apiRef, 'none', flatSortingMethod); /** diff --git a/packages/grid/_modules_/grid/hooks/features/statePersistence/GridStatePersistenceApi.ts b/packages/grid/_modules_/grid/hooks/features/statePersistence/GridStatePersistenceApi.ts new file mode 100644 index 000000000000..0edcfdda98ca --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/statePersistence/GridStatePersistenceApi.ts @@ -0,0 +1,27 @@ +import { GridInitialState } from '../../../models'; + +export interface GridStatePersistenceApi { + /** + * Generates a serializable object containing the exportable parts of the DataGrid state. + * These values can then be passed to the `initialState` prop or injected using the `restoreState` method. + * @returns {GridInitialState} The exported state. + */ + exportState: () => GridInitialState; + /** + * Inject the given values into the state of the DataGrid. + * @param {GridInitialState} stateToRestore The exported state to restore. + */ + restoreState: (stateToRestore: GridInitialState) => void; +} + +export interface GridRestoreStatePreProcessingValue { + /** + * Functions to run after the state has been updated but before re-rendering. + * This is usually used to apply derived states like `applyFilters` or `applySorting` + */ + callbacks: (() => void)[]; +} + +export interface GridRestoreStatePreProcessingContext { + stateToRestore: GridInitialState; +} diff --git a/packages/grid/_modules_/grid/hooks/features/statePersistence/index.ts b/packages/grid/_modules_/grid/hooks/features/statePersistence/index.ts new file mode 100644 index 000000000000..799d23a98feb --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/statePersistence/index.ts @@ -0,0 +1 @@ +export * from './GridStatePersistenceApi'; diff --git a/packages/grid/_modules_/grid/hooks/features/statePersistence/useGridStatePersistence.ts b/packages/grid/_modules_/grid/hooks/features/statePersistence/useGridStatePersistence.ts new file mode 100644 index 000000000000..a2b1a6ac5806 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/statePersistence/useGridStatePersistence.ts @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { GridApiRef, GridInitialState } from '../../../models'; +import { GridStatePersistenceApi } from './GridStatePersistenceApi'; +import { useGridApiMethod } from '../../utils'; + +export const useGridStatePersistence = (apiRef: GridApiRef) => { + const exportState = React.useCallback(() => { + const stateToExport = apiRef.current.unstable_applyPreProcessors('exportState', {}); + + return stateToExport as GridInitialState; + }, [apiRef]); + + const restoreState = React.useCallback( + (stateToRestore) => { + const response = apiRef.current.unstable_applyPreProcessors( + 'restoreState', + { + callbacks: [], + }, + { + stateToRestore, + }, + ); + + response.callbacks.forEach((callback) => { + callback(); + }); + + apiRef.current.forceUpdate(); + }, + [apiRef], + ); + + const statePersistenceApi: GridStatePersistenceApi = { + exportState, + restoreState, + }; + + useGridApiMethod(apiRef, statePersistenceApi, 'GridStatePersistenceApi'); +}; diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts index d6901d82c0b9..9f6866b617fb 100644 --- a/packages/grid/_modules_/grid/models/api/gridApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridApi.ts @@ -26,6 +26,7 @@ import type { GridRowGroupsPreProcessingApi } from '../../hooks/core/rowGroupsPe import type { GridDimensionsApi } from '../../hooks/features/dimensions'; import type { GridRowGroupingApi } from '../../hooks/features/rowGrouping'; import type { GridPaginationApi } from '../../hooks/features/pagination'; +import type { GridStatePersistenceApi } from '../../hooks/features/statePersistence'; /** * The full grid API. @@ -58,4 +59,5 @@ export interface GridApi GridScrollApi, GridRowGroupingApi, GridVirtualScrollerApi, - GridColumnPinningApi {} + GridColumnPinningApi, + GridStatePersistenceApi {} diff --git a/packages/grid/x-data-grid-generator/src/useMovieData.ts b/packages/grid/x-data-grid-generator/src/useMovieData.ts index 9dbcf0ae44b9..068bcc8bfa34 100644 --- a/packages/grid/x-data-grid-generator/src/useMovieData.ts +++ b/packages/grid/x-data-grid-generator/src/useMovieData.ts @@ -285,7 +285,7 @@ const ROWS: GridRowModel[] = [ { id: 20, title: 'Minions', - gross: 11159398397, + gross: 1159398397, director: 'Pierre Coffin & Kyle Balda', company: 'Universal Pictures', year: 2015, diff --git a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx index ff7f606d1e51..b0491af25f5a 100644 --- a/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/columnPinning.DataGridPro.test.tsx @@ -213,7 +213,7 @@ describe(' - Column pinning', () => { }); describe('props: onPinnedColumnsChange', () => { - it('shoull call when a column is pinned', () => { + it('should call when a column is pinned', () => { const handlePinnedColumnsChange = spy(); render(); apiRef.current.pinColumn('currencyPair', GridPinnedPosition.left); @@ -228,7 +228,7 @@ describe(' - Column pinning', () => { }); }); - it('shoull not change the pinned columns when it is called', () => { + it('should not change the pinned columns when it is called', () => { const handlePinnedColumnsChange = spy(); render( - Column pinning', () => { }); describe('props: pinnedColumns', () => { - it('shoull pin the columns specified', () => { + it('should pin the columns specified', () => { render(); const leftColumns = document.querySelector( `.${gridClasses['pinnedColumns--left']}`, @@ -274,7 +274,7 @@ describe(' - Column pinning', () => { ).not.to.equal(null); }); - it('shoull filter our duplicated columns', () => { + it('should filter our duplicated columns', () => { render(); const leftColumns = document.querySelector( `.${gridClasses['pinnedColumns--left']}`, diff --git a/packages/grid/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx new file mode 100644 index 000000000000..3690e5afe211 --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/tests/statePersistence.DataGridPro.test.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import { + DataGridPro, + DataGridProProps, + GridApiRef, + GridInitialState, + GridPreferencePanelsValue, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { createRenderer, screen } from '@mui/monorepo/test/utils'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import { expect } from 'chai'; +import { getColumnHeadersTextContent, getColumnValues } from '../../../../../test/utils/helperFn'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +describe(' - State Persistence', () => { + const { render, clock } = createRenderer({ clock: 'fake' }); + + let apiRef: GridApiRef; + + const TestCase = (props: Omit) => { + const data = useMovieData(); + + apiRef = useGridApiRef(); + + return ( +

+ +
+ ); + }; + + describe('apiRef: exportState', () => { + const FULL_INITIAL_STATE: GridInitialState = { + columns: { + columnVisibilityModel: { year: false }, + }, + filter: { + filterModel: { + items: [{ columnField: 'gross', operatorValue: '>', value: '1500000000' }], + }, + }, + pagination: { + page: 1, + pageSize: 2, + }, + pinnedColumns: { + left: ['company'], + }, + preferencePanel: { + open: true, + openedPanelValue: GridPreferencePanelsValue.filters, + }, + sorting: { + sortModel: [{ field: 'director', sort: 'asc' }], + }, + rowGrouping: { + model: ['director'], + }, + }; + + it('should not return the default values of the models', () => { + render(); + expect(apiRef.current.exportState()).to.deep.equal({}); + }); + + it('should export the initial values of the models', () => { + render(); + expect(apiRef.current.exportState()).to.deep.equal(FULL_INITIAL_STATE); + }); + + it('should export the current version of the exportable state', () => { + render(); + apiRef.current.setPageSize(2); + apiRef.current.setPage(1); + apiRef.current.setPinnedColumns({ left: ['company'] }); + apiRef.current.showPreferences(GridPreferencePanelsValue.filters); + apiRef.current.setSortModel([{ field: 'director', sort: 'asc' }]); + apiRef.current.setFilterModel({ + items: [{ columnField: 'gross', operatorValue: '>', value: '1500000000' }], + }); + apiRef.current.setRowGroupingModel(['director']); + apiRef.current.setColumnVisibilityModel({ year: false }); + + expect(apiRef.current.exportState()).to.deep.equal(FULL_INITIAL_STATE); + }); + }); + + describe('apiRef: restoreState', () => { + it('should restore the whole exportable state', () => { + render(); + + apiRef.current.restoreState({ + columns: { + columnVisibilityModel: { year: false }, + }, + filter: { + filterModel: { + items: [{ columnField: 'gross', operatorValue: '>', value: '1500000000' }], + }, + }, + pagination: { + page: 1, + pageSize: 2, + }, + pinnedColumns: { + left: ['company'], + }, + preferencePanel: { + open: true, + openedPanelValue: GridPreferencePanelsValue.filters, + }, + sorting: { + sortModel: [{ field: 'director', sort: 'asc' }], + }, + rowGrouping: { + model: ['director'], + }, + }); + + // Pagination + expect(getColumnValues(0)).to.deep.equal(['', 'Disney Studios', '', 'Universal Pictures']); + + // Sorting and row grouping + expect(getColumnValues(1)).to.deep.equal(['J. J. Abrams (1)', '', 'Colin Trevorrow (1)', '']); + + // Filtering + expect(getColumnValues(3)).to.deep.equal(['', '2,068,223,624$', '', '1,671,713,208$']); + + // Preference panel + expect(screen.getByRole('button', { name: /Add Filter/i })).to.not.equal(null); + + // Columns visibility + expect(getColumnHeadersTextContent()).to.not.include('Year'); + + // Pinning + expect( + document.querySelector('.MuiDataGrid-pinnedColumnHeaders--left')?.textContent, + ).to.deep.equal('Company'); + }); + + it('should restore partial exportable state', () => { + render(); + + apiRef.current.restoreState({ + pagination: { + page: 1, + pageSize: 2, + }, + }); + + expect(getColumnValues(0)).to.deep.equal(['Titanic', 'Star Wars: The Force Awakens']); + }); + + it('should restore controlled sub-state', () => { + const ControlledTest = () => { + const [page, setPage] = React.useState(0); + + return ( + { + setPage(newPage); + }} + /> + ); + }; + + render(); + apiRef.current.restoreState({ + pagination: { + page: 1, + pageSize: 2, + }, + }); + clock.runToLast(); + expect(getColumnValues(0)).to.deep.equal(['Titanic', 'Star Wars: The Force Awakens']); + }); + }); +}); diff --git a/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx b/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx index dbe89d47e9b2..f28166cdb981 100644 --- a/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx @@ -31,6 +31,7 @@ import { useGridDimensions } from '../../_modules_/grid/hooks/features/dimension import { useGridTreeData } from '../../_modules_/grid/hooks/features/treeData/useGridTreeData'; import { useGridRowGrouping } from '../../_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping'; import { useGridColumnPinning } from '../../_modules_/grid/hooks/features/columnPinning/useGridColumnPinning'; +import { useGridStatePersistence } from '../../_modules_/grid/hooks/features/statePersistence/useGridStatePersistence'; export const useDataGridProComponent = ( inputApiRef: GridApiRef | undefined, @@ -65,6 +66,7 @@ export const useDataGridProComponent = ( useGridClipboard(apiRef); useGridDimensions(apiRef, props); useGridEvents(apiRef, props); + useGridStatePersistence(apiRef); return apiRef; }; diff --git a/packages/grid/x-data-grid/src/useDataGridComponent.tsx b/packages/grid/x-data-grid/src/useDataGridComponent.tsx index 570b9f807cdc..e678fd6172fb 100644 --- a/packages/grid/x-data-grid/src/useDataGridComponent.tsx +++ b/packages/grid/x-data-grid/src/useDataGridComponent.tsx @@ -24,6 +24,7 @@ import { useGridScroll } from '../../_modules_/grid/hooks/features/scroll/useGri import { useGridEvents } from '../../_modules_/grid/hooks/features/events/useGridEvents'; import { useGridDimensions } from '../../_modules_/grid/hooks/features/dimensions/useGridDimensions'; import { useGridRowsMeta } from '../../_modules_/grid/hooks/features/rows/useGridRowsMeta'; +import { useGridStatePersistence } from '../../_modules_/grid/hooks/features/statePersistence/useGridStatePersistence'; export const useDataGridComponent = (props: DataGridProcessedProps) => { const apiRef = useGridInitialization(undefined, props); @@ -49,6 +50,7 @@ export const useDataGridComponent = (props: DataGridProcessedProps) => { useGridClipboard(apiRef); useGridDimensions(apiRef, props); useGridEvents(apiRef, props); + useGridStatePersistence(apiRef); return apiRef; };