Skip to content

Commit f5b815b

Browse files
authored
Merge pull request #1222 from securityscorecard/next
Merge next release 2.13.0 NOTICKET
2 parents 22acd78 + 91538f0 commit f5b815b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2293
-479
lines changed
Loading
Loading
Loading
Binary file not shown.
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

src/components/DatatableV2/Datatable.types.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export interface ParsedDatatableOptions<D>
231231
rowActions?: DatatableOptions<D>['rowActions'];
232232
rowCount?: DatatableOptions<D>['rowCount'];
233233
rowsPerPageOptions?: DatatableOptions<D>['rowsPerPageOptions'];
234+
rowSelectionMode?: DatatableOptions<D>['rowSelectionMode'];
234235
selectAllMode?: DatatableOptions<D>['selectAllMode'];
235236
state?: DatatableOptions<D>['state'];
236237
}
@@ -311,7 +312,7 @@ export interface DatatableOptions<D>
311312
> {
312313
/**
313314
* Definition of the table columns. Each column definition is REQUIRED to have `header` property
314-
* set. If you define a column that is not used for displaing row data (e.g. have button...),
315+
* set. If you define a column that is not used for displaing row data (e.g. have ...),
315316
* please provide `columnDefType: 'display'`. Setting this property will disable some unnecessary
316317
* functionalities and filter out the column from column orderdering.
317318
*/
@@ -523,12 +524,12 @@ export interface DatatableOptions<D>
523524
/**
524525
* You can provide your own implementation of the row actions container. This property accepts
525526
* React component with properties:
526-
* - `selectedRows` - array of currently selected rows
527+
* - `selectedRows` - array of currently selected rows or list of row ids if manualPagination is enabled and rowSelectionMode is set to 'multi-page'
527528
* - `totalRowCount` - count of all rows in the table
528529
* - `table` - current instance of the table
529530
*/
530531
renderRowSelectionActions?: (props: {
531-
selectedRows: D[];
532+
selectedRows: D[] | (string | number)[];
532533
totalRowCount: number;
533534
table: DatatableInstance<D>;
534535
}) => ReactNode;
@@ -550,6 +551,12 @@ export interface DatatableOptions<D>
550551
* default: `[10, 25, 50, 100]`
551552
*/
552553
rowsPerPageOptions?: number[];
554+
/**
555+
* Available only if `manualPagination: true`.
556+
*
557+
* default: 'single-page`
558+
*/
559+
rowSelectionMode?: 'single-page' | 'multi-page';
553560
/**
554561
* Switch mode for the select all checkbox in the table header. When `page` is set checkbox will
555562
* select all rows in the current page. When `all` is set checkbox will select all rows in the

src/components/DatatableV2/buttons/SelectButton.tsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ const SelectButton = <D,>({
5959
} & ComponentPropsWithoutRef<'input'>) => {
6060
const {
6161
getState,
62-
options: { enableMultiRowSelection, selectAllMode },
62+
options: {
63+
enableMultiRowSelection,
64+
selectAllMode,
65+
manualPagination,
66+
rowSelectionMode,
67+
},
6368
} = table;
64-
const { isLoading } = getState();
69+
const { isLoading, rowSelection } = getState();
6570
const { t } = useSafeTranslation();
6671

6772
const allRowsSelected =
@@ -104,9 +109,12 @@ const SelectButton = <D,>({
104109
<IndeterminateCheckbox
105110
className="ds-table-select-multi-button ds-table-select-button"
106111
indeterminate={
107-
isHeaderCheckbox
108-
? table.getIsSomeRowsSelected() && !allRowsSelected
109-
: false
112+
!isHeaderCheckbox
113+
? false
114+
: manualPagination && rowSelectionMode === 'single-page'
115+
? table.getIsSomePageRowsSelected() && !allRowsSelected
116+
: Object.values(rowSelection ?? {}).filter(Boolean).length > 0 &&
117+
!allRowsSelected
110118
}
111119
{...common}
112120
style={styles}

src/components/DatatableV2/hooks/useOptions.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const useOptions = <D>({
3131
renderNoDataFallback,
3232
renderRowSelectionActions,
3333
rowsPerPageOptions = [10, 25, 50, 100],
34+
rowSelectionMode = 'single-page',
3435
selectAllMode = 'page',
3536
...restDatatableOptions
3637
}: Partial<DatatableOptions<D>>): ParsedDatatableOptions<D> => {
@@ -90,6 +91,7 @@ export const useOptions = <D>({
9091
renderNoDataFallback,
9192
renderRowSelectionActions,
9293
rowsPerPageOptions,
94+
rowSelectionMode,
9395
selectAllMode,
9496
...restDatatableOptions,
9597
columnResizeMode: 'onChange',

src/components/DatatableV2/stories/Selection.stories.tsx

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useState } from 'react';
22
import { Meta } from '@storybook/react';
33
import { action } from '@storybook/addon-actions';
4-
import { RowSelectionState } from '@tanstack/react-table';
4+
import { PaginationState, RowSelectionState } from '@tanstack/react-table';
55

66
import Datatable from '../Datatable';
77
import Template, { Story } from './Template';
88
import { Button } from '../../Button';
9+
import { fetchData, useQuery } from '../mocks/externalData';
910

1011
export default {
1112
title: 'components/DatatableV2/Selection',
@@ -141,3 +142,57 @@ export const SelectionManagedState: Story = (args) => {
141142
);
142143
};
143144
SelectionManagedState.args = SelectionEnabled.args;
145+
146+
export const ManualPaginationWithSinglePageSelection: Story = (args) => {
147+
const [pagination, setPagination] = useState<PaginationState>({
148+
pageIndex: 1,
149+
pageSize: 10,
150+
});
151+
152+
const dataQuery = useQuery({
153+
queryKey: ['data', pagination],
154+
queryFn: () => fetchData(pagination),
155+
keepPreviousData: true,
156+
});
157+
158+
return (
159+
<Datatable
160+
{...args}
161+
data={dataQuery?.data?.entries ?? []}
162+
pageCount={dataQuery?.data?.pageCount ?? -1}
163+
rowCount={dataQuery?.data?.rowCount}
164+
state={{ pagination }}
165+
onPaginationChange={setPagination}
166+
/>
167+
);
168+
};
169+
ManualPaginationWithSinglePageSelection.args = {
170+
...Template.args,
171+
rowSelectionMode: 'single-page',
172+
renderRowSelectionActions: ({ selectedRows }) => (
173+
<Button onClick={() => action('batch action')(selectedRows)}>
174+
Show selected rows
175+
</Button>
176+
),
177+
manualPagination: true,
178+
initialState: {
179+
rowSelection: {
180+
'55ea9935-7f59-4e30-b132-5372d214c20e': true,
181+
'72badeb8-8974-4dc3-82f5-b638b381b9c4': true,
182+
'ffcc21d9-7fe7-4c26-b708-4b8ba6432ad6': true,
183+
},
184+
},
185+
};
186+
ManualPaginationWithSinglePageSelection.parameters = {
187+
screenshot: { skip: false },
188+
};
189+
190+
export const ManualPaginationWithMultiPageSelection: Story =
191+
ManualPaginationWithSinglePageSelection.bind({});
192+
ManualPaginationWithMultiPageSelection.args = {
193+
...ManualPaginationWithSinglePageSelection.args,
194+
rowSelectionMode: 'multi-page',
195+
};
196+
ManualPaginationWithMultiPageSelection.parameters = {
197+
screenshot: { skip: false },
198+
};

src/components/DatatableV2/table/TableSurface.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Surface } from '../../layout';
44
import { DatatableInstance } from '../Datatable.types';
55
import Table from './Table';
66
import TopToolbar from '../toolbar/TopToolbar';
7-
import SelectionToolbar from '../../_internal/toolbars/SelectionToolbar';
7+
import { SelectionToolbarReactTable } from '../../_internal/toolbars/SelectionToolbar';
88
import PaginationToolbar from '../../_internal/toolbars/PaginationToolbar';
99

1010
const DatatableRoot = styled.div<{ $isFullscreen }>`
@@ -87,7 +87,7 @@ const TableSurface = <D,>({ table }: { table: DatatableInstance<D> }) => {
8787
</Surface>
8888
{table.options.enableRowSelection &&
8989
table.options.enableSelectionToolbar && (
90-
<SelectionToolbar<D> instance={table} />
90+
<SelectionToolbarReactTable<D> instance={table} />
9191
)}
9292
{table.options.enablePagination &&
9393
table.getRowModel().rows.length > 0 && (

src/components/DatatableV2/tests/selection.test.tsx

+184
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
11
import { screen, waitFor } from '@testing-library/react';
2+
import { useState } from 'react';
3+
import { type PaginationState } from '@tanstack/react-table';
24

35
import { setup } from '../../../utils/tests/setup';
46
import Datatable from '../Datatable';
57
import { columns, data } from './mocks';
8+
import { fetchData, useQuery } from '../mocks/externalData';
9+
10+
const SSPDatatable = ({ rowAction, rowSelectionMode }) => {
11+
const [pagination, setPagination] = useState<PaginationState>({
12+
pageIndex: 0,
13+
pageSize: 10,
14+
});
15+
16+
const dataQuery = useQuery({
17+
queryKey: ['data', pagination],
18+
queryFn: () => fetchData(pagination),
19+
keepPreviousData: true,
20+
});
21+
22+
return (
23+
<Datatable
24+
id="test"
25+
getRowId={(row) => row.id}
26+
columns={[
27+
{ accessorKey: 'organization.name', header: 'Name' },
28+
{ accessorKey: 'organization.domain', header: 'domain' },
29+
]}
30+
data={dataQuery?.data?.entries ?? []}
31+
pageCount={dataQuery?.data?.pageCount ?? -1}
32+
renderRowSelectionActions={({ selectedRows }) => (
33+
<button
34+
type="button"
35+
onClick={() => {
36+
rowAction(selectedRows);
37+
}}
38+
>
39+
Row action
40+
</button>
41+
)}
42+
rowCount={dataQuery?.data?.rowCount}
43+
rowSelectionMode={rowSelectionMode}
44+
state={{ pagination }}
45+
manualPagination
46+
onPaginationChange={setPagination}
47+
/>
48+
);
49+
};
650

751
describe('DatatableV2/selection', () => {
852
it('should have selection enabled by default', () => {
@@ -164,6 +208,7 @@ describe('DatatableV2/selection', () => {
164208
screen.getAllByLabelText('Toggle select row')[0],
165209
).not.toBeChecked();
166210
});
211+
167212
it('should select all rows when "selectionMode=all"', async () => {
168213
const { user } = setup(
169214
<Datatable
@@ -186,6 +231,145 @@ describe('DatatableV2/selection', () => {
186231
expect(screen.getAllByLabelText('Toggle select all')[0]).toBeChecked();
187232
expect(screen.getAllByLabelText('Toggle select row')[0]).toBeChecked();
188233
});
234+
235+
describe('with server-side pagination', () => {
236+
describe('when "rowSelectionMode=multi-page"', () => {
237+
it('should maintain selection toolbar during pagination', async () => {
238+
const rowAction = vi.fn();
239+
const { user } = setup(
240+
<SSPDatatable
241+
rowSelectionMode="multi-page"
242+
rowAction={rowAction}
243+
/>,
244+
);
245+
246+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
247+
expect(
248+
screen.getAllByLabelText('Toggle select row')[0],
249+
).toBeChecked();
250+
expect(
251+
screen.getByLabelText('Toggle select all'),
252+
).toBePartiallyChecked();
253+
254+
await user.click(
255+
screen.getByLabelText('Go to the next page of table'),
256+
);
257+
258+
await waitFor(() => {
259+
expect(
260+
screen.getAllByLabelText('Toggle select all')[0],
261+
).toBePartiallyChecked();
262+
});
263+
await waitFor(() => {
264+
expect(
265+
screen.getByRole('button', { name: /Clear selection/i }),
266+
).toBeInTheDocument();
267+
});
268+
});
269+
it('should return list of selected rows ids', async () => {
270+
const rowAction = vi.fn();
271+
const { user } = setup(
272+
<SSPDatatable
273+
rowSelectionMode="multi-page"
274+
rowAction={rowAction}
275+
/>,
276+
);
277+
278+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
279+
280+
await user.click(
281+
screen.getByLabelText('Go to the next page of table'),
282+
);
283+
284+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
285+
286+
await user.click(
287+
screen.getByRole('button', { name: /Row action/i }),
288+
);
289+
290+
await waitFor(() => {
291+
expect(rowAction).toBeCalledWith([
292+
'5cf2bc99-2721-407d-8592-ba00fbdf302f',
293+
'484e5bdb-2bb5-4844-8c6c-4d9e1808ac3a',
294+
]);
295+
});
296+
});
297+
});
298+
299+
describe('when "rowSelectionMode=single-page"', () => {
300+
it('should reset selection toolbar during pagination', async () => {
301+
const rowAction = vi.fn();
302+
const { user } = setup(
303+
<SSPDatatable
304+
rowSelectionMode="single-page"
305+
rowAction={rowAction}
306+
/>,
307+
);
308+
309+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
310+
expect(
311+
screen.getAllByLabelText('Toggle select row')[0],
312+
).toBeChecked();
313+
expect(
314+
screen.getByLabelText('Toggle select all'),
315+
).toBePartiallyChecked();
316+
317+
await user.click(
318+
screen.getByLabelText('Go to the next page of table'),
319+
);
320+
321+
await waitFor(() => {
322+
expect(
323+
screen.getAllByLabelText('Toggle select all')[0],
324+
).not.toBePartiallyChecked();
325+
});
326+
await waitFor(() => {
327+
expect(
328+
screen.queryByRole('button', { name: /Clear selection/i }),
329+
).not.toBeInTheDocument();
330+
});
331+
});
332+
it('should return list of selected rows data only for current page', async () => {
333+
const rowAction = vi.fn();
334+
const { user } = setup(
335+
<SSPDatatable
336+
rowSelectionMode="single-page"
337+
rowAction={rowAction}
338+
/>,
339+
);
340+
341+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
342+
343+
await user.click(
344+
screen.getByRole('button', { name: /Row action/i }),
345+
);
346+
347+
expect(rowAction).toBeCalledWith([
348+
expect.objectContaining({
349+
id: '5cf2bc99-2721-407d-8592-ba00fbdf302f',
350+
ipAddress: '5.74.134.156',
351+
}),
352+
]);
353+
rowAction.mockReset();
354+
355+
await user.click(
356+
screen.getByLabelText('Go to the next page of table'),
357+
);
358+
359+
await user.click(screen.getAllByLabelText('Toggle select row')[0]);
360+
361+
await user.click(
362+
screen.getByRole('button', { name: /Row action/i }),
363+
);
364+
expect(rowAction).toBeCalledWith([
365+
expect.objectContaining({
366+
id: '484e5bdb-2bb5-4844-8c6c-4d9e1808ac3a',
367+
ipAddress: '86.7.96.148',
368+
}),
369+
]);
370+
});
371+
});
372+
});
189373
});
190374

191375
it('should circle through selected rows when multi select is disabled', async () => {

0 commit comments

Comments
 (0)