1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

fix(app): datatable global checkbox doesn't reflect the selected state (#470)

This commit is contained in:
James Player 2025-03-10 09:21:20 +13:00 committed by GitHub
parent 438b1f9815
commit b57855f20d
10 changed files with 110 additions and 18 deletions

View file

@ -1,4 +1,5 @@
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import {
createColumnHelper,
@ -170,7 +171,7 @@ describe('Datatable', () => {
fireEvent.click(selectAllCheckbox);
// Check if all rows on the page are selected
expect(screen.getByText('2 item(s) selected')).toBeInTheDocument();
expect(screen.getByText('2 items selected')).toBeInTheDocument();
// Deselect
fireEvent.click(selectAllCheckbox);
@ -192,13 +193,44 @@ describe('Datatable', () => {
fireEvent.click(selectAllCheckbox, { shiftKey: true });
// Check if all rows on the page are selected
expect(screen.getByText('3 item(s) selected')).toBeInTheDocument();
expect(screen.getByText('3 items selected')).toBeInTheDocument();
// Deselect
fireEvent.click(selectAllCheckbox, { shiftKey: true });
const checkboxes: HTMLInputElement[] = screen.queryAllByRole('checkbox');
expect(checkboxes.filter((checkbox) => checkbox.checked).length).toBe(0);
});
it('shows indeterminate state and correct footer text when hidden rows are selected', async () => {
const user = userEvent.setup();
render(
<DatatableWithStore
dataset={mockData}
columns={mockColumns}
data-cy="test-table"
title="Test table with search"
/>
);
// Select Jane
const checkboxes = screen.getAllByRole('checkbox');
await user.click(checkboxes[2]); // Select the second row
// Search for John (will hide selected Jane)
const searchInput = screen.getByPlaceholderText('Search...');
await user.type(searchInput, 'John');
// Check if the footer text is correct
expect(
await screen.findByText('1 item selected (1 hidden by filters)')
).toBeInTheDocument();
// Check if the checkbox is indeterminate
const selectAllCheckbox: HTMLInputElement =
screen.getByLabelText('Select all rows');
expect(selectAllCheckbox.indeterminate).toBe(true);
expect(selectAllCheckbox.checked).toBe(false);
});
});
// Test the defaultGlobalFilterFn used in searches

View file

@ -171,6 +171,14 @@ export function Datatable<D extends DefaultType>({
const selectedRowModel = tableInstance.getSelectedRowModel();
const selectedItems = selectedRowModel.rows.map((row) => row.original);
const filteredItems = tableInstance
.getFilteredRowModel()
.rows.map((row) => row.original);
const hiddenSelectedItems = useMemo(
() => _.difference(selectedItems, filteredItems),
[selectedItems, filteredItems]
);
return (
<Table.Container noWidget={noWidget} aria-label={title}>
@ -203,6 +211,7 @@ export function Datatable<D extends DefaultType>({
pageSize={tableState.pagination.pageSize}
pageCount={tableInstance.getPageCount()}
totalSelected={selectedItems.length}
totalHiddenSelected={hiddenSelectedItems.length}
/>
</Table.Container>
);

View file

@ -5,6 +5,7 @@ import { SelectedRowsCount } from './SelectedRowsCount';
interface Props {
totalSelected: number;
totalHiddenSelected: number;
pageSize: number;
page: number;
onPageChange(page: number): void;
@ -14,6 +15,7 @@ interface Props {
export function DatatableFooter({
totalSelected,
totalHiddenSelected,
pageSize,
page,
onPageChange,
@ -22,7 +24,7 @@ export function DatatableFooter({
}: Props) {
return (
<Table.Footer>
<SelectedRowsCount value={totalSelected} />
<SelectedRowsCount value={totalSelected} hidden={totalHiddenSelected} />
<PaginationControls
showAll
pageLimit={pageSize}

View file

@ -1,9 +1,15 @@
import { addPlural } from '@/react/common/string-utils';
interface SelectedRowsCountProps {
value: number;
hidden: number;
}
export function SelectedRowsCount({ value }: SelectedRowsCountProps) {
export function SelectedRowsCount({ value, hidden }: SelectedRowsCountProps) {
return value !== 0 ? (
<div className="infoBar">{value} item(s) selected</div>
<div className="infoBar">
{addPlural(value, 'item')} selected
{hidden !== 0 && ` (${hidden} hidden by filters)`}
</div>
) : null;
}

View file

@ -1,7 +1,19 @@
import { ColumnDef, Row } from '@tanstack/react-table';
import { ColumnDef, Row, Table } from '@tanstack/react-table';
import { Checkbox } from '@@/form-components/Checkbox';
function allRowsSelected<T>(table: Table<T>) {
return table.getCoreRowModel().rows.every((row) => row.getIsSelected());
}
function someRowsSelected<T>(table: Table<T>) {
return table.getCoreRowModel().rows.some((row) => row.getIsSelected());
}
function somePageRowsSelected<T>(table: Table<T>) {
return table.getRowModel().rows.some((row) => row.getIsSelected());
}
export function createSelectColumn<T>(dataCy: string): ColumnDef<T> {
let lastSelectedId = '';
@ -11,15 +23,15 @@ export function createSelectColumn<T>(dataCy: string): ColumnDef<T> {
<Checkbox
id="select-all"
data-cy={`select-all-checkbox-${dataCy}`}
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
checked={allRowsSelected(table)}
indeterminate={!allRowsSelected(table) && someRowsSelected(table)}
onChange={(e) => {
// Select all rows if shift key is held down, otherwise only page rows
if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.shiftKey) {
table.getToggleAllRowsSelectedHandler()(e);
table.toggleAllRowsSelected();
return;
}
table.getToggleAllPageRowsSelectedHandler()(e);
table.toggleAllPageRowsSelected(!somePageRowsSelected(table));
}}
disabled={table.getRowModel().rows.every((row) => !row.getCanSelect())}
onClick={(e) => {