Conversation
- Replace Load Users button with a live search input; query fires on any input - Email search uses listUsers with contains operator - User ID search (UUID format) uses admin.getUser directly for exact lookup - Remove outer border on user table that rendered white in dark mode - Reset pagination to page 0 on new search Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split searchInput (controlled input) from searchQuery (committed value) so the hook only fires on Search click or Enter, not every keystroke - Gate table render on searchQuery.length > 0 to prevent stale results showing after input is cleared Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace. To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR replaces the manual "Load Users" button in the admin panel with a persistent search input that routes to The implementation is clean and well-structured: dual Issues found:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
actor Admin
participant UI as Admin Component
participant Hook as useAdminUsers
participant API as Better-Auth Client
Admin->>UI: Types in search input
Admin->>UI: Presses Enter / clicks Search
UI->>UI: handleSearch() — sets searchQuery, resets offset
alt searchQuery matches UUID regex
Hook->>API: admin.getUser({ id: searchQuery })
API-->>Hook: { data: single user }
Hook-->>UI: { users: [user], total: 1 }
else searchQuery is email / partial
Hook->>API: admin.listUsers({ searchField: 'email', searchOperator: 'contains', searchValue: searchQuery, limit, offset })
API-->>Hook: { data: { users[], total } }
Hook-->>UI: { users[], total }
end
UI->>Admin: Renders user table (gated on searchQuery.length > 0)
opt Paginate
Admin->>UI: Clicks Previous / Next
UI->>UI: setUsersOffset(prev ± PAGE_SIZE)
Hook->>API: admin.listUsers({ ..., offset: new offset })
API-->>Hook: next page of results
Hook-->>UI: updated users
end
Last reviewed commit: "fix(admin): replace ..." |
| <Button variant='primary' onClick={handleSearch} disabled={usersLoading}> | ||
| {usersLoading ? 'Searching...' : 'Search'} | ||
| </Button> |
There was a problem hiding this comment.
Search button enabled on empty input
The Search button is currently enabled whenever usersLoading is false, even when searchInput is empty. Clicking Search with an empty (or whitespace-only) input will call handleSearch, reset usersOffset to 0, and set searchQuery to '' — causing any previously displayed results to immediately disappear (since the table is gated on searchQuery.length > 0). Adding || !searchInput.trim() to the disabled condition would prevent this accidental result-clearing and avoid unnecessary state updates.
| <Button variant='primary' onClick={handleSearch} disabled={usersLoading}> | |
| {usersLoading ? 'Searching...' : 'Search'} | |
| </Button> | |
| <Button variant='primary' onClick={handleSearch} disabled={usersLoading || !searchInput.trim()}> | |
| {usersLoading ? 'Searching...' : 'Search'} |
| return useQuery({ | ||
| queryKey: adminUserKeys.list(offset, limit), | ||
| queryFn: () => fetchAdminUsers(offset, limit), | ||
| enabled, | ||
| queryKey: adminUserKeys.list(offset, limit, searchQuery), | ||
| queryFn: () => fetchAdminUsers(offset, limit, searchQuery), | ||
| enabled: searchQuery.length > 0, | ||
| staleTime: 30 * 1000, | ||
| placeholderData: keepPreviousData, | ||
| }) |
There was a problem hiding this comment.
isLoading misses background re-fetches on the Search button
usersLoading maps to isLoading (isPending && isFetching). For subsequent searches against a new query key this is fine (no cached data → isPending is true). However, if the exact same searchQuery is submitted again after the 30-second staleTime window elapses, the query re-fetches in the background: isFetching is true but isPending is false, so isLoading stays false and the button is not disabled. Using isFetching instead provides a consistent disabled state during every in-flight request:
| return useQuery({ | |
| queryKey: adminUserKeys.list(offset, limit), | |
| queryFn: () => fetchAdminUsers(offset, limit), | |
| enabled, | |
| queryKey: adminUserKeys.list(offset, limit, searchQuery), | |
| queryFn: () => fetchAdminUsers(offset, limit, searchQuery), | |
| enabled: searchQuery.length > 0, | |
| staleTime: 30 * 1000, | |
| placeholderData: keepPreviousData, | |
| }) | |
| } = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery) |
In admin-users.ts, expose isFetching alongside isLoading (or return the full query object), and in the component use it:
const {
data: usersData,
isLoading: usersLoading,
isFetching: usersFetching,
error: usersError,
} = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery)
// ...
<Button variant='primary' onClick={handleSearch} disabled={usersFetching || !searchInput.trim()}>
{usersFetching ? 'Searching...' : 'Search'}| <EmcnInput | ||
| value={searchInput} | ||
| onChange={(e) => setSearchInput(e.target.value)} | ||
| onKeyDown={(e) => e.key === 'Enter' && handleSearch()} |
There was a problem hiding this comment.
Enter key fires on empty input
The onKeyDown handler fires handleSearch when Enter is pressed regardless of whether searchInput has content. This has the same side-effect as the empty button click: it resets usersOffset to 0 and sets searchQuery to '', making any previously visible results disappear. Adding a searchInput.trim() guard before calling handleSearch keeps keyboard and click behaviour consistent.
Summary
listUserswith acontainsoperator; pasting a UUID switches toadmin.getUserfor exact ID lookup. Pagination resets on each new search.border-[var(--border-secondary)]on the user table container, which was rendering white in dark mode.Test plan