Skip to content

feat: Saved Views for Issues list#1528

Open
labatt wants to merge 7 commits intopaperclipai:masterfrom
labatt:feature/saved-views
Open

feat: Saved Views for Issues list#1528
labatt wants to merge 7 commits intopaperclipai:masterfrom
labatt:feature/saved-views

Conversation

@labatt
Copy link

@labatt labatt commented Mar 22, 2026

Thinking Path

  • Paperclip orchestrates AI agents for zero-human companies
  • But humans need to watch the agents and oversee their work
  • Humans look at the Issues list constantly, applying filters, sort, and grouping to find what matters
  • But teams develop recurring ways to look at their board (e.g. "my stuck issues", "high priority todo", "done this week")
  • Each time they have to manually re-apply those same filters, sort, and grouping settings
  • So this PR adds Saved Views — save any combination of filter/sort/group as a named view for one-click recall
  • The benefit is faster board navigation without repeatedly configuring the same filters, especially useful when managing large agent fleets

What Changed

Database:

  • New saved_views table (migration 0038) with company FK (ON DELETE CASCADE), index on company_id
  • Schema: id, company_id, name (varchar 255), filters (JSONB), group_by, sort_field, sort_direction, timestamps

Server:

  • CRUD API at /api/companies/:companyId/saved-views (GET, POST, PATCH, DELETE)
  • assertCompanyAccess on all routes — no cross-company access
  • Input validation: name length (255 chars), filters shape validated, sortField/sortDirection/groupBy whitelisted to valid values, max 50 views per company

UI (3 components in SavedViews.tsx):

  • SavedViewsBar — pill buttons for each saved view with active state highlighting, "+Save View" button, manage gear icon
  • SaveViewDialog — modal to name and save current filter/sort/group state, includes a summary of what's being saved
  • ManageViewsDialog — inline rename, update to current filters, delete — all with hover-reveal actions

Before / After

Before — no way to save filter/sort/group combos:

before

After — saved view pills appear below the filter bar:

after

How to Verify

  1. Go to the Issues page
  2. Apply some filters (e.g. filter by status, sort by priority)
  3. Click "+Save View", give it a name, save
  4. A pill button appears — click it to instantly re-apply that view
  5. Click the gear icon to manage views (rename, update to current filters, delete)
  6. All tests pass: npm test → 85 files, 421 tests, 0 failures

Security

  • assertCompanyAccess on all endpoints
  • All DB queries double-scoped by id + companyId (no IDOR)
  • Name: varchar(255), validated server-side
  • Rate limit: max 50 views per company
  • Filters: shape validated ({statuses[], priorities[], assignees[], labels[]})
  • sortField: whitelisted to valid columns (updated, created, priority, status, identifier)
  • sortDirection: must be asc or desc
  • groupBy: whitelisted (none, status, priority, assignee, label)
  • ON DELETE CASCADE on company FK — no orphans
  • No raw SQL — all Drizzle ORM parameterized queries

Risks

  • Low risk. New table, new routes, new UI components. No changes to existing code paths beyond wiring the SavedViewsBar into IssuesList.tsx (3 lines added).
  • Migration is additive — no schema changes to existing tables.

Files Changed (13 files, ~576 additions)

packages/db/src/migrations/0038_saved_views.sql
packages/db/src/migrations/meta/_journal.json
packages/db/src/schema/index.ts
packages/db/src/schema/saved_views.ts
server/src/app.ts
server/src/routes/index.ts
server/src/routes/saved-views.ts
server/src/services/index.ts
server/src/services/saved-views.ts
ui/src/api/saved-views.ts
ui/src/components/IssuesList.tsx
ui/src/components/SavedViews.tsx
ui/src/lib/queryKeys.ts

Discussed in Discord #dev first. Happy to adjust based on feedback.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This 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, assertCompanyAccess, React Query), keeps the change additive, and touches existing code in only three lines.

