mirror of
https://github.com/supabase/supabase.git
synced 2026-09-07 10:29:14 +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? Bug fix ## What is the current behavior? Fixes #47318 Supabase Studio's batched table edit queue has a few related row identity issues: - Editing a row's primary key can make later queued edits or deletes lose track of the original row. - Editing a primary key and another column in the same row before saving can save only the primary key change, because later updates still use the old primary key in the `WHERE` clause. - Adding a row in batched edit mode and then deleting it before saving may not remove the pending row correctly. ## What is the new behavior? - Preserves the original row identity for queued operations after primary key edits. - Applies multiple queued edits for the same row as a single update when saving. - Correctly deletes newly added pending rows before they are saved. - Adds regression coverage for these batched table edit cases. ## Additional context https://github.com/user-attachments/assets/75672361-d781-4fe5-a542-071574ad57bd <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved row identity handling for grid edits, optimistic updates, and queued operations so changes stay correctly attached when primary keys are edited, reverted, or “taken” by another row. * Updated header row deletion to delete from the currently visible/targeted rows rather than relying on the full dataset. * Reduced retry noise for missing tables by clearing conflicting sorts and preventing repeated retries for the same “does not exist” error. * More reliably consolidated queued edits for the same row into fewer combined save statements. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
571 lines
18 KiB
TypeScript
571 lines
18 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest'
|
|
|
|
import type { SupaRow } from '../types'
|
|
import {
|
|
formatGridDataWithOperationValues,
|
|
generateTableChangeKey,
|
|
getStableRowIdentifiers,
|
|
queueRowDeletesWithOptimisticUpdate,
|
|
rowMatchesIdentifiers,
|
|
} from './queueOperationUtils'
|
|
import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
|
|
import {
|
|
QueuedOperationType,
|
|
type NewAddRowOperation,
|
|
type NewDeleteRowOperation,
|
|
type NewEditCellContentOperation,
|
|
type QueuedOperation,
|
|
} 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('stable queued row identifiers', () => {
|
|
test('should use original row identifiers when present', () => {
|
|
const row = { idx: 0, id: 3, __originalRowIdentifiers: { id: 1 } }
|
|
|
|
expect(getStableRowIdentifiers(row, { id: 3 })).toEqual({ id: 1 })
|
|
})
|
|
|
|
test('should fall back to current identifiers when original row identifiers are not present', () => {
|
|
const row = { idx: 0, id: 3 }
|
|
|
|
expect(getStableRowIdentifiers(row, { id: 3 })).toEqual({ id: 3 })
|
|
})
|
|
})
|
|
|
|
describe('formatGridDataWithOperationValues', () => {
|
|
const makeRow = (idx: number, data: Record<string, unknown> = {}): SupaRow => ({
|
|
idx,
|
|
...data,
|
|
})
|
|
|
|
const makeEditOp = (
|
|
overrides: Partial<QueuedOperation & { payload: any }> = {}
|
|
): QueuedOperation => ({
|
|
id: 'op-1',
|
|
tableId: 1,
|
|
timestamp: Date.now(),
|
|
type: QueuedOperationType.EDIT_CELL_CONTENT,
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
...overrides,
|
|
})
|
|
|
|
const makeDeleteOp = (
|
|
rowIdentifiers: Record<string, unknown>,
|
|
originalRow: SupaRow
|
|
): QueuedOperation => ({
|
|
id: 'op-del',
|
|
tableId: 1,
|
|
timestamp: Date.now(),
|
|
type: QueuedOperationType.DELETE_ROW,
|
|
payload: { rowIdentifiers, originalRow, table: {} as any },
|
|
})
|
|
|
|
const makeAddOp = (tempId: string, rowData: Record<string, unknown> = {}): QueuedOperation => ({
|
|
id: 'op-add',
|
|
tableId: 1,
|
|
timestamp: Date.now(),
|
|
type: QueuedOperationType.ADD_ROW,
|
|
payload: {
|
|
tempId,
|
|
rowData: { idx: Number(tempId), __tempId: tempId, ...rowData },
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
test('should return rows unchanged when there are no operations', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const result = formatGridDataWithOperationValues({ operations: [], rows })
|
|
expect(result).toEqual(rows)
|
|
})
|
|
|
|
test('should apply EDIT_CELL_CONTENT to matching row', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const op = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result[0]).toMatchObject({ idx: 0, id: 1, name: 'Updated' })
|
|
expect(result[1]).toEqual(rows[1])
|
|
})
|
|
|
|
test('should not modify rows when EDIT_CELL_CONTENT does not match any row', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const op = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { id: 999 },
|
|
columnName: 'name',
|
|
oldValue: 'old',
|
|
newValue: 'new',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result).toEqual(rows)
|
|
})
|
|
|
|
test('should apply multiple EDIT_CELL_CONTENT operations to different rows', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const op1 = makeEditOp({
|
|
id: 'op-1',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated Alice',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const op2 = makeEditOp({
|
|
id: 'op-2',
|
|
payload: {
|
|
rowIdentifiers: { id: 2 },
|
|
columnName: 'name',
|
|
oldValue: 'Bob',
|
|
newValue: 'Updated Bob',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
|
|
expect(result[0].name).toBe('Updated Alice')
|
|
expect(result[1].name).toBe('Updated Bob')
|
|
})
|
|
|
|
test('multiple operations targeting the same row preserve all column changes', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice', email: 'alice@test.com' })]
|
|
const op1 = makeEditOp({
|
|
id: 'op-1',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const op2 = makeEditOp({
|
|
id: 'op-2',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'email',
|
|
oldValue: 'alice@test.com',
|
|
newValue: 'updated@test.com',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
|
|
// Both column edits should be preserved
|
|
expect(result[0].name).toBe('Updated')
|
|
expect(result[0].email).toBe('updated@test.com')
|
|
})
|
|
|
|
test('should mark matching row as deleted for DELETE_ROW operation', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const op = makeDeleteOp({ id: 1 }, rows[0])
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result[0].__isDeleted).toBe(true)
|
|
expect(result[1].__isDeleted).toBeUndefined()
|
|
})
|
|
|
|
test('should not modify rows when DELETE_ROW does not match any row', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const op = makeDeleteOp({ id: 999 }, makeRow(0, { id: 999 }))
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result[0].__isDeleted).toBeUndefined()
|
|
})
|
|
|
|
test('should not mutate the original rows array', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const op = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(rows[0].name).toBe('Alice')
|
|
expect(result[0].name).toBe('Updated')
|
|
})
|
|
|
|
test('should handle mixed operation types', () => {
|
|
const rows = [
|
|
makeRow(0, { id: 1, name: 'Alice' }),
|
|
makeRow(1, { id: 2, name: 'Bob' }),
|
|
makeRow(2, { id: 3, name: 'Charlie' }),
|
|
]
|
|
|
|
const editOp = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated Alice',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const deleteOp = makeDeleteOp({ id: 2 }, rows[1])
|
|
|
|
const result = formatGridDataWithOperationValues({
|
|
operations: [editOp, deleteOp],
|
|
rows,
|
|
})
|
|
|
|
expect(result[0].name).toBe('Updated Alice')
|
|
expect(result[1].__isDeleted).toBe(true)
|
|
expect(result[2]).toEqual(rows[2])
|
|
})
|
|
|
|
test('should prepend new row for ADD_ROW operation', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const op = makeAddOp('-100', { name: 'New Row' })
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
|
|
expect(result[1]).toEqual(rows[0])
|
|
})
|
|
|
|
test('should update existing pending row for ADD_ROW with same tempId', () => {
|
|
const rows: SupaRow[] = [
|
|
{ idx: -100, __tempId: '-100', name: 'Original' } as any,
|
|
makeRow(1, { id: 1, name: 'Alice' }),
|
|
]
|
|
const op = makeAddOp('-100', { name: 'Updated' })
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0]).toMatchObject({ __tempId: '-100', name: 'Updated' })
|
|
})
|
|
|
|
test('should handle multiple ADD_ROW operations', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const op1 = makeAddOp('-100', { name: 'Row 1' })
|
|
const op2 = makeAddOp('-200', { name: 'Row 2' })
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
|
|
expect(result).toHaveLength(3)
|
|
expect(result[0]).toMatchObject({ __tempId: '-200', name: 'Row 2' })
|
|
expect(result[1]).toMatchObject({ __tempId: '-100', name: 'Row 1' })
|
|
})
|
|
|
|
test('should correctly delete a row after adding a new row (ADD then DELETE)', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const addOp = makeAddOp('-100', { name: 'New Row' })
|
|
const deleteOp = makeDeleteOp({ id: 1 }, rows[0])
|
|
|
|
const result = formatGridDataWithOperationValues({
|
|
operations: [addOp, deleteOp],
|
|
rows,
|
|
})
|
|
|
|
expect(result).toHaveLength(3)
|
|
// New row should be preserved at position 0
|
|
expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
|
|
expect(result[0].__isDeleted).toBeUndefined()
|
|
// Deleted row should be marked
|
|
expect(result[1].__isDeleted).toBe(true)
|
|
expect(result[1].id).toBe(1)
|
|
// Other row unaffected
|
|
expect(result[2]).toEqual(rows[1])
|
|
})
|
|
|
|
test('should correctly edit a row after adding a new row (ADD then EDIT)', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const addOp = makeAddOp('-100', { name: 'New Row' })
|
|
const editOp = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated Alice',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({
|
|
operations: [addOp, editOp],
|
|
rows,
|
|
})
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
|
|
expect(result[1].name).toBe('Updated Alice')
|
|
})
|
|
|
|
test('should handle EDIT_CELL_CONTENT with composite primary keys', () => {
|
|
const rows = [makeRow(0, { tenant_id: 'a', user_id: 1, name: 'Alice' })]
|
|
const op = makeEditOp({
|
|
payload: {
|
|
rowIdentifiers: { tenant_id: 'a', user_id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [op], rows })
|
|
expect(result[0].name).toBe('Updated')
|
|
})
|
|
|
|
test('should preserve row identity after a primary key edit', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' })]
|
|
const idEdit = makeEditOp({
|
|
id: 'edit_cell_content:1:id:id:1',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'id',
|
|
oldValue: 1,
|
|
newValue: 3,
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const nameEdit = makeEditOp({
|
|
id: 'edit_cell_content:1:name:id:1',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'name',
|
|
oldValue: 'Alice',
|
|
newValue: 'Updated Alice',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({ operations: [idEdit, nameEdit], rows })
|
|
|
|
expect(result[0]).toMatchObject({ idx: 0, id: 3, name: 'Updated Alice' })
|
|
expect(getStableRowIdentifiers(result[0], { id: result[0].id })).toEqual({ id: 1 })
|
|
})
|
|
|
|
test('should keep two rows distinct when one row takes another row primary key value', () => {
|
|
const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
|
|
const aliceIdEdit = makeEditOp({
|
|
id: 'edit_cell_content:1:id:id:1',
|
|
payload: {
|
|
rowIdentifiers: { id: 1 },
|
|
columnName: 'id',
|
|
oldValue: 1,
|
|
newValue: 3,
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const bobIdEdit = makeEditOp({
|
|
id: 'edit_cell_content:1:id:id:2',
|
|
payload: {
|
|
rowIdentifiers: { id: 2 },
|
|
columnName: 'id',
|
|
oldValue: 2,
|
|
newValue: 1,
|
|
table: {} as any,
|
|
},
|
|
})
|
|
const bobNameEdit = makeEditOp({
|
|
id: 'edit_cell_content:1:name:id:2',
|
|
payload: {
|
|
rowIdentifiers: { id: 2 },
|
|
columnName: 'name',
|
|
oldValue: 'Bob',
|
|
newValue: 'Updated Bob',
|
|
table: {} as any,
|
|
},
|
|
})
|
|
|
|
const result = formatGridDataWithOperationValues({
|
|
operations: [aliceIdEdit, bobIdEdit, bobNameEdit],
|
|
rows,
|
|
})
|
|
|
|
expect(result[0]).toMatchObject({ idx: 0, id: 3, name: 'Alice' })
|
|
expect(getStableRowIdentifiers(result[0], { id: result[0].id })).toEqual({ id: 1 })
|
|
expect(result[1]).toMatchObject({ idx: 1, id: 1, name: 'Updated Bob' })
|
|
expect(getStableRowIdentifiers(result[1], { id: result[1].id })).toEqual({ id: 2 })
|
|
})
|
|
})
|
|
|
|
describe('queueRowDeletesWithOptimisticUpdate', () => {
|
|
test('should queue pending add row deletes with the temp row as original row', () => {
|
|
const queueOperation = vi.fn()
|
|
const row = { idx: -100, __tempId: '-100', name: 'New Row' } as SupaRow
|
|
|
|
queueRowDeletesWithOptimisticUpdate({
|
|
rows: [row],
|
|
table: {
|
|
id: 1,
|
|
schema: 'public',
|
|
name: 'users',
|
|
entity_type: ENTITY_TYPE.TABLE,
|
|
primary_keys: [{ name: 'id' }],
|
|
} as Parameters<typeof queueRowDeletesWithOptimisticUpdate>[0]['table'],
|
|
queueOperation,
|
|
projectRef: 'project-ref',
|
|
})
|
|
|
|
expect(queueOperation).toHaveBeenCalledWith({
|
|
type: QueuedOperationType.DELETE_ROW,
|
|
tableId: 1,
|
|
payload: expect.objectContaining({
|
|
rowIdentifiers: { id: undefined, __tempId: '-100' },
|
|
originalRow: row,
|
|
}),
|
|
})
|
|
})
|
|
})
|