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

[data grid] Smart children selection when toggling a group #4248

Closed
Tracked by #9328 ...
elyesbenabdelkader opened this issue Mar 21, 2022 · 19 comments · Fixed by #13757
Closed
Tracked by #9328 ...

[data grid] Smart children selection when toggling a group #4248

elyesbenabdelkader opened this issue Mar 21, 2022 · 19 comments · Fixed by #13757
Assignees
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature feature: Row grouping Related to the data grid Row grouping feature feature: Selection Related to the data grid Selection feature feature: Tree data Related to the data grid Tree data feature plan: Premium Impact at least one Premium user waiting for 👍 Waiting for upvotes

Comments

@elyesbenabdelkader
Copy link
Contributor

elyesbenabdelkader commented Mar 21, 2022

Summary 💡

When you group rows and then select the group header row, I would expect:

  1. The selection of all of the rows of the group.
  2. onSelectionModelChange to returns an array of the selected group rows ids instead of a single row with a generated id.

Examples 🌈

Motivation 🔦

Having to manually look for a group rows on selection is (in my own opinion) an unnecessarily difficult task in the current state. Add to that if you deselect group rows (while selecting other rows), the task becomes even harder.

Order ID 💳 (optional)

No response

Other user requests

@elyesbenabdelkader elyesbenabdelkader added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Mar 21, 2022
@flaviendelangle
Copy link
Member

Hi,

Thank's for your contribution
I listed this behavior on #2994, for me depending on the use case you might want, or not the selection of a group to select its children.
This could probably be handled by a prop.

For your 2nd point, I don't think we should change the return format of onSelectionModelChange, that would mean changing the format of the model which is massively breaking.
What exactly are you trying to achieve that requires this callback to return only the groups ?

@flaviendelangle flaviendelangle added component: data grid This is the name of the generic UI component, not the React module! plan: Premium Impact at least one Premium user feature: Tree data Related to the data grid Tree data feature feature: Row grouping Related to the data grid Row grouping feature and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Mar 21, 2022
@flaviendelangle flaviendelangle changed the title onSelectionModelChange and row grouping [data-grid] Smart children selection when toggling a group Mar 21, 2022
@flaviendelangle flaviendelangle added the feature: Selection Related to the data grid Selection feature label Mar 21, 2022
@elyesbenabdelkader
Copy link
Contributor Author

Suppose we have these rows
[ { "id": 1, "street": "A" }, { "id": 2, "street": "B" }, { "id": 3, "street": "A" } ]
If I select the group header row "A", I expect onSelectionModelChange to return: [1, 3]. Here nothing breaks we still return a list of the selected rows ids.

@flaviendelangle
Copy link
Member

flaviendelangle commented Mar 21, 2022

It breaks because if you are controlling the model, the selected rows are the one you give back to the grid.
So if we pass [1, 3], to onSelectionModelChange you will pass [1, 3] to props.selectionModel.
To be able to keep the group visually selected without passing the groups themselves in the model, we would have not just to say "when you select a group, select its children" but also "if all the children are selected, then select also the parents".

This as several impacts

  1. How do we handle selection with lazy loaded children if there is no trace of the group selection inside the model ?
  2. I consider it to be a breaking change, even if the format does not change. Users could rely on the presence of the group inside the selectionModel to trigger some custom behavior

In v6, we will have to rework the selection to do #208
We could also introduce a notion of "group" in the model that would automatically select its children even when lazy loader later. This needs to be investigated.

In v5, you can easily filter out the IDs of the group before using your model for something custom.

// Remove all the rows generated by the grid
// With the Tree Data it would remove the auto generated parents but not the one based on real rows
// With the Aggregation (not released yet), it would remove the aggregation rows
const modelWithoutGroups = selectionModel.filter(id => !apiRef.current.getRowNode(id)?.isAutoGenerated)

