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