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