// Remove all the rows that are not leaves (ie all the grouping rows)
// With the Tree Data it would remove all the rows that are not of maximal depth (not very usable I think)
// With the Aggregation (not released yet), it would not remove the aggregation rows
const depth = gridRowTreeDepthSelector(apiRef)
const modelWithoutGroups = selectionModel.filter(id => apiRef.current.getRowNode(id)?.depth === depth - 1)

@elyesbenabdelkader
Copy link
Contributor Author

Fair enough, but could we at least have a method in the api that fetches the rows ids of a given group id? Maybe something like:
apiRef.current.getGroupRowsIds("A") that should return [1, 3].

@flaviendelangle
Copy link
Member

flaviendelangle commented Mar 22, 2022

Here is a method that retrieves all the leaves (filtered or not) of a group: https://codesandbox.io/s/datagridpro-v5-quick-start-forked-ltkt4g?file=/src/App.tsx
It should work even when grouping according to several criterias.

We could probably add a built-in version of this method at some point.

@flaviendelangle
Copy link
Member

flaviendelangle commented Mar 24, 2022

See #4270 for the method returning the rows of a group.

@m4theushw
Copy link
Member

To be able to keep the group visually selected without passing the groups themselves in the model, we would have not just to say "when you select a group, select its children" but also "if all the children are selected, then select also the parents".

Yes, internally the group row would never be selected, but its selection state would be based on its children. Or we even keep it selectable, being present in selectionModel, but we add some sort of method or selector to filter out auto-generated rows from the model. We'll need to sanitize the model, because if we chose to add auto-generated rows to selectionModel, these rows may not be there during the first render, so we'll need to add them.

How do we handle selection with lazy loaded children if there is no trace of the group selection inside the model ?

We have this same problem with the "Select All" checkbox. selectionModel might not be empty but if the IDs are not also in rows, the checkbox is displayed as unchecked. I would reproduce the same behavior here, initially at least.

@inflrscns
Copy link

inflrscns commented Jun 2, 2022

I ran into this issue in development and wrote a gist that may be helpful for adding this feature, or anyone who needs it working: https://gist.github.com/inflrscns/fa5f3d4551947cbf4735ac8373d9bbef

note: I built this for single column row grouping, probably doesn't work for multiple

@HansBrende
Copy link

HansBrende commented Oct 9, 2022

@inflrscns I've built on your code example to display indeterminate-state checkboxes in the parent rows whenever the children are partially selected, by adding the following column definition (my tree is only of depth 2, but this could be extended to support higher depths):

import {
    GRID_CHECKBOX_SELECTION_COL_DEF, GridCellCheckboxRenderer, GridColDef, 
    GridApiPro, selectedIdsLookupSelector
} from "@mui/x-data-grid-pro";

const checkboxColumn: GridColDef = {
    ...GRID_CHECKBOX_SELECTION_COL_DEF,
    renderCell: (params: GridRenderCellParams) => {
        const rowNode = params.rowNode;
        const selectionLookup = selectedIdsLookupSelector(params.api.state, params.api.instanceId);
        const indeterminate = rowNode.children != null && rowNode.parent == null &&
            rowNode.children.some(child => selectionLookup[child] === undefined) &&
            rowNode.children.some(child => selectionLookup[child] !== undefined);
        return <GridCellCheckboxRenderer
            {...params}
            // @ts-ignore
            indeterminate={indeterminate}
            size={rowNode.parent == null ? undefined : 'small'}
        />
    }
};

@DanailH DanailH changed the title [data-grid] Smart children selection when toggling a group [data grid] Smart children selection when toggling a group Jun 19, 2023
@cstephens-cni
Copy link

cstephens-cni commented Jun 22, 2023

Further building off @HansBrende if you want the selection logic in the cell

2 things to note:

  • still have the ts error comment (i thought GridRenderCellParams & GridRenderCellParamsPremium) should do it but it didn't fix it.
  • when you use the top header selection it will set the internal state of the group as selected, which is not ideal...so it will count it in the footer as well as any method that get selected, I will work this out later.

Otherwise this is working surprising well for me.

