import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MouseEvent } from 'react'; import clsx from 'clsx'; import { DataGridPro as MiuDataGrid, LicenseInfo, useGridApiRef } from '@mui/x-data-grid-pro'; import type { GridColumnHeaderParams, GridEventsStr } from '@mui/x-data-grid-pro'; import MuiGrid from '@mui/material/Grid'; import { FilterArea } from '../filter-area'; import { Filters } from '../filters'; import { Pagination } from '../@navigation/pagination'; import { throttleEvent, useWindowEventListener } from '../../utils'; import { getGridColumnTypes, gridCheckboxSelectionColDef, gridDetailPanelToggleColDef, isHoverableColumn, useBaseFilterItem, useGridColDef } from './column-types'; import { ArrowIcon, BackToTop, CustomGridTreeDataGroupingCell, DeferredGridCell, LazyLoadingGridCell, LazyLoadingGridRow, NoRowsOverlay, SkeletonizedGridCell, SkeletonizedGridHeader, Toolbar } from './components'; import { getDetailPanelContent } from './detail-panel/utils'; import createClasses from './styles'; import { GridFeatureModes, GridSortDirections } from './types'; import type { DataGridProps, FieldKeys, GridColDef } from './types'; import { convertChildsToTreeData, createColumnHeaderRenderer, skeletonizedTableData, useStickyColumnHeader, useStickyHorizontalScrollerHandler, useStickyMasterDetailPanel, GRID_COLUMN_HEADER_HEIGHT, GRID_LICENSE_KEY, GRID_ROW_HEIGHT } from './utils'; LicenseInfo.setLicenseKey(GRID_LICENSE_KEY); const DataGrid = forwardRef((props: DataGridProps, ref) => { const { tableConfig, tableData, detailPanelField, detailPanelProps, treeDataField, filters: _filters, filterAreaProps, filtersProps, handleFilterChange, toolbarProps, numOfRowsOptions = [], paginationProps, backToTopProps, disableColumnHover, disableStickyHeader, disableStickyPagination, stickyHeaderPositionTop, selected, classes, getRowId, pushFunc, ...otherProps } = props; const apiRef = useGridApiRef(); const styles = createClasses({ stickyHeaderPositionTop }); // COMPONENTS const ColumnSortedAscendingIcon = useCallback( () => , [styles.arrowIcon] ); const ColumnSortedDescendingIcon = useCallback( () => , [styles.arrowIcon] ); // Lazy & deferred loading only works properly with autoHeight enabled const OptimizedGridCell = otherProps.lazyLoading ? LazyLoadingGridCell : DeferredGridCell; const GridCell = otherProps.autoHeight ? OptimizedGridCell : undefined; const GridRow = otherProps.autoHeight && otherProps.lazyLoading ? LazyLoadingGridRow : undefined; // FILTERS const [filters, setFilters] = useState({ ..._filters }); const { createFilterItemsFromValues } = useBaseFilterItem(); useEffect(() => { if (otherProps.filterMode !== GridFeatureModes.SERVER) { // reset filters at loading if (otherProps.loading) { return apiRef.current.setFilterModel({ items: [] }); } // sync filters values with DataGrid's filterModel const filtersValues = (filterAreaProps?.values || filtersProps?.values || {}) as DataGridProps['filters']; if (filtersValues) { const gridFilterItems = createFilterItemsFromValues(filtersValues); apiRef.current.setFilterModel({ items: gridFilterItems }); setFilters(filtersValues); } } }, [ apiRef, filterAreaProps?.values, filtersProps?.values, otherProps.filterMode, otherProps.loading, createFilterItemsFromValues ]); // PAGINATION const [paginationCount, setPaginationCount] = useState(paginationProps?.count || 0); const [paginationHidden, setPaginationHidden] = useState( Boolean(paginationProps?.hideNavigation && paginationProps?.hideRowCount) ); const onStateChange = useCallback(() => { if (!paginationProps) return; if (otherProps.paginationMode !== GridFeatureModes.SERVER) { const paginationState = apiRef.current?.state?.pagination; if (paginationState) { // override pagination props in 'client' mode setPaginationCount(paginationState.rowCount); setPaginationHidden(!paginationState.rowCount); } } else { setPaginationCount(otherProps.rowCount || paginationProps.count); setPaginationHidden(Boolean(paginationProps.hideNavigation && paginationProps.hideRowCount)); } }, [apiRef, otherProps.paginationMode, otherProps.rowCount, paginationProps]); useEffect(() => onStateChange, [onStateChange]); // STICKY HEADER const toggleStickyHeader = useCallback( (isSticky: boolean) => { const headersContainerEl = apiRef?.current?.columnHeadersContainerElementRef?.current; if (isSticky) { headersContainerEl?.classList.add(styles.stickyHeader); } else { headersContainerEl?.classList.remove(styles.stickyHeader); } }, [apiRef, styles.stickyHeader] ); useStickyColumnHeader(apiRef, toggleStickyHeader, { autoHeight: otherProps.autoHeight, disableStickyHeader }); // COLUMN HOVER const [columnNameHover, setColumnNameHover] = useState(''); const handleColumnMouseEvents = useCallback( (field: FieldKeys, hovered?: boolean) => () => { // Prevent infinite events on sort icon hover if (disableColumnHover || (hovered && field === columnNameHover)) { return; } setColumnNameHover(field === columnNameHover ? '' : field); }, [columnNameHover, disableColumnHover] ); // cancel hover when dragging column heading const cancelColumHoverOnEvent = useCallback( (eventName: GridEventsStr) => !disableColumnHover ? apiRef.current.subscribeEvent(eventName, () => setColumnNameHover('')) : undefined, [apiRef, disableColumnHover] ); useEffect(() => cancelColumHoverOnEvent('columnHeaderDragEnter'), [cancelColumHoverOnEvent]); useEffect(() => cancelColumHoverOnEvent('columnHeaderDragEnd'), [cancelColumHoverOnEvent]); // STICKY HORIZONTAL SCROLLER const stickyPaginationRef = useRef(null); const stickyHorizontalScrollerHandler = useStickyHorizontalScrollerHandler( apiRef, !disableStickyPagination ? stickyPaginationRef : undefined, otherProps.headerHeight || GRID_COLUMN_HEADER_HEIGHT, otherProps.autoHeight ); const handleWindowEventThrottled = throttleEvent(event => { if (otherProps.autoHeight) stickyHorizontalScrollerHandler(event); }); useWindowEventListener(['load', 'resize', 'scroll'], handleWindowEventThrottled, true); useEffect(() => { if (!otherProps.autoHeight) return stickyHorizontalScrollerHandler(); handleWindowEventThrottled(); return () => { throttleEvent(() => stickyHorizontalScrollerHandler(undefined, true))(); }; }, [handleWindowEventThrottled, otherProps.autoHeight, stickyHorizontalScrollerHandler]); // STICKY MASTER DETAIL PANEL const isDetailPanelEnabled = Boolean(detailPanelField && detailPanelProps); useStickyMasterDetailPanel(apiRef, isDetailPanelEnabled); // COLUMNS DATA const getGridColDef = useGridColDef({ pushFunc }); const renderColumnHeader = useMemo( () => createColumnHeaderRenderer({ filters, setFilters: filters => setFilters(filters || {}), handleFilterChange, textClassName: styles.columnHeaderTitle }), [filters, handleFilterChange, styles.columnHeaderTitle] ); const dataGridColDef = useCallback( (col: GridColDef, index?: number) => { const colDef = getGridColDef(col); const colTypeClass = `colType--${colDef.type || 'default'}`; const colClasses = { [styles.firstColumn]: !index || index === 0, [styles.hoverableColumn]: !disableColumnHover && isHoverableColumn(colDef.field), [styles.pinnableColumn]: colDef.pinnable, [styles.resizableColumn]: !otherProps.disableColumnResize && colDef.resizable !== false, [colTypeClass]: true }; return { ...colDef, cellClassName: clsx(colClasses, colDef.cellClassName), headerClassName: clsx(colClasses, colDef.headerClassName), renderHeader: colDef.renderHeader || renderColumnHeader }; }, [disableColumnHover, getGridColDef, otherProps.disableColumnResize, renderColumnHeader, styles] ); const dataGridColumns = useMemo(() => { // extend other columns definitions let tableColDefs = tableConfig.map(dataGridColDef); if (treeDataField) { // prevent column duplicating tableColDefs = tableColDefs.filter(colDef => colDef.field !== treeDataField); } // prepend the checkbox and master details column definitions // https://mui.com/x/react-data-grid/master-detail/#customizing-the-detail-panel-toggle return [ gridCheckboxSelectionColDef, { ...gridDetailPanelToggleColDef, detailPanelField, detailPanelProps }, ...tableColDefs ]; }, [dataGridColDef, detailPanelField, detailPanelProps, tableConfig, treeDataField]); // prevents the MUI's warning on unsupported column type const dataGridColumnTypes = useMemo(() => getGridColumnTypes(), []); // ROWS DATA const dataGridRows = useMemo(() => { const rowsData = otherProps.loading ? skeletonizedTableData : tableData.data.slice(); return treeDataField ? // convert rows to switch to tree data convertChildsToTreeData(rowsData, '_id') : rowsData; }, [otherProps.loading, tableData, treeDataField]); // TREEDATA GROUPING let groupingColDef: DataGridProps['groupingColDef']; if (treeDataField) { const treeDataColDef = dataGridColDef( tableConfig.find(col => col.field === treeDataField) || { field: treeDataField, type: undefined } ); groupingColDef = { ...treeDataColDef, renderCell: params => ( ) }; } // PRESELECT ROWS useEffect(() => { if (selected && selected.length) { apiRef.current?.selectRows(selected, true); } }, [apiRef, selected]); return ( {filterAreaProps && ( )} {filtersProps && ( )} {toolbarProps && ( )} handleColumnMouseEvents(params.field, true)() } onColumnHeaderLeave={(params: GridColumnHeaderParams) => handleColumnMouseEvents(params.field, false)() } // COLUMN HOVER (EXCEPT CHECKBOXES) getCellClassName={params => { const isHovered = columnNameHover === params.field; return clsx({ [styles.actionsCell]: params.field === 'actions', [styles.hoveredCell]: isHovered && isHoverableColumn(params.field) }); }} // ROWS disableSelectionOnClick getRowId={entry => getRowId?.(entry) || entry._id} getRowClassName={params => (styles.disabledRow ? params.row.disabled : undefined)} rowHeight={GRID_ROW_HEIGHT} // TREE (ROW GROUPING) disableChildrenSorting getTreeDataPath={row => row.path} groupingColDef={groupingColDef} treeData={!!treeDataField} // MASTER DETAIL {...(isDetailPanelEnabled && { getDetailPanelContent, getDetailPanelHeight: () => 'auto' })} // PAGINATION pagination page={paginationProps?.page} pageSize={paginationProps?.rowsPerPage} rowsPerPageOptions={numOfRowsOptions.map(item => item.name as number)} onPageChange={newPage => { paginationProps?.onChangePage(null, newPage); }} onPageSizeChange={newPageSize => { if (paginationProps) paginationProps.rowsPerPage = newPageSize; const event = null as unknown as MouseEvent; toolbarProps?.onChangeNumOfRows?.(newPageSize)(event); }} onStateChange={onStateChange} // VIRTUALIZATION columnThreshold={2} rowThreshold={2} // MISC hideFooter sortingOrder={[GridSortDirections.DESC, GridSortDirections.ASC]} throttleRowsMs={40} {...otherProps} components={{ BaseCheckbox: otherProps.loading ? SkeletonizedGridHeader : undefined, Cell: otherProps.loading ? SkeletonizedGridCell : GridCell, ColumnSortedAscendingIcon, ColumnSortedDescendingIcon, LoadingOverlay: () => null, NoRowsOverlay, Row: GridRow, ...otherProps.components }} /> ); }); const m = memo(DataGrid); export { m as DataGrid };