feat: Saved Views for Issues list#1528
Open
labatt wants to merge 7 commits intopaperclipai:masterfrom
Open
Conversation
Contributor
Greptile SummaryThis PR introduces Saved Views for the Issues list — a well-scoped feature that lets teams persist and recall named filter/sort/group configurations with one click. The implementation follows established patterns (Drizzle ORM, Key findings:
Confidence Score: 3/5
Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/db/src/migrations/0038_saved_views.sql
Line: 4
Comment:
**Schema mismatch: `name` column is `text` but Drizzle schema declares `varchar(255)`**
The SQL migration creates `name` as `text NOT NULL`, which in PostgreSQL has no length constraint. However, the Drizzle schema in `saved_views.ts` declares it as `varchar("name", { length: 255 })`, which would enforce the 255-char limit at the DB level.
Because the actual table column is `text`, the 255-char limit is enforced only by application code. If any future process writes directly to the table or bypasses the API, names could exceed 255 chars — which would then be inconsistent with what the ORM schema expects.
The migration should match the ORM definition:
```suggestion
"name" varchar(255) NOT NULL,
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: server/src/routes/saved-views.ts
Line: 65
Comment:
**PATCH allows empty string name**
The PATCH validation condition `name !== undefined && (typeof name !== "string" || name.length > 255)` permits `name: ""` — an empty string is a string of length 0, which passes both checks. A view could then be saved with a blank name.
The POST route correctly guards against this with `if (!name || ...)`, but the PATCH route should do the same:
```suggestion
if (name !== undefined && (typeof name !== "string" || name.length === 0 || name.length > 255)) {
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: server/src/routes/saved-views.ts
Line: 45-49
Comment:
**TOCTOU race condition on the 50-view limit**
The limit check reads the current count (`svc.list()`) and then creates a new view in two separate DB round-trips. Under concurrent requests, both could read `length < 50` and then both insert, ending up with 51+ views.
This is low-impact (the limit is a soft guard, not a security boundary), but if enforcement matters, you can move it to a single atomic DB operation, e.g. using a `SELECT COUNT(*)` inside the insert transaction, or a `CHECK` constraint on the table.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: server/src/app.ts
Line: 266
Comment:
**`allowedHosts: true` disables Vite HMR host-checking entirely**
Previously, when `privateHostnameGateEnabled` is false the value was `undefined`, which lets Vite apply its default host-validation behaviour. Changing it to `true` explicitly disables all host checking, bypassing Vite's built-in DNS-rebinding protection.
Even in development this can be a concern if the dev server is reachable from a network (CI runners, shared dev machines, tunnels). Unless there's a specific reason to allow all hosts unconditionally, it would be safer to keep `undefined` as the fallback so Vite uses its default safeguards.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: ui/src/components/SavedViews.tsx
Line: 148-154
Comment:
**No error feedback for failed mutations**
All three mutations (`createMutation`, `updateMutation`, `deleteMutation`) discard error state silently. If a request fails (e.g. network error, server validation failure), the user sees no feedback — the dialog just stays open or nothing happens.
Consider at least rendering `createMutation.error?.message` (and equivalents for update/delete) somewhere visible near the action, for example:
```tsx
{createMutation.isError && (
<p className="text-xs text-destructive">{createMutation.error?.message ?? "Failed to save view."}</p>
)}
```
The same pattern applies to `updateMutation` and `deleteMutation` in `ManageViewsDialog`.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "docs: add before/aft..." |
ae150f0 to
b78e33d
Compare
Add a full-stack Saved Views feature that lets users save and quickly apply named combinations of filters, grouping, and sort settings on the Issues page. Views are stored per-company so all team members share the same set. - Database: saved_views table (Drizzle schema + SQL migration) - Server: CRUD API at /api/companies/:companyId/saved-views - UI: SavedViewsBar (pill buttons), SaveViewDialog, ManageViewsDialog - Integrates into IssuesList toolbar with save/apply/manage workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add varchar(255) constraint on name column in schema and route validation - Rate limit: reject creation when company already has 50 saved views - Validate filters shape (must be object with statuses/priorities/assignees/labels as string arrays) - Whitelist sortField, sortDirection, and groupBy to allowed values in both create and update - Add migration 0039 to change FK to ON DELETE CASCADE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migration 0039_saved_views_cascade.sql existed but was not registered in the journal, causing test failures. Since 0038 hasn't shipped, fold the ON DELETE CASCADE directly into it and delete 0039. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… remove screenshot files
- Fix pushToast calls to use {title, tone} instead of {type, message}
- Make SavedViewsBar horizontally scrollable on mobile (overflow-x-auto)
- Add shrink-0 + whitespace-nowrap to prevent pill truncation
- Increase touch targets on mobile (py-1.5 sm:py-1)
- Add scrollbar-none utility for hidden scrollbar UX
Co-Authored-By: Paperclip <noreply@paperclip.ing>
a736c0f to
9ebb42e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
What Changed
Database:
saved_viewstable (migration 0038) with company FK (ON DELETE CASCADE), index oncompany_idid,company_id,name(varchar 255),filters(JSONB),group_by,sort_field,sort_direction, timestampsServer:
/api/companies/:companyId/saved-views(GET, POST, PATCH, DELETE)assertCompanyAccesson all routes — no cross-company accesssortField/sortDirection/groupBywhitelisted to valid values, max 50 views per companyUI (3 components in
SavedViews.tsx):SavedViewsBar— pill buttons for each saved view with active state highlighting, "+Save View" button, manage gear iconSaveViewDialog— modal to name and save current filter/sort/group state, includes a summary of what's being savedManageViewsDialog— inline rename, update to current filters, delete — all with hover-reveal actionsBefore / After
Before — no way to save filter/sort/group combos:
After — saved view pills appear below the filter bar:
How to Verify
npm test→ 85 files, 421 tests, 0 failuresSecurity
assertCompanyAccesson all endpointsid+companyId(no IDOR)varchar(255), validated server-side{statuses[], priorities[], assignees[], labels[]})sortField: whitelisted to valid columns (updated,created,priority,status,identifier)sortDirection: must beascordescgroupBy: whitelisted (none,status,priority,assignee,label)ON DELETE CASCADEon company FK — no orphansRisks
SavedViewsBarintoIssuesList.tsx(3 lines added).Files Changed (13 files, ~576 additions)
Discussed in Discord #dev first. Happy to adjust based on feedback.