Skip to content

Commit 3fec72a

Browse files
sorenbsclaude
andauthored
fix: save staged edits for rows loaded via infinite scroll (#1545)
* fix: save staged edits for rows loaded via infinite scroll With infinite scroll enabled, the grid queries pageIndex 0 with a grown pageSize window (25-row batches), but the row mutation hooks resolved their TanStack DB collection from the paginated pageIndex/pageSize. The two query scopes diverge once more than one batch is loaded, so collection.update() targeted a collection missing rows beyond the first batch and threw UpdateKeyNotFoundError, making "Save n rows" fail silently (issue #1466). The mutation hooks (update, updateMany, delete, insert) now receive the view's exact query props and resolve their collection through the new useActiveTableQueryCollection hook, so mutations always target the same collection scope the grid displays — including the search term dimension that was previously omitted as well. Verified end-to-end in the ppg demo: editing and deleting rows beyond the first 25-row batch with infinite scroll enabled now persists and shows the success toast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address PR #1545 review feedback - useActiveTableUpdate: drop the ignored AdapterUpdateOptions/table params from UseActiveTableUpdateParams. Persistence is delegated to the rows collection's onUpdate handler, so a per-call options channel could never reach the adapter; removing it makes that explicit instead of silently ignoring the values (API migration; the hook has no product callers). - ActiveTableView: keep row mutations targeting the visible window during infinite-scroll growth. While a grown window is fetching, the grid keeps showing the previous settled window; mutation query props now stay pinned to that settled window and swap to the grown scope atomically together with the rows (new resolveVisibleTableWindow helper + tests), closing the save/delete race during the transition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1b72949 commit 3fec72a

14 files changed

Lines changed: 647 additions & 106 deletions

.changeset/tidy-lions-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prisma/studio-core": patch
3+
---
4+
5+
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.

Architecture/db-state.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,26 @@ There is currently no `onInsert` optimistic handler in the rows collection. Do n
185185
- Views MUST use `useActiveTableDelete` for row deletion.
186186
- Views MUST NOT call adapter query/update/delete directly.
187187

188+
### Mutation scope alignment
189+
190+
Row mutation hooks (`useActiveTableUpdate`, `useActiveTableUpdateMany`,
191+
`useActiveTableDelete`, `useActiveTableInsert`) take the view's query props
192+
(`UseActiveTableQueryProps`) and resolve their collection through
193+
`useActiveTableQueryCollection`. The view MUST pass the exact same query props
194+
it uses for display, so mutations target the `queryScopeKey` that actually
195+
contains the visible rows. With infinite scroll enabled that scope is
196+
`pageIndex: 0` with the grown batch-window `pageSize`, not the paginated page —
197+
deriving mutation scope independently (for example from `usePagination`) makes
198+
`collection.update`/`collection.delete` silently miss rows loaded beyond the
199+
first batch.
200+
201+
While a grown infinite-scroll window is still fetching, the grid keeps showing
202+
the previous settled window. During that transition the view MUST keep the
203+
mutation query props pinned to the settled window too:
204+
`resolveVisibleTableWindow` pairs the visible rows with the query props of the
205+
scope they were loaded from, and both swap to the grown window atomically once
206+
its query finishes.
207+
188208
## Lifecycle Rules
189209

190210
Studio context owns collection lifecycle:

FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ When the ledger has migrations but no contract snapshots to diff (a database wri
182182
Table data is shown in a grid with server-backed pagination, filtered-row counts, loading feedback, and explicit empty states.
183183
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.
184184
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.
185+
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.
185186
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.
186187

187188
## Table Row Count Display

ui/hooks/use-active-table-delete.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
import { useMutation } from "@tanstack/react-query";
22

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

8-
export function useActiveTableDelete() {
9-
const { paginationState } = usePagination();
10-
const { sortingState } = useSorting();
11-
const { appliedFilter } = useFiltering();
12-
const { activeTable, collection, refetch } = useActiveTableRowsCollection({
13-
pageIndex: paginationState.pageIndex,
14-
pageSize: paginationState.pageSize,
15-
sortOrder: sortingState,
16-
filter: appliedFilter,
17-
});
8+
export function useActiveTableDelete(query: UseActiveTableQueryProps) {
9+
const { activeTable, collection, refetch } =
10+
useActiveTableQueryCollection(query);
1811
const { schema = null, name: table = null } = activeTable ?? {};
1912

2013
return useMutation({

ui/hooks/use-active-table-insert.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import { useMutation } from "@tanstack/react-query";
22

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

10-
export function useActiveTableInsert() {
10+
export function useActiveTableInsert(query: UseActiveTableQueryProps) {
1111
const { adapter, onEvent } = useStudio();
12-
const { paginationState } = usePagination();
13-
const { sortingState } = useSorting();
14-
const { appliedFilter } = useFiltering();
15-
const { activeTable, refetch } = useActiveTableRowsCollection({
16-
pageIndex: paginationState.pageIndex,
17-
pageSize: paginationState.pageSize,
18-
sortOrder: sortingState,
19-
filter: appliedFilter,
20-
});
12+
const { activeTable, refetch } = useActiveTableQueryCollection(query);
2113
const { schema = null, name: table = null } = activeTable ?? {};
2214

2315
return useMutation({

ui/hooks/use-active-table-query.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import type {
44
SortOrderItem,
55
Table,
66
} from "../../data/adapter";
7-
import { useActiveTableRowsCollection } from "./use-active-table-rows-collection";
7+
import {
8+
type ActiveTableRowsCollectionState,
9+
useActiveTableRowsCollection,
10+
} from "./use-active-table-rows-collection";
811
import { useNavigation } from "./use-navigation";
912

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

30-
export function useActiveTableQuery(
33+
/**
34+
* Resolves the rows-collection state for the exact query scope described by
35+
* `props`. Row mutation hooks must resolve their collection through this hook
36+
* with the same query props the view uses for display, so mutations target the
37+
* collection that actually contains the visible rows (for example the grown
38+
* `pageIndex: 0` window used by infinite scroll).
39+
*/
40+
export function useActiveTableQueryCollection(
3141
props: UseActiveTableQueryProps,
32-
): UseActiveTableQueryResult {
42+
): ActiveTableRowsCollectionState {
3343
const { filter, pageIndex, pageSize, sortOrder } = props;
3444
const {
3545
metadata: { activeTable },
@@ -39,13 +49,20 @@ export function useActiveTableQuery(
3949
searchScope: props.searchScope ?? "table",
4050
searchTerm: props.searchTerm ?? "",
4151
});
42-
const state = useActiveTableRowsCollection({
52+
53+
return useActiveTableRowsCollection({
4354
filter,
4455
fullTableSearchTerm,
4556
pageIndex,
4657
pageSize,
4758
sortOrder,
4859
});
60+
}
61+
62+
export function useActiveTableQuery(
63+
props: UseActiveTableQueryProps,
64+
): UseActiveTableQueryResult {
65+
const state = useActiveTableQueryCollection(props);
4966

5067
return {
5168
data: state.activeTable

0 commit comments

Comments
 (0)