Key findings:

  • Schema/migration mismatch — the SQL migration creates name as text, but the Drizzle schema declares it varchar(255). The DB-level length constraint is absent; enforcement is application-only.
  • PATCH allows empty names — the PATCH validation condition permits name: "" because an empty string satisfies typeof name === "string" && name.length <= 255. The POST route blocks this correctly with !name.
  • TOCTOU race on the 50-view cap — the rate-limit check (svc.list() then svc.create()) is not atomic; concurrent requests can both see count < 50 and both insert, slightly exceeding the limit.
  • allowedHosts: true in Vite HMR config — an unrelated change in app.ts flips the fallback from undefined (Vite default) to true (all hosts allowed), which disables DNS-rebinding protection on the dev server when privateHostnameGateEnabled is false.
  • Silent mutation failures in the UI — none of the three mutations in SavedViews.tsx surface error state to the user; failed saves/updates/deletes produce no visible feedback.

Confidence Score: 3/5

  • Safe to merge after addressing the migration/schema mismatch and the empty-name PATCH bug; the other findings are low-severity.
  • The core feature is well-structured and the security fundamentals (authz on every route, double-scoped DB queries, whitelisted enum values) are solid. Two correctness issues need fixing before merge: the name column type mismatch between migration and ORM schema, and the PATCH route accepting empty string names. The remaining findings (TOCTOU race, silent UI errors, allowedHosts: true) are worth addressing but are lower risk.
  • packages/db/src/migrations/0038_saved_views.sql (name column type mismatch) and server/src/routes/saved-views.ts (empty-name PATCH bug, TOCTOU race).

Important Files Changed

Filename Overview
packages/db/src/migrations/0038_saved_views.sql Additive migration creating saved_views table. The name column is defined as text but the Drizzle schema declares it varchar(255) — the DB-level length constraint is absent from the migration.
packages/db/src/schema/saved_views.ts Well-structured Drizzle schema with correct FK, cascade delete, index, and typed JSONB. Mismatch with the SQL migration on the name column type (varchar(255) here vs text in SQL).
server/src/routes/saved-views.ts CRUD routes with assertCompanyAccess on all endpoints. Two issues: PATCH validation accepts empty string names, and the 50-view rate-limit check is a TOCTOU race condition.
server/src/services/saved-views.ts Clean service layer with parameterised Drizzle queries, double-scoped by id + companyId for all mutating operations, and a strict validateFilters helper.
server/src/app.ts Route wired in correctly. Unrelated change flips Vite HMR allowedHosts from undefined to true, which disables host checking when privateHostnameGateEnabled is false.
server/src/routes/plugins.ts Adds auto-detection of local filesystem paths so the isLocalPath flag can be omitted from API callers. Logic is sound — explicit false still suppresses auto-detection correctly.
ui/src/components/SavedViews.tsx Three-component file covering bar, save dialog, and manage dialog. Logic is clean; only concern is that all mutations swallow errors silently — users get no feedback on failure.
ui/src/api/saved-views.ts Thin API wrapper with well-typed interfaces matching the server contract. No issues.
ui/src/components/IssuesList.tsx Minimal, clean integration — imports SavedViewsBar and renders it with viewState and onApplyView. No issues.
ui/src/lib/queryKeys.ts Adds savedViews.list(companyId) query key correctly scoped by company. No issues.
ui/src/pages/PluginManager.tsx Unrelated to saved views. UI now shows a helper label when a local path is detected, consistent with the backend auto-detection logic added in plugins.ts.
ui/src/components/AgentConfigForm.tsx Unrelated to saved views. Adds openclaw_gateway to the set of enabled adapter types. No issues.
Prompt To Fix All With AI
This 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..."

@labatt labatt force-pushed the feature/saved-views branch from ae150f0 to b78e33d Compare March 22, 2026 03:33
Liv and others added 7 commits March 23, 2026 14:24
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>
- 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>
@labatt labatt force-pushed the feature/saved-views branch from a736c0f to 9ebb42e Compare March 23, 2026 14:24
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