queueOperationUtils.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // @ts-nocheck
  2. import { isPendingAddRow, PendingAddRow, SupaRow } from '../types'
  3. import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types'
  4. import {
  5. EditCellContentOperation,
  6. NewQueuedOperation,
  7. QueuedOperation,
  8. QueuedOperationType,
  9. } from '@/state/table-editor-operation-queue.types'
  10. import type { Dictionary } from '@/types'
  11. interface EditCellKeyOperation extends Omit<
  12. EditCellContentOperation,
  13. 'payload' | 'id' | 'timestamp'
  14. > {
  15. type: QueuedOperationType.EDIT_CELL_CONTENT
  16. tableId: number
  17. payload: {
  18. columnName: string
  19. rowIdentifiers: Dictionary<unknown>
  20. }
  21. }
  22. export function generateTableChangeKey(
  23. operation: NewQueuedOperation | EditCellKeyOperation
  24. ): string {
  25. if (operation.type === QueuedOperationType.EDIT_CELL_CONTENT) {
  26. const { columnName, rowIdentifiers } = operation.payload
  27. const rowIdentifiersKey = Object.entries(rowIdentifiers)
  28. .sort(([a], [b]) => a.localeCompare(b))
  29. .map(([key, value]) => `${key}:${value}`)
  30. .join('|')
  31. return `${operation.type}:${operation.tableId}:${columnName}:${rowIdentifiersKey}`
  32. }
  33. if (operation.type === QueuedOperationType.ADD_ROW) {
  34. return `${operation.type}:${operation.tableId}:${operation.payload.tempId}`
  35. }
  36. if (operation.type === QueuedOperationType.DELETE_ROW) {
  37. const { rowIdentifiers } = operation.payload
  38. const rowIdentifiersKey = Object.entries(rowIdentifiers)
  39. .sort(([a], [b]) => a.localeCompare(b))
  40. .map(([key, value]) => `${key}:${value}`)
  41. .join('|')
  42. return `${operation.type}:${operation.tableId}:${rowIdentifiersKey}`
  43. }
  44. // Exhaustive check - TypeScript will error if we miss a case
  45. const _exhaustiveCheck: never = operation
  46. throw new Error(`Unknown operation type: ${(_exhaustiveCheck as { type: string }).type}`)
  47. }
  48. export function rowMatchesIdentifiers(
  49. row: Dictionary<unknown>,
  50. rowIdentifiers: Dictionary<unknown>
  51. ): boolean {
  52. const identifierEntries = Object.entries(rowIdentifiers)
  53. if (identifierEntries.length === 0) return false
  54. return identifierEntries.every(([key, value]) => row[key] === value)
  55. }
  56. export function removeRow(rows: SupaRow[], rowIdentifiers: Dictionary<unknown>): SupaRow[] {
  57. return rows.filter((row) => !rowMatchesIdentifiers(row, rowIdentifiers))
  58. }
  59. interface QueueCellEditParams {
  60. queueOperation: (operation: NewQueuedOperation) => void
  61. tableId: number
  62. table: Entity
  63. row: SupaRow
  64. rowIdentifiers: Dictionary<unknown>
  65. columnName: string
  66. oldValue: unknown
  67. newValue: unknown
  68. enumArrayColumns?: string[]
  69. }
  70. export function queueCellEditWithOptimisticUpdate({
  71. queueOperation,
  72. tableId,
  73. table,
  74. row,
  75. rowIdentifiers: callerRowIdentifiers,
  76. columnName,
  77. oldValue,
  78. newValue,
  79. enumArrayColumns,
  80. }: QueueCellEditParams) {
  81. // Updated row identifiers to include __tempId for pending add rows so edits merge into ADD_ROW operation
  82. const rowIdentifiers: Dictionary<unknown> = { ...callerRowIdentifiers }
  83. if (isPendingAddRow(row)) {
  84. rowIdentifiers.__tempId = row.__tempId
  85. }
  86. // Queue the operation
  87. queueOperation({
  88. type: QueuedOperationType.EDIT_CELL_CONTENT,
  89. tableId,
  90. payload: {
  91. rowIdentifiers,
  92. columnName,
  93. oldValue,
  94. newValue,
  95. table,
  96. enumArrayColumns,
  97. },
  98. })
  99. }
  100. interface QueueRowAddParams {
  101. queueOperation: (operation: NewQueuedOperation) => void
  102. tableId: number
  103. table: Entity
  104. rowData: PendingAddRow
  105. enumArrayColumns?: string[]
  106. }
  107. export function queueRowAddWithOptimisticUpdate({
  108. queueOperation,
  109. tableId,
  110. table,
  111. rowData,
  112. enumArrayColumns,
  113. }: QueueRowAddParams) {
  114. // Generate unique idx and tempId for this pending row
  115. const idx = -Date.now()
  116. const tempId = String(idx)
  117. // Queue the operation
  118. queueOperation({
  119. type: QueuedOperationType.ADD_ROW,
  120. tableId,
  121. payload: {
  122. tempId,
  123. rowData,
  124. table,
  125. enumArrayColumns,
  126. },
  127. })
  128. }
  129. export const formatGridDataWithOperationValues = ({
  130. operations,
  131. rows,
  132. }: {
  133. operations: QueuedOperation[]
  134. rows: SupaRow[]
  135. }) => {
  136. const formattedRows = rows.slice()
  137. operations.forEach((op) => {
  138. if (op.type === QueuedOperationType.EDIT_CELL_CONTENT) {
  139. const { rowIdentifiers, columnName, newValue } = op.payload
  140. const rowIdx = formattedRows.findIndex((row) => rowMatchesIdentifiers(row, rowIdentifiers))
  141. if (rowIdx !== -1) {
  142. formattedRows[rowIdx] = { ...formattedRows[rowIdx], [columnName]: newValue }
  143. }
  144. } else if (op.type === QueuedOperationType.ADD_ROW) {
  145. const { tempId, rowData } = op.payload
  146. const idx = Number(tempId)
  147. // Check if row with this tempId already exists
  148. const existingIndex = formattedRows.findIndex(
  149. (row) => isPendingAddRow(row) && row.__tempId === tempId
  150. )
  151. if (existingIndex >= 0) {
  152. // Update existing row in place
  153. formattedRows[existingIndex] = {
  154. ...formattedRows[existingIndex],
  155. ...rowData,
  156. __tempId: tempId,
  157. }
  158. } else {
  159. const newRow: PendingAddRow = { ...rowData, idx, __tempId: tempId }
  160. formattedRows.unshift(newRow)
  161. }
  162. } else if (op.type === QueuedOperationType.DELETE_ROW) {
  163. const { rowIdentifiers } = op.payload
  164. const rowIdx = formattedRows.findIndex((row) => rowMatchesIdentifiers(row, rowIdentifiers))
  165. if (rowIdx !== -1) {
  166. formattedRows[rowIdx] = { ...formattedRows[rowIdx], __isDeleted: true }
  167. }
  168. }
  169. })
  170. return formattedRows
  171. }
  172. interface QueueRowDeletesParams {
  173. rows: SupaRow[]
  174. table: Entity
  175. queueOperation: (operation: NewQueuedOperation) => void
  176. projectRef: string | undefined
  177. }
  178. /**
  179. * Queue multiple row delete operations with optimistic updates.
  180. * Caller is responsible for checking if queue mode is enabled before calling.
  181. */
  182. export function queueRowDeletesWithOptimisticUpdate({
  183. rows,
  184. table,
  185. queueOperation,
  186. projectRef,
  187. }: QueueRowDeletesParams): void {
  188. // [Ali] We can handle these better in the future
  189. // right now this is a pretty abnormal case of this occurring
  190. if (!projectRef) {
  191. console.error('Cannot queue row deletes: projectRef is required')
  192. return
  193. }
  194. if (!isTableLike(table)) {
  195. console.error('Cannot queue row deletes: table must be a TableLike entity')
  196. return
  197. }
  198. if (table.primary_keys.length === 0) {
  199. console.error('Cannot queue row deletes: table has no primary keys')
  200. return
  201. }
  202. for (const row of rows) {
  203. const rowIdentifiers: Record<string, unknown> = {}
  204. table.primary_keys.forEach((pk) => {
  205. rowIdentifiers[pk.name] = row[pk.name]
  206. })
  207. queueOperation({
  208. type: QueuedOperationType.DELETE_ROW,
  209. tableId: table.id,
  210. payload: {
  211. rowIdentifiers,
  212. originalRow: row,
  213. table,
  214. },
  215. })
  216. }
  217. }