From 4b238747b6c83902e458466da6a80bd5e6d8f272 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Tue, 13 May 2025 16:23:06 +0200 Subject: [PATCH 1/4] Fix : ensure table selections respect applied filters Signed-off-by: Ayoub LABIDI --- src/pages/groups/groups-table.tsx | 10 +++------- src/pages/profiles/profiles-table.tsx | 17 +++-------------- src/pages/users/users-table.tsx | 9 ++------- src/utils/hooks.ts | 24 ++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 16cca42..beeac2e 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -10,11 +10,12 @@ import { useIntl } from 'react-intl'; import { GroupAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { GroupInfos, UserAdminSrv, UserInfos } from '../../services'; -import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; +import { ColDef, GetRowIdParams, RowClickedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; +import { useTableSelection } from 'utils/hooks'; export interface GroupsTableProps { gridRef: RefObject>; @@ -26,18 +27,13 @@ const GroupsTable: FunctionComponent = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const [rowsSelection, setRowsSelection] = useState([]); + const { rowsSelection, onSelectionChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { return params.data.name; } - const onSelectionChanged = useCallback( - (event: SelectionChangedEvent) => setRowsSelection(event.api.getSelectedRows() ?? []), - [setRowsSelection] - ); - const onAddButton = useCallback(() => props.setOpenAddGroupDialog(true), [props]); const deleteGroups = useCallback((): Promise | undefined => { diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index e2dd66f..00c70a7 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -10,18 +10,12 @@ import { useIntl } from 'react-intl'; import { ManageAccounts } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { UserAdminSrv, UserProfile } from '../../services'; -import { - ColDef, - GetRowIdParams, - ITooltipParams, - RowClickedEvent, - SelectionChangedEvent, - TextFilterParams, -} from 'ag-grid-community'; +import { ColDef, GetRowIdParams, ITooltipParams, RowClickedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import ValidityCellRenderer from './validity-cell-renderer'; +import { useTableSelection } from 'utils/hooks'; export interface ProfilesTableProps { gridRef: RefObject>; @@ -33,18 +27,13 @@ const ProfilesTable: FunctionComponent = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const [rowsSelection, setRowsSelection] = useState([]); + const { rowsSelection, onSelectionChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { return params.data.id ?? ''; } - const onSelectionChanged = useCallback( - (event: SelectionChangedEvent) => setRowsSelection(event.api.getSelectedRows() ?? []), - [setRowsSelection] - ); - const onAddButton = useCallback(() => props.setOpenAddProfileDialog(true), [props]); const deleteProfiles = useCallback(() => { diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 08663d2..d8c2024 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -15,13 +15,13 @@ import { GetRowIdParams, ICheckboxCellRendererParams, RowClickedEvent, - SelectionChangedEvent, TextFilterParams, } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; +import { useTableSelection } from 'utils/hooks'; export interface UsersTableProps { gridRef: RefObject>; @@ -33,18 +33,13 @@ const UsersTable: FunctionComponent = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const [rowsSelection, setRowsSelection] = useState([]); + const { rowsSelection, onSelectionChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { return params.data.sub ?? ''; } - const onSelectionChanged = useCallback( - (event: SelectionChangedEvent) => setRowsSelection(event.api.getSelectedRows() ?? []), - [setRowsSelection] - ); - const onAddButton = useCallback(() => props.setOpenAddUserDialog(true), [props]); const deleteUsers = useCallback(() => { diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 29fce4e..4952940 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -5,6 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { SelectionChangedEvent } from "ag-grid-community"; +import { useCallback, useState } from "react"; + export function useDebugRender(label: string) { // uncomment when you want the output in the console /*if (import.meta.env.DEV) { @@ -13,3 +16,24 @@ export function useDebugRender(label: string) { console.timeStamp?.(label); }*/ } + +/** + * Custom hook to handle table row selection with proper filtering support + * @returns Selection state and handler for AG Grid's onSelectionChanged + */ +export function useTableSelection() { + const [rowsSelection, setRowsSelection] = useState([]); + + const onSelectionChanged = useCallback((event: SelectionChangedEvent) => { + // Get only selected rows that are currently visible (after filtering) + const visibleSelectedRows: T[] = []; + event.api.forEachNodeAfterFilterAndSort((node) => { + if (node.isSelected() && node.data) { + visibleSelectedRows.push(node.data); + } + }); + setRowsSelection(visibleSelectedRows); + }, []); + + return { rowsSelection, setRowsSelection, onSelectionChanged }; +} From 6d5e3d19eb6d63ce36aa80ec7dc23823ae89c2e6 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Tue, 13 May 2025 18:10:31 +0200 Subject: [PATCH 2/4] fixes Signed-off-by: Ayoub LABIDI --- src/pages/common/table-selection.tsx | 23 +++++++++------------- src/pages/groups/groups-table.tsx | 3 ++- src/pages/profiles/profiles-table.tsx | 3 ++- src/pages/users/users-table.tsx | 3 ++- src/utils/hooks.ts | 28 ++++++++++++++++++++------- 5 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index c6ee6d9..6074e9a 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -5,13 +5,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react'; +import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { CustomAGGrid } from '@gridsuite/commons-ui'; import { Grid, Typography } from '@mui/material'; import { AgGridReact } from 'ag-grid-react'; import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community'; import { defaultColDef, defaultRowSelection } from './table-config'; +import { useTableSelection } from 'utils/hooks'; export interface TableSelectionProps { itemName: string; @@ -21,19 +22,12 @@ export interface TableSelectionProps { } const TableSelection: FunctionComponent = (props) => { - const [selectedRowsLength, setSelectedRowsLength] = useState(0); + const { rowsSelection, onSelectionChanged: handleSelection, onFilterChanged } = useTableSelection<{ id: string }>(); const gridRef = useRef(null); - const handleEquipmentSelectionChanged = useCallback(() => { - const selectedRows = gridRef.current?.api.getSelectedRows(); - if (selectedRows == null) { - setSelectedRowsLength(0); - props.onSelectionChanged([]); - } else { - setSelectedRowsLength(selectedRows.length); - props.onSelectionChanged(selectedRows.map((r) => r.id)); - } - }, [props]); + useEffect(() => { + props.onSelectionChanged(rowsSelection.map((r) => r.id)); + }, [rowsSelection, props]); const rowData = useMemo(() => { return props.tableItems.map((str) => ({ id: str })); @@ -72,7 +66,7 @@ const TableSelection: FunctionComponent = (props) => { - {` (${selectedRowsLength} / ${rowData?.length ?? 0})`} + {` (${rowsSelection?.length} / ${rowData?.length ?? 0})`} @@ -84,7 +78,8 @@ const TableSelection: FunctionComponent = (props) => { defaultColDef={defaultColDef} rowSelection={defaultRowSelection} getRowId={getRowId} - onSelectionChanged={handleEquipmentSelectionChanged} + onSelectionChanged={handleSelection} + onFilterChanged={onFilterChanged} onGridReady={onGridReady} /> diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index beeac2e..92e0ea5 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -27,7 +27,7 @@ const GroupsTable: FunctionComponent = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const { rowsSelection, onSelectionChanged } = useTableSelection(); + const { rowsSelection, onSelectionChanged, onFilterChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { @@ -110,6 +110,7 @@ const GroupsTable: FunctionComponent = (props) => { rowSelection={defaultRowSelection} onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} + onFilterChanged={onFilterChanged} > = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const { rowsSelection, onSelectionChanged } = useTableSelection(); + const { rowsSelection, onSelectionChanged, onFilterChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { @@ -125,6 +125,7 @@ const ProfilesTable: FunctionComponent = (props) => { rowSelection={defaultRowSelection} onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} + onFilterChanged={onFilterChanged} > = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const { rowsSelection, onSelectionChanged } = useTableSelection(); + const { rowsSelection, onSelectionChanged, onFilterChanged } = useTableSelection(); const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { @@ -142,6 +142,7 @@ const UsersTable: FunctionComponent = (props) => { rowSelection={defaultRowSelection} onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} + onFilterChanged={onFilterChanged} tooltipShowDelay={1000} > () { const [rowsSelection, setRowsSelection] = useState([]); - const onSelectionChanged = useCallback((event: SelectionChangedEvent) => { - // Get only selected rows that are currently visible (after filtering) + // update visible selections based on current filter state + const updateVisibleSelection = useCallback((api: GridApi) => { const visibleSelectedRows: T[] = []; - event.api.forEachNodeAfterFilterAndSort((node) => { + api.forEachNodeAfterFilterAndSort((node: IRowNode) => { if (node.isSelected() && node.data) { visibleSelectedRows.push(node.data); } @@ -35,5 +35,19 @@ export function useTableSelection() { setRowsSelection(visibleSelectedRows); }, []); - return { rowsSelection, setRowsSelection, onSelectionChanged }; + const onSelectionChanged = useCallback( + (event: SelectionChangedEvent) => { + updateVisibleSelection(event.api); + }, + [updateVisibleSelection] + ); + + const onFilterChanged = useCallback( + (event: FilterChangedEvent) => { + updateVisibleSelection(event.api); + }, + [updateVisibleSelection] + ); + + return { rowsSelection, onSelectionChanged, onFilterChanged }; } From 0a0683c504b416ac38143ff2f2ccc42b4a306b25 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Wed, 14 May 2025 10:30:44 +0200 Subject: [PATCH 3/4] fix test Signed-off-by: Ayoub LABIDI --- src/pages/common/table-selection.tsx | 2 +- src/pages/groups/groups-table.tsx | 2 +- src/pages/profiles/profiles-table.tsx | 2 +- src/pages/users/users-table.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index 60e0e4b..bbc9cbf 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -12,7 +12,7 @@ import { Grid, Typography } from '@mui/material'; import { AgGridReact } from 'ag-grid-react'; import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community'; import { defaultColDef, defaultRowSelection } from './table-config'; -import { useTableSelection } from 'utils/hooks'; +import { useTableSelection } from '../../utils/hooks'; export interface TableSelectionProps { itemName: string; diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 92e0ea5..94591b6 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -15,7 +15,7 @@ import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; -import { useTableSelection } from 'utils/hooks'; +import { useTableSelection } from '../../utils/hooks'; export interface GroupsTableProps { gridRef: RefObject>; diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index a8dd33b..ad1b69c 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -15,7 +15,7 @@ import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import ValidityCellRenderer from './validity-cell-renderer'; -import { useTableSelection } from 'utils/hooks'; +import { useTableSelection } from '../../utils/hooks'; export interface ProfilesTableProps { gridRef: RefObject>; diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 2e0e74b..513e06f 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -21,7 +21,7 @@ import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; -import { useTableSelection } from 'utils/hooks'; +import { useTableSelection } from '../../utils/hooks'; export interface UsersTableProps { gridRef: RefObject>; From a62feaefb46c383b0a255dc6ded29a5323582c67 Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Thu, 15 May 2025 10:48:42 +0200 Subject: [PATCH 4/4] Fix Signed-off-by: Ayoub LABIDI --- src/pages/common/table-selection.tsx | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index bbc9cbf..8cf4c22 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -5,14 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'; +import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { CustomAGGrid } from '@gridsuite/commons-ui'; import { Grid, Typography } from '@mui/material'; import { AgGridReact } from 'ag-grid-react'; import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community'; import { defaultColDef, defaultRowSelection } from './table-config'; -import { useTableSelection } from '../../utils/hooks'; export interface TableSelectionProps { itemName: string; @@ -21,13 +20,25 @@ export interface TableSelectionProps { onSelectionChanged: (selectedItems: string[]) => void; } +const rowSelection = { + ...defaultRowSelection, + headerCheckbox: false, +}; + const TableSelection: FunctionComponent = (props) => { - const { rowsSelection, onSelectionChanged: handleSelection, onFilterChanged } = useTableSelection<{ id: string }>(); + const [selectedRowsLength, setSelectedRowsLength] = useState(0); const gridRef = useRef(null); - useEffect(() => { - props.onSelectionChanged(rowsSelection.map((r) => r.id)); - }, [rowsSelection, props]); + const handleEquipmentSelectionChanged = useCallback(() => { + const selectedRows = gridRef.current?.api.getSelectedRows(); + if (selectedRows == null) { + setSelectedRowsLength(0); + props.onSelectionChanged([]); + } else { + setSelectedRowsLength(selectedRows.length); + props.onSelectionChanged(selectedRows.map((r) => r.id)); + } + }, [props]); const rowData = useMemo(() => { return props.tableItems.map((str) => ({ id: str })); @@ -66,7 +77,7 @@ const TableSelection: FunctionComponent = (props) => { - {` (${rowsSelection?.length} / ${rowData?.length ?? 0})`} + {` (${selectedRowsLength} / ${rowData?.length ?? 0})`} @@ -76,10 +87,9 @@ const TableSelection: FunctionComponent = (props) => { rowData={rowData} columnDefs={columnDefs} defaultColDef={defaultColDef} - rowSelection={defaultRowSelection} + rowSelection={rowSelection} getRowId={getRowId} - onSelectionChanged={handleSelection} - onFilterChanged={onFilterChanged} + onSelectionChanged={handleEquipmentSelectionChanged} onGridReady={onGridReady} accentedSort={true} />