Skip to content

fix(tables): fix bulk ops truncation for tables larger than one page#4532

Open
waleedlatif1 wants to merge 9 commits intostagingfrom
fix/tabls
Open

fix(tables): fix bulk ops truncation for tables larger than one page#4532
waleedlatif1 wants to merge 9 commits intostagingfrom
fix/tabls

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Bulk ops (column clear, copy, cut, delete, run) were silently truncated to 1000 rows on tables with more than one page of data
  • Added background page drain via `useEffect` + `fetchNextPage` so all pages load automatically after mount
  • Added `ensureAllRowsLoaded` helper that gates bulk ops — awaits any remaining pages before proceeding
  • Fixed undo/redo regression: batch update mutations now chunk by `MAX_BULK_OPERATION_SIZE` to stay within server limits on large tables
  • Added new `tableRowsInfiniteOptions` factory so the hook and imperative drain calls share the same cache key
  • Added tests for chunking logic in `use-table-undo` and drain logic in `use-table`

Type of Change

  • Bug fix

Testing

Tested manually — verified all rows clear/copy/cut/delete correctly on tables >1000 rows. All unit tests passing.

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)

Bulk operations (column-header delete, select-all copy/cut/delete/run)
were silently truncated to the first 1000 rows because handlers only
iterated the loaded pages from useInfiniteQuery.

Fix:
- Extract tableRowsInfiniteOptions factory (infiniteQueryOptions) so
  the hook and imperative drain share the same typed cache key
- Add background drain via useEffect watching hasNextPage/isFetchingNextPage
  — chains fetchNextPage until getNextPageParam returns undefined
- Add ensureAllRowsLoaded to use-table: reads cache via getQueryData +
  calls fetchNextPage in a while loop until the last page is partial
- Await ensureAllRowsLoaded at every kind:'all' bulk-op entry point in
  table-grid (column delete, copy, cut, action-bar delete/run)
- Add chunkBatchUpdates to send updates in MAX_BULK_OPERATION_SIZE=1000
  chunks so server validation never rejects oversized batches
- Fix undo-redo: make executeAction async and chunk clear-cells,
  update-cells, and delete-column cell-restore with mutateAsync loops

Tests: 41 passing across use-table, tables queries, and use-table-undo
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 9, 2026 7:08am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 9, 2026

PR Summary

Medium Risk
Touches table grid bulk edit/copy/cut and React Query pagination/polling; mistakes could cause incomplete updates or extra network load, but changes are localized and covered by new unit tests.

Overview
Fixes bulk table operations that previously only acted on the first page of rows by adding ensureAllRowsLoaded (and a background page drain) and using it to gate clear/copy/cut/column-clear paths in the table grid.

Adds shared infinite-query option/key helpers (tableRowsInfiniteOptions, tableRowsParamsKey) plus per-page polling that refreshes only pages containing in-flight workflow executions, preserving row identity when unchanged.

Hardens bulk updates/undo to respect server batch limits by chunking mutateAsync calls, improves number editing/validation (type="number", NaN→null), expands MAX_TABLE_NAME_LENGTH and enforces it during CSV import, and tweaks CSV import error/toast handling. Tests added for row draining and undo chunking behavior.

Reviewed by Cursor Bugbot for commit d4719f3. Configure here.

Comment thread apps/sim/hooks/use-table-undo.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 9, 2026

Greptile Summary

This PR fixes bulk operations (clear, copy, cut, delete, run) being silently truncated to 1000 rows on multi-page tables by introducing a background page drain and an ensureAllRowsLoaded gate used by all bulk handlers. It also fixes a undo/redo regression on large tables by chunking batch updates to MAX_BULK_OPERATION_SIZE.

  • Core fix: useEffect in useTable drains pages progressively on mount; ensureAllRowsLoaded awaits any remaining pages before bulk ops execute, using a getQueryData loop that terminates when the last page is partial.
  • Undo/redo fix: executeAction is now async; all three bulk-cell mutation sites in use-table-undo chunk their updates by MAX_BULK_OPERATION_SIZE.
  • Incidental changes: cleanCellValue now returns null (not 0) for NaN number inputs; InlineTextEditor switches to type=\"number\" for number columns; handleCellDoubleClick unconditionally opens the expanded editor rather than only on cell overflow.

Confidence Score: 3/5

The bulk-op drain and chunking logic is correct, but the incidental type="number" change to the inline cell editor introduces a regression blocking negative and decimal number entry.

