cleaned up source

This commit is contained in:
Ali Waseem committed 2026-01-22 13:49:23 -07:00
1 parent e3474d9096
commit bfba68aa38
1 file changed
+79 -106
@@ -6,7 +6,7 @@ import { toast } from 'sonner'
import { SupaRow } from 'components/grid/types'
import { convertByteaToHex } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
import { DocsButton } from 'components/ui/DocsButton'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { Entity, isTableLike } from 'data/table-editor/table-editor-types'
import { tableRowKeys } from 'data/table-rows/keys'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import type { TableRowsData } from 'data/table-rows/table-rows-query'
@@ -16,11 +16,33 @@ import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
import { CellEditHistoryItem, useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import type { Dictionary } from 'types'
/**
* Creates optimistic update handlers for table row mutations.
* These handlers update the UI immediately while the API call is in progress,
* and rollback if the API call fails.
*/
const UNDO_TOAST_DURATION = 5000
/** Gets enum array column names from a table (needed for proper SQL generation) */
function getEnumArrayColumns(table: Entity): string[] {
return (
table.columns
?.filter((col) => (col?.enums ?? []).length > 0 && col.data_type.toLowerCase() === 'array')
.map((col) => col.name) ?? []
)
}
/** Builds primary key identifiers for a row */
function buildRowIdentifiers(table: Entity, row: SupaRow): Dictionary<any> {
const identifiers: Dictionary<any> = {}
if (isTableLike(table)) {
table.primary_keys.forEach((pk) => {
const col = table.columns.find((c) => c.name === pk.name)
identifiers[pk.name] =
col?.format === 'bytea' ? convertByteaToHex(row[pk.name]) : row[pk.name]
})
}
return identifiers
}
/** Creates optimistic update handlers for React Query mutations */
function createOptimisticUpdateHandlers(queryClient: ReturnType<typeof useQueryClient>) {
return {
async onMutate({ projectRef, table, configuration, payload }: any) {
@@ -28,24 +50,17 @@ function createOptimisticUpdateHandlers(queryClient: ReturnType<typeof useQueryC
const queryKey = tableRowKeys.tableRows(projectRef, { table: { id: table.id } })
await queryClient.cancelQueries({ queryKey })
const previousRowsQueries = queryClient.getQueriesData<TableRowsData>({ queryKey })
queryClient.setQueriesData<TableRowsData>({ queryKey }, (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
}) ?? [],
}
})
queryClient.setQueriesData<TableRowsData>({ queryKey }, (old) => ({
rows:
old?.rows.map((row) => {
const matchesPrimaryKey = Object.entries(row)
.filter(([key]) => primaryKeyColumns.has(key))
.every(([key, value]) => value === configuration.identifiers[key])
return matchesPrimaryKey ? { ...row, ...payload } : row
}) ?? [],
}))
return { previousRowsQueries }
},
@@ -72,74 +87,67 @@ export function useOnRowsChange(rows: SupaRow[]) {
const snap = useTableEditorTableStateSnapshot()
const getImpersonatedRoleState = useGetImpersonatedRoleState()
// Track the current toast ID so we can dismiss it when a new edit happens
const toastIdRef = useRef<string | number | undefined>(undefined)
// Use a ref to hold the undo function to avoid circular dependency
const undoCellEditRef = useRef<() => void>(() => {})
const optimisticHandlers = createOptimisticUpdateHandlers(queryClient)
// Mutation for undo operations (toast is shown optimistically in undoCellEdit)
const { mutate: mutateUndoTableRow } = useTableRowUpdateMutation(optimisticHandlers)
// Mutation for regular updates (toast is shown optimistically in onRowsChange)
const { mutate: mutateUpdateTableRow } = useTableRowUpdateMutation(optimisticHandlers)
// Undo function that reverts the most recent cell edit from the history stack
/** Dismisses current toast and shows a new undo toast */
const showUndoToast = useCallback(
(message: string, editCount: number, variant: 'default' | 'success' = 'default') => {
if (toastIdRef.current) toast.dismiss(toastIdRef.current)
const toastFn = variant === 'success' ? toast.success : toast
if (editCount > 0) {
const description =
editCount === 1
? 'Press Ctrl+Z or Cmd+Z to undo'
: `${editCount} edits can be undone (Ctrl+Z / Cmd+Z)`
toastIdRef.current = toastFn(message, {
description,
action: {
label: variant === 'success' ? 'Undo more' : 'Undo',
onClick: () => undoCellEditRef.current(),
},
duration: UNDO_TOAST_DURATION,
})
} else {
toastFn(message)
}
},
[]
)
const undoCellEdit = useCallback(() => {
if (!project || snap.cellEditHistory.length === 0) return
// Pop the most recent edit from the stack
const lastEdit = snap.popCellEdit()
if (!lastEdit) return
const { columnName, previousValue, identifiers } = lastEdit
const table = snap.originalTable
const enumArrayColumns = snap.originalTable.columns
?.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column) => column.name)
showUndoToast('Cell edit undone', snap.cellEditHistory.length, 'success')
// Dismiss the current toast and show undo confirmation immediately (optimistically)
if (toastIdRef.current) {
toast.dismiss(toastIdRef.current)
toastIdRef.current = undefined
}
const remainingEdits = snap.cellEditHistory.length
if (remainingEdits > 1) {
toastIdRef.current = toast.success('Cell edit undone', {
description: `${remainingEdits} more edit${remainingEdits > 1 ? 's' : ''} can be undone`,
action: {
label: 'Undo more',
onClick: () => undoCellEditRef.current(),
},
duration: 5000,
})
} else {
toast.success('Cell edit undone')
}
// [Ali] Defer the mutation to avoid conflicts with React's rendering cycle
// This prevents "Canceled" errors when undo is triggered during other state updates
const tableRef = snap.originalTable
// Defer mutation to avoid conflicts with React's rendering cycle
const roleState = getImpersonatedRoleState()
setTimeout(() => {
mutateUndoTableRow({
projectRef: project.ref,
connectionString: project.connectionString,
table: tableRef,
table,
configuration: { identifiers },
payload: { [columnName]: previousValue },
enumArrayColumns,
enumArrayColumns: getEnumArrayColumns(table),
roleImpersonationState: roleState,
})
}, 0)
}, [project, snap, getImpersonatedRoleState, mutateUndoTableRow])
}, [project, snap, getImpersonatedRoleState, mutateUndoTableRow, showUndoToast])
// Keep the ref updated with the latest undo function
undoCellEditRef.current = undoCellEdit
const onRowsChange = useCallback(
@@ -147,32 +155,13 @@ export function useOnRowsChange(rows: SupaRow[]) {
if (!project) return
const rowData = _rows[data.indexes[0]]
const previousRow = rows.find((x) => x.idx == rowData.idx)
const changedColumn = Object.keys(rowData).find(
(name) => rowData[name] !== previousRow![name]
)
const previousRow = rows.find((r) => r.idx === rowData.idx)
const changedColumn = Object.keys(rowData).find((col) => rowData[col] !== previousRow?.[col])
if (!previousRow || !changedColumn) return
const updatedData = { [changedColumn]: rowData[changedColumn] }
const identifiers = buildRowIdentifiers(snap.originalTable, previousRow)
const enumArrayColumns = snap.originalTable.columns
?.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column) => column.name)
const identifiers = {} as Dictionary<any>
isTableLike(snap.originalTable) &&
snap.originalTable.primary_keys.forEach((column) => {
const col = snap.originalTable.columns.find((c) => c.name === column.name)
identifiers[column.name] =
col?.format === 'bytea'
? convertByteaToHex(previousRow[column.name])
: previousRow[column.name]
})
const configuration = { identifiers }
if (Object.keys(identifiers).length === 0) {
return toast('Unable to update row as table has no primary keys', {
description: (
@@ -189,7 +178,6 @@ export function useOnRowsChange(rows: SupaRow[]) {
})
}
// Store the edit in history stack for undo functionality
const editHistoryItem: CellEditHistoryItem = {
rowIdx: rowData.idx,
columnName: changedColumn,
@@ -199,34 +187,19 @@ export function useOnRowsChange(rows: SupaRow[]) {
}
snap.pushCellEdit(editHistoryItem)
// Show undo toast immediately (optimistically)
if (toastIdRef.current) {
toast.dismiss(toastIdRef.current)
}
const historyLength = snap.cellEditHistory.length
toastIdRef.current = toast('Cell updated', {
description:
historyLength > 1
? `${historyLength} edits can be undone (Ctrl+Z / Cmd+Z)`
: `Press Ctrl+Z or Cmd+Z to undo`,
action: {
label: 'Undo',
onClick: () => undoCellEditRef.current(),
},
duration: 5000,
})
showUndoToast('Cell updated', snap.cellEditHistory.length)
mutateUpdateTableRow({
projectRef: project.ref,
connectionString: project.connectionString,
table: snap.originalTable,
configuration,
payload: updatedData,
enumArrayColumns,
configuration: { identifiers },
payload: { [changedColumn]: rowData[changedColumn] },
enumArrayColumns: getEnumArrayColumns(snap.originalTable),
roleImpersonationState: getImpersonatedRoleState(),
})
},
[getImpersonatedRoleState, mutateUpdateTableRow, project, rows, snap]
[getImpersonatedRoleState, mutateUpdateTableRow, project, rows, snap, showUndoToast]
)
return { onRowsChange, undoCellEdit }