Skip to content

feat(tables): virtualize data grid with bounded copy and chunked delete#4693

Open
TheodoreSpeaks wants to merge 1 commit into
stagingfrom
feat/table-grid-virtualization
Open

feat(tables): virtualize data grid with bounded copy and chunked delete#4693
TheodoreSpeaks wants to merge 1 commit into
stagingfrom
feat/table-grid-virtualization

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

Summary

  • Virtualize the user-tables grid with windowed rows (@tanstack/react-virtual spacer-row approach over the native table) so 4k+ row tables scroll smoothly — DOM goes from one <tr> per loaded row to visible + overscan. Pages kept unbounded so bulk ops keep working; memory drops.
  • Copy/cut now page through all (filter-aware) rows with "Copying… / Copied N rows" toasts and a promise-based ClipboardItem (fixes clipboard gesture-expiry); capped at 50k rows with a "use Export CSV" steer beyond that. Cut clears only the copied rows.
  • Select-all → delete now covers all matching rows (not just the loaded window) and chunks deletes into ≤1000-row requests, fixing the prior >1000-row validation failure.

Type of Change

  • New feature / performance improvement

Testing

Tested manually. tsc --noEmit clean, vitest hooks/queries lib/table (202 passing), bun run lint (16/16), and check:api-validation:strict all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 21, 2026 9:04am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 21, 2026

PR Summary

Medium Risk
Touches core table-grid rendering and bulk clipboard/delete flows; virtualization and async clipboard paging could introduce subtle selection/scrolling regressions and edge cases on very large tables.

Overview
Table grid performance: switches the data grid to windowed row rendering using @tanstack/react-virtual (sticky-header-aware) so only visible rows (+ overscan) mount, with spacer rows representing off-screen content.

Large-selection correctness: adds ensureRowsLoadedUpTo to page rows only up to a new TABLE_LIMITS.MAX_COPY_ROWS cap (50k) for copy/cut, uses promise-based ClipboardItem writes with progress/success toasts, and ensures select-all delete loads all matching rows before invoking deletion; the useDeleteTableRows mutation now chunks requests to respect the server’s MAX_BULK_OPERATION_SIZE limit.

Reviewed by Cursor Bugbot for commit 8956ef5. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR virtualizes the user-tables data grid using @tanstack/react-virtual (spacer-row approach inside a native <table>), rewrites copy/cut to use the promise-based ClipboardItem API to survive async row paging, and fixes select-all delete to chunk large row sets into ≤1 000-row server requests.

  • Virtualization: useVirtualizer replaces the full row render with visible + overscan rows; <thead> height is tracked via ResizeObserver to set the correct scrollMargin; row height is measured from a live DOM cell after load.
  • Clipboard: writeSelectionToClipboard passes an async blob promise to ClipboardItem, capping at 50 k rows and steering users to Export CSV beyond that; cut operations call clearCutRows after the write resolves.
  • Chunked delete: useDeleteTableRows now loops through rowIds in 1 000-row slices to stay within the server validation limit that previously caused bulk deletes to fail.

Confidence Score: 3/5

The virtualization and chunked-delete paths look mechanically sound, but the clipboard error handler can actively mislead users when failures are unrelated to table size.

The catch handler in writeSelectionToClipboard always shows 'Selection too large to copy — use Export CSV' regardless of the actual failure. A network error during row paging, or a server error while clearing cut cells, produces that same message — pointing users toward Export CSV when no size limit was involved. For cut, this is particularly confusing because the data landed on the clipboard successfully before the clear failed.

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx — specifically the writeSelectionToClipboard catch handler and the stale loading toast.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx Core grid component: adds @tanstack/react-virtual spacer-row windowing, rewrites copy/cut to use ClipboardItem with promise-based blob, and fixes select-all delete to drain all pages first. The catch-all error message in writeSelectionToClipboard is misleading for non-size failures, and a stale loading toast is never dismissed.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts Adds ensureRowsLoadedUpTo: pages the query cache until maxRows loaded, returning rows + hasMore flag. The hasMore heuristic can false-positive when table size equals MAX_COPY_ROWS exactly.
apps/sim/hooks/queries/tables.ts useDeleteTableRows mutation now chunks rowIds into ≤1000-row sequential batches, fixing the prior server-side validation limit for large selections. Straightforward and correct.
apps/sim/lib/table/constants.ts Adds MAX_COPY_ROWS: 50000 constant to TABLE_LIMITS for clipboard size cap.
apps/sim/package.json Adds @tanstack/react-virtual 3.13.24 dependency for row virtualization.

Sequence Diagram

