mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
cleaned up source
This commit is contained in:
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 }
|
||||
|
||||
Reference in new issue
Block a user