From 11021c594fd859495b762a1098fb529a64f13702 Mon Sep 17 00:00:00 2001 From: Enrique Moreno Date: Wed, 11 Jun 2025 13:55:02 +0200 Subject: [PATCH 01/14] Implementing logic for adding a trigger event for hierarchical data expand --- packages/lib/src/data-grid/DataGrid.tsx | 3 +- packages/lib/src/data-grid/types.ts | 12 +++ packages/lib/src/data-grid/utils.tsx | 105 +++++++++++++----------- 3 files changed, 69 insertions(+), 51 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index a21c4ffa7..177e5e89c 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -166,6 +166,7 @@ const DxcDataGrid = ({ itemsPerPage = 5, itemsPerPageOptions, itemsPerPageFunction, + loadChildren, onSort, onPageChange, totalItems, @@ -234,7 +235,7 @@ const DxcDataGrid = ({ if ((row as HierarchyGridRow).childRows?.length) { return ( - {renderHierarchyTrigger(rowsToRender, row, uniqueRowId, firstColumnKey, setRowsToRender)} + {renderHierarchyTrigger(rowsToRender, row, uniqueRowId, firstColumnKey, setRowsToRender, loadChildren)} ); } diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index 5a6161fc0..dc34776df 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -67,6 +67,10 @@ export type ExpandableRows = { * This prop indicates the unique key that can be used to identify each row. This prop is mandatory if selectable is set to true, expandable is set to true or rows is of type HierarchyGridRow[]. */ uniqueRowId: string; + /** + * Function called whenever a cell with children is expanded. + */ + loadChildren?: never; }; export type HierarchyRows = { @@ -79,6 +83,10 @@ export type HierarchyRows = { * Whether the rows can expand or not. */ expandable?: false; + /** + * Function called whenever a cell with children is expanded. Returns the children array + */ + loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise; }; export type SelectableGridProps = @@ -202,6 +210,10 @@ export type BasicGridProps = { * Whether the rows can expand or not. */ expandable?: false; + /** + * Function called whenever a cell with children is expanded. + */ + loadChildren?: never; }; type Props = CommonProps & diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index 0aa8f13a2..639e1a1a8 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -112,6 +112,7 @@ export const renderExpandableTrigger = ( * @param {string} uniqueRowId - Unique identifier for each row. * @param {string} columnKey - Key of the column that displays the hierarchy trigger. * @param {Function} setRowsToRender - Function to update the rows being rendered. + * @param {Function} loadChildren - Function called whenever a cell with children is expanded. Returns the children array * @returns {JSX.Element} Button that toggles visibility of child rows. */ export const renderHierarchyTrigger = ( @@ -119,57 +120,61 @@ export const renderHierarchyTrigger = ( triggerRow: HierarchyGridRow, uniqueRowId: string, columnKey: string, - setRowsToRender: (_value: SetStateAction) => void -) => ( - -); + triggerRow.visibleChildren = !triggerRow.visibleChildren; + setRowsToRender(newRowsToRender); + }} + > + + {triggerRow[columnKey]} + + ); +}; /** * Renders a checkbox for row selection. From 46a61f5251a542acfa0eb0b2f60bfa2587701df9 Mon Sep 17 00:00:00 2001 From: Enrique Moreno Date: Wed, 11 Jun 2025 17:09:55 +0200 Subject: [PATCH 02/14] Fixed behavior for loadChildren --- .../lib/src/data-grid/DataGrid.stories.tsx | 57 +++++++++++++++++++ packages/lib/src/data-grid/DataGrid.tsx | 17 +++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index ea4a0728f..c0a6b4860 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -444,6 +444,29 @@ const childRows: HierarchyGridRow[] = [ }, ] as HierarchyGridRow[]; +const childRowsLazy: HierarchyGridRow[] = [ + { + name: "Root Node 1 Lazy", + value: "1", + id: "lazy-a", + }, + { + name: "Root Node 2 Lazy", + value: "2", + id: "lazy-b", + }, + { + name: "Root Node 3 Lazy", + value: "3", + id: "lazy-c", + }, + { + name: "Root Node 4 Lazy", + value: "4", + id: "lazy-d", + }, +] as HierarchyGridRow[]; + const childRowsPaginated: HierarchyGridRow[] = [ { name: "Paginated Node 1", @@ -728,6 +751,31 @@ const DataGridControlled = () => { onSelectRows={setSelectedChildRows} /> + + + <DxcDataGrid + columns={childcolumns} + rows={childRowsLazy} + uniqueRowId="id" + selectable + selectedRows={selectedRows} + onSelectRows={setSelectedRows} + loadChildren={(triggerRow) => { + return [ + { + name: `${triggerRow.name} Child 1`, + value: triggerRow.value, + id: `${triggerRow.id}-child-1`, + }, + { + name: `${triggerRow.name} Child 2`, + value: triggerRow.value, + id: `${triggerRow.id}-child-2`, + }, + ] as HierarchyGridRow[]; + }} + /> + </ExampleContainer> <ExampleContainer> <Title title="Empty Data Grid" theme="light" level={4} /> <DxcDataGrid @@ -997,6 +1045,15 @@ export const Chromatic: Story = { export const Controlled: Story = { render: DataGridControlled, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText("Root Node 1 Lazy")); + await userEvent.click(canvas.getByText("Root Node 2 Lazy")); + await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 1")); + await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 2")); + await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 1")); + await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 2")); + }, }; export const CustomSort: Story = { diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index 177e5e89c..aa14676cf 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -225,17 +225,28 @@ const DxcDataGrid = ({ ...expectedColumns, ]; } - if (!expandable && rows.some((row) => Array.isArray(row.childRows) && row.childRows.length > 0) && uniqueRowId) { + if ( + !expandable && + (rows.some((row) => Array.isArray(row.childRows) && row.childRows.length > 0) || loadChildren) && + uniqueRowId + ) { // only the first column will be clickable and will expand the rows const firstColumnKey = expectedColumns[0]?.key; if (firstColumnKey) { expectedColumns[0] = { ...expectedColumns[0]!, renderCell({ row }) { - if ((row as HierarchyGridRow).childRows?.length) { + if ((row as HierarchyGridRow).childRows?.length || loadChildren) { return ( <HierarchyContainer level={typeof row.rowLevel === "number" ? row.rowLevel : 0}> - {renderHierarchyTrigger(rowsToRender, row, uniqueRowId, firstColumnKey, setRowsToRender, loadChildren)} + {renderHierarchyTrigger( + rowsToRender, + row, + uniqueRowId, + firstColumnKey, + setRowsToRender, + loadChildren + )} </HierarchyContainer> ); } From 2d15e670da38168ccdcaf4f1821c34978c5099bc Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Wed, 11 Jun 2025 17:29:40 +0200 Subject: [PATCH 03/14] Updated doc --- .../data-grid/code/DataGridCodePage.tsx | 16 ++++++++++++++++ packages/lib/src/data-grid/types.ts | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index 8a9d45911..f178a667b 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -282,6 +282,22 @@ const sections = [ <td>Number of total items.</td> <td>-</td> </tr> + <tr> + <td> + <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> + <StatusBadge status="new" /> + loadChildren + </DxcFlex> + </td> + <td> + <TableCode>{`(triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>`}</TableCode> + </td> + <td> + Function called whenever a cell with children (<Code>HierarchyGridRow</Code>) is expanded. Returns the + children array. + </td> + <td>-</td> + </tr> </tbody> </DxcTable> ), diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index dc34776df..d866c11a7 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -68,7 +68,7 @@ export type ExpandableRows = { */ uniqueRowId: string; /** - * Function called whenever a cell with children is expanded. + * Function called whenever a cell with children is expanded. Returns the children array. */ loadChildren?: never; }; @@ -86,7 +86,7 @@ export type HierarchyRows = { /** * Function called whenever a cell with children is expanded. Returns the children array */ - loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>; + loadChildren?: (triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>; }; export type SelectableGridProps = @@ -211,7 +211,7 @@ export type BasicGridProps = { */ expandable?: false; /** - * Function called whenever a cell with children is expanded. + * Function called whenever a cell with children is expanded. Returns the children array. */ loadChildren?: never; }; From 44fce81309f11801a72295d4c1d2a6cd83643e23 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Thu, 12 Jun 2025 08:55:51 +0200 Subject: [PATCH 04/14] Added test for new Datagrid feature --- .../data-grid/code/DataGridCodePage.tsx | 162 +++++++++--------- packages/lib/src/data-grid/DataGrid.test.tsx | 62 ++++++- 2 files changed, 142 insertions(+), 82 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index f178a667b..c8007127a 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -116,26 +116,11 @@ const sections = [ <td>-</td> </tr> <tr> + <td>defaultPage</td> <td> - <StatusBadge status="required" /> - rows - </td> - <td> - <TableCode>GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]</TableCode> - <p>Each one of them being in order:</p> - <p> - <ExtendedTableCode>{GridRowTypeString}</ExtendedTableCode> - </p> - <p> - <ExtendedTableCode>{HierarchyGridRowTypeString}</ExtendedTableCode> - </p> - <p> - <ExtendedTableCode>{ExpandableGridRowTypeString}</ExtendedTableCode> - </p> - </td> - <td> - List of rows that will be rendered in each cell based on the <Code>key</Code> in each column. + <TableCode>number</TableCode> </td> + <td>Default page in which the datagrid is rendered.</td> <td>-</td> </tr> <tr> @@ -147,32 +132,45 @@ const sections = [ <td>-</td> </tr> <tr> - <td>selectable</td> + <td>itemsPerPage</td> <td> - <TableCode>boolean</TableCode> + <TableCode>number</TableCode> </td> - <td>Whether the rows are selectable or not.</td> - <td>-</td> + <td>Number of items per page.</td> + <td>5</td> </tr> <tr> - <td>selectedRows</td> + <td>itemsPerPageFunction</td> <td> - <TableCode>{`Set<string | number>`}</TableCode> + <TableCode>{`(value: number) => void`}</TableCode> </td> <td> - Set of selected rows. This prop is mandatory if <Code>selectable</Code> is set to true. The{" "} - <Code>uniqueRowId</Code> key will be used to identify the each row. + This function will be called when the user selects an item per page option. The value selected will be + passed as a parameter. </td> <td>-</td> </tr> <tr> - <td>onSelectRows</td> + <td>itemsPerPageOptions</td> <td> - <TableCode>{`(selectedRows: Set<number | string>) => void`}</TableCode> + <TableCode>number[]</TableCode> </td> + <td>An array of numbers representing the items per page options.</td> + <td>-</td> + </tr> + <tr> <td> - Function called whenever the selected values changes. This prop is mandatory if <Code>selectable</Code> is - set to true.The <Code>uniqueRowId</Code> key will be used to identify the rows. + <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> + <StatusBadge status="new" /> + loadChildren + </DxcFlex> + </td> + <td> + <TableCode>{`(triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>`}</TableCode> + </td> + <td> + Function called whenever a cell with children (<Code>HierarchyGridRow</Code>) is expanded. Returns the + children array. </td> <td>-</td> </tr> @@ -185,94 +183,100 @@ const sections = [ <td>-</td> </tr> <tr> - <td>onSort</td> + <td>onSelectRows</td> <td> - <TableCode>{`(sortColumn?: { columnKey: string, direction: 'ASC' | 'DESC' }) => void`}</TableCode> + <TableCode>{`(selectedRows: Set<number | string>) => void`}</TableCode> </td> <td> - Function called whenever a column is sorted. Receives the sorted column and direction, or `undefined` if - no sorting is applied. + Function called whenever the selected values changes. This prop is mandatory if <Code>selectable</Code> is + set to true.The <Code>uniqueRowId</Code> key will be used to identify the rows. </td> <td>-</td> </tr> <tr> - <td>uniqueRowId</td> + <td>onSort</td> <td> - <TableCode>string</TableCode> + <TableCode>{`(sortColumn?: { columnKey: string, direction: 'ASC' | 'DESC' }) => void`}</TableCode> </td> <td> - This prop indicates the unique key that can be used to identify each row. The value of that key can be - either a number or a string. This prop is mandatory if <Code>selectable</Code> is set to true,{" "} - <Code>expandable</Code> is set to true or <Code>rows</Code> is of type <Code>HierarchyGridRow[]</Code>. + Function called whenever a column is sorted. Receives the sorted column and direction, or `undefined` if + no sorting is applied. </td> <td>-</td> </tr> <tr> - <td>summaryRow</td> + <td>onPageChange</td> <td> - <TableCode>GridRow</TableCode> + <TableCode>{`(page: number) => void`}</TableCode> </td> - <td>Extra row that will be always visible.</td> + <td>Function called whenever the current page is changed.</td> <td>-</td> </tr> <tr> - <td>showPaginator</td> <td> - <TableCode>boolean</TableCode> + <StatusBadge status="required" /> + rows </td> - <td>If true, paginator will be displayed.</td> - <td>false</td> - </tr> - <tr> - <td>defaultPage</td> <td> - <TableCode>number</TableCode> + <TableCode>GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]</TableCode> + <p>Each one of them being in order:</p> + <p> + <ExtendedTableCode>{GridRowTypeString}</ExtendedTableCode> + </p> + <p> + <ExtendedTableCode>{HierarchyGridRowTypeString}</ExtendedTableCode> + </p> + <p> + <ExtendedTableCode>{ExpandableGridRowTypeString}</ExtendedTableCode> + </p> </td> - <td>Default page in which the datagrid is rendered.</td> - <td>-</td> - </tr> - <tr> - <td>itemsPerPage</td> <td> - <TableCode>number</TableCode> + List of rows that will be rendered in each cell based on the <Code>key</Code> in each column. </td> - <td>Number of items per page.</td> - <td>5</td> + <td>-</td> </tr> <tr> - <td>itemsPerPageOptions</td> + <td>selectable</td> <td> - <TableCode>number[]</TableCode> + <TableCode>boolean</TableCode> </td> - <td>An array of numbers representing the items per page options.</td> + <td>Whether the rows are selectable or not.</td> <td>-</td> </tr> <tr> - <td>itemsPerPageFunction</td> + <td>selectedRows</td> <td> - <TableCode>{`(value: number) => void`}</TableCode> + <TableCode>{`Set<string | number>`}</TableCode> </td> <td> - This function will be called when the user selects an item per page option. The value selected will be - passed as a parameter. + Set of selected rows. This prop is mandatory if <Code>selectable</Code> is set to true. The{" "} + <Code>uniqueRowId</Code> key will be used to identify the each row. </td> <td>-</td> </tr> <tr> - <td>onPageChange</td> + <td>showGoToPage</td> <td> - <TableCode>{`(page: number) => void`}</TableCode> + <TableCode>boolean</TableCode> </td> - <td>Function called whenever the current page is changed.</td> - <td>-</td> + <td>If true, a select component for navigation between pages will be displayed.</td> + <td>true</td> </tr> <tr> - <td>showGoToPage</td> + <td>showPaginator</td> <td> <TableCode>boolean</TableCode> </td> - <td>If true, a select component for navigation between pages will be displayed.</td> - <td>true</td> + <td>If true, paginator will be displayed.</td> + <td>false</td> + </tr> + <tr> + <td>summaryRow</td> + <td> + <TableCode>GridRow</TableCode> + </td> + <td>Extra row that will be always visible.</td> + <td>-</td> </tr> <tr> <td>totalItems</td> @@ -283,18 +287,14 @@ const sections = [ <td>-</td> </tr> <tr> + <td>uniqueRowId</td> <td> - <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> - <StatusBadge status="new" /> - loadChildren - </DxcFlex> - </td> - <td> - <TableCode>{`(triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>`}</TableCode> + <TableCode>string</TableCode> </td> <td> - Function called whenever a cell with children (<Code>HierarchyGridRow</Code>) is expanded. Returns the - children array. + This prop indicates the unique key that can be used to identify each row. The value of that key can be + either a number or a string. This prop is mandatory if <Code>selectable</Code> is set to true,{" "} + <Code>expandable</Code> is set to true or <Code>rows</Code> is of type <Code>HierarchyGridRow[]</Code>. </td> <td>-</td> </tr> diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx index d421b7b27..83e4678f0 100644 --- a/packages/lib/src/data-grid/DataGrid.test.tsx +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, waitFor } from "@testing-library/react"; import DxcDataGrid from "./DataGrid"; -import { GridColumn } from "./types"; +import { GridColumn, HierarchyGridRow } from "./types"; Object.defineProperty(window, "getComputedStyle", { value: () => ({ @@ -205,6 +205,34 @@ const hierarchyRows: HierarchyGridRow[] = [ }, ] as HierarchyGridRow[]; +const hierarchyRowsLazy: HierarchyGridRow[] = [ + { + name: "Root Node 1 Lazy", + value: "1", + id: "lazy-a", + }, + { + name: "Root Node 2 Lazy", + value: "2", + id: "lazy-b", + }, + { + name: "Root Node 3 Lazy", + value: "3", + id: "lazy-c", + }, + { + name: "Root Node 4 Lazy", + value: "4", + id: "lazy-d", + }, + { + name: "Root Node 5 Lazy", + value: "5", + id: "lazy-e", + }, +] as HierarchyGridRow[]; + describe("Data grid component tests", () => { beforeAll(() => { (global as any).CSS = { @@ -237,6 +265,38 @@ describe("Data grid component tests", () => { expect(rows.length).toBe(5); }); + test("Triggers loadChildren when expanding hierarchy row", async () => { + const onSelectRows = jest.fn(); + const selectedRows = new Set<number | string>(); + + const mockLoadedChildren = [ + { id: "child-1", name: "Child 1", value: "Child 1" }, + { id: "child-2", name: "Child 2", value: "Child 2" }, + ]; + + const loadChildrenMock = jest.fn().mockResolvedValue(mockLoadedChildren); + + const { getAllByRole } = render( + <DxcDataGrid + columns={columns} + rows={hierarchyRowsLazy} + uniqueRowId="id" + selectable + onSelectRows={onSelectRows} + selectedRows={selectedRows} + loadChildren={loadChildrenMock} + /> + ); + + expect(getAllByRole("row").length).toBe(5); + + const buttons = getAllByRole("button"); + + buttons[0] && fireEvent.click(buttons[0]); + + expect(loadChildrenMock).toHaveBeenCalledWith(expect.objectContaining({ id: "lazy-a" })); + }); + test("Renders column headers", () => { const { getByText } = render(<DxcDataGrid columns={columns} rows={expandableRows} />); expect(getByText("ID")).toBeTruthy(); From 9ea9757e306513ddf3b87bc5d40e155a5f5b5312 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Thu, 12 Jun 2025 08:56:53 +0200 Subject: [PATCH 05/14] Sorted props alphabetically in doc --- .../data-grid/code/DataGridCodePage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index c8007127a..18d46f49f 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -182,6 +182,14 @@ const sections = [ <td>Function called whenever a cell is edited.</td> <td>-</td> </tr> + <tr> + <td>onPageChange</td> + <td> + <TableCode>{`(page: number) => void`}</TableCode> + </td> + <td>Function called whenever the current page is changed.</td> + <td>-</td> + </tr> <tr> <td>onSelectRows</td> <td> @@ -204,14 +212,6 @@ const sections = [ </td> <td>-</td> </tr> - <tr> - <td>onPageChange</td> - <td> - <TableCode>{`(page: number) => void`}</TableCode> - </td> - <td>Function called whenever the current page is changed.</td> - <td>-</td> - </tr> <tr> <td> <StatusBadge status="required" /> From ef3617fd1c5d9b7dac82148ce363a000be7213d7 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Tue, 1 Jul 2025 15:06:29 +0200 Subject: [PATCH 06/14] Added fix for lazy children selection from parent --- .../lib/src/data-grid/DataGrid.stories.tsx | 30 ++++++++++ packages/lib/src/data-grid/DataGrid.tsx | 3 +- packages/lib/src/data-grid/utils.tsx | 60 +++++++++++-------- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index c0a6b4860..089305460 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -838,6 +838,36 @@ const DataGridControlled = () => { totalItems={expandableRows.length} /> </ExampleContainer> + <ExampleContainer> + <Title title="DataGrid with loadChildren function" theme="light" level={4} /> + <DxcDataGrid + columns={childcolumns} + rows={childRowsLazy} + uniqueRowId="id" + selectable + selectedRows={selectedRows} + onSelectRows={setSelectedRows} + loadChildren={(triggerRow) => { + console.log("loadChildren"); + return new Promise<HierarchyGridRow[]>((resolve) => { + setTimeout(() => { + resolve([ + { + name: `${triggerRow.name} Child 1`, + value: triggerRow.value, + id: `${triggerRow.id}-child-1`, + }, + { + name: `${triggerRow.name} Child 2`, + value: triggerRow.value, + id: `${triggerRow.id}-child-2`, + }, + ]); + }, 5000); + }); + }} + /> + </ExampleContainer> </> ); }; diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index aa14676cf..0e0a988a7 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -245,7 +245,8 @@ const DxcDataGrid = ({ uniqueRowId, firstColumnKey, setRowsToRender, - loadChildren + loadChildren, + selectedRows )} </HierarchyContainer> ); diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index 639e1a1a8..44c1e6901 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -113,6 +113,7 @@ export const renderExpandableTrigger = ( * @param {string} columnKey - Key of the column that displays the hierarchy trigger. * @param {Function} setRowsToRender - Function to update the rows being rendered. * @param {Function} loadChildren - Function called whenever a cell with children is expanded. Returns the children array + * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. * @returns {JSX.Element} Button that toggles visibility of child rows. */ export const renderHierarchyTrigger = ( @@ -121,7 +122,8 @@ export const renderHierarchyTrigger = ( uniqueRowId: string, columnKey: string, setRowsToRender: (_value: SetStateAction<GridRow[] | ExpandableGridRow[] | HierarchyGridRow[]>) => void, - loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]> + loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>, + selectedRows?: Set<string | number> ) => { return ( <button @@ -131,8 +133,16 @@ export const renderHierarchyTrigger = ( let newRowsToRender = [...rows]; if (!triggerRow.visibleChildren) { const rowIndex = rows.findIndex((row) => triggerRow === row); - const loadedChildren = loadChildren ? await loadChildren(triggerRow) : (triggerRow.childRows ?? []); - loadedChildren.forEach((childRow: HierarchyGridRow, index: number) => { + if (loadChildren) { + const dynamicChildren = await loadChildren(triggerRow); + triggerRow.childRows = dynamicChildren; + if (selectedRows.has(rowKeyGetter(triggerRow, uniqueRowId))) { + dynamicChildren.forEach((child) => { + selectedRows.add(rowKeyGetter(child, uniqueRowId)); + }); + } + } + triggerRow.childRows.forEach((childRow: HierarchyGridRow, index: number) => { childRow.rowLevel = triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); @@ -191,27 +201,29 @@ export const renderCheckbox = ( uniqueRowId: string, selectedRows: Set<string | number>, onSelectRows: (_selected: Set<string | number>) => void -) => ( - <DxcCheckbox - checked={selectedRows.has(rowKeyGetter(row, uniqueRowId))} - onChange={(checked) => { - const selected = new Set(selectedRows); - if (checked) { - selected.add(rowKeyGetter(row, uniqueRowId)); - } else { - selected.delete(rowKeyGetter(row, uniqueRowId)); - } - if (row.childRows && Array.isArray(row.childRows)) { - getChildrenSelection(row.childRows, uniqueRowId, selected, checked); - } - if (row.parentKey) { - getParentSelectedState(rows, row.parentKey, uniqueRowId, selected, checked); - } - onSelectRows(selected); - }} - disabled={!rows.some((row) => uniqueRowId in row)} - /> -); +) => { + return ( + <DxcCheckbox + checked={selectedRows.has(rowKeyGetter(row, uniqueRowId))} + onChange={(checked) => { + const selected = new Set(selectedRows); + if (checked) { + selected.add(rowKeyGetter(row, uniqueRowId)); + } else { + selected.delete(rowKeyGetter(row, uniqueRowId)); + } + if (row.childRows && Array.isArray(row.childRows)) { + getChildrenSelection(row.childRows, uniqueRowId, selected, checked); + } + if (row.parentKey) { + getParentSelectedState(rows, row.parentKey, uniqueRowId, selected, checked); + } + onSelectRows(selected); + }} + disabled={!rows.some((row) => uniqueRowId in row)} + /> + ); +}; /** * Renders a header checkbox that controls the selection of all rows. From d216d427e36047a029463ab2fd24d9979a40e97c Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Wed, 2 Jul 2025 10:44:32 +0200 Subject: [PATCH 07/14] Added optional chaining to prevent error --- packages/lib/src/data-grid/utils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index 44c1e6901..d6d42accf 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -136,13 +136,13 @@ export const renderHierarchyTrigger = ( if (loadChildren) { const dynamicChildren = await loadChildren(triggerRow); triggerRow.childRows = dynamicChildren; - if (selectedRows.has(rowKeyGetter(triggerRow, uniqueRowId))) { + if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { dynamicChildren.forEach((child) => { selectedRows.add(rowKeyGetter(child, uniqueRowId)); }); } } - triggerRow.childRows.forEach((childRow: HierarchyGridRow, index: number) => { + triggerRow.childRows?.forEach((childRow: HierarchyGridRow, index: number) => { childRow.rowLevel = triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); From aa313a85264615626ae02421aa940cd054c7378b Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Wed, 2 Jul 2025 12:48:30 +0200 Subject: [PATCH 08/14] Added spinner while lazy loading datagrid --- packages/lib/src/data-grid/utils.tsx | 116 ++++++++++++++------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index d6d42accf..e78f41d25 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -1,13 +1,14 @@ // TODO: Remove eslint disable /* eslint-disable no-param-reassign */ -import { ReactNode, SetStateAction } from "react"; +import { ReactNode, SetStateAction, useState } from "react"; import { Column, RenderSortStatusProps, SortColumn, textEditor } from "react-data-grid"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcCheckbox from "../checkbox/Checkbox"; import { DeepPartial, HalstackProvider } from "../HalstackContext"; import DxcIcon from "../icon/Icon"; import { GridColumn, HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; +import DxcSpinner from "../spinner/Spinner"; /** * Converts grid columns into react-data-grid column format. @@ -125,62 +126,67 @@ export const renderHierarchyTrigger = ( loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>, selectedRows?: Set<string | number> ) => { - return ( - <button - type="button" - disabled={!rows.some((row) => uniqueRowId in row)} - onClick={async () => { - let newRowsToRender = [...rows]; - if (!triggerRow.visibleChildren) { - const rowIndex = rows.findIndex((row) => triggerRow === row); - if (loadChildren) { - const dynamicChildren = await loadChildren(triggerRow); - triggerRow.childRows = dynamicChildren; - if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { - dynamicChildren.forEach((child) => { - selectedRows.add(rowKeyGetter(child, uniqueRowId)); - }); - } - } - triggerRow.childRows?.forEach((childRow: HierarchyGridRow, index: number) => { - childRow.rowLevel = - triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; - childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); - addRow(newRowsToRender, rowIndex + 1 + index, childRow); + const [loading, setLoading] = useState(false); + const onClick = async () => { + if (loading) return; // Prevent double clicks while loading + setLoading(true); + let newRowsToRender = [...rows]; + if (!triggerRow.visibleChildren) { + const rowIndex = rows.findIndex((row) => triggerRow === row); + if (loadChildren) { + const dynamicChildren = await loadChildren(triggerRow); + triggerRow.childRows = dynamicChildren; + if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { + dynamicChildren.forEach((child) => { + selectedRows.add(rowKeyGetter(child, uniqueRowId)); }); - } else { - // The children of the row that is being collapsed are added to an array - const rowsToRemove: HierarchyGridRow[] = [ - ...rows.filter( - (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) - ), - ]; - // The children are checked if any of them has any other children of their own - const rowsToCheck = [...rowsToRemove]; - while (rowsToCheck.length > 0) { - const currentRow = rowsToCheck.pop(); - const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; - - rowsToRemove.push(...childRows); - rowsToCheck.push(...childRows); - } - newRowsToRender = rows.filter( - (row) => - !rowsToRemove - .map((rowToRemove) => { - if (rowToRemove.visibleChildren) { - rowToRemove.visibleChildren = false; - } - return rowKeyGetter(rowToRemove, uniqueRowId); - }) - .includes(rowKeyGetter(row, uniqueRowId)) - ); } - triggerRow.visibleChildren = !triggerRow.visibleChildren; - setRowsToRender(newRowsToRender); - }} - > - <DxcIcon icon={triggerRow.visibleChildren ? "Keyboard_Arrow_Down" : "Chevron_Right"} /> + } + triggerRow.childRows?.forEach((childRow: HierarchyGridRow, index: number) => { + childRow.rowLevel = + triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; + childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); + addRow(newRowsToRender, rowIndex + 1 + index, childRow); + }); + } else { + // The children of the row that is being collapsed are added to an array + const rowsToRemove: HierarchyGridRow[] = [ + ...rows.filter( + (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) + ), + ]; + // The children are checked if any of them has any other children of their own + const rowsToCheck = [...rowsToRemove]; + while (rowsToCheck.length > 0) { + const currentRow = rowsToCheck.pop(); + const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; + + rowsToRemove.push(...childRows); + rowsToCheck.push(...childRows); + } + newRowsToRender = rows.filter( + (row) => + !rowsToRemove + .map((rowToRemove) => { + if (rowToRemove.visibleChildren) { + rowToRemove.visibleChildren = false; + } + return rowKeyGetter(rowToRemove, uniqueRowId); + }) + .includes(rowKeyGetter(row, uniqueRowId)) + ); + } + triggerRow.visibleChildren = !triggerRow.visibleChildren; + setRowsToRender(newRowsToRender); + setLoading(false); + }; + return ( + <button type="button" disabled={!rows.some((row) => uniqueRowId in row)} onClick={onClick}> + {loading ? ( + <DxcSpinner mode="small"/> + ) : ( + <DxcIcon icon={triggerRow.visibleChildren ? "Keyboard_Arrow_Down" : "Chevron_Right"} /> + )} <span className="ellipsis-cell">{triggerRow[columnKey]}</span> </button> ); From a7209c1c3ce2c06c2a6f43dd59f68407403d73c9 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Wed, 2 Jul 2025 14:15:35 +0200 Subject: [PATCH 09/14] Converted loadChildren into an internal prop for each row --- .../data-grid/code/DataGridCodePage.tsx | 3 + .../lib/src/data-grid/DataGrid.stories.tsx | 60 ++++++++----------- packages/lib/src/data-grid/DataGrid.tsx | 17 +++--- packages/lib/src/data-grid/types.ts | 24 ++++---- 4 files changed, 49 insertions(+), 55 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index 18d46f49f..e5f47f781 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -33,6 +33,9 @@ const GridRowTypeString = `{ const HierarchyGridRowTypeString = `GridRow & { childRows?: HierarchyGridRow[] | GridRow[]; + loadChildren?: ( + triggerRow: HierarchyGridRow + ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>; }`; const ExpandableGridRowTypeString = `GridRow & { diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index 089305460..6760d5af9 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -2,7 +2,7 @@ import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcDataGrid from "./DataGrid"; import DxcContainer from "../container/Container"; -import { GridColumn, HierarchyGridRow } from "./types"; +import { GridColumn, GridRow, HierarchyGridRow } from "./types"; import { isValidElement, useState } from "react"; import { disabledRules } from "../../test/accessibility/rules/specific/data-grid/disabledRules"; import preview from "../../.storybook/preview"; @@ -444,26 +444,51 @@ const childRows: HierarchyGridRow[] = [ }, ] as HierarchyGridRow[]; +const loadChildren = (triggerRow: HierarchyGridRow) => { + return new Promise<HierarchyGridRow[]>((resolve) => { + setTimeout(() => { + resolve([ + { + name: `${triggerRow.name} Child 1`, + value: triggerRow.value, + id: `${triggerRow.id}-child-1`, + loadChildren, + }, + { + name: `${triggerRow.name} Child 2`, + value: triggerRow.value, + id: `${triggerRow.id}-child-2`, + loadChildren, + }, + ]); + }, 5000); + }); +}; + const childRowsLazy: HierarchyGridRow[] = [ { name: "Root Node 1 Lazy", value: "1", id: "lazy-a", + loadChildren, }, { name: "Root Node 2 Lazy", value: "2", id: "lazy-b", + loadChildren, }, { name: "Root Node 3 Lazy", value: "3", id: "lazy-c", + loadChildren, }, { name: "Root Node 4 Lazy", value: "4", id: "lazy-d", + loadChildren, }, ] as HierarchyGridRow[]; @@ -760,20 +785,6 @@ const DataGridControlled = () => { selectable selectedRows={selectedRows} onSelectRows={setSelectedRows} - loadChildren={(triggerRow) => { - return [ - { - name: `${triggerRow.name} Child 1`, - value: triggerRow.value, - id: `${triggerRow.id}-child-1`, - }, - { - name: `${triggerRow.name} Child 2`, - value: triggerRow.value, - id: `${triggerRow.id}-child-2`, - }, - ] as HierarchyGridRow[]; - }} /> </ExampleContainer> <ExampleContainer> @@ -847,25 +858,6 @@ const DataGridControlled = () => { selectable selectedRows={selectedRows} onSelectRows={setSelectedRows} - loadChildren={(triggerRow) => { - console.log("loadChildren"); - return new Promise<HierarchyGridRow[]>((resolve) => { - setTimeout(() => { - resolve([ - { - name: `${triggerRow.name} Child 1`, - value: triggerRow.value, - id: `${triggerRow.id}-child-1`, - }, - { - name: `${triggerRow.name} Child 2`, - value: triggerRow.value, - id: `${triggerRow.id}-child-2`, - }, - ]); - }, 5000); - }); - }} /> </ExampleContainer> </> diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index 0e0a988a7..28cf57418 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -166,7 +166,6 @@ const DxcDataGrid = ({ itemsPerPage = 5, itemsPerPageOptions, itemsPerPageFunction, - loadChildren, onSort, onPageChange, totalItems, @@ -225,18 +224,20 @@ const DxcDataGrid = ({ ...expectedColumns, ]; } - if ( - !expandable && - (rows.some((row) => Array.isArray(row.childRows) && row.childRows.length > 0) || loadChildren) && - uniqueRowId - ) { + const rowHasHierarchy = (row: GridRow | HierarchyGridRow): row is HierarchyGridRow => { + return ( + (Array.isArray(row.childRows) && row.childRows.length > 0) || + typeof (row as HierarchyGridRow).loadChildren === "function" + ); + }; + if (!expandable && rows.some((row) => rowHasHierarchy) && uniqueRowId) { // only the first column will be clickable and will expand the rows const firstColumnKey = expectedColumns[0]?.key; if (firstColumnKey) { expectedColumns[0] = { ...expectedColumns[0]!, renderCell({ row }) { - if ((row as HierarchyGridRow).childRows?.length || loadChildren) { + if (rowHasHierarchy(row)) { return ( <HierarchyContainer level={typeof row.rowLevel === "number" ? row.rowLevel : 0}> {renderHierarchyTrigger( @@ -245,7 +246,7 @@ const DxcDataGrid = ({ uniqueRowId, firstColumnKey, setRowsToRender, - loadChildren, + row.loadChildren, selectedRows )} </HierarchyContainer> diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index d866c11a7..21e73b590 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -44,11 +44,21 @@ export type GridRow = { /** * List of rows that will be rendered in each cell based on the key in each column. */ - [key: string]: ReactNode | undefined; + [key: string]: ReactNode | Function | undefined; }; export type HierarchyGridRow = GridRow & { + /** + * Array of child rows nested under this row, enabling hierarchical (tree-like) structures. + * These child rows will be displayed when the parent row is expanded. + */ childRows?: HierarchyGridRow[] | GridRow[]; + /** + * Function called whenever a cell with children is expanded. Returns the children array + */ + loadChildren?: ( + triggerRow: HierarchyGridRow + ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>; }; export type ExpandableGridRow = GridRow & { @@ -67,10 +77,6 @@ export type ExpandableRows = { * This prop indicates the unique key that can be used to identify each row. This prop is mandatory if selectable is set to true, expandable is set to true or rows is of type HierarchyGridRow[]. */ uniqueRowId: string; - /** - * Function called whenever a cell with children is expanded. Returns the children array. - */ - loadChildren?: never; }; export type HierarchyRows = { @@ -83,10 +89,6 @@ export type HierarchyRows = { * Whether the rows can expand or not. */ expandable?: false; - /** - * Function called whenever a cell with children is expanded. Returns the children array - */ - loadChildren?: (triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>; }; export type SelectableGridProps = @@ -210,10 +212,6 @@ export type BasicGridProps = { * Whether the rows can expand or not. */ expandable?: false; - /** - * Function called whenever a cell with children is expanded. Returns the children array. - */ - loadChildren?: never; }; type Props = CommonProps & From f1ca9fa94ddc22a9ee71d7a677bf85b458632ab4 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Fri, 4 Jul 2025 08:55:21 +0200 Subject: [PATCH 10/14] Added possibility to handle both expand and collapse actions --- .../data-grid/code/DataGridCodePage.tsx | 7 +- .../lib/src/data-grid/DataGrid.stories.tsx | 79 ++++----- packages/lib/src/data-grid/DataGrid.test.tsx | 20 +-- packages/lib/src/data-grid/DataGrid.tsx | 12 +- packages/lib/src/data-grid/types.ts | 7 +- packages/lib/src/data-grid/utils.tsx | 157 ++++++++++++------ 6 files changed, 166 insertions(+), 116 deletions(-) diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx index e5f47f781..50569816f 100644 --- a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -33,7 +33,8 @@ const GridRowTypeString = `{ const HierarchyGridRowTypeString = `GridRow & { childRows?: HierarchyGridRow[] | GridRow[]; - loadChildren?: ( + childrenTrigger?: ( + open: boolean, triggerRow: HierarchyGridRow ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>; }`; @@ -165,11 +166,11 @@ const sections = [ <td> <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> <StatusBadge status="new" /> - loadChildren + childrenTrigger </DxcFlex> </td> <td> - <TableCode>{`(triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>`}</TableCode> + <TableCode>{`(open: boolean, triggerRow: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>`}</TableCode> </td> <td> Function called whenever a cell with children (<Code>HierarchyGridRow</Code>) is expanded. Returns the diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index 6760d5af9..97b754972 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -325,7 +325,7 @@ const childcolumns: GridColumn[] = [ }, ]; -const childRows: HierarchyGridRow[] = [ +const childRows = [ { name: "Root Node 1", value: "1", @@ -444,55 +444,59 @@ const childRows: HierarchyGridRow[] = [ }, ] as HierarchyGridRow[]; -const loadChildren = (triggerRow: HierarchyGridRow) => { - return new Promise<HierarchyGridRow[]>((resolve) => { - setTimeout(() => { - resolve([ - { - name: `${triggerRow.name} Child 1`, - value: triggerRow.value, - id: `${triggerRow.id}-child-1`, - loadChildren, - }, - { - name: `${triggerRow.name} Child 2`, - value: triggerRow.value, - id: `${triggerRow.id}-child-2`, - loadChildren, - }, - ]); - }, 5000); - }); +const childrenTrigger = (open: boolean, triggerRow: HierarchyGridRow) => { + if (open) { + return new Promise<HierarchyGridRow[]>((resolve) => { + setTimeout(() => { + resolve([ + { + name: `${triggerRow.name} Child 1`, + value: triggerRow.value, + id: `${triggerRow.id}-child-1`, + childrenTrigger, + }, + { + name: `${triggerRow.name} Child 2`, + value: triggerRow.value, + id: `${triggerRow.id}-child-2`, + childrenTrigger, + }, + ] as HierarchyGridRow[]); + }, 5000); + }); + } else { + return [] as HierarchyGridRow[]; + } }; -const childRowsLazy: HierarchyGridRow[] = [ +const childRowsLazy = [ { name: "Root Node 1 Lazy", value: "1", id: "lazy-a", - loadChildren, + childrenTrigger, }, { name: "Root Node 2 Lazy", value: "2", id: "lazy-b", - loadChildren, + childrenTrigger, }, { name: "Root Node 3 Lazy", value: "3", id: "lazy-c", - loadChildren, + childrenTrigger, }, { name: "Root Node 4 Lazy", value: "4", id: "lazy-d", - loadChildren, + childrenTrigger, }, -] as HierarchyGridRow[]; +]; -const childRowsPaginated: HierarchyGridRow[] = [ +const childRowsPaginated = [ { name: "Paginated Node 1", value: "1", @@ -777,7 +781,7 @@ const DataGridControlled = () => { /> </ExampleContainer> <ExampleContainer> - <Title title="DataGrid with loadChildren function" theme="light" level={4} /> + <Title title="DataGrid with childrenTrigger function" theme="light" level={4} /> <DxcDataGrid columns={childcolumns} rows={childRowsLazy} @@ -849,17 +853,6 @@ const DataGridControlled = () => { totalItems={expandableRows.length} /> </ExampleContainer> - <ExampleContainer> - <Title title="DataGrid with loadChildren function" theme="light" level={4} /> - <DxcDataGrid - columns={childcolumns} - rows={childRowsLazy} - uniqueRowId="id" - selectable - selectedRows={selectedRows} - onSelectRows={setSelectedRows} - /> - </ExampleContainer> </> ); }; @@ -1071,10 +1064,10 @@ export const Controlled: Story = { const canvas = within(canvasElement); await userEvent.click(canvas.getByText("Root Node 1 Lazy")); await userEvent.click(canvas.getByText("Root Node 2 Lazy")); - await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 1")); - await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 2")); - await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 1")); - await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 2")); + // await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 1")); + // await userEvent.click(canvas.getByText("Root Node 1 Lazy Child 2")); + // await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 1")); + // await userEvent.click(canvas.getByText("Root Node 2 Lazy Child 2")); }, }; diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx index 83e4678f0..2e51ce806 100644 --- a/packages/lib/src/data-grid/DataGrid.test.tsx +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -205,11 +205,19 @@ const hierarchyRows: HierarchyGridRow[] = [ }, ] as HierarchyGridRow[]; +const loadedChildrenMock = [ + { id: "child-1", name: "Child 1", value: "Child 1" }, + { id: "child-2", name: "Child 2", value: "Child 2" }, +]; + +const childrenTriggerMock = jest.fn().mockResolvedValue(loadedChildrenMock); + const hierarchyRowsLazy: HierarchyGridRow[] = [ { name: "Root Node 1 Lazy", value: "1", id: "lazy-a", + childrenTrigger: () => childrenTriggerMock, }, { name: "Root Node 2 Lazy", @@ -265,17 +273,10 @@ describe("Data grid component tests", () => { expect(rows.length).toBe(5); }); - test("Triggers loadChildren when expanding hierarchy row", async () => { + test("Triggers childrenTrigger when expanding hierarchy row", async () => { const onSelectRows = jest.fn(); const selectedRows = new Set<number | string>(); - const mockLoadedChildren = [ - { id: "child-1", name: "Child 1", value: "Child 1" }, - { id: "child-2", name: "Child 2", value: "Child 2" }, - ]; - - const loadChildrenMock = jest.fn().mockResolvedValue(mockLoadedChildren); - const { getAllByRole } = render( <DxcDataGrid columns={columns} @@ -284,7 +285,6 @@ describe("Data grid component tests", () => { selectable onSelectRows={onSelectRows} selectedRows={selectedRows} - loadChildren={loadChildrenMock} /> ); @@ -294,7 +294,7 @@ describe("Data grid component tests", () => { buttons[0] && fireEvent.click(buttons[0]); - expect(loadChildrenMock).toHaveBeenCalledWith(expect.objectContaining({ id: "lazy-a" })); + expect(childrenTriggerMock).toHaveBeenCalledWith(expect.objectContaining({ id: "lazy-a" })); }); test("Renders column headers", () => { diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index 28cf57418..2bc4c25cc 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, ReactNode } from "react"; import DataGrid, { SortColumn } from "react-data-grid"; import styled from "styled-components"; import DataGridPropsType, { HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; @@ -211,7 +211,7 @@ const DxcDataGrid = ({ renderCell({ row }) { if (row.isExpandedChildContent) { // if it is expanded content - return row.expandedChildContent || null; + return (row.expandedChildContent as ReactNode) || null; } // if row has expandable content return ( @@ -227,10 +227,10 @@ const DxcDataGrid = ({ const rowHasHierarchy = (row: GridRow | HierarchyGridRow): row is HierarchyGridRow => { return ( (Array.isArray(row.childRows) && row.childRows.length > 0) || - typeof (row as HierarchyGridRow).loadChildren === "function" + typeof (row as HierarchyGridRow).childrenTrigger === "function" ); }; - if (!expandable && rows.some((row) => rowHasHierarchy) && uniqueRowId) { + if (!expandable && rows.some((row) => rowHasHierarchy(row)) && uniqueRowId) { // only the first column will be clickable and will expand the rows const firstColumnKey = expectedColumns[0]?.key; if (firstColumnKey) { @@ -246,7 +246,7 @@ const DxcDataGrid = ({ uniqueRowId, firstColumnKey, setRowsToRender, - row.loadChildren, + row.childrenTrigger, selectedRows )} </HierarchyContainer> @@ -254,7 +254,7 @@ const DxcDataGrid = ({ } return ( <HierarchyContainer level={typeof row.rowLevel === "number" ? row.rowLevel : 0} className="ellipsis-cell"> - {row[firstColumnKey]} + {row[firstColumnKey] as ReactNode} </HierarchyContainer> ); }, diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index 21e73b590..4228cf966 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -54,10 +54,11 @@ export type HierarchyGridRow = GridRow & { */ childRows?: HierarchyGridRow[] | GridRow[]; /** - * Function called whenever a cell with children is expanded. Returns the children array + * Function called whenever a cell with children is expanded or collapsed. Returns the children array. */ - loadChildren?: ( - triggerRow: HierarchyGridRow + childrenTrigger?: ( + open?: boolean, + triggerRow?: HierarchyGridRow ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>; }; diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index e78f41d25..baab7752c 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -5,7 +5,6 @@ import { ReactNode, SetStateAction, useState } from "react"; import { Column, RenderSortStatusProps, SortColumn, textEditor } from "react-data-grid"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcCheckbox from "../checkbox/Checkbox"; -import { DeepPartial, HalstackProvider } from "../HalstackContext"; import DxcIcon from "../icon/Icon"; import { GridColumn, HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; import DxcSpinner from "../spinner/Spinner"; @@ -30,13 +29,13 @@ export const convertToRDGColumns = ( renderEditCell: gridColumn.textEditable ? textEditor : undefined, renderCell: ({ row }) => ( <div className={`ellipsis-cell ${gridColumn.alignment ? `align-${gridColumn.alignment}` : "align-left"}`}> - {row[gridColumn.key]} + {row[gridColumn.key] as ReactNode} </div> ), renderSummaryCell: () => gridColumn.summaryKey ? ( <div className={`ellipsis-cell ${gridColumn.alignment ? `align-${gridColumn.alignment}` : "align-left"}`}> - {summaryRow?.[gridColumn.summaryKey]} + {summaryRow?.[gridColumn.summaryKey] as ReactNode} </div> ) : undefined, }); @@ -113,7 +112,7 @@ export const renderExpandableTrigger = ( * @param {string} uniqueRowId - Unique identifier for each row. * @param {string} columnKey - Key of the column that displays the hierarchy trigger. * @param {Function} setRowsToRender - Function to update the rows being rendered. - * @param {Function} loadChildren - Function called whenever a cell with children is expanded. Returns the children array + * @param {Function} childrenTrigger - Function called whenever a cell with children is expanded or collapsed. Returns the children array * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. * @returns {JSX.Element} Button that toggles visibility of child rows. */ @@ -123,71 +122,127 @@ export const renderHierarchyTrigger = ( uniqueRowId: string, columnKey: string, setRowsToRender: (_value: SetStateAction<GridRow[] | ExpandableGridRow[] | HierarchyGridRow[]>) => void, - loadChildren?: (expandChildren: HierarchyGridRow) => HierarchyGridRow[] | Promise<HierarchyGridRow[]>, + childrenTrigger?: ( + _open: boolean, + _selectedRow: HierarchyGridRow + ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>, selectedRows?: Set<string | number> ) => { const [loading, setLoading] = useState(false); const onClick = async () => { if (loading) return; // Prevent double clicks while loading setLoading(true); - let newRowsToRender = [...rows]; + if (!triggerRow.visibleChildren) { - const rowIndex = rows.findIndex((row) => triggerRow === row); - if (loadChildren) { - const dynamicChildren = await loadChildren(triggerRow); - triggerRow.childRows = dynamicChildren; - if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { - dynamicChildren.forEach((child) => { - selectedRows.add(rowKeyGetter(child, uniqueRowId)); + if (childrenTrigger) { + try { + const dynamicChildren = await childrenTrigger(true, triggerRow); + triggerRow.childRows = dynamicChildren; + + if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { + dynamicChildren.forEach((child) => { + selectedRows.add(rowKeyGetter(child, uniqueRowId)); + }); + } + + setRowsToRender((currentRows) => { + const newRowsToRender = [...currentRows]; + const rowIndex = currentRows.findIndex((row) => triggerRow === row); + + dynamicChildren.forEach((childRow: HierarchyGridRow, index: number) => { + childRow.rowLevel = + triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; + childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); + addRow(newRowsToRender, rowIndex + 1 + index, childRow); + }); + + return newRowsToRender; }); + } catch (error) { + console.error("Error loading children:", error); } + } else if (triggerRow.childRows) { + setRowsToRender((currentRows) => { + const newRowsToRender = [...currentRows]; + const rowIndex = currentRows.findIndex((row) => triggerRow === row); + + triggerRow.childRows?.forEach((childRow: HierarchyGridRow, index: number) => { + childRow.rowLevel = + triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; + childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); + addRow(newRowsToRender, rowIndex + 1 + index, childRow); + }); + + return newRowsToRender; + }); } - triggerRow.childRows?.forEach((childRow: HierarchyGridRow, index: number) => { - childRow.rowLevel = - triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; - childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); - addRow(newRowsToRender, rowIndex + 1 + index, childRow); - }); } else { - // The children of the row that is being collapsed are added to an array - const rowsToRemove: HierarchyGridRow[] = [ - ...rows.filter( - (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) - ), - ]; - // The children are checked if any of them has any other children of their own - const rowsToCheck = [...rowsToRemove]; - while (rowsToCheck.length > 0) { - const currentRow = rowsToCheck.pop(); - const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; - - rowsToRemove.push(...childRows); - rowsToCheck.push(...childRows); + setRowsToRender((currentRows) => { + const rowsToRemove: HierarchyGridRow[] = [ + ...currentRows.filter( + (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) + ), + ]; + + const rowsToCheck = [...rowsToRemove]; + while (rowsToCheck.length > 0) { + const currentRow = rowsToCheck.pop(); + const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; + + rowsToRemove.push(...childRows); + rowsToCheck.push(...childRows); + } + + const newRowsToRender = currentRows.filter( + (row) => + !rowsToRemove + .map((rowToRemove) => { + if (rowToRemove.visibleChildren) { + rowToRemove.visibleChildren = false; + } + return rowKeyGetter(rowToRemove, uniqueRowId); + }) + .includes(rowKeyGetter(row, uniqueRowId)) + ); + + return newRowsToRender; + }); + + if (childrenTrigger) { + try { + const dynamicChildren = await childrenTrigger(false, triggerRow); + if (dynamicChildren.length > 0) { + const parentKey = rowKeyGetter(triggerRow, uniqueRowId); + const enrichedChildren = dynamicChildren.map((child) => ({ + ...child, + parentKey, + })); + + setRowsToRender((prevRows) => { + const index = prevRows.findIndex((row) => rowKeyGetter(row, uniqueRowId) === parentKey); + const before = prevRows.slice(0, index + 1); + const after = prevRows.slice(index + 1); + + return [...before, ...enrichedChildren, ...after]; + }); + } + } catch (error) { + console.error("Error loading children:", error); + } } - newRowsToRender = rows.filter( - (row) => - !rowsToRemove - .map((rowToRemove) => { - if (rowToRemove.visibleChildren) { - rowToRemove.visibleChildren = false; - } - return rowKeyGetter(rowToRemove, uniqueRowId); - }) - .includes(rowKeyGetter(row, uniqueRowId)) - ); } + triggerRow.visibleChildren = !triggerRow.visibleChildren; - setRowsToRender(newRowsToRender); setLoading(false); }; return ( <button type="button" disabled={!rows.some((row) => uniqueRowId in row)} onClick={onClick}> {loading ? ( - <DxcSpinner mode="small"/> + <DxcSpinner mode="small" /> ) : ( <DxcIcon icon={triggerRow.visibleChildren ? "Keyboard_Arrow_Down" : "Chevron_Right"} /> )} - <span className="ellipsis-cell">{triggerRow[columnKey]}</span> + <span className="ellipsis-cell">{triggerRow[columnKey] as ReactNode}</span> </button> ); }; @@ -222,7 +277,7 @@ export const renderCheckbox = ( getChildrenSelection(row.childRows, uniqueRowId, selected, checked); } if (row.parentKey) { - getParentSelectedState(rows, row.parentKey, uniqueRowId, selected, checked); + getParentSelectedState(rows, row.parentKey as ReactNode, uniqueRowId, selected, checked); } onSelectRows(selected); }} @@ -339,7 +394,7 @@ export const sortRows = ( if (sortValueA && sortValueB) { const sortFn = sortFunctions?.find(({ column }) => column === sort.columnKey)?.sortFn; - newCompResult = compareRows(sortValueA, sortValueB, sortFn); + newCompResult = compareRows(sortValueA as ReactNode, sortValueB as ReactNode, sortFn); } if (newCompResult !== 0) { @@ -524,7 +579,7 @@ export const getParentSelectedState = ( // Recursively check the parent's parent if necessary if (parentRow.parentKey) { - getParentSelectedState(rowList, parentRow.parentKey, uniqueRowId, selectedRows, checkedStateToMatch); + getParentSelectedState(rowList, parentRow.parentKey as ReactNode, uniqueRowId, selectedRows, checkedStateToMatch); } }; @@ -593,7 +648,7 @@ export const getPaginatedNodes = ( (rowToPaginate.contentIsExpanded && row?.triggerRowKey === rowToPaginate[uniqueRowId] && row?.isExpandedChildContent) || - rowToPaginate?.childRows?.some((child) => isRowInHierarchy(child, row[uniqueRowId])) + rowToPaginate?.childRows?.some((child) => isRowInHierarchy(child, row[uniqueRowId] as ReactNode)) ) ); }; From 95e4473b3493e41c89bf8c760d8b12b06a280eb3 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Fri, 4 Jul 2025 11:08:47 +0200 Subject: [PATCH 11/14] Added unknown workaround to prevent index signature limitations --- packages/lib/src/data-grid/DataGrid.stories.tsx | 4 ++-- packages/lib/src/data-grid/types.ts | 16 ++++++++++++++-- packages/lib/src/data-grid/utils.tsx | 16 +++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index 97b754972..cef1b41bd 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -461,7 +461,7 @@ const childrenTrigger = (open: boolean, triggerRow: HierarchyGridRow) => { id: `${triggerRow.id}-child-2`, childrenTrigger, }, - ] as HierarchyGridRow[]); + ] as unknown as HierarchyGridRow[]); }, 5000); }); } else { @@ -494,7 +494,7 @@ const childRowsLazy = [ id: "lazy-d", childrenTrigger, }, -]; +] as unknown as HierarchyGridRow[]; const childRowsPaginated = [ { diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index 4228cf966..e6b895424 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -44,7 +44,7 @@ export type GridRow = { /** * List of rows that will be rendered in each cell based on the key in each column. */ - [key: string]: ReactNode | Function | undefined; + [key: string]: ReactNode | undefined; }; export type HierarchyGridRow = GridRow & { @@ -54,12 +54,24 @@ export type HierarchyGridRow = GridRow & { */ childRows?: HierarchyGridRow[] | GridRow[]; /** - * Function called whenever a cell with children is expanded or collapsed. Returns the children array. + * Function called whenever a cell with children is expanded or collapsed. Returns the children array */ childrenTrigger?: ( open?: boolean, triggerRow?: HierarchyGridRow ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>; + /** + * Indicates the level of nesting for this row in the hierarchy. + */ + rowLevel?: number; + /** + * Reference to the parent row's unique identifier. + */ + parentKey?: ReactNode; + /** + * Indicates whether child rows are currently visible. + */ + visibleChildren?: boolean; }; export type ExpandableGridRow = GridRow & { diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index baab7752c..aa3b09611 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -29,13 +29,13 @@ export const convertToRDGColumns = ( renderEditCell: gridColumn.textEditable ? textEditor : undefined, renderCell: ({ row }) => ( <div className={`ellipsis-cell ${gridColumn.alignment ? `align-${gridColumn.alignment}` : "align-left"}`}> - {row[gridColumn.key] as ReactNode} + {row[gridColumn.key]} </div> ), renderSummaryCell: () => gridColumn.summaryKey ? ( <div className={`ellipsis-cell ${gridColumn.alignment ? `align-${gridColumn.alignment}` : "align-left"}`}> - {summaryRow?.[gridColumn.summaryKey] as ReactNode} + {summaryRow?.[gridColumn.summaryKey]} </div> ) : undefined, }); @@ -242,7 +242,9 @@ export const renderHierarchyTrigger = ( ) : ( <DxcIcon icon={triggerRow.visibleChildren ? "Keyboard_Arrow_Down" : "Chevron_Right"} /> )} - <span className="ellipsis-cell">{triggerRow[columnKey] as ReactNode}</span> + <span className="ellipsis-cell"> + {triggerRow[columnKey]} + </span> </button> ); }; @@ -277,7 +279,7 @@ export const renderCheckbox = ( getChildrenSelection(row.childRows, uniqueRowId, selected, checked); } if (row.parentKey) { - getParentSelectedState(rows, row.parentKey as ReactNode, uniqueRowId, selected, checked); + getParentSelectedState(rows, row.parentKey, uniqueRowId, selected, checked); } onSelectRows(selected); }} @@ -394,7 +396,7 @@ export const sortRows = ( if (sortValueA && sortValueB) { const sortFn = sortFunctions?.find(({ column }) => column === sort.columnKey)?.sortFn; - newCompResult = compareRows(sortValueA as ReactNode, sortValueB as ReactNode, sortFn); + newCompResult = compareRows(sortValueA, sortValueB, sortFn); } if (newCompResult !== 0) { @@ -579,7 +581,7 @@ export const getParentSelectedState = ( // Recursively check the parent's parent if necessary if (parentRow.parentKey) { - getParentSelectedState(rowList, parentRow.parentKey as ReactNode, uniqueRowId, selectedRows, checkedStateToMatch); + getParentSelectedState(rowList, parentRow.parentKey, uniqueRowId, selectedRows, checkedStateToMatch); } }; @@ -648,7 +650,7 @@ export const getPaginatedNodes = ( (rowToPaginate.contentIsExpanded && row?.triggerRowKey === rowToPaginate[uniqueRowId] && row?.isExpandedChildContent) || - rowToPaginate?.childRows?.some((child) => isRowInHierarchy(child, row[uniqueRowId] as ReactNode)) + rowToPaginate?.childRows?.some((child) => isRowInHierarchy(child, row[uniqueRowId])) ) ); }; From 5fd3d83ff5029112175ad679394394945950b108 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Mon, 7 Jul 2025 11:42:21 +0200 Subject: [PATCH 12/14] Fixed test for lazy loading mock --- packages/lib/src/data-grid/DataGrid.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx index 2e51ce806..fd871fdbd 100644 --- a/packages/lib/src/data-grid/DataGrid.test.tsx +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -217,7 +217,7 @@ const hierarchyRowsLazy: HierarchyGridRow[] = [ name: "Root Node 1 Lazy", value: "1", id: "lazy-a", - childrenTrigger: () => childrenTriggerMock, + childrenTrigger: childrenTriggerMock, }, { name: "Root Node 2 Lazy", @@ -294,7 +294,7 @@ describe("Data grid component tests", () => { buttons[0] && fireEvent.click(buttons[0]); - expect(childrenTriggerMock).toHaveBeenCalledWith(expect.objectContaining({ id: "lazy-a" })); + expect(childrenTriggerMock).toHaveBeenCalledWith(true, expect.objectContaining({ id: "lazy-a" })); }); test("Renders column headers", () => { From 7ffd7969b673a90f8afc24246775e10d589b12c2 Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Mon, 7 Jul 2025 15:09:15 +0200 Subject: [PATCH 13/14] Fixed types from params --- packages/lib/src/data-grid/utils.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index fd89104c3..383c36e70 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -269,7 +269,7 @@ export const renderHierarchyTrigger = ( * @param {GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]} rows - Array of rows that are currently displayed. * @param {GridRow | HierarchyGridRow | ExpandableGridRow} row - Row object to render the checkbox for. * @param {string} uniqueRowId - The key used to uniquely identify each row. - * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. + * @param {Set<string | number>} selectedRows - Set of selected rows. * @param {Function} onSelectRows - Callback function that triggers when rows are selected/deselected. * @returns {JSX.Element} Checkbox for selecting the row. */ @@ -307,7 +307,7 @@ export const renderCheckbox = ( * Renders a header checkbox that controls the selection of all rows. * @param {GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]} rows - Array of rows that are currently displayed. * @param {string} uniqueRowId - The key used to uniquely identify each row. - * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. + * @param {Set<string | number>} selectedRows - Set of selected rows. * @param {Function} onSelectRows - Callback function that triggers when rows are selected/deselected. * @returns {JSX.Element} Checkbox for the header checkbox. */ @@ -534,13 +534,13 @@ export const rowFinderBasedOnId = ( * Recursively selects or deselects children rows based on the checked state. * @param {HierarchyGridRow[]} rowList - List of child rows that need to be checked/unchecked. * @param {string} uniqueRowId - Key used to uniquely identify each row. - * @param {Set<ReactNode>} selectedRows - Set of selected rows. + * @param {Set<string | number>} selectedRows - Set of selected rows. * @param {boolean} checked - Boolean indicating whether the rows should be selected (true) or deselected (false). */ export const getChildrenSelection = ( rowList: HierarchyGridRow[], uniqueRowId: string, - selectedRows: Set<ReactNode>, + selectedRows: Set<string | number>, checked: boolean ) => { rowList.forEach((row) => { @@ -562,14 +562,14 @@ export const getChildrenSelection = ( * @param {ReactNode} uniqueRowKeyValue Unique value of the selected row * @param {ReactNode} parentKeyValue Unique value of the parent Row * @param {string} uniqueRowId Key where the unique value is located - * @param {Set<ReactNode>} changedRows + * @param {Set<string | number>} selectedRows - Set of selected rows. * @param {boolean} checkedStateToMatch */ export const getParentSelectedState = ( rowList: HierarchyGridRow[], parentKeyValue: ReactNode, uniqueRowId: string, - selectedRows: Set<ReactNode>, + selectedRows: Set<string | number>, checkedStateToMatch: boolean ) => { if (!rowList.some((row) => uniqueRowId in row)) { From 16b157055f78aa702f75dc9a3fa02a5e8a7631fb Mon Sep 17 00:00:00 2001 From: Enrique Moreno <enrique.moreno@dxc.com> Date: Tue, 8 Jul 2025 14:28:06 +0200 Subject: [PATCH 14/14] Added fix for selection while loading hierarchy --- packages/lib/src/data-grid/DataGrid.tsx | 5 +++-- packages/lib/src/data-grid/types.ts | 2 +- packages/lib/src/data-grid/utils.tsx | 24 ++++++++++++++++-------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx index ab05f4493..6902e3283 100644 --- a/packages/lib/src/data-grid/DataGrid.tsx +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -252,8 +252,9 @@ const DxcDataGrid = ({ uniqueRowId, firstColumnKey, setRowsToRender, - row.childrenTrigger, - selectedRows + row.childrenTrigger + // TODO: remove + // selectedRows )} </HierarchyContainer> ); diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts index e6b895424..d657fa786 100644 --- a/packages/lib/src/data-grid/types.ts +++ b/packages/lib/src/data-grid/types.ts @@ -67,7 +67,7 @@ export type HierarchyGridRow = GridRow & { /** * Reference to the parent row's unique identifier. */ - parentKey?: ReactNode; + parentKey?: string | number; /** * Indicates whether child rows are currently visible. */ diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx index 383c36e70..ef5233cae 100644 --- a/packages/lib/src/data-grid/utils.tsx +++ b/packages/lib/src/data-grid/utils.tsx @@ -122,6 +122,8 @@ export const renderExpandableTrigger = ( /> ); +// TODO: REMOVE COMMENTED CODE (LEAVING IT JUST IN CASE IT IS NEEDED WHILE REVIEWING) + /** * Renders a trigger for hierarchical row expansion in the grid. * @param {HierarchyGridRow[]} rows - List of all hierarchy grid rows. @@ -130,7 +132,7 @@ export const renderExpandableTrigger = ( * @param {string} columnKey - Key of the column that displays the hierarchy trigger. * @param {Function} setRowsToRender - Function to update the rows being rendered. * @param {Function} childrenTrigger - Function called whenever a cell with children is expanded or collapsed. Returns the children array - * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. +// * @param {Set<string | number>} selectedRows - Set containing the IDs of selected rows. * @returns {JSX.Element} Button that toggles visibility of child rows. */ export const renderHierarchyTrigger = ( @@ -143,7 +145,7 @@ export const renderHierarchyTrigger = ( _open: boolean, _selectedRow: HierarchyGridRow ) => (HierarchyGridRow[] | GridRow[]) | Promise<HierarchyGridRow[] | GridRow[]>, - selectedRows?: Set<string | number> + // selectedRows?: Set<string | number> ) => { const [loading, setLoading] = useState(false); const onClick = async () => { @@ -156,11 +158,17 @@ export const renderHierarchyTrigger = ( const dynamicChildren = await childrenTrigger(true, triggerRow); triggerRow.childRows = dynamicChildren; - if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { - dynamicChildren.forEach((child) => { - selectedRows.add(rowKeyGetter(child, uniqueRowId)); - }); - } + // TODO: REMOVED, NOW THE LOGIC IS HANDLED INSIDE RENDERCHECKBOX + // TODO: I HAVE LEFT IT FOR NOW BECAUSE I AM WORRIED ABOUT THE BEHAVIOR + // TODO: WHEN THERE ARE MULTIPLE HIERARCHY LEVELS EXPANDED AT ONCE + // TODO (AS RENDERCHECKBOX ONLY CHECKS DIRECT PARENT, AND HAVING A RECURSIVE CHECK + // TODO: SEEMS OVERKILL) + // + // if (selectedRows?.has(rowKeyGetter(triggerRow, uniqueRowId))) { + // dynamicChildren.forEach((child) => { + // selectedRows.add(rowKeyGetter(child, uniqueRowId)); + // }); + // } setRowsToRender((currentRows) => { const newRowsToRender = [...currentRows]; @@ -282,7 +290,7 @@ export const renderCheckbox = ( ) => { return ( <DxcCheckbox - checked={selectedRows.has(rowKeyGetter(row, uniqueRowId))} + checked={selectedRows.has(rowKeyGetter(row, uniqueRowId)) || selectedRows.has(row.parentKey as string | number)} onChange={(checked) => { const selected = new Set(selectedRows); if (checked) {