Skip to content

Add search/filtering and infinite scroll to the admin users page - #738

Merged
kody-bot merged 3 commits into
mainfrom
admin-users-search-infinite-scroll
Jul 13, 2026
Merged

kody-bot merged 3 commits into
mainfrom
admin-users-search-infinite-scroll

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds server-side search (q, matching username or email) and a role filter (role) to the admin users page, mirroring the secrets page's URL-backed filter UX. Filters apply in SQL with a WHERE clause shared by the page slice and its COUNT(*), so the reported total always matches the filtered set.
  • Replaces Previous/Next pagination with infinite scroll: an IntersectionObserver sentinel (rendered as an accessible "Load more" button fallback) appends pages via the existing-but-unused createInfiniteList, with stale in-flight pages invalidated whenever filters change.
  • Shares only what has real reuse (AHA): replace-location.ts (extracted from the secrets page), AccountManagementSearchField (now used by secrets and admin users, ready for the other sidebar list pages), and infinite-scroll.ts.
  • Role/plan mutation responses now include the updated target user (updatedUser) so the client patches it into a deeply-scrolled list instead of resetting to page one.
  • The admin_user_list MCP capability gains matching query/role inputs.

Test plan

  • Handler tests: q/role/pagination composition, unknown-role tolerance, updatedUser mutation contract (admin-users.node.test.ts)
  • New unit tests for createInfiniteList: append/dedupe, stale-page invalidation on reset, error handling
  • E2E (admin-rbac.spec.ts): typing in search filters the list and syncs the URL, filtered API totals, no-matches empty state, sentinel auto-load with ?pageSize=1
  • npm run validate green locally
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ d3034257 · Head: 4d0d5242

Classification: extends — the admin users JSON API contract gains q/role query params and an updatedUser field on mutation responses; the admin_user_list MCP capability input schema gains query/role. No primitives added; primitives.yaml unchanged.

Primitives touched

Primitive Group Impact
app-ui surfaces extends — /admin/users(.json) filter params, infinite scroll, shared client utilities
rbac auth extends — admin_user_list capability accepts query/role; guards unchanged
d1-app-db storage composes — new WHERE clauses on existing users/user_roles tables; no schema change

System map

Search and scroll flow from the admin users UI through the RBAC-guarded JSON API into filtered D1 queries.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app (Remix 3)"]:::extended
	rbac["rbac<br/>Role-based access control"]:::extended
	d1AppDb["d1-app-db<br/>D1 app database"]:::touched
	mcpServer["mcp-server<br/>MCP endpoint (/mcp)"]:::untouched
	appUi -->|"GET /admin/users.json?q=&role=&page="| rbac
	rbac -->|"filtered LIKE + role-membership WHERE on users/user_roles"| d1AppDb
	rbac -->|"POST mutations return updatedUser for in-place patch"| appUi
	mcpServer -->|"admin_user_list query/role inputs"| rbac
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Contract Before After
GET /admin/users.json page, pageSize + q (username/email substring), role (known role name)
POST /admin/users.json full first-page payload + updatedUser (refreshed target for in-place patching)
admin_user_list capability page, pageSize + query, role (validated against roleNameSchema)
Users page pagination Previous/Next buttons honoring ?page=N infinite scroll; initial loads ignore ?page so stale deep links seed from page 1

Invariants

Admin routes still expose account metadata only (adminUserListItemFieldNames unchanged; e2e privacy assertions still pass); all queries remain guarded by read:user:any / update:user:any.

Plan vs actual

  • Shipped as planned: server-side q/role filters, infinite scroll via createInfiniteList, shared replace-location.ts / AccountManagementSearchField / infinite-scroll.ts, updatedUser mutation patching.
  • Review-driven drift: mutations now reconcile against the active role filter (drop non-matching rows, refresh total), and initial loads strip ?page=N so stale deep links cannot orphan earlier rows.

Note

Medium Risk
Extends the admin users JSON API and in-place role/plan mutation handling on RBAC-guarded routes; behavior is well tested but touches entitlement and role assignment flows.

Overview
Adds server-side search (q on username/email) and a role filter to the admin users list, with URL-backed controls via replaceLocation and a shared AccountManagementSearchField (secrets page refactored to use the same helpers).

Replaces Previous/Next pagination with infinite scroll: createInfiniteList, an intersection-observer sentinel (infinite-scroll.ts), and initial loads that strip ?page so deep links always seed from page one. Filter changes reset the list and invalidate in-flight loadMore requests.

Role/plan POST responses now include updatedUser so the client can patch a scrolled list in place; rows drop when a mutation removes the active role filter. D1 list queries share one filtered WHERE for rows and COUNT(*).

The admin_user_list MCP capability accepts matching query/role inputs. E2E and handler/unit tests cover search, scroll, mutations, and privacy boundaries.

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

Summary by CodeRabbit

  • New Features
    • Admin users can be searched (username/email) and filtered by role, now backed by an infinite-scroll list.
    • Admin users integrations now support query and role filters, and mutation updates refresh the list plus the updated user.
    • Account secrets filters/search now update the URL smoothly (no new history entries).
  • Bug Fixes
    • Filtered totals and results are now consistent end-to-end, including correct deep-linking and prevention of stale page anchoring.

