queueConflictResolution.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // @ts-nocheck
  2. import { isEqual } from 'lodash'
  3. import { isPendingAddRow } from '../types'
  4. import { generateTableChangeKey, rowMatchesIdentifiers } from './queueOperationUtils'
  5. import { tryParseJson } from '@/lib/helpers'
  6. import {
  7. isDeleteRowOperation,
  8. isEditCellContentOperation,
  9. NewQueuedOperation,
  10. QueuedOperation,
  11. QueuedOperationType,
  12. type NewDeleteRowOperation,
  13. type NewEditCellContentOperation,
  14. } from '@/state/table-editor-operation-queue.types'
  15. export type DeleteConflictResult =
  16. | { action: 'skip'; filteredOperations: QueuedOperation[] }
  17. | { action: 'add'; filteredOperations: QueuedOperation[] }
  18. export type EditConflictResult =
  19. | { action: 'reject'; reason: string }
  20. | { action: 'merge'; updatedOperations: QueuedOperation[] }
  21. | { action: 'add' }
  22. export type UpsertResult = {
  23. operations: QueuedOperation[]
  24. }
  25. function editOperationMatchesTempId(operation: QueuedOperation, tempId: string): boolean {
  26. if (!isEditCellContentOperation(operation)) return false
  27. return operation.payload.rowIdentifiers.__tempId === tempId
  28. }
  29. export function operationMatchesRow(
  30. operation: QueuedOperation,
  31. tableId: number,
  32. rowIdentifiers: Record<string, unknown>
  33. ): boolean {
  34. if (operation.tableId !== tableId) return false
  35. if (
  36. operation.type === QueuedOperationType.EDIT_CELL_CONTENT ||
  37. operation.type === QueuedOperationType.DELETE_ROW
  38. ) {
  39. return rowMatchesIdentifiers(operation.payload.rowIdentifiers, rowIdentifiers)
  40. }
  41. return false
  42. }
  43. export function resolveDeleteRowConflicts(
  44. operations: readonly QueuedOperation[],
  45. deleteOperation: NewDeleteRowOperation
  46. ): DeleteConflictResult {
  47. const rowIdentifiers = deleteOperation.payload.rowIdentifiers
  48. // Check if this row was newly added (by tempId)
  49. // If deleting a newly added row, filter out the ADD_ROW operation
  50. const originalRow = deleteOperation.payload.originalRow
  51. if (isPendingAddRow(originalRow)) {
  52. const tempId = originalRow.__tempId
  53. const addRowKey = generateTableChangeKey({
  54. type: QueuedOperationType.ADD_ROW,
  55. tableId: deleteOperation.tableId,
  56. payload: {
  57. tempId,
  58. rowData: originalRow,
  59. table: deleteOperation.payload.table,
  60. },
  61. })
  62. let filteredOperations = operations
  63. .filter((op) => op.id !== addRowKey)
  64. .filter((op) => !editOperationMatchesTempId(op, tempId))
  65. return { action: 'skip', filteredOperations }
  66. }
  67. // For existing rows, remove any pending EDIT_CELL operations for the row being deleted
  68. const filteredOperations = operations.filter(
  69. (op) => !operationMatchesRow(op, deleteOperation.tableId, rowIdentifiers)
  70. )
  71. return { action: 'add', filteredOperations }
  72. }
  73. export function resolveEditCellConflicts(
  74. operations: readonly QueuedOperation[],
  75. editOperation: NewEditCellContentOperation
  76. ): EditConflictResult {
  77. const rowIdentifiers = editOperation.payload.rowIdentifiers
  78. // Check if this row is pending deletion
  79. const isPendingDeletion = operations.filter(isDeleteRowOperation).some((op) => {
  80. if (op.tableId === editOperation.tableId) {
  81. return Object.entries(op.payload.rowIdentifiers).every(
  82. ([key, value]) => rowIdentifiers[key] === value
  83. )
  84. }
  85. return false
  86. })
  87. if (isPendingDeletion) {
  88. return {
  89. action: 'reject',
  90. reason:
  91. 'Cannot edit a cell on a row that is pending deletion. Remove the delete operation first.',
  92. }
  93. }
  94. // Check if this edit is on a newly added row (by tempId)
  95. const tempId = rowIdentifiers.__tempId
  96. if (tempId) {
  97. const addRowIndex = operations.findIndex((op) => {
  98. if (op.type === QueuedOperationType.ADD_ROW && op.tableId === editOperation.tableId) {
  99. return op.payload.tempId === tempId
  100. }
  101. return false
  102. })
  103. if (addRowIndex >= 0) {
  104. // Merge the edit into the ADD_ROW's rowData
  105. const updatedOperations = [...operations]
  106. const addOp = updatedOperations[addRowIndex]
  107. if (addOp.type === QueuedOperationType.ADD_ROW) {
  108. const addPayload = { ...addOp.payload }
  109. addPayload.rowData = {
  110. ...addPayload.rowData,
  111. [editOperation.payload.columnName]: editOperation.payload.newValue,
  112. }
  113. updatedOperations[addRowIndex] = {
  114. ...addOp,
  115. payload: addPayload,
  116. timestamp: Date.now(),
  117. }
  118. }
  119. return { action: 'merge', updatedOperations }
  120. }
  121. }
  122. return { action: 'add' }
  123. }
  124. export function upsertOperation(
  125. operations: readonly QueuedOperation[],
  126. newOperation: NewQueuedOperation
  127. ): UpsertResult {
  128. const operationKey = generateTableChangeKey(newOperation)
  129. const existingOpIndex = operations.findIndex((op) => op.id === operationKey)
  130. const queuedOperation: QueuedOperation = {
  131. ...newOperation,
  132. id: operationKey,
  133. timestamp: Date.now(),
  134. }
  135. if (existingOpIndex >= 0) {
  136. const updatedOperations = [...operations]
  137. // Keep the old value of the operation that is being overwritten
  138. // When a user edits the same cell multiple times before saving,
  139. // we need to preserve the original "before edit" value
  140. const existingOp = operations[existingOpIndex]
  141. if (
  142. queuedOperation.type === QueuedOperationType.EDIT_CELL_CONTENT &&
  143. existingOp.type === QueuedOperationType.EDIT_CELL_CONTENT
  144. ) {
  145. queuedOperation.payload.oldValue = existingOp.payload.oldValue
  146. const { oldValue, newValue } = queuedOperation.payload
  147. // [Joshen] These comparisons by data type are because of how the table editor renders the values
  148. if (
  149. (typeof oldValue === 'number' && Number(oldValue) === Number(newValue)) ||
  150. (typeof newValue === 'object' && isEqual(tryParseJson(oldValue), newValue)) ||
  151. oldValue === newValue
  152. ) {
  153. updatedOperations.splice(existingOpIndex, 1)
  154. return { operations: updatedOperations }
  155. }
  156. }
  157. updatedOperations[existingOpIndex] = queuedOperation
  158. return { operations: updatedOperations }
  159. }
  160. return { operations: [...operations, queuedOperation] }
  161. }