Skip to content

Commit 31eafdd

Browse files
authored
Implement checkbox selection (#1964)
* Implement checkbox selection * Add test for batch selection
1 parent 43d2cff commit 31eafdd

File tree

8 files changed

+179
-49
lines changed

8 files changed

+179
-49
lines changed

webview/__mocks__/@vscode/webview-ui-toolkit/react.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ export const VSCodeButton: React.FC<MockButtonProps> = ({
1616
export const VSCodeDivider: React.FC = () => {
1717
return <hr />
1818
}
19+
20+
type MockCheckboxProps = {
21+
onClick: () => void
22+
}
23+
24+
export const VSCodeCheckbox: React.FC<MockCheckboxProps> = ({ onClick }) => {
25+
return <input type="checkbox" onClick={onClick} />
26+
}

webview/src/experiments/components/App.test.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -796,11 +796,11 @@ describe('App', () => {
796796
})
797797
)
798798

799-
const firstRow = screen.getByTestId('timestamp___1.exp-e7a67')
800-
fireEvent.click(firstRow)
799+
const firstRowCheckbox = within(getRow('4fb124a')).getByRole('checkbox')
800+
fireEvent.click(firstRowCheckbox)
801801

802-
const secondRow = screen.getByTestId('timestamp___1.test-branch')
803-
fireEvent.click(secondRow)
802+
const secondRowCheckbox = within(getRow('42b8736')).getByRole('checkbox')
803+
fireEvent.click(secondRowCheckbox)
804804

805805
const target = screen.getByText('4fb124a')
806806
fireEvent.contextMenu(target, { bubbles: true })
@@ -810,6 +810,40 @@ describe('App', () => {
810810
const itemLabels = menuitems.map(item => item.textContent)
811811
expect(itemLabels).toContain('Remove Selected Rows')
812812
})
813+
814+
it('should allow batch selection of rows by shift-clicking a range of them', () => {
815+
render(<App />)
816+
817+
fireEvent(
818+
window,
819+
new MessageEvent('message', {
820+
data: {
821+
data: {
822+
...tableDataFixture,
823+
hasRunningExperiment: false
824+
},
825+
type: MessageToWebviewType.SET_DATA
826+
}
827+
})
828+
)
829+
830+
const firstRowCheckbox = within(getRow('4fb124a')).getByRole('checkbox')
831+
fireEvent.click(firstRowCheckbox)
832+
833+
const tailRow = within(getRow('42b8736')).getByRole('checkbox')
834+
fireEvent.click(tailRow, { shiftKey: true })
835+
836+
const selectedRows = screen.getAllByRole('row', { selected: true })
837+
expect(selectedRows.length).toBe(4)
838+
839+
const target = screen.getByText('4fb124a')
840+
fireEvent.contextMenu(target, { bubbles: true })
841+
842+
jest.advanceTimersByTime(100)
843+
const menuitems = screen.getAllByRole('menuitem')
844+
const itemLabels = menuitems.map(item => item.textContent)
845+
expect(itemLabels).toContain('Star Experiments')
846+
})
813847
})
814848

