mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.08k stars 1.26k forks source link

[data grid] all nodes are closing while row is being selected #13470

Closed agirgol closed 3 months ago

agirgol commented 3 months ago

I am using treedata in the DataGridPro table and keeping the row ids to perform operations based on the selected rows. But when retrieving the ids, the table nodes are closed. In other words, the nodes close in every line I select. What method should I follow so that the nodes do not close and remain as they are?

import * as React from 'react';
import { DataGridPro, GridColDef, GridPaginationModel, GridRowId } from '@mui/x-data-grid-pro';
import axios from 'axios';
import CompanyClaimTableListTopBar from './CompanyClaimTableListTopBar';
import { Box } from '@mui/material';

interface Item {
  id: string;
  companyClaimCode: string;
  companyClaimName: string;
  status: number;
  level: number;
  upperCompanyClaimId: string | null;
}

const status = [
  {
    value: 0,
    label: 'Active',
  },
  {
    value: 1,
    label: 'Inactive',
  },
  {
    value: 2,
    label: 'Archive',
  },
  {
    value: 3,
    label: 'Cancel',
  },
];

const transformData = (
  items: Item[],
): {
  path: string;
  id: string;
  companyClaimCode: string;
  companyClaimName: string;
  status: number;
}[] => {
  items.sort((a, b) => {
    if (a.level !== b.level) {
      return a.level - b.level;
    } else {
      return a.companyClaimName.localeCompare(b.companyClaimName);
    }
  });

  const transformedData: {
    path: string;
    id: string;
    companyClaimCode: string;
    companyClaimName: string;
    status: number;
  }[] = [];

  const map: {
    [id: string]: {
      path: string;
      id: string;
      companyClaimCode: string;
      companyClaimName: string;
      status: number;
    };
  } = {};

  items.forEach((item) => {
    map[item.id] = {
      path: item.companyClaimName,
      id: item.id,
      companyClaimCode: item.companyClaimCode,
      companyClaimName: item.companyClaimName,
      status: item.status,
    };

    if (item.upperCompanyClaimId) {
      const upperItem = map[item.upperCompanyClaimId];
      map[item.id].path = `${upperItem.path}/${map[item.id].path}`;
    }

    transformedData.push(map[item.id]);
  });

  return transformedData;
};

function getStatusLabel(value: number): string {
  const statusObj = status.find((s) => s.value === value);
  return statusObj ? statusObj.label : 'Unknown';
}

const columns: GridColDef[] = [
  { field: 'companyClaimCode', headerName: 'Company Claim Code', width: 150 },
  { field: 'companyClaimName', headerName: 'Company Claim Name', width: 250 },
  {
    field: 'status',
    headerName: 'Status',
    width: 150,
    renderCell: (params) => getStatusLabel(params.value as number),
  },
];

export default function TreeDataGrid() {
  const [rows, setRows] = React.useState<{ path: string; id: string; companyClaimName: string }[]>(
    [],
  );
  const [loading, setLoading] = React.useState<boolean>(true);
  const [paginationModel, setPaginationModel] = React.useState<GridPaginationModel>({
    page: 0,
    pageSize: 5,
  });
  const [rowCount, setRowCount] = React.useState<number>(0);
  const [selectedRowIds, setSelectedRowIds] = React.useState<Set<GridRowId>>(new Set());

  const fetchData = async (page: number, pageSize: number) => {
    setLoading(true);
    try {
      const response = await axios.get('/api/CompanyClaims', {
        params: { page, pageSize },
      });
      const { items, count } = response.data;
      const transformedRows = transformData(items);
      setRows(transformedRows);
      setRowCount(count);
    } catch (error) {
      console.error('Failed to fetch data', error);
    } finally {
      setLoading(false);
    }
  };

  const handlePaginationModelChange = (newModel: GridPaginationModel) => {
    setPaginationModel(newModel);
  };

  const handleRowSelectionChange = (newSelection: GridRowId[]) => {
    setSelectedRowIds(new Set(newSelection));
  };

  const handleDelete = () => {
    const idsArray = Array.from(selectedRowIds);
    const requestBody = { ids: idsArray };

    axios
      .delete('/api/CompanyClaims', {
        data: requestBody,
      })
      .then((response) => {
        console.log('API Response:', response.data);
        fetchData(paginationModel.page, paginationModel.pageSize);
      })
      .catch((error) => {
        console.error('Error:', error);
      });
  };

  React.useEffect(() => {
    fetchData(paginationModel.page, paginationModel.pageSize);
  }, [paginationModel]);

  return (
    <Box>
      <CompanyClaimTableListTopBar
        numSelected={selectedRowIds.size}
        selectedIds={selectedRowIds}
        onDelete={handleDelete}
      />
      <DataGridPro
        treeData
        getTreeDataPath={(row) => row.path.split('/')}
        rows={rows}
        columns={columns}
        loading={loading}
        pagination
        paginationMode="server"
        rowCount={rowCount}
        pageSizeOptions={[5, 10, 20]}
        paginationModel={paginationModel}
        onPaginationModelChange={handlePaginationModelChange}
        checkboxSelection
        onRowSelectionModelChange={handleRowSelectionChange}
      />
    </Box>
  );
}
michelengelen commented 3 months ago

@agirgol We currently have no way of controlling the open/closed state of tree data.

The code you provided looks ok, but could you please try to replicate this in a codesandbox with a dummy server? It would really help to see it in action.

agirgol commented 3 months ago

Of course, you can access it from the codesandbox link below. DEMO

michelengelen commented 3 months ago

Ah, now I get it. So the problem is that you are controlling the selectedRowIds. The state update will trigger a rerender and since we have no controlled state for the opened/closed grouped rows in the tree data it will default to the initial state. Not sure id we can do something about that atm. @MBilalShafi do you have any idea if we can provide a workaround for that?

cherniavskii commented 3 months ago

@agirgol

Move the getTreeDataPath definition outside of the component so it has stable reference:

+const getTreeDataPath: DataGridProProps<"getTreeDataPath"> = (row) => row.path.split("/");

<DataGridPro
-  getTreeDataPath={(row) => row.path.split("/")}
+  getTreeDataPath={getTreeDataPath}

Or wrap it with useCallback:

+const getTreeDataPath = React.useCallback<
+  DataGridProProps<"getTreeDataPath">
+>((row) => row.path.split("/"), []);

<DataGridPro
-  getTreeDataPath={(row) => row.path.split("/")}
+  getTreeDataPath={getTreeDataPath}
+

Working demo: https://codesandbox.io/p/sandbox/treedata-forked-tq5xhd?file=%2Fsrc%2Fcomponents%2FTreeDataGrid.tsx%3A121%2C5

agirgol commented 3 months ago

Thank you very much for your support. Your suggestions worked.

github-actions[bot] commented 3 months ago

:warning: 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.

@agirgol: How did we do? Your experience with our support team matters to us. If you have a moment, please share your thoughts in this short Support Satisfaction survey.

cherniavskii commented 3 months ago

@agirgol I've added a warning to the Tree Data docs in https://github.com/mui/mui-x/pull/13519, thanks for your feedback!

agirgol commented 3 months ago

Since my data template is not the same template, I used useCallback by changing it according to the data as follows. https://codesandbox.io/p/sandbox/treedata-h9zn94

const getTreeDataPath: DataGridProProps['getTreeDataPath'] = React.useCallback(
    (row: any) => {
      const pathArray = row.path.split("/");
      return pathArray;
    },
    []
  );