import {
    GRID_CHECKBOX_SELECTION_COL_DEF,
    GridCellCheckboxRenderer,
    GridColDef,
    GridRenderCellParams,
    selectedIdsLookupSelector
} from "@mui/x-data-grid-premium";
import { GridRenderCellParamsPremium } from "@mui/x-data-grid-premium/typeOverloads";

export const MUICustomCheckboxColumn: GridColDef = {
    ...GRID_CHECKBOX_SELECTION_COL_DEF,
    renderCell: (params: GridRenderCellParams & GridRenderCellParamsPremium) => {
        const rowNode = params.rowNode;
        const selectionLookup = selectedIdsLookupSelector(params.api.state, params.api.instanceId);
        let indeterminate: boolean | undefined = undefined;
        let checked: boolean | undefined = undefined;
        const extraData ={};
        if (rowNode.type === "group") {
            const isBottomGroup = rowNode.children != null && (rowNode.parent == null || rowNode.parent === "auto-generated-group-node-root");
            indeterminate = isBottomGroup &&
                rowNode.children.some(child => selectionLookup[child] === undefined) &&
                rowNode.children.some(child => selectionLookup[child] !== undefined);
            checked = isBottomGroup && rowNode.children.every(child => selectionLookup[child] !== undefined);
            if(indeterminate){
                extraData["indeterminate"]=indeterminate;
            }
            if(checked){
                extraData["checked"]=checked;
            }
            extraData["onClick"] = (e) => {
                if (rowNode.type === "group") {
                    if (rowNode.children) {
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                        // @ts-ignore
                        params.api.selectRows(rowNode.children, indeterminate || !checked);
                    }
                    e.preventDefault();
                }
            }
        }
        return <GridCellCheckboxRenderer
            {...params}
            {...extraData}
        />;
    }
};

@m4theushw m4theushw added the waiting for 👍 Waiting for upvotes label Jun 26, 2023
@cstephens-cni
Copy link

cstephens-cni commented Jun 26, 2023

Further building off @HansBrende if you want the selection logic in the cell

2 things to note:

  • still have the ts error comment (i thought GridRenderCellParams & GridRenderCellParamsPremium) should do it but it didn't fix it.
  • when you use the top header selection it will set the internal state of the group as selected, which is not ideal...so it will count it in the footer as well as any method that get selected, I will work this out later.

Otherwise this is working surprising well for me.

import {
    GRID_CHECKBOX_SELECTION_COL_DEF,
    GridCellCheckboxRenderer,
    GridColDef,
    GridRenderCellParams,
    selectedIdsLookupSelector
} from "@mui/x-data-grid-premium";
import { GridRenderCellParamsPremium } from "@mui/x-data-grid-premium/typeOverloads";

export const MUICustomCheckboxColumn: GridColDef = {
    ...GRID_CHECKBOX_SELECTION_COL_DEF,
    renderCell: (params: GridRenderCellParams & GridRenderCellParamsPremium) => {
        const rowNode = params.rowNode;
        const selectionLookup = selectedIdsLookupSelector(params.api.state, params.api.instanceId);
        let indeterminate: boolean | undefined = undefined;
        let checked: boolean | undefined = undefined;
        const extraData ={};
        if (rowNode.type === "group") {
            const isBottomGroup = rowNode.children != null && (rowNode.parent == null || rowNode.parent === "auto-generated-group-node-root");
            indeterminate = isBottomGroup &&
                rowNode.children.some(child => selectionLookup[child] === undefined) &&
                rowNode.children.some(child => selectionLookup[child] !== undefined);
            checked = isBottomGroup && rowNode.children.every(child => selectionLookup[child] !== undefined);
            if(indeterminate){
                extraData["indeterminate"]=indeterminate;
            }
            if(checked){
                extraData["checked"]=checked;
            }
            extraData["onClick"] = (e) => {
                if (rowNode.type === "group") {
                    if (rowNode.children) {
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                        // @ts-ignore
                        params.api.selectRows(rowNode.children, indeterminate || !checked);
                    }
                    e.preventDefault();
                }
            }
        }
        return <GridCellCheckboxRenderer
            {...params}
            {...extraData}
        />;
    }
};

