mirror of
https://github.com/supabase/supabase.git
synced 2026-06-20 19:16:04 +08:00
## 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? Completion of batch edits on the table editor ## Demo https://github.com/user-attachments/assets/ab5a7112-3dcc-456a-a5fc-1c9a99fccf34 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Queued add/edit/delete operations with optimistic UI, conflict resolution, and queue-based flows * Side-panel items showing queued add/delete row previews * **UI** * Pending-add placeholders plus a visible "DEFAULT" marker in grid cells * Visual row states: green for pending adds, red with strike-through for pending deletes * Queue-based deletes can bypass confirmation when queue mode is enabled * **Tests** * Expanded tests covering queue conflict resolution and queue utilities <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <a@alaisteryoung.com>
246 lines
7.7 KiB
TypeScript
246 lines
7.7 KiB
TypeScript
import { describe, test, expect } from 'vitest'
|
|
import { generateTableChangeKey, rowMatchesIdentifiers, applyCellEdit } from './queueOperationUtils'
|
|
import {
|
|
type NewEditCellContentOperation,
|
|
type NewAddRowOperation,
|
|
type NewDeleteRowOperation,
|
|
QueuedOperationType,
|
|
} from '@/state/table-editor-operation-queue.types'
|
|
|
|
describe('generateTableChangeKey', () => {
|
|
test('should generate key for EDIT_CELL_CONTENT with row identifiers', () => {
|
|
const operation: NewEditCellContentOperation = {
|
|
type: QueuedOperationType.EDIT_CELL_CONTENT,
|
|
tableId: 1,
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
}
|
|
const key = generateTableChangeKey(operation)
|
|
expect(key).toBe('edit_cell_content:1:name:id:1')
|
|
})
|
|
|
|
test('should generate key for EDIT_CELL_CONTENT with empty row identifiers', () => {
|
|
const operation: NewEditCellContentOperation = {
|
|
type: QueuedOperationType.EDIT_CELL_CONTENT,
|
|
tableId: 1,
|
|
payload: {
|
|
rowIdentifiers: {},
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
}
|
|
const key = generateTableChangeKey(operation)
|
|
expect(key).toBe('edit_cell_content:1:name:')
|
|
})
|
|
|
|
test('should generate key with multiple row identifiers sorted alphabetically', () => {
|
|
const operation: NewEditCellContentOperation = {
|
|
type: QueuedOperationType.EDIT_CELL_CONTENT,
|
|
tableId: 1,
|
|
payload: {
|
|
rowIdentifiers: { z_id: 3, a_id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
}
|
|
const key = generateTableChangeKey(operation)
|
|
expect(key).toBe('edit_cell_content:1:name:a_id:1|z_id:3')
|
|
})
|
|
|
|
test('should generate key for ADD_ROW operation', () => {
|
|
const operation: NewAddRowOperation = {
|
|
type: QueuedOperationType.ADD_ROW,
|
|
tableId: 1,
|
|
payload: {
|
|
tempId: 'temp-123',
|
|
rowData: { idx: -1, __tempId: 'temp-123' },
|
|
table: {} as any,
|
|
},
|
|
}
|
|
const key = generateTableChangeKey(operation)
|
|
expect(key).toBe('add_row:1:temp-123')
|
|
})
|
|
|
|
test('should generate key for DELETE_ROW operation', () => {
|
|
const operation: NewDeleteRowOperation = {
|
|
type: QueuedOperationType.DELETE_ROW,
|
|
tableId: 1,
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
originalRow: { idx: 0, id: 1 },
|
|
table: {} as any,
|
|
},
|
|
}
|
|
const key = generateTableChangeKey(operation)
|
|
expect(key).toBe('delete_row:1:id:1')
|
|
})
|
|
|
|
test('should throw error for unknown operation type', () => {
|
|
const operation = {
|
|
type: 'unknown' as any,
|
|
tableId: 1,
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
}
|
|
expect(() => generateTableChangeKey(operation)).toThrow('Unknown operation type')
|
|
})
|
|
})
|
|
|
|
describe('rowMatchesIdentifiers', () => {
|
|
test('should return false for empty row identifiers', () => {
|
|
const result = rowMatchesIdentifiers({ id: 1 }, {})
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('should match row with single identifier', () => {
|
|
const result = rowMatchesIdentifiers({ id: 1 }, { id: 1 })
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test('should match row with multiple identifiers', () => {
|
|
const result = rowMatchesIdentifiers(
|
|
{ id: 1, email: 'test@test.com' },
|
|
{ id: 1, email: 'test@test.com' }
|
|
)
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test('should not match row with different values', () => {
|
|
const result = rowMatchesIdentifiers({ id: 2 }, { id: 1 })
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('should not match row with missing identifier keys', () => {
|
|
const result = rowMatchesIdentifiers({ id: 1 }, { id: 1, email: 'test@test.com' })
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('should match row with extra keys', () => {
|
|
const result = rowMatchesIdentifiers({ id: 1, name: 'John', age: 30 }, { id: 1 })
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test('should match with null values', () => {
|
|
const result = rowMatchesIdentifiers({ id: null }, { id: null })
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
test('should not match with undefined values in row', () => {
|
|
const result = rowMatchesIdentifiers({ id: undefined, name: 'test' }, { id: 1 })
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('applyCellEdit', () => {
|
|
test('should apply cell edit to matching row', () => {
|
|
const rows = [
|
|
{ idx: 0, id: 1, name: 'old' },
|
|
{ idx: 1, id: 2, name: 'test' },
|
|
]
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, 'new')
|
|
expect(result).toEqual([
|
|
{ idx: 0, id: 1, name: 'new' },
|
|
{ idx: 1, id: 2, name: 'test' },
|
|
])
|
|
})
|
|
|
|
test('should not affect non-matching rows', () => {
|
|
const rows = [
|
|
{ idx: 0, id: 1, name: 'old' },
|
|
{ idx: 1, id: 2, name: 'test' },
|
|
]
|
|
const result = applyCellEdit(rows, 'name', { id: 3 }, 'new')
|
|
expect(result).toEqual([
|
|
{ idx: 0, id: 1, name: 'old' },
|
|
{ idx: 1, id: 2, name: 'test' },
|
|
])
|
|
})
|
|
|
|
test('should create new row instances for matching row', () => {
|
|
const rows = [{ idx: 0, id: 1, name: 'old' }]
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, 'new')
|
|
expect(result[0]).not.toBe(rows[0])
|
|
expect(result[0]).toEqual({ idx: 0, id: 1, name: 'new' })
|
|
})
|
|
|
|
test('should not modify original array', () => {
|
|
const rows = [{ idx: 0, id: 1, name: 'old' }]
|
|
const originalRows = [...rows]
|
|
applyCellEdit(rows, 'name', { id: 1 }, 'new')
|
|
expect(rows).toEqual(originalRows)
|
|
})
|
|
|
|
test('should handle multiple matching rows with composite keys', () => {
|
|
const rows = [
|
|
{ idx: 0, id: 1, org_id: 10, name: 'old1' },
|
|
{ idx: 1, id: 1, org_id: 20, name: 'old2' },
|
|
{ idx: 2, id: 2, org_id: 10, name: 'old3' },
|
|
]
|
|
const result = applyCellEdit(rows, 'name', { id: 1, org_id: 10 }, 'new')
|
|
expect(result).toEqual([
|
|
{ idx: 0, id: 1, org_id: 10, name: 'new' },
|
|
{ idx: 1, id: 1, org_id: 20, name: 'old2' },
|
|
{ idx: 2, id: 2, org_id: 10, name: 'old3' },
|
|
])
|
|
})
|
|
|
|
test('should handle setting value to null', () => {
|
|
const rows = [{ idx: 0, id: 1, name: 'test' }]
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, null)
|
|
expect(result).toEqual([{ idx: 0, id: 1, name: null }])
|
|
})
|
|
|
|
test('should handle setting value to undefined', () => {
|
|
const rows = [{ idx: 0, id: 1, name: 'test' }]
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, undefined)
|
|
expect(result).toEqual([{ idx: 0, id: 1, name: undefined }])
|
|
})
|
|
|
|
test('should handle numeric values', () => {
|
|
const rows = [{ idx: 0, id: 1, count: 0 }]
|
|
const result = applyCellEdit(rows, 'count', { id: 1 }, 42)
|
|
expect(result).toEqual([{ idx: 0, id: 1, count: 42 }])
|
|
})
|
|
|
|
test('should handle object values', () => {
|
|
const rows = [{ idx: 0, id: 1, data: null }]
|
|
const newValue = { nested: { value: 123 } }
|
|
const result = applyCellEdit(rows, 'data', { id: 1 }, newValue)
|
|
expect(result).toEqual([{ idx: 0, id: 1, data: newValue }])
|
|
})
|
|
|
|
test('should handle empty rows array', () => {
|
|
const rows: any[] = []
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, 'new')
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
test('should update all matching rows with same identifier', () => {
|
|
const rows = [
|
|
{ idx: 0, id: 1, name: 'row1' },
|
|
{ idx: 1, id: 1, name: 'row2' },
|
|
{ idx: 2, id: 2, name: 'row3' },
|
|
]
|
|
const result = applyCellEdit(rows, 'name', { id: 1 }, 'updated')
|
|
expect(result).toEqual([
|
|
{ idx: 0, id: 1, name: 'updated' },
|
|
{ idx: 1, id: 1, name: 'updated' },
|
|
{ idx: 2, id: 2, name: 'row3' },
|
|
])
|
|
})
|
|
})
|