815849
describe('Star Experiments', () => {
@@ -915,10 +949,10 @@ describe('App', () => {
915949
)
916950

917951
mockPostMessage.mockReset()
918-
const mainRow = getRow('main')
952+
const mainRow = within(getRow('main')).getByRole('checkbox')
919953
fireEvent.click(mainRow)
920954

921-
const firstTipRow = getRow('4fb124a')
955+
const firstTipRow = within(getRow('4fb124a')).getByRole('checkbox')
922956
fireEvent.click(firstTipRow)
923957

924958
fireEvent.contextMenu(mainRow, { bubbles: true })

webview/src/experiments/components/table/Cell.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import cx from 'classnames'
3+
import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'
34
import styles from './styles.module.scss'
45
import { CellProp, RowProp } from './interfaces'
56
import ClockIcon from '../../../shared/components/icons/Clock'
@@ -33,13 +34,15 @@ const RowExpansionButton: React.FC<RowProp> = ({ row }) =>
3334
export const FirstCell: React.FC<
3435
CellProp & {
3536
bulletColor?: string
37+
isRowSelected: boolean
3638
toggleExperiment: () => void
3739
toggleRowSelection: () => void
3840
toggleStarred: () => void
3941
}
4042
> = ({
4143
cell,
4244
bulletColor,
45+
isRowSelected,
4346
toggleExperiment,
4447
toggleRowSelection,
4548
toggleStarred
@@ -58,19 +61,24 @@ export const FirstCell: React.FC<
5861
isPlaceholder && styles.groupPlaceholder
5962
)
6063
})}
61-
{...clickAndEnterProps(toggleRowSelection)}
6264
>
6365
<div className={styles.innerCell}>
64-
<div
65-
className={styles.starSwitch}
66-
role="switch"
67-
aria-checked={starred}
68-
tabIndex={0}
69-
{...clickAndEnterProps(toggleStarred)}
70-
data-testid="star-icon"
71-
>
72-
{starred && <StarFull />}
73-
{!starred && <StarEmpty />}
66+
<div className={styles.rowActions}>
67+
<VSCodeCheckbox
68+
{...clickAndEnterProps(toggleRowSelection)}
69+
checked={isRowSelected}
70+
/>
71+
<div
72+
className={styles.starSwitch}
73+
role="switch"
74+
aria-checked={starred}
75+
tabIndex={0}
76+
{...clickAndEnterProps(toggleStarred)}
77+
data-testid="star-icon"
78+
>
79+
{starred && <StarFull />}
80+
{!starred && <StarEmpty />}
81+
</div>
7482
</div>
7583
<RowExpansionButton row={row} />
7684
<span

webview/src/experiments/components/table/Row.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { sendMessage } from '../../../shared/vscode'
1010
import { ContextMenu } from '../../../shared/components/contextMenu/ContextMenu'
1111
import { MessagesMenu } from '../../../shared/components/messagesMenu/MessagesMenu'
1212
import { MessagesMenuOptionProps } from '../../../shared/components/messagesMenu/MessagesMenuOption'
13-
import { clickAndEnterProps } from '../../../util/props'
13+
import { HandlerFunc } from '../../../util/props'
1414
import { cond } from '../../../util/helpers'
1515

1616
const getExperimentTypeClass = ({ running, queued, selected }: Experiment) => {
@@ -255,14 +255,19 @@ const getRowClassNames = (
255255
)
256256
}
257257

258+
export type BatchSelectionProp = {
259+
batchRowSelection: (prop: RowProp) => void
260+
}
261+
258262
export const RowContent: React.FC<
259-
RowProp & { className?: string } & WithChanges
263+
RowProp & { className?: string } & WithChanges & BatchSelectionProp
260264
> = ({
261265
row,
262266
className,
263267
changes,
264268
contextMenuDisabled,
265-
projectHasCheckpoints
269+
projectHasCheckpoints,
270+
batchRowSelection
266271
}): JSX.Element => {
267272
const {
268273
getRowProps,
@@ -294,11 +299,18 @@ export const RowContent: React.FC<
294299

295300
const isRowSelected = !!selectedRows[id]
296301

297-
const toggleRowSelection = React.useCallback(() => {
298-
if (!isWorkspace) {
299-
toggleRowSelected?.({ row })
300-
}
301-
}, [row, toggleRowSelected, isWorkspace])
302+
const toggleRowSelection = React.useCallback<HandlerFunc<HTMLElement>>(
303+
args => {
304+
if (!isWorkspace) {
305+
if (args?.mouse?.shiftKey) {
306+
batchRowSelection({ row })
307+
} else {
308+
toggleRowSelected?.({ row })
309+
}
310+
}
311+
},
312+
[row, toggleRowSelected, isWorkspace, batchRowSelection]
313+
)
302314

303315
return (
304316
<ContextMenu
@@ -323,13 +335,13 @@ export const RowContent: React.FC<
323335
})}
324336
tabIndex={0}
325337
role="row"
326-
{...clickAndEnterProps(toggleRowSelection)}
327338
aria-selected={isRowSelected}
328339
data-testid={isWorkspace && 'workspace-row'}
329340
>
330341
<FirstCell
331342
cell={firstCell}
332343
bulletColor={displayColor}
344+
isRowSelected={isRowSelected}
333345
toggleExperiment={toggleExperiment}
334346
toggleRowSelection={toggleRowSelection}
335347
toggleStarred={toggleStarred}

webview/src/experiments/components/table/RowSelectionContext.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { RowProp } from './interfaces'
44
export interface RowSelectionContextValue {
55
selectedRows: Record<string, RowProp | undefined>
66
toggleRowSelected: ((row: RowProp) => void) | undefined
7+
batchSelection: ((batch: RowProp[]) => void) | undefined
78
clearSelectedRows: (() => void) | undefined
89
}
910

1011
export const RowSelectionContext = createContext<RowSelectionContextValue>({
12+
batchSelection: undefined,
1113
clearSelectedRows: undefined,
1214
selectedRows: {},
1315
toggleRowSelected: undefined
@@ -32,13 +34,33 @@ export const RowSelectionProvider: React.FC<{ children: React.ReactNode }> = ({
3234
})
3335
}
3436

37+
const batchSelection = (batch: RowProp[]) => {
38+
const selectedRowsCopy = { ...selectedRows }
39+
40+
for (const rowProp of batch) {
41+
const {
42+
row: {
43+
values: { id }
44+
}
45+
} = rowProp
46+
selectedRowsCopy[id] = rowProp
47+
}
48+
49+
setSelectedRows(selectedRowsCopy)
50+
}
51+
3552
const clearSelectedRows = () => {
3653
setSelectedRows({})
3754
}
3855

3956
return (
4057
<RowSelectionContext.Provider
41-
value={{ clearSelectedRows, selectedRows, toggleRowSelected }}
58+
value={{
59+
batchSelection,
60+
clearSelectedRows,
61+
selectedRows,
62+
toggleRowSelected
63+
}}
4264
>
4365
{children}
4466
</RowSelectionContext.Provider>

0 commit comments

Comments
 (0)