mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## 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>
118 lines
3.8 KiB
TypeScript
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>
|
|
)
|
|
}
|