Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-lions-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@prisma/studio-core": patch
---

Fix saving staged cell edits silently failing for rows loaded beyond the first batch when infinite scroll is enabled. Row update, delete, and insert mutations now target the same rows-collection scope the grid displays instead of the paginated first page.
20 changes: 20 additions & 0 deletions Architecture/db-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ There is currently no `onInsert` optimistic handler in the rows collection. Do n
- Views MUST use `useActiveTableDelete` for row deletion.
- Views MUST NOT call adapter query/update/delete directly.

### Mutation scope alignment

Row mutation hooks (`useActiveTableUpdate`, `useActiveTableUpdateMany`,
`useActiveTableDelete`, `useActiveTableInsert`) take the view's query props
(`UseActiveTableQueryProps`) and resolve their collection through
`useActiveTableQueryCollection`. The view MUST pass the exact same query props
it uses for display, so mutations target the `queryScopeKey` that actually
contains the visible rows. With infinite scroll enabled that scope is
`pageIndex: 0` with the grown batch-window `pageSize`, not the paginated page —
deriving mutation scope independently (for example from `usePagination`) makes
`collection.update`/`collection.delete` silently miss rows loaded beyond the
first batch.

While a grown infinite-scroll window is still fetching, the grid keeps showing
the previous settled window. During that transition the view MUST keep the
mutation query props pinned to the settled window too:
`resolveVisibleTableWindow` pairs the visible rows with the query props of the
scope they were loaded from, and both swap to the grown window atomically once
its query finishes.

## Lifecycle Rules

Studio context owns collection lifecycle:
Expand Down
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ When the ledger has migrations but no contract snapshots to diff (a database wri
Table data is shown in a grid with server-backed pagination, filtered-row counts, loading feedback, and explicit empty states.
The footer keeps page navigation, a page jump field, a fixed rows-per-page dropdown, and infinite-scroll mode in one compact control group, so users can either jump directly to a page, switch page density from a known preset, or turn on lazy-loading without leaving the grid.
Rows-per-page and infinite-scroll preferences persist across tables through local storage, while the known filtered-row count keeps the footer stable during page transitions for the same filtered result set. Infinite scroll preloads before the hard bottom edge, always appends in fixed 25-row chunks regardless of the paginated page-size setting, keeps filling tall viewports until the grid is actually scrollable, and appends new rows in place without snapping the grid back to the top.
Row editing, deletion, and insertion operate on the same loaded row window the grid displays, so with infinite scroll enabled staged cell edits save correctly even for rows loaded beyond the first 25-row batch.
Rapid sort and filter changes keep the latest request authoritative, and superseded table reads are aborted so a slower older result cannot overwrite the visible grid.

## PostgreSQL Stored Temporal Values
Expand Down
21 changes: 7 additions & 14 deletions ui/hooks/use-active-table-delete.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import { useMutation } from "@tanstack/react-query";

import { useActiveTableRowsCollection } from "./use-active-table-rows-collection";
import { useFiltering } from "./use-filtering";
import { usePagination } from "./use-pagination";
import { useSorting } from "./use-sorting";
import {
useActiveTableQueryCollection,
type UseActiveTableQueryProps,
} from "./use-active-table-query";

export function useActiveTableDelete() {
const { paginationState } = usePagination();
const { sortingState } = useSorting();
const { appliedFilter } = useFiltering();
const { activeTable, collection, refetch } = useActiveTableRowsCollection({
pageIndex: paginationState.pageIndex,
pageSize: paginationState.pageSize,
sortOrder: sortingState,
filter: appliedFilter,
});
export function useActiveTableDelete(query: UseActiveTableQueryProps) {
const { activeTable, collection, refetch } =
useActiveTableQueryCollection(query);
const { schema = null, name: table = null } = activeTable ?? {};

return useMutation({
Expand Down
20 changes: 6 additions & 14 deletions ui/hooks/use-active-table-insert.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import { useMutation } from "@tanstack/react-query";

import { useStudio } from "../studio/context";
import { useActiveTableRowsCollection } from "./use-active-table-rows-collection";
import { useFiltering } from "./use-filtering";
import { usePagination } from "./use-pagination";
import { useSorting } from "./use-sorting";
import {
useActiveTableQueryCollection,
type UseActiveTableQueryProps,
} from "./use-active-table-query";
import { addRowIdToResult } from "./utils/add-row-id-to-result";

export function useActiveTableInsert() {
export function useActiveTableInsert(query: UseActiveTableQueryProps) {
const { adapter, onEvent } = useStudio();
const { paginationState } = usePagination();
const { sortingState } = useSorting();
const { appliedFilter } = useFiltering();
const { activeTable, refetch } = useActiveTableRowsCollection({
pageIndex: paginationState.pageIndex,
pageSize: paginationState.pageSize,
sortOrder: sortingState,
filter: appliedFilter,
});
const { activeTable, refetch } = useActiveTableQueryCollection(query);
const { schema = null, name: table = null } = activeTable ?? {};

return useMutation({
Expand Down
25 changes: 21 additions & 4 deletions ui/hooks/use-active-table-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type {
SortOrderItem,
Table,
} from "../../data/adapter";
import { useActiveTableRowsCollection } from "./use-active-table-rows-collection";
import {
type ActiveTableRowsCollectionState,
useActiveTableRowsCollection,
} from "./use-active-table-rows-collection";
import { useNavigation } from "./use-navigation";

export interface UseActiveTableQueryProps {
Expand All @@ -27,9 +30,16 @@ export interface UseActiveTableQueryResult {
refetch: () => Promise<void>;
}

export function useActiveTableQuery(
/**
* Resolves the rows-collection state for the exact query scope described by
* `props`. Row mutation hooks must resolve their collection through this hook
* with the same query props the view uses for display, so mutations target the
* collection that actually contains the visible rows (for example the grown
* `pageIndex: 0` window used by infinite scroll).
*/
export function useActiveTableQueryCollection(
props: UseActiveTableQueryProps,
): UseActiveTableQueryResult {
): ActiveTableRowsCollectionState {
const { filter, pageIndex, pageSize, sortOrder } = props;
const {
metadata: { activeTable },
Expand All @@ -39,13 +49,20 @@ export function useActiveTableQuery(
searchScope: props.searchScope ?? "table",
searchTerm: props.searchTerm ?? "",
});
const state = useActiveTableRowsCollection({

return useActiveTableRowsCollection({
filter,
fullTableSearchTerm,
pageIndex,
pageSize,
sortOrder,
});
}

export function useActiveTableQuery(
props: UseActiveTableQueryProps,
): UseActiveTableQueryResult {
const state = useActiveTableQueryCollection(props);

return {
data: state.activeTable
Expand Down
Loading