queueOperationUtils.ts240 lines · main
1// @ts-nocheck
2import { isPendingAddRow, PendingAddRow, SupaRow } from '../types'
3import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types'
4import {
5 EditCellContentOperation,
6 NewQueuedOperation,
7 QueuedOperation,
8 QueuedOperationType,
9} from '@/state/table-editor-operation-queue.types'
10import type { Dictionary } from '@/types'
11
12interface 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
24export 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
54export 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
63export function removeRow(rows: SupaRow[], rowIdentifiers: Dictionary<unknown>): SupaRow[] {
64 return rows.filter((row) => !rowMatchesIdentifiers(row, rowIdentifiers))
65}
66
67interface 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
79export 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
111interface QueueRowAddParams {
112 queueOperation: (operation: NewQueuedOperation) => void
113 tableId: number
114 table: Entity
115 rowData: PendingAddRow
116 enumArrayColumns?: string[]
117}
118
119export 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
143export 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
190interface 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 */
201export 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}