Wanted to make a quick note, it's not as clean, so I'm treating it separately, rather then editing what I did above.

If you do what I'm doing, an issue I discovered was that the selected row count is counting the groups as selected if you use the select all header button.

To stop that you can add

        isRowSelectable: (params)=>{
                return params.id.indexOf('auto-generated-row-') === -1 
            },

to your grid...this will disable selecting it. Which also mean it marks it as disabled in the code that I gave above (you can no longer click it). to fix that you can add a single line to the example above.

       if (rowNode.type === "group") {
       ...
            extraData["disabled"] = false;
       ...

This will overwrite the disabled attribute on the grouping checkbox which will no longer count as selected in the footer etc.

@nickcarnival
Copy link

@cstephens-cni your examples have been of tremendous help to me so here's a slightly cleaned up version with the removal of // ts-ignore and eslint-disable this version does require you to use the api object

const checkboxColumn = {
  ...GRID_CHECKBOX_SELECTION_COL_DEF,
  renderCell: (
    params: GridRenderCellParams & GridRenderCellParamsPremium
  ) => {
    const { rowNode } = params;

    if (rowNode.groupingField == null)
      return <GridCellCheckboxRenderer {...params} />;

    const selectionLookup = selectedIdsLookupSelector(
      apiRef.current.state,
      apiRef.current.instanceId
    );

    const isBottomGroup =
      rowNode.children != null &&
      (rowNode.parent == null ||
        rowNode.parent === 'auto-generated-group-node-root');

    const indeterminate =
      isBottomGroup &&
      rowNode?.children?.some(
        (child) => selectionLookup[child] === undefined
      ) &&
      rowNode.children.some((child) => selectionLookup[child] !== undefined);

    const checked =
      isBottomGroup &&
      rowNode?.children?.every(
        (child) => selectionLookup[child] !== undefined
      );

    const extraData: GridRenderCellParams &
      GridRenderCellParamsPremium & {
        indeterminate?: boolean;
        checked?: boolean;
        disabled?: boolean;
        onClick?: (e: MouseEvent) => void;
      } = {
      ...params,
      disabled: false,
      onClick: (e) => {
        if (rowNode.groupingField != null) {
          if (rowNode.children) {
            apiRef.current.selectRows(
              rowNode.children,
              indeterminate || !checked
            );
          }
          e.preventDefault();
        }
      },
      indeterminate,
      checked,
    };

    return <GridCellCheckboxRenderer {...extraData} />;
  },
};

@jaballogian
Copy link

jaballogian commented Aug 15, 2023

Before the row grouping on the MUI DataGridPremium component arrived, I had already used the tree data feature on the MUI DataGridPro component.
I could migrate from the MUI DataGridPro component to the MUI DataGridPremium component but it would consume a lot of effort since the project has become huge.
Based on the user perspective, both have the same display.
The code for both features is similar.

So here is the solution for anyone who is still using the tree data feature on the MUI DataGridPro component based on @HansBrende @cstephens-cni and @nickcarnival solutions.

  1. Replace the @mui/x-data-grid-premium into the @mui/x-data-grid-pro dependency
  2. There is no rowNode.groupingField and rowNode.type values on the tree data feature from the MUI DataGridPro component. We could use the rowNode.children value as the substitution to check whether the rowNode is a parent/generated node or a children node.

I tried to implement those two steps but apparently, nothing happens after the parent/group checkbox is clicked.
So, the best way to implement this smart children selection feature on the tree data feature is to migrate from the MUI DataGridPro component to the MUI DataGridPremium component 😂.

@oliviertassinari oliviertassinari added the enhancement This is not a bug, nor a new feature label Sep 8, 2023
@cclews1
Copy link

cclews1 commented Sep 15, 2023

DataGrid Checkbox Nested Group Intermediate Select Functionality + Select All Functionality

@nickcarnival @cstephens-cni

I found your snippets extremely useful, but I noticed it broke in cases where nested groups were used. It also didn't allow the "select all" header to work properly.

Code below:

  • Allows selecting groups by checkbox - including nested groups
  • Applies the Table's filter and sort settings to the selected rows
  • Adds the same nested group selection ability to the "Select All" column header checkbox.
const checkboxColumn = (
  apiRef: MutableRefObject<GridApiPremium>
): GridColDef => {
  return {
    ...GRID_CHECKBOX_SELECTION_COL_DEF,
    renderHeader: (params) => {
      const children = gridFilteredSortedRowIdsSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      ).filter((id) => !id.toString().includes("auto-generated"));

      const selectionLookup = selectedIdsLookupSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      );

      const indeterminate =
        children?.some((child) => selectionLookup[child] === undefined) &&
        children?.some((child) => selectionLookup[child] !== undefined);

      const checked = children?.every(
        (child) => selectionLookup[child] !== undefined
      );
      const data: GridColumnHeaderParams & {
        indeterminate?: boolean;
        checked?: boolean;
        disabled?: boolean;
        onClick?: (e: MouseEvent) => void;
      } = {
        ...params,
        onClick: (e) => {
          apiRef.current.selectRows(children, indeterminate || !checked);
          e.preventDefault();
        },
        indeterminate,
        checked,
      };
      return (
        <>
          <GridHeaderCheckbox {...data} />
        </>
      );
    },
    renderCell: (params) => {
      const { rowNode } = params;

      if (rowNode.type !== "group")
        return <GridCellCheckboxRenderer {...params} />;

      const selectionLookup = selectedIdsLookupSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      );
      const children = apiRef.current.getRowGroupChildren({
        groupId: rowNode.id,
        applyFiltering: true,
        applySorting: true,
      });

      const indeterminate =
        children?.some((child) => selectionLookup[child] === undefined) &&
        children?.some((child) => selectionLookup[child] !== undefined);

      const checked = children?.every(
        (child) => selectionLookup[child] !== undefined
      );

      const extraData: GridRenderCellParams &
        GridRenderCellParamsPremium & {
          indeterminate?: boolean;
          checked?: boolean;
          disabled?: boolean;
          onClick?: (e: MouseEvent) => void;
        } = {
        ...params,
        disabled: false,
        onClick: (e) => {
          if (rowNode.groupingField != null) {
            if (children) {
              apiRef.current.selectRows(children, indeterminate || !checked);
            }
            e.preventDefault();
          }
        },
        indeterminate,
        checked,
      };

      return <GridCellCheckboxRenderer {...extraData} />;
    },
  };
};
export default checkboxColumn;