The users list previously paginated with Previous/Next buttons and had no
way to find an account. Search (q) and role filters now apply server-side
in loadAdminUsersData (shared WHERE clause for the slice and its COUNT),
surface in the admin_user_list MCP capability, and the sidebar list loads
additional pages via an IntersectionObserver sentinel instead of paging.

Shared where the reuse is real: replace-location.ts (URL-backed filter
state, extracted from the secrets page), AccountManagementSearchField
(used by secrets and users), and infinite-scroll.ts pairing with the
previously unused infinite-list.ts. Mutation responses now include the
updated target user so the client patches it into the scrolled window
instead of resetting to page one.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds server-side admin-user filters, infinite scrolling, URL-synchronized search and role filters, mutation payload synchronization, shared client navigation components, MCP filter inputs, and regression coverage.

Changes

Admin users filtering and synchronization

Layer / File(s) Summary
Filtered admin-user data and capability inputs
packages/worker/src/app/admin-users-data.ts, packages/worker/src/app/handlers/admin-users.node.test.ts, packages/worker/src/mcp/capabilities/admin/admin-user-list.ts
Admin-user queries, totals, test doubles, and MCP requests now support q and role filters.
Mutation response synchronization
packages/worker/src/loader-data.ts, packages/worker/src/app/handlers/admin-users.ts, packages/worker/src/app/handlers/admin-users.node.test.ts
Role and plan mutations return both the refreshed list payload and the updated target user.
Shared client search and list primitives
packages/worker/client/infinite-list.node.test.ts, packages/worker/client/infinite-scroll.ts, packages/worker/client/replace-location.ts, packages/worker/client/routes/account-management-components.tsx, packages/worker/client/routes/account-secrets.tsx
Shared URL replacement, search-field, infinite-scroll, and infinite-list behavior is added or adopted by account secrets.
Admin users infinite-scroll page
packages/worker/client/routes/admin-users.tsx, e2e/admin-rbac.spec.ts
The admin users page uses URL filters, infinite-list loading, in-place mutation updates, filter-aware states, and infinite-scroll coverage.

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

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AdminUsersPage
  participant AdminUsersHandler
  participant LoadAdminUsersData
  participant D1
  Browser->>AdminUsersPage: Enter q or role filter
  AdminUsersPage->>Browser: Replace URL and dispatch navigate
  AdminUsersPage->>AdminUsersHandler: Request filtered admin users
  AdminUsersHandler->>LoadAdminUsersData: Load filtered page
  LoadAdminUsersData->>D1: Count and select filtered users
  D1-->>AdminUsersPage: Return users and total
  AdminUsersPage->>AdminUsersPage: Append pages through infiniteScrollSentinel
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.81% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: admin users search/filtering plus infinite scroll.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch admin-users-search-infinite-scroll

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.

@github-actions

github-actions Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-738.kody-a99.workers.dev

Worker: kody-pr-738
D1: kody-pr-738-db
KV: kody-pr-738-oauth-kv

Mocks:

Comment thread packages/worker/client/routes/admin-users.tsx
Comment thread packages/worker/client/routes/admin-users.tsx

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/worker/client/routes/admin-users.tsx (1)

558-563: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Debounce the server-side search input.

Because admin search is server-side, onInput → replaceLocation → re-render triggers loadAdminUsers on every keystroke (currentHref !== lastLoadedHref). loadRequestId keeps results correct, but a fast typist fires a request per character. Unlike account-secrets (which filters client-side), this hits the API repeatedly. Consider debouncing the URL write / fetch.

🤖 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 `@packages/worker/client/routes/admin-users.tsx` around lines 558 - 563,
Debounce the server-side search update in the input handler around
replaceLocation and buildHrefWithUpdatedFilters, delaying URL writes and the
resulting loadAdminUsers request until typing pauses. Preserve the latest search
value and ensure pending updates are cancelled or superseded so only the final
input triggers navigation.
packages/worker/client/infinite-scroll.ts (1)

21-32: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Optional: a rejecting loadMore is swallowed and stalls the sentinel.

The async IIFE has no catch, so if a future loadMore rejects (the type allows Promise<boolean>), it becomes an unhandled rejection and the sentinel never re-observes. Today’s only caller (loadMoreUsers) catches internally and returns false, so this is latent — worth a defensive catch if the helper is reused.

🤖 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 `@packages/worker/client/infinite-scroll.ts` around lines 21 - 32, Update the
async IIFE around loadMore in the infinite-scroll observer to catch rejected
promises, ensuring the sentinel is re-observed and the observer does not remain
stalled when loadMore fails. Preserve the existing aborted or false-result
behavior and running cleanup in the finally block.
packages/worker/src/mcp/capabilities/admin/admin-user-list.ts (1)

