Skip to content

feat(tables): saved views with filter, sort, and column presets - #5961

Open
TheodoreSpeaks wants to merge 44 commits into
stagingfrom
feat/table-views
Open

feat(tables): saved views with filter, sort, and column presets#5961
TheodoreSpeaks wants to merge 44 commits into
stagingfrom
feat/table-views

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Saved views on tables: name a filter + sort + column-visibility preset, switch between them from a View dropdown in the options bar
  • New Columns menu to show/hide columns, with workflow outputs nested under their group (hiding all of a group's children drops its spanning header)
  • Save button appears only when the current state diverges from the active view — ad-hoc filtering stays throwaway
  • Backed by a new table_views table; "All" is the absence of a view, so no backfill and the table is always reachable unfiltered
  • Gated behind the table-views feature flag (UI only — routes and migration ship ungated so a view saved during rollout survives the flag being turned off)

Type of Change

  • New feature

Testing

Tested manually. bun run type-check, bun run lint, all 11 CI audits, and check:migrations origin/staging pass. 29 unit tests across the views service, API contracts, and feature flags.

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

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 29, 2026 2:16am

Request Review

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large surface area across grid layout, URL state, and concurrent view/metadata writes; APIs are ungated while UI is flag-gated, so behavior depends on correct auth on new routes.

Overview
Introduces saved table views: named presets of filter, sort, hidden columns, and column layout (widths, order, pinning). "All" stays the default unfiltered state; views are persisted in a new table_views table with server-side config pruning and configPatch merges for concurrent layout saves.

API & data: GET/POST /api/table/[tableId]/views and PATCH/DELETE .../views/[viewId] with workspace-scoped auth; contracts, React Query hooks, and a views service layer.

Table UI: Views dropdown (switch, rename, delete, new view), Columns show/hide menu (including workflow group toggles), Save / Save as view when state diverges from the active view, and ?table-view= URL state (with an explicit All sentinel). The grid routes layout persistence to the active view or shared table metadata; undo tracks view ownership for layout actions.

Rollout: New table-views feature flag (TABLE_VIEWS env fallback) resolves on the table page and home/chat routes so embedded mothership tables get the same UI. Resource.Options gains asideEnd and trailing slots for the new bar layout.

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

Comment thread apps/sim/hooks/queries/tables.ts
Comment thread apps/sim/lib/table/views/service.ts
Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds feature-gated saved table views.

  • Persists named filter, sort, hidden-column, and column-layout presets in a new table_views database table.
  • Adds view, column-visibility, and save controls to standalone and embedded tables.
  • Adds view-aware layout persistence, undo ownership handling, API contracts, routes, queries, migrations, and tests.

Confidence Score: 3/5

The PR is not yet safe to merge because a resize or drag completed after a view switch can still persist layout into the destination view.

The grid rebinds its persistence sink whenever the active view changes, while resize and drag completion handlers dereference that sink without verifying the owner under which the interaction began, leaving the previously reported cross-view corruption path reachable.

Files Needing Attention: apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx Coordinates active-view selection, live layout snapshots, partial view updates, and create/save workflows.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx Adds view-owned layout seeding and persistence across grid interactions.
apps/sim/hooks/use-table-undo.ts Stamps layout-bearing undo entries with view ownership and gates asynchronous layout restoration.
apps/sim/lib/table/views/service.ts Implements validated CRUD operations and database-side partial configuration merging.
packages/db/migrations/0273_table_views.sql Introduces persisted table views with table and workspace relationships.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant T as Table UI
  participant API as Table Views API
  participant DB as table_views
  U->>T: Select or edit saved view
  T->>API: Create or patch view configuration
  API->>DB: Validate and persist configuration
  DB-->>API: Return saved view
  API-->>T: Refresh active view
  T-->>U: Apply filter, sort, visibility, and layout
Loading

Reviews (32): Last reviewed commit: "merge: staging into feat/table-views" | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

… the persist

The undo success callbacks applied the recorded view's order/widths/pinning to
the grid unconditionally and only gated the PATCH, so switching views before a
column create/delete mutation resolved left the destination displaying the
origin view's layout until switched away and back.

Each callback now checks ownership where its layout work begins: three are
purely layout and return at the top; delete-column undo restores cell data
first (row data, runs everywhere) and gates only the layout block below it.
In a non-owning view the restored column still appears via the grid's append
effect — at the end, leaving that view's layout untouched.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx Outdated
…concile order at seed

Two holes closed:

The sink binding toggled on activeView, leaving a render-frame gap after the
views query settled but before the resolve effect adopted a default — writes in
that gap fell through to shared metadata. The sink is now always bound while
views are enabled and handlePersistLayout is the single router, reading the
owner at call time: unresolved buffers, a view patches the view, All writes
metadata. The error branch stamps the owner so post-error writes stop
buffering.

The append effect only fires when the schema changes, so a column that arrived
while another source owned the layout rendered via the displayColumns fallback
but was never written into the adopted owner's stored order until a drag
happened to heal it. Seeding now reconciles the incoming order against the
schema and persists the appended tail through the current sink.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
Insert-column and the delete chain persist layout from mutation callbacks
through updateMetadataRef, which always targets the current sink — so a view
switch mid-flight wrote the origin view's order/widths/pins into the
destination. Undo got this guard already; the live paths never did.

Both now capture viewLayoutKey at dispatch and compare at the callback. On a
mismatch the layout work is skipped: the destination re-seeded its own layout,
the new column lands there via the append effect when the refetch arrives, and
the deleted column's dangling keys are pruned on read.

pushUndo also takes the captured owner as an override — stamped at callback
time it would record the destination, letting a later undo apply the origin's
layout to it.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1576c41. Configure here.

A failed background refetch flips isError while the cached list stays usable,
and every view mutation invalidates the views query — so one blip made the
resolve effect treat views as terminally failed and stop applying switches
until the next successful refetch.

The axis is now whether a list exists: error with no list ever fetched settles
to All; error with a cached list resolves normally against the cache; owner is
unknown only while the initial fetch is in flight.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b49a061. Configure here.

Route-count baseline conflict: staging moved to 984 on its own; merged truth
with this branch's 2 view routes is 986, confirmed by running the audit.
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using high effort and found 2 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 5e5fd7f. Configure here.

layoutTouchedWhileUnownedRef.current = false
if (adoptedView || !userPermissions.canEdit) return
updateMetadataMutation.mutate(readLayout())
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Load-window flush wipes pins

Medium Severity

resolvePendingLayout persists a full readLayout() snapshot when views settle to All. That snapshot always includes pinnedColumns, which stays [] until the grid seeds from table metadata. If views finish before the detail query and the user resized during the unowned window, the flush merges empty pins (and a partial columnWidths map) over the stored metadata and drops existing pins/widths.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5e5fd7f. Configure here.

updateMetadataMutation.mutate(patch)
},
[activeView, userPermissions.canEdit, viewOwnerUnknown]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Layout writes misrouted after view adoption

Medium Severity

handlePersistLayout routes by activeView, but buffering only covers seededViewIdRef === undefined. After default adoption or create success stamps seededViewIdRef and writes the view id through the URL, activeView still lags a render — so layout patches fall through to shared table metadata instead of the adopted view. The comment already notes that lag; the guard does not cover it.

Additional Locations (2)
Fix in Cursor Fix in Web

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