The inline editor change is a regression on the critical data-entry path: with a React-controlled type="number" input, browsers return empty string from e.target.value for incomplete states like a lone minus sign or trailing decimal, immediately clearing the character the user just typed. Entering -5 produces 5, and 1.5 typed digit-by-digit produces 15. This affects every table with a number column.

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx — the type="number" controlled input change needs to be reverted or replaced with an uncontrolled approach for intermediate states.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors.tsx Adds type="number" to inline editor for number columns; the controlled-input pattern drops intermediate states like - and 1., breaking negative and decimal number entry.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts Adds background page drain via useEffect and new ensureAllRowsLoaded helper that gates bulk ops; logic is sound with correct termination on partial page.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx Bulk ops (clear, copy, cut) now drain pages before operating and chunk mutations; row-selection cut correctly returns after NotAllowedError.
apps/sim/hooks/queries/tables.ts Extracts tableRowsInfiniteOptions factory for cache-key sharing; adds targeted per-page polling with identity-preserving merge; isMutating guard prevents optimistic-update races.
apps/sim/hooks/use-table-undo.ts executeAction is now async; clear-cells and update-cells undo/redo chunk by MAX_BULK_OPERATION_SIZE; delete-column cell-restore also chunked.
apps/sim/app/workspace/[workspaceId]/tables/tables.tsx Removes the per-file failed[] array and its associated error toast; non-validation upload errors are still surfaced via the mutation's onError handler.
apps/sim/lib/table/constants.ts MAX_TABLE_NAME_LENGTH raised 50→128; DB column is TEXT (no length constraint), so no migration needed. Adds MAX_BULK_OPERATION_SIZE constant.

Reviews (4): Last reviewed commit: "fix(tables): push undo before chunkBatch..." | Re-trigger Greptile

Comment thread apps/sim/hooks/queries/tables.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment on lines 199 to 204
<input
ref={inputRef}
type='text'
value={draft}
type={isNumber ? 'number' : 'text'}
step={isNumber ? 'any' : undefined}
value={draft ?? ''}
onChange={(e) => setDraft(e.target.value)}
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 type="number" controlled input silently discards intermediate states

Browsers return '' from e.target.value whenever the current text is not a valid, complete number — this includes the intermediate states - (negative sign only), 1. (trailing decimal), and 1e (partial scientific notation). Because draft is set to '' on every such event, React immediately overwrites the DOM value back to '', so the character the user just typed visibly disappears.

Concrete failure: open a number-column cell for inline editing, type -5. After pressing -, e.target.value is '', draft becomes '', and the input clears; the 5 then lands on the now-empty field as 5, not -5. Similarly, 1.5 typed digit-by-digit becomes 15 because the decimal point is discarded.

The standard workaround is to keep type="text" for the draft state and rely on cleanCellValue (already used in doSave) for validation — the call-site already converts the string to a number or null before persisting, so no server-side change is needed. Alternatively, use inputMode="decimal" with type="text" to get mobile numeric keyboards without the intermediate-state problem.

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 3 potential issues.

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 d4719f3. Configure here.

cancelled = true
if (timeoutId !== null) clearTimeout(timeoutId)
}
}, [enabled, workspaceId, tableId, pageSize, filter, sort, queryClient, queryKey])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Polling loop runs indefinitely even without running executions

Medium Severity

The polling useEffect in useInfiniteTableRows unconditionally reschedules a setTimeout every 2 seconds regardless of whether any pages have running executions. Every open table view — including tables with no workflows at all — continuously polls the cache and reschedules indefinitely. The tick function only skips the network fetch when dirty.length === 0, but the loop itself never pauses or stops. This wastes CPU cycles doing cache reads and timer bookkeeping across all mounted table views.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d4719f3. Configure here.

onError: (error) => {
if (isValidationError(error)) return
logger.error('Failed to upload CSV:', error)
toast.error(error.message, { duration: 5000 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CSV upload validation errors silently swallowed without user feedback

Medium Severity

The mutation's onError now returns early for isValidationError(error) without showing a toast, and the calling code in tables.tsx removed the failed array and its associated error toast. When a CSV import fails with a validation error (e.g., exceeding row limits or schema violations), the user sees no feedback — the error is only logged server-side.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d4719f3. Configure here.

void chunkBatchUpdates(batchUpdates, batchUpdateAsyncRef.current).catch((error) => {
logger.error('Failed to cut selected cells', { error })
toast.error('Failed to cut — please try again')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Range-selection cut path doesn't drain pages for column selection

Medium Severity

The range-selection Cut path (when rowSelectionIsEmpty is true) operates on currentRows which only contains loaded pages. When a column is selected via column-header click, isColumnSelection is true but row selection is empty, so the code falls into this range-selection branch. Unlike the column-selection Delete handler (which calls ensureAllRowsLoaded), this Cut path silently truncates to loaded rows — only cutting visible data and leaving unloaded rows intact on tables larger than one page.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d4719f3. 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