It is making use of the GridApi for fetching group children (and subgroup's children) with the apiRef.current.getRowGroupChildren, which includes grandchildren by default. This method also automatically filters out autogenerated group row IDs. It accepts the applyFiltering: true, applySorting: true props which ensure the row selection applies only to the rows that haven't been filtered, in their sort-order.

The code added to the renderHeader prop allows the "Select All" checkbox to basically function the same as the group checkboxes, only it cannot make use of the apiRef.current.getRowGroupChildren method because that method takes a group row's id as a parameter. Unfortunately, this means we have to again manually filter out the group row IDs.

@mtsmith
Copy link

mtsmith commented May 6, 2024

  • Handle header checkbox state when rows are empty
  • Handles click for all rows of type === 'group' instead of groupingField !== null
const checkboxColumn = (
  apiRef: MutableRefObject<GridApiPremium>
): GridColDef => {
  return {
    ...GRID_CHECKBOX_SELECTION_COL_DEF,
    renderHeader: (params) => {
      const children = gridFilteredSortedRowIdsSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      ).filter((id) => !id.toString().includes("auto-generated"));

      const selectionLookup = selectedIdsLookupSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      );

      const indeterminate =
        children?.some((child) => selectionLookup[child] === undefined) &&
        children?.some((child) => selectionLookup[child] !== undefined);

      const checked = Object.keys(selectionLookup).length > 0 && children?.every(
        (child) => selectionLookup[child] !== undefined
      );
      const data: GridColumnHeaderParams & {
        indeterminate?: boolean;
        checked?: boolean;
        disabled?: boolean;
        onClick?: (e: MouseEvent) => void;
      } = {
        ...params,
        onClick: (e) => {
          apiRef.current.selectRows(children, indeterminate || !checked);
          e.preventDefault();
        },
        indeterminate,
        checked,
      };
      return (
        <>
          <GridHeaderCheckbox {...data} />
        </>
      );
    },
    renderCell: (params) => {
      const { rowNode } = params;

      if (rowNode.type !== "group")
        return <GridCellCheckboxRenderer {...params} />;

      const selectionLookup = selectedIdsLookupSelector(
        apiRef.current.state,
        apiRef.current.instanceId
      );
      const children = apiRef.current.getRowGroupChildren({
        groupId: rowNode.id,
        applyFiltering: true,
        applySorting: true,
      });

      const indeterminate =
        children?.some((child) => selectionLookup[child] === undefined) &&
        children?.some((child) => selectionLookup[child] !== undefined);

      const checked = children?.every(
        (child) => selectionLookup[child] !== undefined
      );

      const extraData: GridRenderCellParams &
        GridRenderCellParamsPremium & {
          indeterminate?: boolean;
          checked?: boolean;
          disabled?: boolean;
          onClick?: (e: MouseEvent) => void;
        } = {
        ...params,
        disabled: false,
        onClick: (e) => {
          if (rowNode.type === 'group') {
            if (children) {
              apiRef.current.selectRows(children, indeterminate || !checked);
            }
            e.preventDefault();
          }
        },
        indeterminate,
        checked,
      };

      return <GridCellCheckboxRenderer {...extraData} />;
    },
  };
};
export default checkboxColumn;

