From bfba68aa3868d7566e9d16dc5cd320580c412c86 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 22 Jan 2026 13:49:23 -0700 Subject: [PATCH] cleaned up source --- .../grid/components/grid/Grid.utils.tsx | 185 ++++++++---------- 1 file changed, 79 insertions(+), 106 deletions(-) diff --git a/apps/studio/components/grid/components/grid/Grid.utils.tsx b/apps/studio/components/grid/components/grid/Grid.utils.tsx index c3e3aea564d..ea325ba85ac 100644 --- a/apps/studio/components/grid/components/grid/Grid.utils.tsx +++ b/apps/studio/components/grid/components/grid/Grid.utils.tsx @@ -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 { + const identifiers: Dictionary = {} + + 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) { return { async onMutate({ projectRef, table, configuration, payload }: any) { @@ -28,24 +50,17 @@ function createOptimisticUpdateHandlers(queryClient: ReturnType({ queryKey }) - queryClient.setQueriesData({ 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({ 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(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 - 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 }