mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 01:40:13 -04:00
e540f9089f
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? - Safari Table Editor cells fail to copy from a focused cell with `⌘C`. - Safari right-click can show the browser menu instead of the custom cell menu. - Copy can leave RDG's copied-cell fill behind. ## What is the new behavior? - Reuses the existing shared `copyToClipboard(value, onSuccess)` pattern, with the Safari clipboard fix inside that util. - Handles selected-cell `⌘C` in the RDG keydown path, preventing browser/RDG defaults and showing the success toast only after copy. - Replaces the row-level synthetic context-menu shim with RDG's `onCellContextMenu`, so we prevent Safari's browser menu at the source and select/focus the target cell. - Keeps the selected-cell outline while the controlled menu is open. ## Additional context - `RowRenderer` was only supporting the old context-menu shim; removing it is part of moving to RDG's cell event path. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Context menu now provides feedback with toast notifications when copying cells or rows. * Selected cells retain their visual styling when context menu is open. * **Bug Fixes** * Improved keyboard shortcut handling for copy functionality. * Enhanced clipboard error handling with user-friendly error messages. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
167 lines
4.8 KiB
TypeScript
167 lines
4.8 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('fakeTable', 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('fakeTable', mockInput)
|
|
expect(output).toStrictEqual([
|
|
{
|
|
table: 'fakeTable',
|
|
column: 'name',
|
|
ascending: true,
|
|
},
|
|
])
|
|
})
|
|
})
|
|
|
|
// 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()
|
|
})
|
|
})
|