25-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared role enum for this input. role is already defined as roleNameSchema; using it here instead of z.string() would reject invalid values up front instead of letting them fall through to the backend and return all users.

🤖 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 `@packages/worker/src/mcp/capabilities/admin/admin-user-list.ts` around lines
25 - 32, Update the role field in the admin user-list input schema to reuse the
existing roleNameSchema instead of z.string(), preserving its optional behavior
and description. This ensures invalid role values are rejected before reaching
the backend.
🤖 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 `@packages/worker/client/routes/admin-users.tsx`:
- Around line 333-343: The applyMutationPayload function must refresh the
user-list snapshot after role mutations that can change membership in the active
role filter. Re-seed the list and total count from the mutation payload when
available, or otherwise update the total and remove rows that no longer match
the active filter, while preserving the existing updatedUser patch for
unaffected mutations.

---

Nitpick comments:
In `@packages/worker/client/infinite-scroll.ts`:
- Around line 21-32: Update the async IIFE around loadMore in the
infinite-scroll observer to catch rejected promises, ensuring the sentinel is
re-observed and the observer does not remain stalled when loadMore fails.
Preserve the existing aborted or false-result behavior and running cleanup in
the finally block.

In `@packages/worker/client/routes/admin-users.tsx`:
- Around line 558-563: Debounce the server-side search update in the input
handler around replaceLocation and buildHrefWithUpdatedFilters, delaying URL
writes and the resulting loadAdminUsers request until typing pauses. Preserve
the latest search value and ensure pending updates are cancelled or superseded
so only the final input triggers navigation.

In `@packages/worker/src/mcp/capabilities/admin/admin-user-list.ts`:
- Around line 25-32: Update the role field in the admin user-list input schema
to reuse the existing roleNameSchema instead of z.string(), preserving its
optional behavior and description. This ensures invalid role values are rejected
before reaching the backend.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 22fb1f6c-f659-494b-89a0-0bc334e4f742

📥 Commits

Reviewing files that changed from the base of the PR and between 77c585e and 4f0821b.

📒 Files selected for processing (12)
  • e2e/admin-rbac.spec.ts
  • packages/worker/client/infinite-list.node.test.ts
  • packages/worker/client/infinite-scroll.ts
  • packages/worker/client/replace-location.ts
  • packages/worker/client/routes/account-management-components.tsx
  • packages/worker/client/routes/account-secrets.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/src/app/admin-users-data.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-list.ts

Comment thread packages/worker/client/routes/admin-users.tsx
… page links

Role mutations now drop the target from the list when it no longer matches
the active role filter and take the refreshed filtered total from the
server. Initial loads (SSR and client) strip the page param so a stale
?page=N link cannot anchor the window past rows infinite scroll can never
reach. The admin_user_list role input reuses roleNameSchema to reject
unknown roles up front.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/worker/client/routes/admin-users.tsx
Comment thread packages/worker/client/routes/admin-users.tsx Outdated

@coderabbitai coderabbitai 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.

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 `@e2e/admin-rbac.spec.ts`:
- Around line 119-123: Strengthen the deep-link regression assertion in the test
around the `/admin/users?pageSize=1&page=99999` navigation so the “Showing” text
requires a positive visible-row count rather than accepting zero. Keep the
existing total-count validation and heading visibility checks unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 251fac4d-dcc8-4f59-ae59-43ec63380766

📥 Commits

Reviewing files that changed from the base of the PR and between 4f0821b and 65b0a4d.

📒 Files selected for processing (4)
  • e2e/admin-rbac.spec.ts
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-list.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/worker/src/mcp/capabilities/admin/admin-user-list.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/client/routes/admin-users.tsx

Comment thread e2e/admin-rbac.spec.ts Outdated
… unknown roles

Mutations now reset the infinite list before replacing the window so an
in-flight load-more from before the mutation cannot merge stale rows back
in, and only known role names count as an active filter (matching the
server, which ignores unknown role values). Tighten the stale-page e2e
assertion to require a non-empty list.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default 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 4d0d524. Configure here.

})
resetPlanDraft()
ensureSelection()
}

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.

Stale page after filter mutation

Medium Severity

When a role mutation removes the updated account from the active role filter, applyMutationPayload shrinks the in-memory list and total but leaves loadedThroughPage unchanged. Later loadMoreUsers requests the next numbered page from the server, which can skip or mis-order accounts relative to the edited client window.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d0d524. Configure here.

pageSize = payload.pageSize
total = payload.total
resetPlanDraft()
applyMutationPayload(payload)

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.

Selection reset after filter drop

Medium Severity

After a role change that drops the target from the active role filter, applyMutationPayload calls ensureSelection to pick a visible account, but submitRoleAction immediately sets selectedUserId back to the mutated user. getSelectedUser then returns null, so no list row is active and the detail pane shows the empty state despite accounts remaining.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d0d524. Configure here.

@kody-bot
kody-bot merged commit 4bc8540 into main Jul 13, 2026
5 checks passed
@kody-bot
kody-bot deleted the admin-users-search-infinite-scroll branch July 13, 2026 16:31
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.

2 participants