useTableRowOperations.ts 8.3 KB

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