Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Use pre-processors for sorting and filtering #3318

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export type PreProcessorCallback = (value: any, params?: any) => any;

export enum GridPreProcessingGroup {
hydrateColumns = 'hydrateColumns',
registerFiltering = 'registerFiltering',
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
registerSorting = 'registerSorting',
}

export interface GridPreProcessingApi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export interface GridRowGroupParams {
}

export interface GridRowGroupingResult {
/**
* Name of the algorithm used to group the rows
* It is useful to decide which filtering / sorting algorithm to apply, to avoid applying tree-data filtering on a grouping-by-column dataset for instance.
*/
treeGroupingName: string;
tree: GridRowTreeConfig;
treeDepth: number;
ids: GridRowId[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getFlatRowTree: GridRowGroupingPreProcessing = ({ ids, idRowsLookup }) =>
}

return {
treeGroupingName: 'none',
tree,
treeDepth: 1,
idRowsLookup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ export interface GridFilterState {
export interface GridFilterInitialState {
filterModel?: GridFilterModel;
}

export interface GridFilteringParams {
isRowMatchingFilters: ((rowId: GridRowId) => boolean) | null;
}

export type GridFilteringMethod = (
params: GridFilteringParams,
) => Pick<GridFilterState, 'visibleRowsLookup' | 'filteredDescendantCountLookup'>;

export type GridFilteringMethodCollection = { [methodName: string]: GridFilteringMethod };
3 changes: 2 additions & 1 deletion packages/grid/_modules_/grid/hooks/features/filter/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './gridFilterState';
export type { GridFilterState, GridFilterInitialState } from './gridFilterState';
export { getDefaultGridFilterModel } from './gridFilterState';
export * from './gridFilterSelector';
203 changes: 94 additions & 109 deletions packages/grid/_modules_/grid/hooks/features/filter/useGridFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ import { GridApiRef } from '../../../models/api/gridApiRef';
import { GridFilterApi } from '../../../models/api/gridFilterApi';
import { GridFeatureModeConstant } from '../../../models/gridFeatureMode';
import { GridFilterItem, GridLinkOperator } from '../../../models/gridFilterItem';
import { GridRowId, GridRowModel, GridRowTreeNodeConfig } from '../../../models/gridRows';
import { GridRowId, GridRowModel } from '../../../models/gridRows';
import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import { useGridLogger } from '../../utils/useGridLogger';
import { filterableGridColumnsIdsSelector } from '../columns/gridColumnsSelector';
import { useGridState } from '../../utils/useGridState';
import { GridPreferencePanelsValue } from '../preferencesPanel/gridPreferencePanelsValue';
import { getDefaultGridFilterModel } from './gridFilterState';
import {
getDefaultGridFilterModel,
GridFilteringMethod,
GridFilteringMethodCollection,
} from './gridFilterState';
import { GridFilterModel } from '../../../models/gridFilterModel';
import { gridVisibleSortedRowEntriesSelector, gridFilterModelSelector } from './gridFilterSelector';
import { gridFilterModelSelector, gridVisibleSortedRowEntriesSelector } from './gridFilterSelector';
import { useGridStateInit } from '../../utils/useGridStateInit';
import { useFirstRender } from '../../utils/useFirstRender';
import { gridRowIdsSelector, gridRowTreeDepthSelector, gridRowTreeSelector } from '../rows';
import { gridRowIdsSelector, gridRowTreeGroupingNameSelector } from '../rows';
import { GridPreProcessingGroup } from '../../core/preProcessing';
import { useGridRegisterFilteringMethod } from './useGridRegisterFilteringMethod';

type GridFilterItemApplier = (rowId: GridRowId) => boolean;

Expand Down Expand Up @@ -47,10 +53,11 @@ export const useGridFilter = (
| 'onFilterModelChange'
| 'filterMode'
| 'disableMultipleColumnsFiltering'
| 'disableChildrenFiltering'
>,
): void => {
const logger = useGridLogger(apiRef, 'useGridFilter');
const filteringMethodCollectionRef = React.useRef<GridFilteringMethodCollection>({});
const lastFilteringMethodApplied = React.useRef<GridFilteringMethod | null>(null);

useGridStateInit(apiRef, (state) => {
if (props.filterModel) {
Expand Down Expand Up @@ -154,110 +161,34 @@ export const useGridFilter = (
*/
const applyFilters = React.useCallback<GridFilterApi['unstable_applyFilters']>(() => {
setGridState((state) => {
const rowGroupingName = gridRowTreeGroupingNameSelector(state);
const filteringMethod = filteringMethodCollectionRef.current[rowGroupingName];
if (!filteringMethod) {
throw new Error('MUI: Invalid filtering method');
}

const filterModel = gridFilterModelSelector(state);
const rowIds = gridRowIdsSelector(state);
const rowTree = gridRowTreeSelector(state);
const shouldApplyTreeFiltering = gridRowTreeDepthSelector(state) > 1;
const filteringMethod =
const isRowMatchingFilters =
props.filterMode === GridFeatureModeConstant.client
? buildAggregatedFilterApplier(filterModel)
: null;

const visibleRowsLookup: Record<GridRowId, boolean> = {};
const filteredDescendantCountLookup: Record<GridRowId, number> = {};
if (shouldApplyTreeFiltering) {
// A node is visible if
// - One of its children is passing the filter
// - It is passing the filter
const filterTreeNode = (
node: GridRowTreeNodeConfig,
isParentMatchingFilters: boolean,
areAncestorsExpanded: boolean,
): number => {
const shouldSkipFilters = props.disableChildrenFiltering && node.depth > 0;

let isMatchingFilters: boolean | null;
if (shouldSkipFilters) {
isMatchingFilters = null;
} else if (!filteringMethod) {
isMatchingFilters = true;
} else {
isMatchingFilters = filteringMethod(node.id);
}

let filteredDescendantCount = 0;
node.children?.forEach((childId) => {
const childNode = rowTree[childId];
const childSubTreeSize = filterTreeNode(
childNode,
isMatchingFilters ?? isParentMatchingFilters,
areAncestorsExpanded && !!node.childrenExpanded,
);

filteredDescendantCount += childSubTreeSize;
});

let shouldPassFilters: boolean;
switch (isMatchingFilters) {
case true: {
shouldPassFilters = true;
break;
}
case false: {
shouldPassFilters = filteredDescendantCount > 0;
break;
}
default: {
shouldPassFilters = isParentMatchingFilters;
break;
}
}

visibleRowsLookup[node.id] = shouldPassFilters && areAncestorsExpanded;

if (!shouldPassFilters) {
return 0;
}

filteredDescendantCountLookup[node.id] = filteredDescendantCount;

// TODO: For column grouping, we do not want to count the intermediate depth nodes in the visible descendant count
return filteredDescendantCount + 1;
};

const nodes = Object.values(rowTree);
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.depth === 0) {
filterTreeNode(node, true, true);
}
}
} else if (props.filterMode === GridFeatureModeConstant.client && filteringMethod) {
for (let i = 0; i < rowIds.length; i += 1) {
const rowId = rowIds[i];
visibleRowsLookup[rowId] = filteringMethod(rowId);
}
}
lastFilteringMethodApplied.current = filteringMethod;
const filteringResult = filteringMethodCollectionRef.current[rowGroupingName]({
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
isRowMatchingFilters,
});

return {
...state,
filter: {
...state.filter,
visibleRowsLookup,
filteredDescendantCountLookup,
...filteringResult,
},
};
});
apiRef.current.publishEvent(GridEvents.visibleRowsSet);
forceUpdate();
}, [
apiRef,
setGridState,
forceUpdate,
props.filterMode,
buildAggregatedFilterApplier,
props.disableChildrenFiltering,
]);
}, [apiRef, setGridState, forceUpdate, props.filterMode, buildAggregatedFilterApplier]);

const cleanFilterItem = React.useCallback(
(item: GridFilterItem) => {
Expand Down Expand Up @@ -404,6 +335,37 @@ export const useGridFilter = (
'FilterApi',
);

/**
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started to structure the big hooks with section comments to make it easier to find something.

* PRE-PROCESSING
*/
const flatFilteringMethod = React.useCallback<GridFilteringMethod>(
(params) => {
if (props.filterMode === GridFeatureModeConstant.client && params.isRowMatchingFilters) {
const rowIds = gridRowIdsSelector(apiRef.current.state);
const visibleRowsLookup: Record<GridRowId, boolean> = {};
for (let i = 0; i < rowIds.length; i += 1) {
const rowId = rowIds[i];
visibleRowsLookup[rowId] = params.isRowMatchingFilters(rowId);
}
return {
visibleRowsLookup,
filteredDescendantCountLookup: {},
};
}

return {
visibleRowsLookup: {},
filteredDescendantCountLookup: {},
};
},
[apiRef, props.filterMode],
);

useGridRegisterFilteringMethod(apiRef, 'none', flatFilteringMethod);

/**
* EVENTS
*/
const handleColumnsChange = React.useCallback<GridEventListener<GridEvents.columnsChange>>(() => {
logger.debug('onColUpdated - GridColumns changed, applying filters');
const filterModel = gridFilterModelSelector(apiRef.current.state);
Expand All @@ -416,23 +378,28 @@ export const useGridFilter = (
}
}, [apiRef, logger]);

React.useEffect(() => {
if (props.filterModel !== undefined) {
apiRef.current.setFilterModel(props.filterModel);
}
}, [apiRef, logger, props.filterModel]);
const handlePreProcessorRegister = React.useCallback<
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See description of the PR for more details about this logic

GridEventListener<GridEvents.preProcessorRegister>
>(
(name) => {
if (name !== GridPreProcessingGroup.registerFiltering) {
return;
}

// The filter options have changed
const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
apiRef.current.unstable_applyFilters();
}, [apiRef, props.disableChildrenFiltering]);
filteringMethodCollectionRef.current = apiRef.current.unstable_applyPreProcessors(
GridPreProcessingGroup.registerFiltering,
{},
);

useFirstRender(() => apiRef.current.unstable_applyFilters());
const rowGroupingName = gridRowTreeGroupingNameSelector(apiRef.current.state);
if (
lastFilteringMethodApplied.current !== filteringMethodCollectionRef.current[rowGroupingName]
) {
apiRef.current.unstable_applyFilters();
}
},
[apiRef],
);

useGridApiEventHandler(apiRef, GridEvents.rowsSet, apiRef.current.unstable_applyFilters);
useGridApiEventHandler(
Expand All @@ -441,4 +408,22 @@ export const useGridFilter = (
apiRef.current.unstable_applyFilters,
);
useGridApiEventHandler(apiRef, GridEvents.columnsChange, handleColumnsChange);
useGridApiEventHandler(apiRef, GridEvents.preProcessorRegister, handlePreProcessorRegister);

/**
* EFFECTS
*/
useFirstRender(() => {
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
filteringMethodCollectionRef.current = apiRef.current.unstable_applyPreProcessors(
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
GridPreProcessingGroup.registerFiltering,
{},
);
apiRef.current.unstable_applyFilters();
});

React.useEffect(() => {
if (props.filterModel !== undefined) {
apiRef.current.setFilterModel(props.filterModel);
}
}, [apiRef, logger, props.filterModel]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { GridFilteringMethod, GridFilteringMethodCollection } from './gridFilterState';
import { GridPreProcessingGroup, useGridRegisterPreProcessor } from '../../core/preProcessing';
import { GridApiRef } from '../../../models';

export const useGridRegisterFilteringMethod = (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small abstraction of the collection update behavior.
It does not bring a lot of value but without it, the code is useGridTreeData / useGridGroupingColumns was less readable.

apiRef: GridApiRef,
groupingName: string,
filteringMethod: GridFilteringMethod,
) => {
const updateRegistration = React.useCallback(
(filteringMethodCollection: GridFilteringMethodCollection) => {
filteringMethodCollection[groupingName] = filteringMethod;
return filteringMethodCollection;
},
[groupingName, filteringMethod],
);

useGridRegisterPreProcessor(apiRef, GridPreProcessingGroup.registerFiltering, updateRegistration);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export const gridRowsLookupSelector = createSelector(

export const gridRowTreeSelector = createSelector(gridRowsStateSelector, (rows) => rows.tree);

export const gridRowTreeGroupingNameSelector = createSelector(
gridRowsStateSelector,
(rows) => rows.treeGroupingName,
);

export const gridRowTreeDepthSelector = createSelector(
gridRowsStateSelector,
(rows) => rows.treeDepth,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GridRowId } from '../../../models/gridRows';
import { GridSortModel } from '../../../models/gridSortModel';
import { GridRowId, GridRowTreeNodeConfig } from '../../../models/gridRows';
import { GridFieldComparatorList, GridSortModel } from '../../../models/gridSortModel';

export interface GridSortingState {
sortedRows: GridRowId[];
Expand All @@ -9,3 +9,12 @@ export interface GridSortingState {
export interface GridSortingInitialState {
sortModel?: GridSortModel;
}

export interface GridSortingParams {
comparatorList: GridFieldComparatorList;
sortRowList: (rowList: GridRowTreeNodeConfig[]) => GridRowId[];
}

export type GridSortingMethod = (params: GridSortingParams) => GridRowId[];

export type GridSortingMethodCollection = { [methodName: string]: GridSortingMethod };
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './gridSortingSelector';
export * from './gridSortingState';
export type { GridSortingState, GridSortingInitialState } from './gridSortingState';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { GridSortingMethod, GridSortingMethodCollection } from './gridSortingState';
import { GridPreProcessingGroup, useGridRegisterPreProcessor } from '../../core/preProcessing';
import { GridApiRef } from '../../../models';

export const useGridRegisterSortingMethod = (
apiRef: GridApiRef,
groupingName: string,
filteringMethod: GridSortingMethod,
) => {
const updateRegistration = React.useCallback(
(sortingMethodCollection: GridSortingMethodCollection) => {
sortingMethodCollection[groupingName] = filteringMethod;
return sortingMethodCollection;
},
[groupingName, filteringMethod],
);

useGridRegisterPreProcessor(apiRef, GridPreProcessingGroup.registerSorting, updateRegistration);
};
Loading