mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Shift-clicking a row checkbox in Unified Logs now selects every row between the last clicked row and the clicked one, following up on #50381. Per review, the legacy logs table now uses react-data-grid's native shift-click selection (the same mechanism as the table editor) instead of the custom anchor logic from #50381, and Unified Logs matches the grid's semantics. **Semantics (all three tables):** a shift-click applies the clicked checkbox's new state to every row between the last clicked row and the clicked one. The last clicked row itself is untouched. In Unified Logs a shift-click after the selection has been cleared is a plain toggle. **Changed:** - `LogTable` passes `selectedRows`, `onSelectedRowsChange`, and `rowKeyGetter` to the grid and renders the checkbox through a small `LogSelectCell` component using `useRowSelection`. The custom anchor ref, its resets, and the inline toggle are gone. Checking a row still closes the single-row side panel. - `getShiftClickSelection` moved from the Logs utils to `apps/studio/lib/shift-click-selection.ts` and rewritten to the grid's rule. Only Unified Logs uses it now, via a `getShiftClickRowSelection` adapter for TanStack Table's `RowSelectionState`. Tests cover both. - Unified Logs owns a selection anchor ref and passes it into the column generator. The checkbox cell handles `onClick` with the shift key, computes the range over the table's displayed row model (so it spans sort order and infinite-scrolled pages), and writes back through the table's own selection setter. Shift mousedown is prevented so no text selection spans rows. - The `LogTable` test mock of react-data-grid now implements the grid's row selection so the component tests exercise the native path. ## To test - Postgres logs: click one checkbox, then shift-click a checkbox further down. Every row in between should be checked and the action bar shows the count. Repeat upward. - Shift-click an already-checked row: it and the rows back to the last clicked row uncheck, the last clicked row stays as it was. - Checking a box closes the single-row side panel. Clicking a row body clears the selection and opens the panel. - Tab to a checkbox and press Space: it still toggles. Arrow keys plus Shift+Space still toggle the focused row. - Unified Logs: same shift-click behavior. Clear the selection or change a filter, then shift-click: only that one row toggles. Scroll to load more rows and shift-click across the boundary. - Copy as JSON/Markdown and Explain with AI still use the selected rows in both tables. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved row selection in Settings Logs and Unified Logs. - Shift-click now selects or deselects the range between the anchor row and clicked row. - Clicking an already selected row clears the relevant selection while preserving the anchor row. - Added more consistent checkbox, keyboard, and range-selection behavior across log tables. - Selecting a checkbox no longer opens the corresponding log, while clicking the row continues to open it. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
196 lines
5.5 KiB
TypeScript
196 lines
5.5 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
|
|
import { getShiftClickRowSelection, getShiftClickSelection } from './shift-click-selection'
|
|
|
|
describe('getShiftClickSelection', () => {
|
|
const orderedKeys = ['a', 'b', 'c', 'd', 'e']
|
|
|
|
test('selects the range downward from the anchor', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['b']),
|
|
anchorKey: 'b',
|
|
targetKey: 'd',
|
|
})
|
|
expect([...result].sort()).toEqual(['b', 'c', 'd'])
|
|
})
|
|
|
|
test('selects the range upward from the anchor', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['d']),
|
|
anchorKey: 'd',
|
|
targetKey: 'b',
|
|
})
|
|
expect([...result].sort()).toEqual(['b', 'c', 'd'])
|
|
})
|
|
|
|
test('deselects the range when the target is already selected', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['a', 'b', 'c', 'd']),
|
|
anchorKey: 'a',
|
|
targetKey: 'd',
|
|
})
|
|
// The anchor row itself is never modified, so 'a' stays selected
|
|
expect([...result].sort()).toEqual(['a'])
|
|
})
|
|
|
|
test('leaves the anchor row untouched when it is not selected', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(),
|
|
anchorKey: 'b',
|
|
targetKey: 'd',
|
|
})
|
|
expect([...result].sort()).toEqual(['c', 'd'])
|
|
})
|
|
|
|
test('applies the target state to the whole range when it is partially selected', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['c']),
|
|
anchorKey: 'a',
|
|
targetKey: 'd',
|
|
})
|
|
expect([...result].sort()).toEqual(['b', 'c', 'd'])
|
|
})
|
|
|
|
test('toggles just the target when it is also the anchor', () => {
|
|
const added = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(),
|
|
anchorKey: 'c',
|
|
targetKey: 'c',
|
|
})
|
|
expect([...added]).toEqual(['c'])
|
|
|
|
const removed = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['c']),
|
|
anchorKey: 'c',
|
|
targetKey: 'c',
|
|
})
|
|
expect([...removed]).toEqual([])
|
|
})
|
|
|
|
test('falls back to adding the target when there is no anchor', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['a']),
|
|
anchorKey: null,
|
|
targetKey: 'c',
|
|
})
|
|
expect([...result].sort()).toEqual(['a', 'c'])
|
|
})
|
|
|
|
test('falls back to removing the target when there is no anchor and it is selected', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['a', 'c']),
|
|
anchorKey: null,
|
|
targetKey: 'c',
|
|
})
|
|
expect([...result].sort()).toEqual(['a'])
|
|
})
|
|
|
|
test('falls back to a plain toggle when the anchor is no longer in the rows', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(),
|
|
anchorKey: 'gone',
|
|
targetKey: 'd',
|
|
})
|
|
expect([...result]).toEqual(['d'])
|
|
})
|
|
|
|
test('falls back to a plain toggle when the target is not in the rows', () => {
|
|
const result = getShiftClickSelection({
|
|
orderedKeys,
|
|
selectedKeys: new Set(['a']),
|
|
anchorKey: 'a',
|
|
targetKey: 'gone',
|
|
})
|
|
expect([...result].sort()).toEqual(['a', 'gone'])
|
|
})
|
|
|
|
test('does not mutate its inputs', () => {
|
|
const selectedKeys = new Set(['b'])
|
|
const keys = [...orderedKeys]
|
|
const result = getShiftClickSelection({
|
|
orderedKeys: keys,
|
|
selectedKeys,
|
|
anchorKey: 'b',
|
|
targetKey: 'd',
|
|
})
|
|
expect(result).not.toBe(selectedKeys)
|
|
expect([...selectedKeys]).toEqual(['b'])
|
|
expect(keys).toEqual(orderedKeys)
|
|
})
|
|
})
|
|
|
|
describe('getShiftClickRowSelection', () => {
|
|
const orderedRowIds = ['a', 'b', 'c', 'd', 'e']
|
|
|
|
test('selects the range between the anchor and the target', () => {
|
|
const result = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection: { b: true },
|
|
anchorRowId: 'b',
|
|
targetRowId: 'd',
|
|
})
|
|
expect(result).toEqual({ b: true, c: true, d: true })
|
|
})
|
|
|
|
test('deselects the range when the target is already selected', () => {
|
|
const result = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection: { a: true, b: true, c: true, d: true },
|
|
anchorRowId: 'a',
|
|
targetRowId: 'd',
|
|
})
|
|
expect(result).toEqual({ a: true })
|
|
})
|
|
|
|
test('falls back to a plain toggle when there is no anchor', () => {
|
|
const added = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection: { a: true },
|
|
anchorRowId: null,
|
|
targetRowId: 'c',
|
|
})
|
|
expect(added).toEqual({ a: true, c: true })
|
|
|
|
const removed = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection: { a: true, c: true },
|
|
anchorRowId: null,
|
|
targetRowId: 'c',
|
|
})
|
|
expect(removed).toEqual({ a: true })
|
|
})
|
|
|
|
test('ignores ids explicitly set to false and never returns false entries', () => {
|
|
const result = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection: { a: false, b: true, c: false },
|
|
anchorRowId: 'b',
|
|
targetRowId: 'd',
|
|
})
|
|
expect(result).toEqual({ b: true, c: true, d: true })
|
|
expect(Object.values(result).every((isSelected) => isSelected === true)).toBe(true)
|
|
})
|
|
|
|
test('does not mutate the row selection it is given', () => {
|
|
const rowSelection = { b: true }
|
|
const result = getShiftClickRowSelection({
|
|
orderedRowIds,
|
|
rowSelection,
|
|
anchorRowId: 'b',
|
|
targetRowId: 'd',
|
|
})
|
|
expect(result).not.toBe(rowSelection)
|
|
expect(rowSelection).toEqual({ b: true })
|
|
})
|
|
})
|