import { isPendingAddRow, PendingAddRow, SupaRow } from '../types' import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types' import { EditCellContentOperation, NewQueuedOperation, QueuedOperation, QueuedOperationType, } from '@/state/table-editor-operation-queue.types' import type { Dictionary } from '@/types' interface EditCellKeyOperation extends Omit< EditCellContentOperation, 'payload' | 'id' | 'timestamp' > { type: QueuedOperationType.EDIT_CELL_CONTENT tableId: number payload: { columnName: string rowIdentifiers: Dictionary } } export function generateTableChangeKey( operation: NewQueuedOperation | EditCellKeyOperation ): string { if (operation.type === QueuedOperationType.EDIT_CELL_CONTENT) { const { columnName, rowIdentifiers } = operation.payload const rowIdentifiersKey = Object.entries(rowIdentifiers) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}:${value}`) .join('|') return `${operation.type}:${operation.tableId}:${columnName}:${rowIdentifiersKey}` } if (operation.type === QueuedOperationType.ADD_ROW) { return `${operation.type}:${operation.tableId}:${operation.payload.tempId}` } if (operation.type === QueuedOperationType.DELETE_ROW) { const { rowIdentifiers } = operation.payload const rowIdentifiersKey = Object.entries(rowIdentifiers) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}:${value}`) .join('|') return `${operation.type}:${operation.tableId}:${rowIdentifiersKey}` } // Exhaustive check - TypeScript will error if we miss a case const _exhaustiveCheck: never = operation throw new Error(`Unknown operation type: ${(_exhaustiveCheck as { type: string }).type}`) } export function rowMatchesIdentifiers( row: Dictionary, rowIdentifiers: Dictionary ): boolean { const identifierEntries = Object.entries(rowIdentifiers) if (identifierEntries.length === 0) return false return identifierEntries.every(([key, value]) => row[key] === value) } export function removeRow(rows: SupaRow[], rowIdentifiers: Dictionary): SupaRow[] { return rows.filter((row) => !rowMatchesIdentifiers(row, rowIdentifiers)) } interface QueueCellEditParams { queueOperation: (operation: NewQueuedOperation) => void tableId: number table: Entity row: SupaRow rowIdentifiers: Dictionary columnName: string oldValue: unknown newValue: unknown enumArrayColumns?: string[] } export function queueCellEditWithOptimisticUpdate({ queueOperation, tableId, table, row, rowIdentifiers: callerRowIdentifiers, columnName, oldValue, newValue, enumArrayColumns, }: QueueCellEditParams) { // Updated row identifiers to include __tempId for pending add rows so edits merge into ADD_ROW operation const rowIdentifiers: Dictionary = { ...callerRowIdentifiers } if (isPendingAddRow(row)) { rowIdentifiers.__tempId = row.__tempId } // Queue the operation queueOperation({ type: QueuedOperationType.EDIT_CELL_CONTENT, tableId, payload: { rowIdentifiers, columnName, oldValue, newValue, table, enumArrayColumns, }, }) } interface QueueRowAddParams { queueOperation: (operation: NewQueuedOperation) => void tableId: number table: Entity rowData: PendingAddRow enumArrayColumns?: string[] } export function queueRowAddWithOptimisticUpdate({ queueOperation, tableId, table, rowData, enumArrayColumns, }: QueueRowAddParams) { // Generate unique idx and tempId for this pending row const idx = -Date.now() const tempId = String(idx) // Queue the operation queueOperation({ type: QueuedOperationType.ADD_ROW, tableId, payload: { tempId, rowData, table, enumArrayColumns, }, }) } export const formatGridDataWithOperationValues = ({ operations, rows, }: { operations: QueuedOperation[] rows: SupaRow[] }) => { const formattedRows = rows.slice() operations.forEach((op) => { if (op.type === QueuedOperationType.EDIT_CELL_CONTENT) { const { rowIdentifiers, columnName, newValue } = op.payload const rowIdx = formattedRows.findIndex((row) => rowMatchesIdentifiers(row, rowIdentifiers)) if (rowIdx !== -1) { formattedRows[rowIdx] = { ...formattedRows[rowIdx], [columnName]: newValue } } } else if (op.type === QueuedOperationType.ADD_ROW) { const { tempId, rowData } = op.payload const idx = Number(tempId) // Check if row with this tempId already exists const existingIndex = formattedRows.findIndex( (row) => isPendingAddRow(row) && row.__tempId === tempId ) if (existingIndex >= 0) { // Update existing row in place formattedRows[existingIndex] = { ...formattedRows[existingIndex], ...rowData, __tempId: tempId, } } else { const newRow: PendingAddRow = { ...rowData, idx, __tempId: tempId } formattedRows.unshift(newRow) } } else if (op.type === QueuedOperationType.DELETE_ROW) { const { rowIdentifiers } = op.payload const rowIdx = formattedRows.findIndex((row) => rowMatchesIdentifiers(row, rowIdentifiers)) if (rowIdx !== -1) { formattedRows[rowIdx] = { ...formattedRows[rowIdx], __isDeleted: true } } } }) return formattedRows } interface QueueRowDeletesParams { rows: SupaRow[] table: Entity queueOperation: (operation: NewQueuedOperation) => void projectRef: string | undefined } /** * Queue multiple row delete operations with optimistic updates. * Caller is responsible for checking if queue mode is enabled before calling. */ export function queueRowDeletesWithOptimisticUpdate({ rows, table, queueOperation, projectRef, }: QueueRowDeletesParams): void { // [Ali] We can handle these better in the future // right now this is a pretty abnormal case of this occurring if (!projectRef) { console.error('Cannot queue row deletes: projectRef is required') return } if (!isTableLike(table)) { console.error('Cannot queue row deletes: table must be a TableLike entity') return } if (table.primary_keys.length === 0) { console.error('Cannot queue row deletes: table has no primary keys') return } for (const row of rows) { const rowIdentifiers: Record = {} table.primary_keys.forEach((pk) => { rowIdentifiers[pk.name] = row[pk.name] }) queueOperation({ type: QueuedOperationType.DELETE_ROW, tableId: table.id, payload: { rowIdentifiers, originalRow: row, table, }, }) } }