Skip to content

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

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

feat(tables): saved views with filter, sort, and column presets#5961
TheodoreSpeaks wants to merge 9 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.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 25, 2026 8:55am

Request Review

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches table detail orchestration, URL state, and layout persistence with jsonb merge semantics; auth follows existing table access patterns but new write paths affect shared presentation state.

Overview
Introduces saved table views: named presets of filter, sort, hidden columns, and grid layout (widths, order, pins), stored in a new table_views table with a per-table default.

API & data: List/create/update/delete routes use existing table checkAccess and workspace validation. Updates support full config replace or server-side configPatch merge for concurrent layout writes. Config is pruned on read when columns are deleted (filters are left intact).

Table UI (gated by table-views, resolved on the server page): A View switcher (?table-view= URL, explicit all sentinel), Columns show/hide menu (workflow groups nest with parent toggle), and a Save / Save as view chip when live state diverges from the active view. Layout auto-saves into the active view; filter/sort/hidden state seeds on view change without wiping ad-hoc edits until switch or save.

Grid: Honors hiddenColumns before workflow header expansion; layout seeds from view config when a view is active, with onPersistLayout routing metadata writes to the view instead of shared table metadata.

Infrastructure: React Query hooks, API contracts, TABLE_VIEWS env / feature flag (UI-only gate; routes and migration ship always).

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

Introduces feature-flagged saved table views.

  • Persists filter, sort, hidden-column, and column-layout presets in a new table_views table.
  • Adds authenticated view CRUD routes, query hooks, validation contracts, and service tests.
  • Adds view switching, view management, column visibility controls, and layout restoration to the table UI.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported layout omission is addressed by capturing, auto-saving, and restoring view-owned layout fields, while concurrent layout and explicit saves now compose through atomic server-side patches.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx Coordinates view selection, creation, saving, deletion, filter/sort state, hidden columns, and independent layout persistence.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx Applies saved visibility and layout settings, re-seeds layout when views change, and routes layout writes to the active view.
apps/sim/lib/table/views/service.ts Implements validated view persistence, including atomic server-side JSONB patch merging for independent concurrent updates.
apps/sim/app/api/table/[tableId]/views/[viewId]/route.ts Adds authorized update and deletion endpoints for individual saved views.
apps/sim/app/api/table/[tableId]/views/route.ts Adds authorized list and creation endpoints for table views.
packages/db/migrations/0270_table_views.sql Adds the table-view persistence schema and associated constraints and indexes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  UI[Table view controls] --> Hooks[Table view query mutations]
  Hooks --> API[Table view API routes]
  API --> Service[Table view service]
  Service --> DB[(table_views)]
  DB --> Service
  Service --> UI
  UI --> Grid[Table grid]
  Grid -->|Layout patch| Hooks
Loading

Reviews (7): Last reviewed commit: "fix(tables): return 404 when patching a ..." | 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
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

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/table/views/service.ts Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Addressing the two points in the latest Greptile summary — the 404 finding is fixed in 37f7db0, but I think both of these are already handled, so pushing back with the specifics rather than changing code:

"View creation reads layout from asynchronously refreshed table metadata rather than the grid's current state"

tableData.metadata is not stale here — useUpdateTableMetadata applies an optimistic onMutate that writes straight into the tableKeys.detail(tableId) cache (hooks/queries/tables.ts:1240-1249, metadata: { ...previous.metadata, ...metadata }). The grid persists layout on drag-end/resize-end, so by the time currentViewConfig reads it the cache already reflects the current state — no refetch is involved.

It also can't diverge by construction: metadata is only the layout source when no view is active, and that is exactly the case where the grid is writing to metadata (onPersistLayout is undefined). With a view active the config comes from activeView.config, which is what the grid writes to instead.

"top-level JSONB merging still allows overlapping updates to the same nested layout field to overwrite one another"

True that || is a shallow merge — but that is parity with, not a regression from, the existing table-metadata path, which does { ...existingMetadata, ...metadata } (lib/table/service.ts:590) and replaces columnWidths wholesale in exactly the same way. The view path is strictly safer: the metadata merge is a read-modify-write against a snapshot read earlier in the request, whereas config || patch is atomic in the database with no read window.

Single-client sequential writes can't lose data either, because the grid always sends the complete map from local state (updateMetadataRef.current({ columnWidths: columnWidthsRef.current })), not a per-column delta. The remaining case is two users resizing different columns of the same view within one request round-trip, where the later write wins on columnWidths. Making that lossless needs a recursive per-key jsonb merge, which would put view layout ahead of the table-metadata semantics the rest of the feature already relies on — happy to do it if you'd rather, but it seemed like scope beyond parity.

@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 37f7db0. Configure here.

onChange([...next])
}

const hiddenCount = hiddenColumns.length

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale hidden ids block column toggles

Medium Severity

ColumnsMenu builds its hide-all guard and active count from the raw hiddenColumns list, while the grid, dirty check, and save path already prune deleted column ids via effectiveHiddenColumns. After a previously hidden column is deleted, stale ids still count toward next.size, so the next.size >= columns.length guard can refuse further hides even though visible columns remain, and the Columns chip can stay lit with nothing actually hidden.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d04c96e. Configure here.

@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 default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

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

columns={columns}
workflowGroups={tableWorkflowGroups}
hiddenColumns={hiddenColumns}
onChange={setHiddenColumns}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale ids block column hiding

Medium Severity

ColumnsMenu still receives raw hiddenColumns even though deleted-column ids are already pruned into effectiveHiddenColumns for the grid, dirty check, and save. After a hidden column is removed, those leftover ids inflate next.size >= columns.length, so further hide toggles no-op and the Columns chip can stay lit with nothing actually hidden.

Additional Locations (1)
Fix in Cursor Fix in Web

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