Now what it's missing is the ability to update after a filter is applied.

@joserodolfofreitas
Copy link
Member

TreeView is also aiming to support this feature soon, so it'd be good for us to keep aligned on the DevEx.

@genepaul
Copy link
Contributor

It looks like possibly v7 may have broken this workaround. It seems this workaround is relying on every checkbox cell re-rendering any time there are changes to the selection state, but in v7 it seems that it's only re-rendering the checkbox cells that changed selection state - so any cell that needs to show as indeterminate isn't updated because it didn't change selection state according to the grid. I have a reproduction at the following link, where if you click a sub-group it will correctly select its children but not render its parent as indeterminate until an update on that row occurs (such as expanding or collapsing that row).

https://react-kckdew.stackblitz.io

Is there something I'm missing here?

@sharmap850
Copy link

Hey , [data grid] Smart children selection when toggling a group #4248
Do you have any demo for MUI data grid premium ?
I am trying to implement the same functionality in MUI data grid like I have grouped on e column like below
const initialState = useKeepGroupedColumnsHidden({
apiRef,
initialState: {
sorting: {
sortModel: [{ field: 'row_group_by_columns_group', sort: 'asc' }],
},
rowGrouping: {
model: ['companyName'],
},
},
});
Now , I want selection checkbox in front of all the rows like in front of Company and their child and when I select checkbox on Company level then all the child rows should be selected and when selecting one of child then should get the id of that row and id of parent so I can use these two id's it my feature to do some action on that row only.
Please help asap

Copy link

github-actions bot commented Oct 4, 2024

This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.

Note

We value your feedback @elyesbenabdelkader! How was your experience with our support team?
We'd love to hear your thoughts in this brief Support Satisfaction survey. Your insights help us improve!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature feature: Row grouping Related to the data grid Row grouping feature feature: Selection Related to the data grid Selection feature feature: Tree data Related to the data grid Tree data feature plan: Premium Impact at least one Premium user waiting for 👍 Waiting for upvotes
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.