Files
supabase/apps/studio/components/grid/SupabaseGrid.tsx
Aaditya BhusalandAli Waseem 719434a7fd fix(studio): batched table edits issues (#47319)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Bug fix

## What is the current behavior?

Fixes #47318

Supabase Studio's batched table edit queue has a few related row
identity issues:

- Editing a row's primary key can make later queued edits or deletes
lose track of the original row.
- Editing a primary key and another column in the same row before saving
can save only the primary key change, because later updates still use
the old primary key in the `WHERE` clause.
- Adding a row in batched edit mode and then deleting it before saving
may not remove the pending row correctly.

## What is the new behavior?

- Preserves the original row identity for queued operations after
primary key edits.
- Applies multiple queued edits for the same row as a single update when
saving.
- Correctly deletes newly added pending rows before they are saved.
- Adds regression coverage for these batched table edit cases.

## Additional context


https://github.com/user-attachments/assets/75672361-d781-4fe5-a542-071574ad57bd


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved row identity handling for grid edits, optimistic updates, and
queued operations so changes stay correctly attached when primary keys
are edited, reverted, or “taken” by another row.
* Updated header row deletion to delete from the currently
visible/targeted rows rather than relying on the full dataset.
* Reduced retry noise for missing tables by clearing conflicting sorts
and preventing repeated retries for the same “does not exist” error.
* More reliably consolidated queued edits for the same row into fewer
combined save statements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-06-29 08:12:18 -06:00

118 lines
3.8 KiB
TypeScript

import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { PropsWithChildren, useRef } from 'react'
import { DataGridHandle } from 'react-data-grid'
import { Shortcuts } from './components/common/Shortcuts'
import { Footer } from './components/footer/Footer'
import { Grid } from './components/grid/Grid'
import { Header, HeaderProps } from './components/header/Header'
import { useTableSort } from './hooks/useTableSort'
import { validateMsSqlSorting } from './MsSqlValidation'
import { GridProps } from './types'
import { formatGridDataWithOperationValues } from './utils/queueOperationUtils'
import { isMsSqlForeignTable } from '@/data/table-editor/table-editor-types'
import { useTableRowsQuery } from '@/data/table-rows/table-rows-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { RoleImpersonationState } from '@/lib/role-impersonation'
import { EMPTY_ARR } from '@/lib/void'
import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
import { useTableEditorStateSnapshot } from '@/state/table-editor'
import { QueuedOperation } from '@/state/table-editor-operation-queue.types'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export const SupabaseGrid = ({
customHeader,
gridProps,
children,
}: PropsWithChildren<
Pick<HeaderProps, 'customHeader'> & {
gridProps?: GridProps
}
>) => {
const { id: _id } = useParams()
const tableId = _id ? Number(_id) : undefined
const { data: project } = useSelectedProjectQuery()
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1)
const gridRef = useRef<DataGridHandle>(null)
const filters = snap.filters
const { sorts, onApplySorts } = useTableSort()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
const msSqlWarning = isMsSqlForeignTable(snap.originalTable)
? validateMsSqlSorting({ filters, sorts, table: snap.originalTable })
: { warning: null }
const tableQueriesEnabled = msSqlWarning.warning === null
const {
data,
error,
isSuccess,
isError,
isPending: isLoading,
isRefetching,
} = useTableRowsQuery(
{
projectRef: project?.ref,
tableId,
sorts,
filters,
page: snap.page,
preflightCheck,
limit: tableEditorSnap.rowsPerPage,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
},
{
placeholderData: keepPreviousData,
enabled: tableQueriesEnabled,
retry: (_, error: any) => {
const doesNotExistError = error && error.message?.includes('does not exist')
if (doesNotExistError) onApplySorts([])
return false
},
}
)
const operations = (tableEditorSnap.operationQueue.operations as QueuedOperation[]).filter(
(op) => op.tableId === tableId
)
const baseRows = data?.rows ?? EMPTY_ARR
const rows = formatGridDataWithOperationValues({ operations, rows: baseRows })
return (
<div className="sb-grid h-full flex flex-col">
<Header
customHeader={customHeader}
isRefetching={isRefetching}
tableQueriesEnabled={tableQueriesEnabled}
rows={rows}
/>
{msSqlWarning.warning !== null && <msSqlWarning.Component />}
{children || (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={rows}
error={error}
isDisabled={!tableQueriesEnabled}
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
/>
<Footer enableForeignRowsQuery={tableQueriesEnabled} />
<Shortcuts gridRef={gridRef} rows={rows} />
</>
)}
</div>
)
}