mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 07:50:20 +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? Revert changes when the value is the same as the old value --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
190 lines
5.9 KiB
TypeScript
190 lines
5.9 KiB
TypeScript
import { isEqual } from 'lodash'
|
|
|
|
import { isPendingAddRow } from '../types'
|
|
import { generateTableChangeKey, rowMatchesIdentifiers } from './queueOperationUtils'
|
|
import { tryParseJson } from '@/lib/helpers'
|
|
import {
|
|
isDeleteRowOperation,
|
|
isEditCellContentOperation,
|
|
NewQueuedOperation,
|
|
QueuedOperation,
|
|
QueuedOperationType,
|
|
type NewDeleteRowOperation,
|
|
type NewEditCellContentOperation,
|
|
} from '@/state/table-editor-operation-queue.types'
|
|
|
|
export type DeleteConflictResult =
|
|
| { action: 'skip'; filteredOperations: QueuedOperation[] }
|
|
| { action: 'add'; filteredOperations: QueuedOperation[] }
|
|
|
|
export type EditConflictResult =
|
|
| { action: 'reject'; reason: string }
|
|
| { action: 'merge'; updatedOperations: QueuedOperation[] }
|
|
| { action: 'add' }
|
|
|
|
export type UpsertResult = {
|
|
operations: QueuedOperation[]
|
|
}
|
|
|
|
function editOperationMatchesTempId(operation: QueuedOperation, tempId: string): boolean {
|
|
if (!isEditCellContentOperation(operation)) return false
|
|
return operation.payload.rowIdentifiers.__tempId === tempId
|
|
}
|
|
|
|
export function operationMatchesRow(
|
|
operation: QueuedOperation,
|
|
tableId: number,
|
|
rowIdentifiers: Record<string, unknown>
|
|
): boolean {
|
|
if (operation.tableId !== tableId) return false
|
|
|
|
if (
|
|
operation.type === QueuedOperationType.EDIT_CELL_CONTENT ||
|
|
operation.type === QueuedOperationType.DELETE_ROW
|
|
) {
|
|
return rowMatchesIdentifiers(operation.payload.rowIdentifiers, rowIdentifiers)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export function resolveDeleteRowConflicts(
|
|
operations: readonly QueuedOperation[],
|
|
deleteOperation: NewDeleteRowOperation
|
|
): DeleteConflictResult {
|
|
const rowIdentifiers = deleteOperation.payload.rowIdentifiers
|
|
|
|
// Check if this row was newly added (by tempId)
|
|
// If deleting a newly added row, filter out the ADD_ROW operation
|
|
const originalRow = deleteOperation.payload.originalRow
|
|
if (isPendingAddRow(originalRow)) {
|
|
const tempId = originalRow.__tempId
|
|
const addRowKey = generateTableChangeKey({
|
|
type: QueuedOperationType.ADD_ROW,
|
|
tableId: deleteOperation.tableId,
|
|
payload: {
|
|
tempId,
|
|
rowData: originalRow,
|
|
table: deleteOperation.payload.table,
|
|
},
|
|
})
|
|
|
|
let filteredOperations = operations
|
|
.filter((op) => op.id !== addRowKey)
|
|
.filter((op) => !editOperationMatchesTempId(op, tempId))
|
|
|
|
return { action: 'skip', filteredOperations }
|
|
}
|
|
|
|
// For existing rows, remove any pending EDIT_CELL operations for the row being deleted
|
|
const filteredOperations = operations.filter(
|
|
(op) => !operationMatchesRow(op, deleteOperation.tableId, rowIdentifiers)
|
|
)
|
|
|
|
return { action: 'add', filteredOperations }
|
|
}
|
|
|
|
export function resolveEditCellConflicts(
|
|
operations: readonly QueuedOperation[],
|
|
editOperation: NewEditCellContentOperation
|
|
): EditConflictResult {
|
|
const rowIdentifiers = editOperation.payload.rowIdentifiers
|
|
|
|
// Check if this row is pending deletion
|
|
const isPendingDeletion = operations.filter(isDeleteRowOperation).some((op) => {
|
|
if (op.tableId === editOperation.tableId) {
|
|
return Object.entries(op.payload.rowIdentifiers).every(
|
|
([key, value]) => rowIdentifiers[key] === value
|
|
)
|
|
}
|
|
return false
|
|
})
|
|
|
|
if (isPendingDeletion) {
|
|
return {
|
|
action: 'reject',
|
|
reason:
|
|
'Cannot edit a cell on a row that is pending deletion. Remove the delete operation first.',
|
|
}
|
|
}
|
|
|
|
// Check if this edit is on a newly added row (by tempId)
|
|
const tempId = rowIdentifiers.__tempId
|
|
if (tempId) {
|
|
const addRowIndex = operations.findIndex((op) => {
|
|
if (op.type === QueuedOperationType.ADD_ROW && op.tableId === editOperation.tableId) {
|
|
return op.payload.tempId === tempId
|
|
}
|
|
return false
|
|
})
|
|
|
|
if (addRowIndex >= 0) {
|
|
// Merge the edit into the ADD_ROW's rowData
|
|
const updatedOperations = [...operations]
|
|
const addOp = updatedOperations[addRowIndex]
|
|
if (addOp.type === QueuedOperationType.ADD_ROW) {
|
|
const addPayload = { ...addOp.payload }
|
|
addPayload.rowData = {
|
|
...addPayload.rowData,
|
|
[editOperation.payload.columnName]: editOperation.payload.newValue,
|
|
}
|
|
|
|
updatedOperations[addRowIndex] = {
|
|
...addOp,
|
|
payload: addPayload,
|
|
timestamp: Date.now(),
|
|
}
|
|
}
|
|
|
|
return { action: 'merge', updatedOperations }
|
|
}
|
|
}
|
|
|
|
return { action: 'add' }
|
|
}
|
|
|
|
export function upsertOperation(
|
|
operations: readonly QueuedOperation[],
|
|
newOperation: NewQueuedOperation
|
|
): UpsertResult {
|
|
const operationKey = generateTableChangeKey(newOperation)
|
|
const existingOpIndex = operations.findIndex((op) => op.id === operationKey)
|
|
|
|
const queuedOperation: QueuedOperation = {
|
|
...newOperation,
|
|
id: operationKey,
|
|
timestamp: Date.now(),
|
|
}
|
|
|
|
if (existingOpIndex >= 0) {
|
|
const updatedOperations = [...operations]
|
|
|
|
// Keep the old value of the operation that is being overwritten
|
|
// When a user edits the same cell multiple times before saving,
|
|
// we need to preserve the original "before edit" value
|
|
const existingOp = operations[existingOpIndex]
|
|
if (
|
|
queuedOperation.type === QueuedOperationType.EDIT_CELL_CONTENT &&
|
|
existingOp.type === QueuedOperationType.EDIT_CELL_CONTENT
|
|
) {
|
|
queuedOperation.payload.oldValue = existingOp.payload.oldValue
|
|
|
|
const { oldValue, newValue } = queuedOperation.payload
|
|
// [Joshen] These comparisons by data type are because of how the table editor renders the values
|
|
if (
|
|
(typeof oldValue === 'number' && Number(oldValue) === Number(newValue)) ||
|
|
(typeof newValue === 'object' && isEqual(tryParseJson(oldValue), newValue)) ||
|
|
oldValue === newValue
|
|
) {
|
|
updatedOperations.splice(existingOpIndex, 1)
|
|
return { operations: updatedOperations }
|
|
}
|
|
}
|
|
|
|
updatedOperations[existingOpIndex] = queuedOperation
|
|
return { operations: updatedOperations }
|
|
}
|
|
|
|
return { operations: [...operations, queuedOperation] }
|
|
}
|