Fix member selection checkboxes showing for all rows regardless of status
Previously checkboxes only appeared for users with status NONE (Not Invited), hiding them for INVITED/ACTIVE members and making "Select all" confusing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c321d4711e
commit
c634982835
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams, usePathname } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
|
|
@ -138,20 +138,20 @@ export function MembersContent() {
|
|||
const { data: currentUser } = trpc.user.me.useQuery()
|
||||
const currentUserRole = currentUser?.role as RoleValue | undefined
|
||||
|
||||
const { data, isLoading } = trpc.user.list.useQuery({
|
||||
roles: roles,
|
||||
search: search || undefined,
|
||||
page,
|
||||
perPage: 20,
|
||||
})
|
||||
|
||||
const invitableIdsQuery = trpc.user.listInvitableIds.useQuery(
|
||||
{
|
||||
roles: roles,
|
||||
search: search || undefined,
|
||||
},
|
||||
{ enabled: false }
|
||||
)
|
||||
const { data, isLoading } = trpc.user.list.useQuery({
|
||||
roles: roles,
|
||||
search: search || undefined,
|
||||
page,
|
||||
perPage: 20,
|
||||
})
|
||||
|
||||
const invitableIdsQuery = trpc.user.listInvitableIds.useQuery(
|
||||
{
|
||||
roles: roles,
|
||||
search: search || undefined,
|
||||
},
|
||||
{ enabled: false }
|
||||
)
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
|
|
@ -171,9 +171,8 @@ export function MembersContent() {
|
|||
},
|
||||
})
|
||||
|
||||
// Users on the current page that are selectable (status NONE)
|
||||
const selectableUsers = useMemo(
|
||||
() => (data?.users ?? []).filter((u) => u.status === 'NONE'),
|
||||
() => data?.users ?? [],
|
||||
[data?.users]
|
||||
)
|
||||
|
||||
|
|
@ -195,7 +194,7 @@ export function MembersContent() {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const toggleAll = useCallback(() => {
|
||||
const toggleAll = useCallback(() => {
|
||||
if (allSelectableSelected) {
|
||||
// Deselect all on this page
|
||||
setSelectedIds((prev) => {
|
||||
|
|
@ -215,22 +214,22 @@ export function MembersContent() {
|
|||
return next
|
||||
})
|
||||
}
|
||||
}, [allSelectableSelected, selectableUsers])
|
||||
|
||||
const selectAllMatching = useCallback(async () => {
|
||||
const result = await invitableIdsQuery.refetch()
|
||||
const ids = result.data?.userIds ?? []
|
||||
if (ids.length === 0) {
|
||||
toast.info('No invitable members match the current filter')
|
||||
return
|
||||
}
|
||||
setSelectedIds(new Set(ids))
|
||||
toast.success(`Selected ${ids.length} matching members`)
|
||||
}, [invitableIdsQuery])
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedIds(new Set())
|
||||
}, [])
|
||||
}, [allSelectableSelected, selectableUsers])
|
||||
|
||||
const selectAllMatching = useCallback(async () => {
|
||||
const result = await invitableIdsQuery.refetch()
|
||||
const ids = result.data?.userIds ?? []
|
||||
if (ids.length === 0) {
|
||||
toast.info('No invitable members match the current filter')
|
||||
return
|
||||
}
|
||||
setSelectedIds(new Set(ids))
|
||||
toast.success(`Selected ${ids.length} matching members`)
|
||||
}, [invitableIdsQuery])
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedIds(new Set())
|
||||
}, [])
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
updateParams({ tab: value === 'all' ? null : value, page: '1' })
|
||||
|
|
@ -279,42 +278,42 @@ export function MembersContent() {
|
|||
</Tabs>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<MembersSkeleton />
|
||||
) : data && data.users.length > 0 ? (
|
||||
<>
|
||||
{/* Bulk selection controls */}
|
||||
<Card>
|
||||
<CardContent className="py-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Selection persists across pages and filters.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={selectAllMatching}
|
||||
disabled={invitableIdsQuery.isFetching}
|
||||
>
|
||||
{invitableIdsQuery.isFetching ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : null}
|
||||
Select all matching
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearSelection}
|
||||
disabled={selectedIds.size === 0}
|
||||
>
|
||||
Clear selection
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Desktop table */}
|
||||
<Card className="hidden md:block">
|
||||
{isLoading ? (
|
||||
<MembersSkeleton />
|
||||
) : data && data.users.length > 0 ? (
|
||||
<>
|
||||
{/* Bulk selection controls */}
|
||||
<Card>
|
||||
<CardContent className="py-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Selection persists across pages and filters.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={selectAllMatching}
|
||||
disabled={invitableIdsQuery.isFetching}
|
||||
>
|
||||
{invitableIdsQuery.isFetching ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : null}
|
||||
Select all matching
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearSelection}
|
||||
disabled={selectedIds.size === 0}
|
||||
>
|
||||
Clear selection
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Desktop table */}
|
||||
<Card className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
@ -323,7 +322,7 @@ export function MembersContent() {
|
|||
<Checkbox
|
||||
checked={allSelectableSelected ? true : someSelectableSelected ? 'indeterminate' : false}
|
||||
onCheckedChange={toggleAll}
|
||||
aria-label="Select all uninvited members"
|
||||
aria-label="Select all members"
|
||||
/>
|
||||
)}
|
||||
</TableHead>
|
||||
|
|
@ -340,15 +339,11 @@ export function MembersContent() {
|
|||
{data.users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
{user.status === 'NONE' ? (
|
||||
<Checkbox
|
||||
checked={selectedIds.has(user.id)}
|
||||
onCheckedChange={() => toggleUser(user.id)}
|
||||
aria-label={`Select ${user.name || user.email}`}
|
||||
/>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Checkbox
|
||||
checked={selectedIds.has(user.id)}
|
||||
onCheckedChange={() => toggleUser(user.id)}
|
||||
aria-label={`Select ${user.name || user.email}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -438,14 +433,12 @@ export function MembersContent() {
|
|||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{user.status === 'NONE' && (
|
||||
<Checkbox
|
||||
checked={selectedIds.has(user.id)}
|
||||
onCheckedChange={() => toggleUser(user.id)}
|
||||
aria-label={`Select ${user.name || user.email}`}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={selectedIds.has(user.id)}
|
||||
onCheckedChange={() => toggleUser(user.id)}
|
||||
aria-label={`Select ${user.name || user.email}`}
|
||||
className="mt-1"
|
||||
/>
|
||||
<UserAvatar
|
||||
user={user}
|
||||
avatarUrl={(user as Record<string, unknown>).avatarUrl as string | undefined}
|
||||
|
|
@ -573,13 +566,13 @@ export function MembersContent() {
|
|||
)}
|
||||
Invite Selected
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearSelection}
|
||||
disabled={bulkInvite.isPending}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearSelection}
|
||||
disabled={bulkInvite.isPending}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Clear
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue