Skip to content

feat(tables): row-gutter drag-select, Cmd+F find, and select-all polish#4901

Merged
TheodoreSpeaks merged 5 commits into
stagingfrom
improvement/table-select-ui
Jun 7, 2026
Merged

feat(tables): row-gutter drag-select, Cmd+F find, and select-all polish#4901
TheodoreSpeaks merged 5 commits into
stagingfrom
improvement/table-select-ui

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

Summary

Three table-grid improvements on this branch:

  1. Row-gutter drag-select — press a row gutter and drag up/down to select a contiguous range (additive, consistent with the existing click-to-toggle model). Auto-scrolls at the viewport edges. Shift-click still range-extends; keyboard toggle unchanged.
  2. Cmd/Ctrl+F find across cells — overrides the browser find with an in-table, server-side, case-insensitive substring search over every cell of every row.
  3. Gutter alignment + select-all polish — row number/checkbox centered in the space left of the run button, the select-all header checkbox centered in the same region so they line up, and an indeterminate (minus) select-all that clears the selection on click.

Find — how it works

  • New GET /api/table/[tableId]/rows/find (contract + useFindTableRows hook). Search is Enter-triggered (Clay-style), respecting the view's active filter/sort.
  • findRowMatches runs a row_number() CTE + CROSS JOIN LATERAL jsonb_each_text + ILIKE, returning each matching cell as { ordinal, rowId, column }, capped at 1000 with a truncated flag.
  • A shared buildRowOrderBySql (now used by both queryRows and findRowMatches, with an id tiebreak) keeps a match's ordinal aligned with its index in the paginated list, so the client can page up to and reveal the exact cell.
  • Cell-by-cell next/prev (Enter / Shift+Enter) loads the target row if it's off-window, then sets the cell anchor — the existing scroll-into-view effect reveals + highlights it.

Performance / scope (deliberate v1 choices)

  • Sequential scan, no migration. ILIKE over jsonb_each_text can't use the JSONB GIN index; the scan is bounded by the table_id btree. Sub-500ms for tables ≪100k rows. A pg_trgm GIN index on a text projection is the documented future accelerator.
  • Far-match jumps use sequential ensureRowsLoadedUpTo (loading state shown); fine for target sizes, with parallel/offset-windowed loading as a follow-up if needed.
  • Match staleness after edits is a per-search snapshot; re-submitting re-queries.

Notable change to shared code

  • emcn Checkbox gains an indeterminate (minus) state — additive; no change to checked/unchecked usages.

Tests / checks

  • New: findRowMatches unit test (mapping, truncation, filter/sort threading) and a /rows/find route test (auth, validation, workspace mismatch, error mapping). 11 passing.
  • bun run lint:check, type-check, and check:api-validation all pass (route-count baseline bumped 791→792 for the new route).
  • Verified end-to-end against a real table: the query returns correct {ordinal, column} matches in ~60ms.

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 7, 2026

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jun 7, 2026 1:39am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented Jun 7, 2026

PR Summary

Low Risk
Documentation and generated docs only—no application logic in this diff—so risk is limited to agents or contributors following outdated vs new guidance if skills disagree (e.g. template count).

Overview
This PR overhauls agent, Cursor, and Claude command/skill documentation rather than product runtime code in the attached diff.

Integrations & blocks: Skills and commands now require exporting {Service}BlockMeta (tags + workflow templates) and registering it in blocksMeta alongside the block in registry.ts. add-integration clarifies tags live only in BlockMeta, not on BlockConfig, and documents valid integrationType values. add-block / commands tighten block output rules (no nested properties; use flat json + description), canonicalParamId pairing rules, and template counts (7+ in some commands vs 2–4 in skills).

Connectors & validation: add-connector documents selector config fields and mandates @/connectors/utils helpers (htmlToPlainText, computeContentHash, parseTagDate, joinTagArray). validate-connector adds contentHash rules for deferred vs inline content.

API & client patterns: Global rules now point BYOK registration at lib/api/contracts/byok-keys.ts, add-integration shows contract-based internal tool routes with withRouteHandler + parseRequest, and query/hook docs require requestJson and broader boundary-raw-fetch coverage.

Architecture & UI: sim-architecture / CLAUDE.md describe the apps/sim + apps/realtime + packages/* monorepo and package boundaries. EMCN/chip rules replace generic button guidance with the chip chrome system (chip-field-chrome, ChipInput, ChipModalField, props-over-className). sim-integrations.md is trimmed to hard rules with detail deferred to skills. sim-testing.md is removed from Claude rules (Cursor testing doc updated with workflowAuthzMock / dbChainMock). New sim-sandbox.md documents isolated-vm worker security.

Docs site: Removes Form deployment and A2A / Circleback tool pages; adds knowledge and table to blocks nav; updates Mothership tables copy to the Table block; quick-reference drops workflow color; icon mapping and several tool page BlockInfoCard colors/currentColor tweaks; docs layout favicon and OG sizing adjustments.

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

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 19cff3c. Configure here.

enabled: Boolean(workspaceId && tableId) && q.trim().length > 0,
staleTime: 30 * 1000,
placeholderData: keepPreviousData,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty find keeps stale matches

Medium Severity

Submitting an empty trimmed find term disables useFindTableRows, but keepPreviousData leaves the last successful findData in place. The find bar can still show the previous match count and allow next/prev navigation even though no active query matches the cleared input.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 19cff3c. Configure here.

useEffect(() => {
setCurrentMatchIndex(0)
if (findMatches.length > 0) goToMatch(0)
}, [findMatches, goToMatch])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Find refetch jumps to first

Medium Severity

A useEffect keyed on findMatches always calls goToMatch(0) when that memo’s result changes. Any refetch or findData update recomputes a new sorted array (new reference), resetting the active match to the first hit even if the user was on a later match and the hits are unchanged.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 19cff3c. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 7, 2026

Greptile Summary

This PR adds three table-grid improvements: row-gutter drag-to-select (with auto-scroll), a Cmd/Ctrl+F in-table find backed by a new GET /api/table/[tableId]/rows/find endpoint, and alignment/polish for the select-all header checkbox (now with an indeterminate "clear" state). The server-side find uses a row_number() CTE + CROSS JOIN LATERAL jsonb_each_text + ILIKE approach, and a new shared buildRowOrderBySql helper keeps ordinals aligned between the find query and the paginated list.

  • Row-gutter drag-select arms isRowDraggingRef on mousedown, unions the swept range onto the pre-click base selection on each mouseenter, and hooks into the existing auto-scroll RAF loop.
  • Cmd+F find introduces useFindTableRows (React Query, Enter-triggered, cached per submitted term), a TableFind toolbar component, and goToMatch which awaits ensureRowsLoadedUpTo before queuing the cell reveal via a pendingMatchTick effect.
  • Select-all polish replaces the boolean checked prop with boolean | 'indeterminate', adds a Minus icon to the Checkbox component, and changes the toggle logic so any existing selection (partial or full) clears rather than promoting to "select all".

Confidence Score: 4/5

The backend find endpoint and drag-select logic are solid; the main risks are in the find navigation UX within table-grid.tsx.

The SQL service layer, API route, contracts, and React Query hook are all well-structured and tested. The three concerns are in the client-side orchestration: the Cmd+F shortcut intercepts globally without checking that the table container has focus; rapid next/prev calls can cause the loading spinner to disappear while a jump is still in flight; and toggling column visibility while find is open silently resets the user's current match position to 0.

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx — find shortcut scoping, isJumping race, and displayColumns-triggered match reset.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx Core orchestration file — adds find state machine, row-gutter drag-select logic, and Cmd+F shortcut override; three P2 concerns: global Cmd+F intercept without focus guard, isJumping race under rapid navigation, and displayColumns change silently resetting match index.
apps/sim/lib/table/service.ts Adds findRowMatches (CTE + CROSS JOIN LATERAL ILIKE scan, LIMIT+1 truncation) and extracts buildRowOrderBySql shared with queryRows; ordinal alignment is sound and the early-return guard on empty columns is in place.
apps/sim/app/api/table/[tableId]/rows/find/route.ts New GET endpoint with auth, workspace-ID mismatch check, JSON parse guards, and TableQueryValidationError mapping; clean and consistent with peer routes.
apps/sim/hooks/queries/tables.ts Adds useFindTableRows with correct enabled guard (q.trim().length > 0), staleTime, keepPreviousData, and a well-structured cache key factory entry.
apps/sim/components/emcn/components/checkbox/checkbox.tsx Adds indeterminate state (Minus icon + CVA variant) to Checkbox; additive and backward-compatible with existing checked/unchecked usages.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-find.tsx New presentational find-bar component; clean prop interface, Enter/Shift+Enter/Escape key handling delegated correctly to parent via callbacks.
apps/sim/lib/table/tests/find-row-matches.test.ts Good unit coverage of mapping, ordinal coercion, LIMIT+1 truncation, and filter/sort threading; correctly mocks DB and SQL helpers.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant TG as TableGrid
    participant TF as TableFind UI
    participant RQ as useFindTableRows
    participant API as GET /rows/find
    participant DB as Postgres

    U->>TG: Cmd+F
    TG->>TF: setFindOpen(true), focus input
    U->>TF: type query, press Enter
    TF->>TG: onSubmit() → setSubmittedQuery(q)
    TG->>RQ: "enabled=true, q=submittedQuery"
    RQ->>API: "GET ?q=...&filter=...&sort=..."
    API->>DB: CTE row_number() + CROSS JOIN LATERAL jsonb_each_text ILIKE
    DB-->>API: "[{ordinal, id, column_name}] (≤1001 rows)"
    API-->>RQ: "{matches, truncated}"
    RQ-->>TG: findData updated
    TG->>TG: findMatches memoized (filter by displayColumns)
    TG->>TG: useEffect → goToMatch(0)
    TG->>TG: await ensureRowsLoadedUpTo(ordinal+1)
    TG->>TG: "pendingMatchRef = match, setPendingMatchTick++"
    TG->>TG: "reveal effect: setSelectionAnchor({rowIndex, colIndex})"
    TG-->>U: cell highlighted + scrolled into view

    U->>TF: press Enter (next)
    TF->>TG: onNext() → goToMatch(currentIndex+1)
    TG->>TG: await ensureRowsLoadedUpTo(match.ordinal+1)
    TG-->>U: next match highlighted
Loading

Comments Outside Diff (2)

  1. apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx, line 621-637 (link)

    P2 isJumping resets early under rapid next/prev

    goToMatch is async with a try/finally that calls setIsJumping(false). If the user presses next/prev quickly, two concurrent invocations will both set isJumping(true) then each independently set it back to false when done — so the loading spinner disappears the moment the first call finishes, while the second is still awaiting ensureRowsLoadedUpTo. The cells could also end up briefly at an intermediate match. Tracking a jumpIdRef (incrementing on each call and only applying setIsJumping(false) and the pending-match assignment when the id still matches) would make the latest call the authoritative one.

  2. apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx, line 660-664 (link)

    P2 Column visibility change silently resets the active match

    findMatches is memoized on [findData, displayColumns], so toggling or reordering a column while the find bar is open produces a new array reference, triggering the "new result set" effect which calls goToMatch(0) and resets currentMatchIndex to 0. A user navigating through matches (say, at match 7 of 30) who accidentally clicks a column header will be silently jumped back to the first match. Splitting the two concerns — resetting only when findData changes, and re-sorting currentMatchIndex when displayColumns changes without jumping — would be more ergonomic.

Reviews (1): Last reviewed commit: "feat(tables): Cmd+F find across cells + ..." | Re-trigger Greptile

Comment on lines +2779 to +2793
useEffect(() => {
if (embedded) return
const handleFindShortcut = (e: KeyboardEvent) => {
if (!(e.metaKey || e.ctrlKey) || e.key !== 'f') return
if (!containerRef.current) return
e.preventDefault()
setFindOpen(true)
requestAnimationFrame(() => {
findInputRef.current?.focus()
findInputRef.current?.select()
})
}
document.addEventListener('keydown', handleFindShortcut)
return () => document.removeEventListener('keydown', handleFindShortcut)
}, [embedded])
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 Cmd+F intercepts globally without focus guard

The handler attaches to document and only checks containerRef.current (component is mounted), not whether focus is within the table container. Any keystroke from an unrelated focused element — a sidebar input, a modal text field, or a browser extension overlay — will call e.preventDefault() and open the table find panel. The existing Ctrl+A handler uses the same pattern, but Cmd+F is a more universally expected browser shortcut and overriding it unconditionally is more likely to surprise users. Adding a containerRef.current.contains(document.activeElement) guard would confine the override to when the table already has focus.

TheodoreSpeaks and others added 5 commits June 6, 2026 18:28
Find:
- New GET /api/table/[tableId]/rows/find endpoint + contract + useFindTableRows hook
- findRowMatches: case-insensitive substring search across every cell via a
  row_number() CTE + jsonb_each_text + ILIKE, returning each matching cell with
  its ordinal in the filtered/sorted view. Shared buildRowOrderBySql keeps the
  find ordinals aligned with the paginated list (with an id tiebreak).
- Cmd/Ctrl+F floating find box (search-on-Enter), cell-by-cell next/prev that
  loads the target row then reveals the cell via the existing anchor scroll.

Gutter UI:
- Row number/checkbox centered in the region left of the run button; the
  select-all header checkbox centers in the same region so they line up.
- emcn Checkbox gains an indeterminate (minus) state; select-all shows the minus
  on any partial selection and clears everything on click.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the existing "Rename" item in the per-table context menu to inline rename
(useInlineRename + InlineRenameInput via the Resource name cell's `content`
override), matching the Files list. Enter/blur saves, Esc cancels; replaces the
prior modal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reuse the workflow search visual language (surface-1 panel, emcn Input, muted
"k of N" counter, size-8 ghost chevron buttons) instead of the flat custom input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TheodoreSpeaks TheodoreSpeaks force-pushed the improvement/table-select-ui branch from 19cff3c to 8205fae Compare June 7, 2026 01:35
@TheodoreSpeaks TheodoreSpeaks changed the base branch from main to staging June 7, 2026 01:35
@TheodoreSpeaks TheodoreSpeaks merged commit c90a1eb into staging Jun 7, 2026
18 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the improvement/table-select-ui branch June 7, 2026 05:28
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