mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
When a user has sorted by some column in the Table Editor and the column is deleted, the sort data is wrong so it causes issues. In the general view in the Table Editor, the error is handled by removing the sort key when a specific error is detected but it can still happen in ForeignRowSelector. To test: 1. Have 2 tables with references between them. 2. In the `sessionStorage`, under the `supabase_grid-<ref>` key, update the sort key to a non-existant column for a table. 3. Try to open the `ForeignRowSelector` for that table by clicking on a cell in the referencing column. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Sorting now validates referenced columns and ignores invalid sort entries. * Local sort restoration and UI sort application now derive sorts from the original table context for more consistent behavior across editors and popovers. * Prefetch logic uses the resolved table context when falling back to saved sorts. * **Tests** * Added cases for malformed and out-of-scope sort parameters to prevent regressions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
import { copyToClipboard } from 'ui'
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
|
|
import {
|
|
formatFilterURLParams,
|
|
formatSortURLParams,
|
|
handleCellKeyDown,
|
|
} from '@/components/grid/SupabaseGrid.utils'
|
|
|
|
const { toastError, toastSuccess } = vi.hoisted(() => ({
|
|
toastError: vi.fn(),
|
|
toastSuccess: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('sonner', () => ({
|
|
toast: {
|
|
error: toastError,
|
|
success: toastSuccess,
|
|
},
|
|
}))
|
|
|
|
// Sort URL syntax: `column:order`
|
|
describe('SupabaseGrid.utils: formatSortURLParams', () => {
|
|
test('should return an array of sort options based on URL params', () => {
|
|
const mockInput = ['id:asc', 'name:desc']
|
|
const output = formatSortURLParams(
|
|
{ name: 'fakeTable', columns: [{ name: 'id' }, { name: 'name' }] },
|
|
mockInput
|
|
)
|
|
expect(output).toStrictEqual([
|
|
{ table: 'fakeTable', column: 'id', ascending: true },
|
|
{ table: 'fakeTable', column: 'name', ascending: false },
|
|
])
|
|
})
|
|
test('should reject any malformed sort options based on URL params', () => {
|
|
const mockInput = ['id', 'name:asc', ':asc']
|
|
const output = formatSortURLParams(
|
|
{ name: 'fakeTable', columns: [{ name: 'name' }] },
|
|
mockInput
|
|
)
|
|
expect(output).toStrictEqual([
|
|
{
|
|
table: 'fakeTable',
|
|
column: 'name',
|
|
ascending: true,
|
|
},
|
|
])
|
|
})
|
|
|
|
test('should reject any sort options with non-existent columns based on URL params', () => {
|
|
const mockInput = ['name2:asc']
|
|
const output = formatSortURLParams(
|
|
{ name: 'fakeTable', columns: [{ name: 'name' }] },
|
|
mockInput
|
|
)
|
|
expect(output).toStrictEqual([])
|
|
})
|
|
})
|
|
|
|
// Filter URL syntax: `column:operatorAbbreviation:value`
|
|
describe('SupabaseGrid.utils: formatFilterURLParams', () => {
|
|
test('should return an array of filter options based on URL params', () => {
|
|
const mockInput = ['id:gte:20', 'id:lte:40']
|
|
const output = formatFilterURLParams(mockInput)
|
|
expect(output).toHaveLength(2)
|
|
expect(output[0]).toStrictEqual({
|
|
column: 'id',
|
|
operator: '>=',
|
|
value: '20',
|
|
})
|
|
expect(output[1]).toStrictEqual({
|
|
column: 'id',
|
|
operator: '<=',
|
|
value: '40',
|
|
})
|
|
})
|
|
test('should format filters for timestamps correctly', () => {
|
|
const mockInput = ['created_at:gte:2022-05-30 03:00:00']
|
|
const output = formatFilterURLParams(mockInput)
|
|
expect(output[0]).toStrictEqual({
|
|
column: 'created_at',
|
|
operator: '>=',
|
|
value: '2022-05-30 03:00:00',
|
|
})
|
|
})
|
|
test('should reject any malformed filter options based on URL params', () => {
|
|
const mockInput = ['id', ':gte', ':50', 'id:eq:10']
|
|
const output = formatFilterURLParams(mockInput)
|
|
expect(output).toHaveLength(1)
|
|
})
|
|
test('should reject any filter options with unrecognized operator', () => {
|
|
const mockInput = ['id:meme:40', 'name:eq:town']
|
|
const output = formatFilterURLParams(mockInput)
|
|
expect(output).toHaveLength(1)
|
|
})
|
|
test('should allow filter options to have empty value based on URL params', () => {
|
|
const mockInput = ['id:ilike:']
|
|
const output = formatFilterURLParams(mockInput)
|
|
expect(output).toHaveLength(1)
|
|
expect(output[0]).toStrictEqual({
|
|
column: 'id',
|
|
operator: '~~*',
|
|
value: '',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('SupabaseGrid.utils: handleCellKeyDown', () => {
|
|
beforeEach(() => {
|
|
toastError.mockReset()
|
|
toastSuccess.mockReset()
|
|
vi.unstubAllGlobals()
|
|
vi.spyOn(window.document, 'hasFocus').mockReturnValue(true)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
test('should copy the selected cell value when Meta+C is pressed', async () => {
|
|
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
vi.stubGlobal('navigator', {
|
|
clipboard: { writeText },
|
|
})
|
|
|
|
const args = {
|
|
mode: 'SELECT',
|
|
column: { key: 'name' },
|
|
row: { name: 'hello from safari' },
|
|
rowIdx: 0,
|
|
selectCell: vi.fn(),
|
|
} as unknown as Parameters<typeof handleCellKeyDown>[0]
|
|
|
|
const event = {
|
|
key: 'C',
|
|
metaKey: true,
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
nativeEvent: new KeyboardEvent('keydown', { key: 'C', metaKey: true }),
|
|
preventDefault: vi.fn(),
|
|
preventGridDefault: vi.fn(),
|
|
} as unknown as Parameters<typeof handleCellKeyDown>[1]
|
|
|
|
handleCellKeyDown(args, event)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(writeText).toHaveBeenCalledWith('hello from safari')
|
|
})
|
|
expect(event.preventDefault).toHaveBeenCalled()
|
|
expect(event.preventGridDefault).toHaveBeenCalled()
|
|
await vi.waitFor(() => {
|
|
expect(toastSuccess).toHaveBeenCalledWith('Copied cell value to clipboard')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('shared clipboard util', () => {
|
|
beforeEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.spyOn(window.document, 'hasFocus').mockReturnValue(true)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
test('should invoke the callback after writing text to the clipboard', async () => {
|
|
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
const onCopy = vi.fn()
|
|
|
|
vi.stubGlobal('navigator', {
|
|
clipboard: { writeText },
|
|
})
|
|
|
|
await copyToClipboard('hello from safari', onCopy)
|
|
expect(writeText).toHaveBeenCalledWith('hello from safari')
|
|
expect(onCopy).toHaveBeenCalled()
|
|
})
|
|
})
|