sequenceDiagram
    participant User
    participant TableGrid
    participant ClipboardAPI
    participant QueryCache
    participant Server

    Note over TableGrid: Copy/Cut (Cmd+C / Cmd+X)
    User->>TableGrid: keyboard event (copy/cut)
    TableGrid->>ClipboardAPI: "clipboard.write([ClipboardItem({blob_promise})])"
    Note over TableGrid,ClipboardAPI: Transient activation captured synchronously

    par Blob resolution (async)
        TableGrid->>QueryCache: ensureRowsLoadedUpTo(50_000)
        loop "while loadedCount < maxRows && lastPage full"
            QueryCache->>Server: fetchNextPage()
            Server-->>QueryCache: page of up to 1000 rows
        end
        QueryCache-->>TableGrid: "{rows, hasMore}"
        TableGrid->>TableGrid: serialize rows to Blob
        TableGrid-->>ClipboardAPI: Blob resolved
    end

    ClipboardAPI-->>TableGrid: write() resolved or rejected
    alt success
        TableGrid->>TableGrid: afterCopy?(copiedRows)
        opt Cut operation
            TableGrid->>Server: chunkBatchUpdates (clear cells)
        end
        TableGrid->>User: toast Copied N rows or first 50k
    else any error
        TableGrid->>User: toast Selection too large use Export CSV
    end

    Note over TableGrid: Delete All (context menu)
    User->>TableGrid: Delete rows (select-all)
    TableGrid->>QueryCache: ensureAllRowsLoaded()
    loop until last page partial
        QueryCache->>Server: fetchNextPage()
        Server-->>QueryCache: page
    end
    QueryCache-->>TableGrid: all rows
    TableGrid->>Server: deleteRows chunked up to 1000 per request sequential
Loading

Reviews (1): Last reviewed commit: "feat(tables): virtualize data grid with ..." | Re-trigger Greptile

Comment on lines +2016 to +2019
.catch((error) => {
logger.error(`Failed to ${opts.verb === 'Copied' ? 'copy' : 'cut'} rows`, { error })
toast.error('Selection too large to copy — use Export CSV')
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Catch-all error message misleads on non-size failures

The .catch() handler unconditionally shows "Selection too large to copy — use Export CSV" for every failure, including two cases where the message is factually wrong:

  1. Network error during row loading — if opts.loadRows() rejects (e.g., a transient HTTP 500 while paging), the blob promise is rejected, clipboard.write() rejects with that error, and the user sees "Selection too large" for a network issue—especially confusing when only a handful of rows are selected.
  2. Cut-clear failure after a successful copyclearCutRows runs inside .then() via afterCopy; if chunkBatchUpdates fails mid-cut, the data is already on the clipboard but .then() throws, which falls to this same .catch(). The user sees "Selection too large" when the real problem is that the server-side cell clear failed.

The previous code at minimum distinguished NotAllowedError from other failures; this PR collapses all errors into a single misleading message. At minimum the catch should differentiate between a size-cap scenario and any other error.

Comment on lines +156 to +162
const all = data?.pages.flatMap((p) => p.rows) ?? []
const lastPage = data?.pages[data.pages.length - 1]
const morePages = lastPage ? lastPage.rows.length === TABLE_LIMITS.MAX_QUERY_LIMIT : false
return {
rows: all.length > maxRows ? all.slice(0, maxRows) : all,
hasMore: morePages || all.length > maxRows,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 morePages heuristic produces a false-positive when the table row count equals MAX_COPY_ROWS exactly

morePages is set to true whenever the last fetched page contains exactly MAX_QUERY_LIMIT (1 000) rows. When a table has exactly 50 000 rows the loop fetches all 50 pages and breaks because loadedCount() >= maxRows. At that point lastPage.rows.length === 1000 so morePages = true, but all.length === maxRows so the all.length > maxRows guard is also false — resulting in hasMore: true even though every row was returned. The clipboard code then shows "Copied first 50 000 rows — export to CSV for the rest" when the copy was actually complete.

toast.error('Clipboard access is unavailable in this context')
return
}
toast({ message: `${opts.verb === 'Copied' ? 'Copying' : 'Cutting'}… loading rows` })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "Loading rows" toast is never dismissed before the result toast appears

toast({ message: 'Copying… loading rows' }) is fired synchronously but there is no handle to dismiss or replace it once the operation finishes. When clipboard.write() resolves or rejects, a second toast is shown. Depending on the toast queue configuration this leaves both toasts visible simultaneously, which could confuse users on fast operations where the "loading" flash and "Copied N rows" appear back-to-back or overlap.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8956ef5. Configure here.

.catch((error) => {
logger.error(`Failed to ${opts.verb === 'Copied' ? 'copy' : 'cut'} rows`, { error })
toast.error('Selection too large to copy — use Export CSV')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut cell-clearing failure shows misleading clipboard error

Medium Severity

The .catch() handler in writeSelectionToClipboard catches errors from both clipboard.write() and the afterCopy callback (i.e., clearCutRows). When a cut operation's cell-clearing fails (e.g., a chunkBatchUpdates network error), the toast displays "Selection too large to copy — use Export CSV" even though the clipboard write already succeeded. The old code correctly showed "Failed to cut — please try again" for this scenario. Additionally, the undo entry inside clearCutRows is pushed before the batch update, so on failure the undo stack has a dangling entry while the clipboard silently holds the data.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8956ef5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant