useTableRowOperations.ts254 lines · main
1// @ts-nocheck
2import { QueryKey, useQueryClient } from '@tanstack/react-query'
3import { useCallback } from 'react'
4import { toast } from 'sonner'
5
6import type { PendingAddRow } from '../types'
7import type { SupaRow } from '@/components/grid/types'
8import {
9 queueCellEditWithOptimisticUpdate,
10 queueRowAddWithOptimisticUpdate,
11 queueRowDeletesWithOptimisticUpdate,
12} from '@/components/grid/utils/queueOperationUtils'
13import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings'
14import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types'
15import { tableRowKeys } from '@/data/table-rows/keys'
16import { useTableRowCreateMutation } from '@/data/table-rows/table-row-create-mutation'
17import { useTableRowUpdateMutation } from '@/data/table-rows/table-row-update-mutation'
18import type { TableRowsData } from '@/data/table-rows/table-rows-query'
19import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
20import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state'
21import { useTableEditorStateSnapshot } from '@/state/table-editor'
22import type { Dictionary } from '@/types'
23
24export interface EditCellParams {
25 table: Entity
26 tableId: number
27 row: SupaRow
28 rowIdentifiers: Dictionary<unknown>
29 columnName: string
30 oldValue: unknown
31 newValue: unknown
32 enumArrayColumns?: string[]
33 /** When true, shows a success toast on non-queue save (used by side panel, not grid inline edits) */
34 onSuccess?: () => void
35}
36
37export interface AddRowParams {
38 table: Entity
39 tableId: number
40 rowData: PendingAddRow
41 enumArrayColumns?: string[]
42}
43
44export interface UpdateRowParams {
45 table: Entity
46 tableId: number
47 row: SupaRow
48 rowIdentifiers: Dictionary<unknown>
49 payload: Dictionary<unknown>
50 enumArrayColumns?: string[]
51 onSuccess?: () => void
52}
53
54export interface DeleteRowsParams {
55 rows: SupaRow[]
56 table: Entity
57 allRowsSelected?: boolean
58 totalRows?: number
59 callback?: () => void
60}
61
62export function useTableRowOperations() {
63 const isQueueEnabled = useIsQueueOperationsEnabled()
64 const queryClient = useQueryClient()
65 const { data: project } = useSelectedProjectQuery()
66 const tableEditorSnap = useTableEditorStateSnapshot()
67 const getImpersonatedRoleState = useGetImpersonatedRoleState()
68
69 // Non-queue mutation for cell edits with optimistic updates
70 const { mutateAsync: mutateUpdateTableRow, isPending: isEditPending } = useTableRowUpdateMutation(
71 {
72 async onMutate({ projectRef, table, configuration, payload }) {
73 const primaryKeyColumns = new Set(Object.keys(configuration.identifiers))
74 const queryKey = tableRowKeys.tableRows(projectRef, { table: { id: table.id } })
75
76 await queryClient.cancelQueries({ queryKey })
77
78 const previousRowsQueries = queryClient.getQueriesData<TableRowsData>({ queryKey })
79
80 queryClient.setQueriesData<TableRowsData>({ queryKey }, (old) => {
81 if (!old) return old
82 return {
83 rows: old.rows.map((row) => {
84 if (
85 Object.entries(row)
86 .filter(([key]) => primaryKeyColumns.has(key))
87 .every(([key, value]) => value === configuration.identifiers[key])
88 ) {
89 return { ...row, ...payload }
90 }
91 return row
92 }),
93 }
94 })
95
96 return { previousRowsQueries }
97 },
98 onError(error, _variables, context) {
99 const { previousRowsQueries } = (context ?? { previousRowsQueries: [] }) as {
100 previousRowsQueries: [QueryKey, TableRowsData | undefined][]
101 }
102
103 previousRowsQueries.forEach(([queryKey, previousRows]) => {
104 if (previousRows) {
105 queryClient.setQueriesData<TableRowsData>({ queryKey }, previousRows)
106 }
107 queryClient.invalidateQueries({ queryKey })
108 })
109
110 toast.error(error?.message ?? error)
111 },
112 }
113 )
114
115 // Non-queue mutation for row creation
116 const { mutateAsync: mutateCreateTableRow } = useTableRowCreateMutation({
117 onSuccess() {
118 toast.success('Successfully created row')
119 },
120 })
121
122 const editCell = useCallback(
123 async (params: EditCellParams) => {
124 if (isQueueEnabled) {
125 queueCellEditWithOptimisticUpdate({
126 queueOperation: tableEditorSnap.queueOperation,
127 tableId: params.tableId,
128 table: params.table,
129 row: params.row,
130 rowIdentifiers: params.rowIdentifiers,
131 columnName: params.columnName,
132 oldValue: params.oldValue,
133 newValue: params.newValue,
134 enumArrayColumns: params.enumArrayColumns,
135 })
136 return
137 }
138
139 if (!project) return
140
141 const updatedData = { [params.columnName]: params.newValue }
142 await mutateUpdateTableRow({
143 projectRef: project.ref,
144 connectionString: project.connectionString,
145 table: params.table,
146 configuration: { identifiers: params.rowIdentifiers },
147 payload: updatedData,
148 enumArrayColumns: params.enumArrayColumns ?? [],
149 roleImpersonationState: getImpersonatedRoleState(),
150 })
151 params.onSuccess?.()
152 },
153 [isQueueEnabled, project, tableEditorSnap, mutateUpdateTableRow, getImpersonatedRoleState]
154 )
155
156 const updateRow = useCallback(
157 async (params: UpdateRowParams) => {
158 if (isQueueEnabled) {
159 // Queue individual cell edits per changed column
160 for (const columnName of Object.keys(params.payload)) {
161 queueCellEditWithOptimisticUpdate({
162 queueOperation: tableEditorSnap.queueOperation,
163 tableId: params.tableId,
164 table: params.table,
165 row: params.row,
166 rowIdentifiers: params.rowIdentifiers,
167 columnName,
168 oldValue: params.row[columnName],
169 newValue: params.payload[columnName],
170 enumArrayColumns: params.enumArrayColumns,
171 })
172 }
173 return
174 }
175
176 if (!project) return
177
178 await mutateUpdateTableRow({
179 projectRef: project.ref,
180 connectionString: project.connectionString,
181 table: params.table,
182 configuration: { identifiers: params.rowIdentifiers },
183 payload: params.payload,
184 enumArrayColumns: params.enumArrayColumns ?? [],
185 roleImpersonationState: getImpersonatedRoleState(),
186 })
187 params.onSuccess?.()
188 },
189 [isQueueEnabled, project, tableEditorSnap, mutateUpdateTableRow, getImpersonatedRoleState]
190 )
191
192 const addRow = useCallback(
193 async (params: AddRowParams) => {
194 // Only queue if the table has primary keys (required for queue conflict resolution)
195 const hasPrimaryKeys = isTableLike(params.table) && params.table.primary_keys.length > 0
196
197 if (isQueueEnabled && hasPrimaryKeys) {
198 queueRowAddWithOptimisticUpdate({
199 queueOperation: tableEditorSnap.queueOperation,
200 tableId: params.tableId,
201 table: params.table,
202 rowData: params.rowData,
203 enumArrayColumns: params.enumArrayColumns,
204 })
205 return
206 }
207
208 if (!project) return
209
210 await mutateCreateTableRow({
211 projectRef: project.ref,
212 connectionString: project.connectionString,
213 table: params.table,
214 payload: params.rowData,
215 enumArrayColumns: params.enumArrayColumns ?? [],
216 roleImpersonationState: getImpersonatedRoleState(),
217 })
218 },
219 [isQueueEnabled, project, tableEditorSnap, mutateCreateTableRow, getImpersonatedRoleState]
220 )
221
222 const deleteRows = useCallback(
223 (params: DeleteRowsParams) => {
224 // When queue is enabled and not all rows are selected, queue the deletes
225 if (isQueueEnabled && !params.allRowsSelected) {
226 queueRowDeletesWithOptimisticUpdate({
227 rows: params.rows,
228 table: params.table,
229 queueOperation: tableEditorSnap.queueOperation,
230 projectRef: project?.ref,
231 })
232 params.callback?.()
233 return
234 }
235
236 // Otherwise, open the confirmation dialog
237 tableEditorSnap.onDeleteRows(params.rows, {
238 allRowsSelected: params.allRowsSelected ?? false,
239 numRows: params.allRowsSelected ? params.totalRows : params.rows.length,
240 callback: params.callback,
241 })
242 },
243 [isQueueEnabled, project, tableEditorSnap]
244 )
245
246 return {
247 editCell,
248 updateRow,
249 addRow,
250 deleteRows,
251 isQueueEnabled,
252 isEditPending,
253 }
254}