import { QueryKey, useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { toast } from 'sonner' import type { PendingAddRow } from '../types' import type { SupaRow } from '@/components/grid/types' import { queueCellEditWithOptimisticUpdate, queueRowAddWithOptimisticUpdate, queueRowDeletesWithOptimisticUpdate, } from '@/components/grid/utils/queueOperationUtils' import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' import { isTableLike, type Entity } from '@/data/table-editor/table-editor-types' import { tableRowKeys } from '@/data/table-rows/keys' import { useTableRowCreateMutation } from '@/data/table-rows/table-row-create-mutation' import { useTableRowUpdateMutation } from '@/data/table-rows/table-row-update-mutation' import type { TableRowsData } from '@/data/table-rows/table-rows-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state' import { useTableEditorStateSnapshot } from '@/state/table-editor' import type { Dictionary } from '@/types' export interface EditCellParams { table: Entity tableId: number row: SupaRow rowIdentifiers: Dictionary columnName: string oldValue: unknown newValue: unknown enumArrayColumns?: string[] /** When true, shows a success toast on non-queue save (used by side panel, not grid inline edits) */ onSuccess?: () => void } export interface AddRowParams { table: Entity tableId: number rowData: PendingAddRow enumArrayColumns?: string[] } export interface UpdateRowParams { table: Entity tableId: number row: SupaRow rowIdentifiers: Dictionary payload: Dictionary enumArrayColumns?: string[] onSuccess?: () => void } export interface DeleteRowsParams { rows: SupaRow[] table: Entity allRowsSelected?: boolean totalRows?: number callback?: () => void } export function useTableRowOperations() { const isQueueEnabled = useIsQueueOperationsEnabled() const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const tableEditorSnap = useTableEditorStateSnapshot() const getImpersonatedRoleState = useGetImpersonatedRoleState() // Non-queue mutation for cell edits with optimistic updates const { mutateAsync: mutateUpdateTableRow, isPending: isEditPending } = useTableRowUpdateMutation( { async onMutate({ projectRef, table, configuration, payload }) { const primaryKeyColumns = new Set(Object.keys(configuration.identifiers)) const queryKey = tableRowKeys.tableRows(projectRef, { table: { id: table.id } }) await queryClient.cancelQueries({ queryKey }) const previousRowsQueries = queryClient.getQueriesData({ queryKey }) queryClient.setQueriesData({ queryKey }, (old) => { if (!old) return old return { rows: old.rows.map((row) => { if ( Object.entries(row) .filter(([key]) => primaryKeyColumns.has(key)) .every(([key, value]) => value === configuration.identifiers[key]) ) { return { ...row, ...payload } } return row }), } }) return { previousRowsQueries } }, onError(error, _variables, context) { const { previousRowsQueries } = (context ?? { previousRowsQueries: [] }) as { previousRowsQueries: [QueryKey, TableRowsData | undefined][] } previousRowsQueries.forEach(([queryKey, previousRows]) => { if (previousRows) { queryClient.setQueriesData({ queryKey }, previousRows) } queryClient.invalidateQueries({ queryKey }) }) toast.error(error?.message ?? error) }, } ) // Non-queue mutation for row creation const { mutateAsync: mutateCreateTableRow } = useTableRowCreateMutation({ onSuccess() { toast.success('Successfully created row') }, }) const editCell = useCallback( async (params: EditCellParams) => { if (isQueueEnabled) { queueCellEditWithOptimisticUpdate({ queueOperation: tableEditorSnap.queueOperation, tableId: params.tableId, table: params.table, row: params.row, rowIdentifiers: params.rowIdentifiers, columnName: params.columnName, oldValue: params.oldValue, newValue: params.newValue, enumArrayColumns: params.enumArrayColumns, }) return } if (!project) return const updatedData = { [params.columnName]: params.newValue } await mutateUpdateTableRow({ projectRef: project.ref, connectionString: project.connectionString, table: params.table, configuration: { identifiers: params.rowIdentifiers }, payload: updatedData, enumArrayColumns: params.enumArrayColumns ?? [], roleImpersonationState: getImpersonatedRoleState(), }) params.onSuccess?.() }, [isQueueEnabled, project, tableEditorSnap, mutateUpdateTableRow, getImpersonatedRoleState] ) const updateRow = useCallback( async (params: UpdateRowParams) => { if (isQueueEnabled) { // Queue individual cell edits per changed column for (const columnName of Object.keys(params.payload)) { queueCellEditWithOptimisticUpdate({ queueOperation: tableEditorSnap.queueOperation, tableId: params.tableId, table: params.table, row: params.row, rowIdentifiers: params.rowIdentifiers, columnName, oldValue: params.row[columnName], newValue: params.payload[columnName], enumArrayColumns: params.enumArrayColumns, }) } return } if (!project) return await mutateUpdateTableRow({ projectRef: project.ref, connectionString: project.connectionString, table: params.table, configuration: { identifiers: params.rowIdentifiers }, payload: params.payload, enumArrayColumns: params.enumArrayColumns ?? [], roleImpersonationState: getImpersonatedRoleState(), }) params.onSuccess?.() }, [isQueueEnabled, project, tableEditorSnap, mutateUpdateTableRow, getImpersonatedRoleState] ) const addRow = useCallback( async (params: AddRowParams) => { // Only queue if the table has primary keys (required for queue conflict resolution) const hasPrimaryKeys = isTableLike(params.table) && params.table.primary_keys.length > 0 if (isQueueEnabled && hasPrimaryKeys) { queueRowAddWithOptimisticUpdate({ queueOperation: tableEditorSnap.queueOperation, tableId: params.tableId, table: params.table, rowData: params.rowData, enumArrayColumns: params.enumArrayColumns, }) return } if (!project) return await mutateCreateTableRow({ projectRef: project.ref, connectionString: project.connectionString, table: params.table, payload: params.rowData, enumArrayColumns: params.enumArrayColumns ?? [], roleImpersonationState: getImpersonatedRoleState(), }) }, [isQueueEnabled, project, tableEditorSnap, mutateCreateTableRow, getImpersonatedRoleState] ) const deleteRows = useCallback( (params: DeleteRowsParams) => { // When queue is enabled and not all rows are selected, queue the deletes if (isQueueEnabled && !params.allRowsSelected) { queueRowDeletesWithOptimisticUpdate({ rows: params.rows, table: params.table, queueOperation: tableEditorSnap.queueOperation, projectRef: project?.ref, }) params.callback?.() return } // Otherwise, open the confirmation dialog tableEditorSnap.onDeleteRows(params.rows, { allRowsSelected: params.allRowsSelected ?? false, numRows: params.allRowsSelected ? params.totalRows : params.rows.length, callback: params.callback, }) }, [isQueueEnabled, project, tableEditorSnap] ) return { editCell, updateRow, addRow, deleteRows, isQueueEnabled, isEditPending, } }