Skip to content

feat(filters): saved filter views - #2193

Open
s0up4200 wants to merge 5 commits into
developfrom
feat/filter-views
Open

feat(filters): saved filter views#2193
s0up4200 wants to merge 5 commits into
developfrom
feat/filter-views

Conversation

@s0up4200

@s0up4200 s0up4200 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Adds saved filter views (#964): named snapshots of the sidebar filter selections (status/category/tag/tracker includes+excludes plus expr), stored server-side and applied with one click from a new Views section at the top of the filter sidebar. Applying a view replaces the current selections; search, sort, and column filters are untouched. Views are global across instances, and the snapshot strips the expanded* UI state so the active view highlights by deep equality. Backend is a small CRUD (/api/filter-views) over a new filter_views table (SQLite 080 + Postgres 082, no FK to user(id) so OIDC-only and auth-disabled installs work), with the filters payload kept as an opaque JSON blob.

Deliberately out of scope: an expression editor, and/or filter builder, per-instance scoping, and drag reordering (sort_order is in the schema for later).

Testing

  • go test -race -count=1 ./internal/models/ ./internal/api/...
  • make test-openapi
  • frontend typecheck, eslint on changed files, pnpm test (incl. new filter-views.test.ts)
  • make precommit and make build
  • Postgres migration applied only by eye (no local PG harness); covered by CI

Screenshots to follow.

Summary by CodeRabbit

  • New Features
    • Added saved torrent filter “views” with server-backed persistence (list, create, update, delete).
    • Added a “Views” section in the filter sidebar to save, rename, and manage views.
    • Introduced new filter-views API endpoints (Swagger) and corresponding UI behavior.
  • Bug Fixes
    • Prevented save/rename actions while create/update is in progress.
  • Tests
    • Added end-to-end API/handler and model store CRUD/validation/order tests.
    • Added unit tests for view filter normalization/equality and persisted “Views” accordion seeding.

s0up4200 added 2 commits July 30, 2026 15:43
Named snapshots of the sidebar TorrentFilters blob, stored per user in a
new filter_views table (SQLite 080 + Postgres 082). The filters payload
is opaque TEXT the backend never interprets. No FK to user(id) since
OIDC-only and auth-disabled installs never populate the user table.

Implements #964
Views accordion at the top of the filter sidebar: click applies the
saved snapshot (replace, not merge), kebab menu for rename/delete, save
dialog that survives name conflicts. Active view highlighted by deep
equality with expanded* UI state stripped. i18n across all nine locales.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 989d643b-7780-4a3f-b91b-93d51885657b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e91973 and ef602c7.

📒 Files selected for processing (2)
  • web/src/hooks/usePersistedAccordion.test.ts
  • web/src/hooks/usePersistedAccordion.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/hooks/usePersistedAccordion.test.ts

Walkthrough

Adds saved torrent filter views with database persistence, authenticated CRUD API endpoints, OpenAPI definitions, React Query integration, sidebar controls, normalization utilities, persisted accordion behavior, tests, and localized UI strings.

Changes

Saved filter views

Layer / File(s) Summary
Persistence model and migrations
internal/database/..., internal/models/filter_view*
Creates SQLite and PostgreSQL filter_views tables and implements tested list, create, update, and delete operations with ordering and duplicate-name handling.
API handlers and routing
cmd/qui/main.go, internal/api/..., internal/web/swagger/openapi.yaml
Wires the store into the server, adds validated authenticated CRUD routes, documents the endpoints and schemas, and tests HTTP behavior.
Frontend contracts and data flow
web/src/types/torrents.ts, web/src/lib/..., web/src/hooks/useFilterViews.ts
Adds filter-view types, deterministic filter normalization and equality, API client methods, and React Query CRUD hooks.
Sidebar UI and localization
web/src/components/torrents/..., web/src/hooks/usePersistedAccordion*, web/src/i18n/locales/*/torrents.json
Adds saved-view sidebar integration, persisted accordion seeding, submission guards, tests, and translations for supported locales.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FilterViewsSection
  participant useFilterViews
  participant ApiClient
  participant FilterViewHandler
  participant FilterViewStore

  FilterViewsSection->>useFilterViews: request saved-view operation
  useFilterViews->>ApiClient: call filter-view API method
  ApiClient->>FilterViewHandler: send HTTP request
  FilterViewHandler->>FilterViewStore: validate and persist view
  FilterViewStore-->>FilterViewHandler: return view or error
  FilterViewHandler-->>ApiClient: return HTTP response
  ApiClient-->>useFilterViews: resolve operation
  useFilterViews-->>FilterViewsSection: refresh displayed views
Loading

Possibly related PRs

  • autobrr/qui#643: Modifies the same FilterSidebar.tsx rendering area where the saved views section is added.

Suggested labels: tests, database, area/frontend

Poem

I hop through views, both saved and bright,
Sorting filters left and right.
Rename, delete, or save anew,
The sidebar knows just what to do.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding saved filter views.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/filter-views

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@s0up4200 s0up4200 linked an issue Jul 30, 2026 that may be closed by this pull request

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/models/filter_view.go`:
- Around line 127-129: Update the RowsAffected handling in the deletion flow to
return or propagate the error from res.RowsAffected() before evaluating the
affected-row count. Preserve returning sql.ErrNoRows when the call succeeds with
zero rows, and avoid treating a RowsAffected failure as successful completion.

In `@web/src/components/torrents/FilterViewsSection.tsx`:
- Around line 83-96: Update submitName to return immediately when either
createView or updateView mutation is pending, before trimming or submitting the
name, while preserving the existing success callback and mutation-specific
behavior.

In `@web/src/hooks/usePersistedAccordion.ts`:
- Around line 12-26: Update the usePersistedAccordion state initializer to
derive the initial “views” expansion without writing VIEWS_SEEDED_KEY during
render. Move persistence of both “qui-accordion” and VIEWS_SEEDED_KEY into the
expandedItems effect, ensuring the marker is written together with the expanded
state.

In `@web/src/types/torrents.ts`:
- Around line 220-234: Introduce a FilterViewFilters transport type as
Partial<TorrentFilters> and use it for the filters fields in FilterView and
FilterViewInput. Update the view-application path to normalize these partial
filters into the complete TorrentFilters shape before consumers use them,
preserving safe defaults for omitted arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88c2be65-4d30-440c-b539-c7b2a71cc6b6

📥 Commits

Reviewing files that changed from the base of the PR and between d53e322 and aff0040.

📒 Files selected for processing (27)
  • cmd/qui/main.go
  • internal/api/handlers/filter_views.go
  • internal/api/handlers/filter_views_test.go
  • internal/api/server.go
  • internal/api/server_test.go
  • internal/database/migrations/080_add_filter_views.sql
  • internal/database/postgres_migrations/082_add_filter_views.sql
  • internal/models/filter_view.go
  • internal/models/filter_view_test.go
  • internal/web/swagger/openapi.yaml
  • web/src/components/torrents/FilterSidebar.tsx
  • web/src/components/torrents/FilterViewsSection.tsx
  • web/src/hooks/useFilterViews.ts
  • web/src/hooks/usePersistedAccordion.ts
  • web/src/i18n/locales/cs/torrents.json
  • web/src/i18n/locales/de/torrents.json
  • web/src/i18n/locales/en/torrents.json
  • web/src/i18n/locales/fr/torrents.json
  • web/src/i18n/locales/it/torrents.json
  • web/src/i18n/locales/ko/torrents.json
  • web/src/i18n/locales/pt-BR/torrents.json
  • web/src/i18n/locales/uk/torrents.json
  • web/src/i18n/locales/zh-CN/torrents.json
  • web/src/lib/api.ts
  • web/src/lib/filter-views.test.ts
  • web/src/lib/filter-views.ts
  • web/src/types/torrents.ts

Comment thread internal/models/filter_view.go Outdated
Comment thread web/src/components/torrents/FilterViewsSection.tsx
Comment thread web/src/hooks/usePersistedAccordion.ts
Comment thread web/src/types/torrents.ts Outdated
Comment on lines +220 to +234
/** A named snapshot of TorrentFilters, saved server-side and shared across instances. */
export interface FilterView {
id: number
name: string
filters: TorrentFilters
sortOrder: number
createdAt: string
updatedAt: string
}

export interface FilterViewInput {
name: string
filters: TorrentFilters
sortOrder?: number
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Model persisted filters as partial data.

The API accepts and returns opaque sparse objects (for example, the backend CRUD test returns {"tags":["hd"]} and updates with {}). Typing both fields as complete TorrentFilters falsely guarantees every array exists and can mask runtime failures in consumers. Introduce a FilterViewFilters = Partial<TorrentFilters> transport type, then normalize before applying a view.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/types/torrents.ts` around lines 220 - 234, Introduce a
FilterViewFilters transport type as Partial<TorrentFilters> and use it for the
filters fields in FilterView and FilterViewInput. Update the view-application
path to normalize these partial filters into the complete TorrentFilters shape
before consumers use them, preserving safe defaults for omitted arrays.

Propagate RowsAffected errors in FilterViewStore.Delete, guard Enter-key
resubmission while a save is pending, type stored view filters as
Partial<TorrentFilters> (backend stores an opaque blob), and move the
accordion seed-marker write out of the render-phase state initializer.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/hooks/usePersistedAccordion.ts`:
- Around line 24-27: Update the useEffect in usePersistedAccordion so it writes
the serialized expandedItems to "qui-accordion" before setting VIEWS_SEEDED_KEY.
Preserve the existing values and dependency behavior while ensuring the seeding
marker is only written after accordion-state persistence succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a3295847-97b5-4b51-85d2-f9f9201b2423

📥 Commits

Reviewing files that changed from the base of the PR and between aff0040 and c60653e.

📒 Files selected for processing (4)
  • internal/models/filter_view.go
  • web/src/components/torrents/FilterViewsSection.tsx
  • web/src/hooks/usePersistedAccordion.ts
  • web/src/types/torrents.ts

Comment thread web/src/hooks/usePersistedAccordion.ts
@itoqa

itoqa Bot commented Jul 30, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: c60653e: 16 test cases ran, 1 failed ❌, 15 passed ✅.

Summary

Coverage spans saved-view creation and editing, filter persistence and normalization, localization, deterministic storage behavior, validation, concurrency, and responsive sidebar interactions. The tested product paths are broadly healthy across normal workflows and adversarial input cases, with one persisted-state rendering edge case affecting access to saved views.

Merge with caution — a PR-attributable medium-severity defect causes the application to fail to render the sidebar and saved views when persisted UI state is malformed, affecting both desktop and mobile users until that state is cleared.

Tests run by Ito

View full run

Result Severity Type Description
Medium severity Accordion With qui-accordion set to {, the page displays Something went wrong! and the JSON parse error on both desktop and mobile. The expected behavior is to ignore or reset malformed persisted state and render the sidebar, including the server-backed Views section.
Accordion Legacy accordion storage was migrated to include Views exactly once, the seed marker was persisted, and collapsing Views remained effective after reload.
Accordion All nine supported locales rendered usable translated Views labels without literal translation keys. The saved Movies view remained available, and the Portuguese action menu plus rename and delete dialogs used translated actions with the view name correctly interpolated.
Normalize The Complete saved view restored all eight include/exclude filter arrays and the non-empty expression, and the UI marked it active with the expected filter counts.
Normalize Applying Partial retained only the hd tag selection and cleared status, category, tracker, and expression filters instead of merging with the previous Complete view.
Normalize Complete was reapplied after subcategory support was disabled. The base category selections remained active, derived expansion fields were cleared, and the torrent request contained only normalized base category arrays.
Sidebar The active Downloading filter was saved as Movies, the dialog closed after success, and Movies appeared in the expanded Views section with the filter still selected.
Sidebar Submitting the existing Movies name displayed the duplicate-name error while retaining the dialog and Movies draft; changing the draft to Replacement closed the dialog and saved the new view.
Sidebar A desktop delete confirmation held for Movies became stale after mobile deleted Movies. Confirming it returned Filter view not found, and the Replacement view remained intact.
Storage The SQLite API created alpha, zeta, and beta with their filters and timestamps intact, then returned them in sort order followed by name.
Storage The local Postgres-backed API created, listed, renamed, and deleted a filter view without identity or timestamp errors, and the final list confirmed deletion.
Storage Two simultaneous local requests produced one 201 response and one 409 conflict, and the shared Postgres list contained exactly one durable Concurrent view.
Validation Whitespace-only and overlong names plus malformed, null, array, and scalar filters returned HTTP 400, and no records persisted.
Validation Whitespace-normalized duplicate names returned HTTP 409, while zero, negative, nonnumeric, and overflowing IDs returned HTTP 400 for both update and delete; the original Movies record remained unchanged.
Validation The existing view was updated successfully, deleted with an empty 204 response, and unknown view IDs returned 404 for both mutations.
Validation The API accepted unexpected and deeply nested object keys with HTTP 201, and GET returned the submitted filter structure unchanged with filtersEqual=true.

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread web/src/hooks/usePersistedAccordion.ts
A corrupt qui-accordion localStorage value threw during the useState
initializer and took the whole torrent page down to the error boundary
(pre-existing, surfaced by Ito QA). Fall back to defaults like the
sibling persistence hooks do; the persist effect then repairs the
stored value. Also write the views seed marker after the accordion
state so a failed write retries seeding.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/hooks/usePersistedAccordion.ts`:
- Around line 13-25: Update the persisted accordion parsing in the hook
initializer to validate that the parsed value is an array and every element is a
string before returning or spreading it. Fall back to DEFAULT_ITEMS for invalid
scalars, objects, or mixed-type arrays, while preserving the existing
VIEWS_SEEDED_KEY and “views” seeding behavior for valid string arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93fe0407-33dd-463f-9d3f-1bc25dc4e573

📥 Commits

Reviewing files that changed from the base of the PR and between c60653e and 2e91973.

📒 Files selected for processing (2)
  • web/src/hooks/usePersistedAccordion.test.ts
  • web/src/hooks/usePersistedAccordion.ts

Comment thread web/src/hooks/usePersistedAccordion.ts
Valid JSON of the wrong shape (e.g. a bare number) slipped past the
try/catch on the already-seeded path and crashed inside Radix instead.
@itoqa

itoqa Bot commented Jul 30, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportc60653eef602c7: 8 test cases ran, 1 fixed ✅, 6 passing ✅, 1 additional finding ⚠️.

Diff Summary

Coverage spans saved-view and filter-sidebar behavior, including normal save and persistence flows, localization across supported languages, desktop/mobile rendering, recovery from malformed or invalid stored settings, and storage access edge cases. The exercised behavior is broadly healthy across UI rendering, state persistence, input handling, and resilience scenarios.

Safe to merge — the only failure is a medium-severity storage-denial resilience issue explicitly determined not to be caused by this PR, with no regressions or PR-attributable failures identified. It is a worthwhile flag for later hardening but is not a merge blocker for this change.

Tests run by Ito

View full run

Result State Severity Type Description
❌->✅ Fixed Accordion Malformed qui-accordion JSON was contained and repaired to the default expanded sections. The desktop sidebar and mobile filter sheet both rendered Views with QA Accordion View and the remaining filter sections.
Passing Accordion Legacy accordion preferences gained the Views section once, recorded the seed marker, and preserved the user's later collapsed state without duplicating Views.
Passing Accordion The saved-view UI rendered localized titles, descriptions, labels, and actions across en, zh-CN, fr, de, cs, it, ko, uk, and pt-BR without exposing translation keys. Source inspection also confirms localized rename and delete dialogs, including delete-name interpolation; the earlier failure was caused by incomplete locale coverage during the initial run.
Passing Accordion Scalar JSON and a mixed-type array were rejected as accordion state. The filter sidebar repaired storage to the complete default Views, Status, Categories, Tags, and Trackers list, and the numeric value was not rendered.
Passing Accordion After both accordion keys were cleared, the local Torrents page persisted the complete expanded default sections, wrote the Views seed marker, and retained the expanded Views section after reload.
Passing Accordion A targeted localStorage read exception was contained during accordion initialization, and the desktop sidebar and mobile Filters sheet both rendered Views with the default filter sections.
Passing Accordion Views was collapsed on desktop, the stored accordion state excluded views, and the mobile Filters dialog retained Views as collapsed while the other filter sections remained available.
⏸️ Skipped Normalize The Complete saved view restored all eight include/exclude filter arrays and the non-empty expression, and the UI marked it active with the expected filter counts.
⏸️ Skipped Normalize Applying Partial retained only the hd tag selection and cleared status, category, tracker, and expression filters instead of merging with the previous Complete view.
⏸️ Skipped Normalize Complete was reapplied after subcategory support was disabled. The base category selections remained active, derived expansion fields were cleared, and the torrent request contained only normalized base category arrays.
⏸️ Skipped Sidebar The active Downloading filter was saved as Movies, the dialog closed after success, and Movies appeared in the expanded Views section with the filter still selected.
⏸️ Skipped Sidebar Submitting the existing Movies name displayed the duplicate-name error while retaining the dialog and Movies draft; changing the draft to Replacement closed the dialog and saved the new view.
⏸️ Skipped Sidebar A desktop delete confirmation held for Movies became stale after mobile deleted Movies. Confirming it returned Filter view not found, and the Replacement view remained intact.
⏸️ Skipped Storage The SQLite API created alpha, zeta, and beta with their filters and timestamps intact, then returned them in sort order followed by name.
⏸️ Skipped Storage The local Postgres-backed API created, listed, renamed, and deleted a filter view without identity or timestamp errors, and the final list confirmed deletion.
⏸️ Skipped Storage Two simultaneous local requests produced one 201 response and one 409 conflict, and the shared Postgres list contained exactly one durable Concurrent view.
⏸️ Skipped Validation Whitespace-only and overlong names plus malformed, null, array, and scalar filters returned HTTP 400, and no records persisted.
⏸️ Skipped Validation Whitespace-normalized duplicate names returned HTTP 409, while zero, negative, nonnumeric, and overflowing IDs returned HTTP 400 for both update and delete; the original Movies record remained unchanged.
⏸️ Skipped Validation The existing view was updated successfully, deleted with an empty 204 response, and unknown view IDs returned 404 for both mutations.
⏸️ Skipped Validation The API accepted unexpected and deeply nested object keys with HTTP 201, and GET returned the submitted filter structure unchanged with filtersEqual=true.
⚠️ Additional Finding Medium severity Accordion The first Views accordion toggle triggered the persistence effect, and the forced localStorage write exception escaped into the application error boundary instead of leaving the sidebar usable with persistence disabled.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Denied localStorage writes crash the filter sidebar
  • Severity: Medium Medium severity
  • Description: The first Views accordion toggle triggered the persistence effect, and the forced localStorage write exception escaped into the application error boundary instead of leaving the sidebar usable with persistence disabled.
  • Impact: When browser storage is unavailable, expanding the Views accordion crashes the sidebar and prevents users from using the remaining sidebar, mobile filter sheet, and Saved Views controls. Users with working browser storage are unaffected, and no data loss is evidenced.
  • Steps to Reproduce:
    1. Run the local application with the authenticated torrent screen available.
    2. Override Storage.prototype.setItem so it throws an error for localStorage writes.
    3. Open the desktop filter sidebar and click Views to trigger accordion persistence.
    4. Observe the error boundary, then verify that the mobile filter sheet and Saved Views controls can no longer be exercised.
  • Stub / mock content: The local application used a QA authentication bypass, and Storage.prototype.setItem was intentionally overridden to throw in order to model denied browser localStorage writes; no production data or external service was used.
  • Code Analysis: usePersistedAccordion.ts lines 32-35 write the expanded accordion state and seed marker directly inside useEffect without a try/catch. The hook's initializer catches read and JSON parsing failures, but that protection does not cover asynchronous persistence after setExpandedItems updates the state. The PR diff changed this effect's write order but did not add the uncaught setItem behavior; the smallest practical fix is to catch or otherwise contain these persistence failures around the effect writes so accordion interaction remains available when storage is denied.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FR: Filter views

1 participant