Skip to content

Commit 9f358d4

Browse files
committed
Users filter
1 parent f98468b commit 9f358d4

File tree

3 files changed

+92
-14
lines changed

3 files changed

+92
-14
lines changed

convex/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const schema = defineSchema({
3232
v.union(...validCapabilities.map((cap) => v.literal(cap)))
3333
),
3434
adsDisabled: v.optional(v.boolean()),
35-
}),
35+
}).index('by_email', ['email']),
3636
})
3737

3838
export default schema

convex/users.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,61 @@ export const listUsers = query({
5050
limit: v.number(),
5151
cursor: v.optional(v.union(v.string(), v.null())),
5252
}),
53+
emailFilter: v.optional(v.string()),
5354
},
5455
handler: async (ctx, args) => {
5556
// Validate admin capability
5657
await requireCapability(ctx, 'admin')
5758

58-
// Return paginated users
59-
return await ctx.db
60-
.query('users')
61-
.order('desc')
62-
.paginate({
63-
numItems: args.pagination.limit,
64-
cursor: args.pagination.cursor ?? null,
65-
})
59+
const limit = args.pagination.limit
60+
const cursor = args.pagination.cursor ?? null
61+
62+
if (args.emailFilter && args.emailFilter.length > 0) {
63+
const start = args.emailFilter
64+
const end = start + '\uffff'
65+
// Prefix range over email using index
66+
return await ctx.db
67+
.query('users')
68+
.withIndex('by_email', (q) => q.gte('email', start))
69+
.filter((q) => q.lt(q.field('email'), end))
70+
.paginate({
71+
numItems: limit,
72+
cursor,
73+
})
74+
}
75+
76+
// Return paginated users without filter
77+
return await ctx.db.query('users').order('desc').paginate({
78+
numItems: limit,
79+
cursor,
80+
})
81+
},
82+
})
83+
84+
export const countUsers = query({
85+
args: {
86+
emailFilter: v.optional(v.string()),
87+
},
88+
handler: async (ctx, args) => {
89+
// Validate admin capability
90+
await requireCapability(ctx, 'admin')
91+
92+
const total = (await ctx.db.query('users').collect()).length
93+
94+
if (args.emailFilter && args.emailFilter.length > 0) {
95+
const start = args.emailFilter
96+
const end = start + '\uffff'
97+
const filtered = (
98+
await ctx.db
99+
.query('users')
100+
.withIndex('by_email', (q) => q.gte('email', start))
101+
.filter((q) => q.lt(q.field('email'), end))
102+
.collect()
103+
).length
104+
return { total, filtered }
105+
}
106+
107+
return { total, filtered: total }
66108
},
67109
})
68110

src/routes/admin/users.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,25 @@ function UsersPage() {
4646
const [updatingAdsUserId, setUpdatingAdsUserId] = useState<string | null>(
4747
null
4848
)
49+
const [emailFilter, setEmailFilter] = useState('')
4950

5051
const user = useConvexQuery(api.auth.getCurrentUser)
52+
const pageSize = 10
5153
const usersQuery = useQuery({
5254
...convexQuery(api.users.listUsers, {
5355
pagination: {
54-
limit: 10,
56+
limit: pageSize,
5557
cursor: cursors[currentPageIndex] || null,
5658
},
59+
emailFilter: emailFilter || undefined,
60+
}),
61+
placeholderData: keepPreviousData,
62+
})
63+
// Cast to any to avoid transient type mismatch until Convex codegen runs
64+
const countUsersRef: any = (api as any).users?.countUsers
65+
const countsQuery = useQuery({
66+
...convexQuery(countUsersRef, {
67+
emailFilter: emailFilter || undefined,
5768
}),
5869
placeholderData: keepPreviousData,
5970
})
@@ -353,6 +364,19 @@ function UsersPage() {
353364
<p className="text-gray-600 dark:text-gray-400">
354365
Manage user accounts and their capabilities.
355366
</p>
367+
<div className="mt-4">
368+
<input
369+
type="text"
370+
value={emailFilter}
371+
onChange={(e) => {
372+
setEmailFilter(e.target.value)
373+
setCurrentPageIndex(0)
374+
setCursors([''])
375+
}}
376+
placeholder="Filter by email"
377+
className="w-full max-w-md px-3 py-2 border rounded-md bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white"
378+
/>
379+
</div>
356380
</div>
357381

358382
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
@@ -413,10 +437,22 @@ function UsersPage() {
413437
{/* Cursor-based pagination controls */}
414438
<div className="mt-6 flex items-center justify-between">
415439
<div className="text-sm text-gray-500 dark:text-gray-400">
416-
Page {currentPageIndex + 1}
417-
{usersQuery.data && (
418-
<span>{usersQuery.data.page?.length} users</span>
419-
)}
440+
{(() => {
441+
const filtered = countsQuery?.data?.filtered ?? 0
442+
const total = countsQuery?.data?.total ?? 0
443+
const totalPages = Math.max(1, Math.ceil(filtered / pageSize))
444+
const showing = usersQuery.data?.page?.length ?? 0
445+
return (
446+
<>
447+
Page {currentPageIndex + 1} of {totalPages}
448+
<span>
449+
{' '}
450+
• showing {showing} of {filtered} users
451+
</span>
452+
{emailFilter ? <span> (from {total} total)</span> : null}
453+
</>
454+
)
455+
})()}
420456
</div>
421457
<div className="flex space-x-2">
422458
<button

0 commit comments

Comments
 (0)