fix(admin): unable to select all users in current page (#11155)

close AF-2380
This commit is contained in:
JimmFly
2025-03-26 10:16:18 +00:00
parent 592f0e8e19
commit eb8fe91525
5 changed files with 228 additions and 143 deletions

View File

@@ -3,15 +3,20 @@ import {
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
} from '@affine/admin/components/ui/avatar'; } from '@affine/admin/components/ui/avatar';
import type { UserType } from '@affine/graphql';
import { FeatureType } from '@affine/graphql'; import { FeatureType } from '@affine/graphql';
import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc'; import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { MailIcon } from 'lucide-react'; import { MailIcon } from 'lucide-react';
import type { ReactNode } from 'react'; import {
type Dispatch,
type ReactNode,
type SetStateAction,
useMemo,
} from 'react';
import { Checkbox } from '../../../components/ui/checkbox'; import { Checkbox } from '../../../components/ui/checkbox';
import type { UserType } from '../schema';
import { DataTableColumnHeader } from './data-table-column-header'; import { DataTableColumnHeader } from './data-table-column-header';
import { DataTableRowActions } from './data-table-row-actions'; import { DataTableRowActions } from './data-table-row-actions';
@@ -47,8 +52,13 @@ const StatusItem = ({
)} )}
</div> </div>
); );
export const useColumns = ({
export const columns: ColumnDef<UserType>[] = [ setSelectedUserIds,
}: {
setSelectedUserIds: Dispatch<SetStateAction<Set<string>>>;
}) => {
const columns: ColumnDef<UserType>[] = useMemo(() => {
return [
{ {
id: 'select', id: 'select',
header: ({ table }) => ( header: ({ table }) => (
@@ -57,7 +67,34 @@ export const columns: ColumnDef<UserType>[] = [
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate') (table.getIsSomePageRowsSelected() && 'indeterminate')
} }
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={value => {
if (value) {
setSelectedUserIds(
prev =>
new Set([
...prev,
...table
.getFilteredRowModel()
.rows.map(row => row.original.id),
])
);
} else {
// remove selected users in the current page
setSelectedUserIds(
prev =>
new Set(
[...prev].filter(
id =>
!table
.getFilteredRowModel()
.rows.some(row => row.original.id === id)
)
)
);
}
table.toggleAllPageRowsSelected(!!value);
}}
aria-label="Select all" aria-label="Select all"
className="translate-y-[2px]" className="translate-y-[2px]"
/> />
@@ -65,7 +102,17 @@ export const columns: ColumnDef<UserType>[] = [
cell: ({ row }) => ( cell: ({ row }) => (
<Checkbox <Checkbox
checked={row.getIsSelected()} checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)} onCheckedChange={value => {
if (value) {
setSelectedUserIds(prev => new Set([...prev, row.original.id]));
} else {
setSelectedUserIds(
prev =>
new Set([...prev].filter(id => id !== row.original.id))
);
}
row.toggleSelected(!!value);
}}
aria-label="Select row" aria-label="Select row"
className="translate-y-[2px]" className="translate-y-[2px]"
/> />
@@ -76,7 +123,11 @@ export const columns: ColumnDef<UserType>[] = [
{ {
accessorKey: 'info', accessorKey: 'info',
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader className="text-xs" column={column} title="Name" /> <DataTableColumnHeader
className="text-xs"
column={column}
title="Name"
/>
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex gap-4 items-center max-w-[50vw] overflow-hidden"> <div className="flex gap-4 items-center max-w-[50vw] overflow-hidden">
@@ -180,6 +231,11 @@ export const columns: ColumnDef<UserType>[] = [
title="Actions" title="Actions"
/> />
), ),
cell: ({ row: { original: user } }) => <DataTableRowActions user={user} />, cell: ({ row: { original: user } }) => (
<DataTableRowActions user={user} />
),
}, },
]; ];
}, [setSelectedUserIds]);
return columns;
};

View File

@@ -1,7 +1,7 @@
import { Button } from '@affine/admin/components/ui/button'; import { Button } from '@affine/admin/components/ui/button';
import { Input } from '@affine/admin/components/ui/input'; import { Input } from '@affine/admin/components/ui/input';
import { useQuery } from '@affine/admin/use-query'; import { useQuery } from '@affine/admin/use-query';
import { getUserByEmailQuery, type UserType } from '@affine/graphql'; import { getUserByEmailQuery } from '@affine/graphql';
import { ExportIcon, ImportIcon, PlusIcon } from '@blocksuite/icons/rc'; import { ExportIcon, ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
import type { Table } from '@tanstack/react-table'; import type { Table } from '@tanstack/react-table';
import { import {
@@ -14,6 +14,7 @@ import {
} from 'react'; } from 'react';
import { useRightPanel } from '../../panel/context'; import { useRightPanel } from '../../panel/context';
import type { UserType } from '../schema';
import { DiscardChanges } from './discard-changes'; import { DiscardChanges } from './discard-changes';
import { ExportUsersDialog } from './export-users-dialog'; import { ExportUsersDialog } from './export-users-dialog';
import { ImportUsersDialog } from './import-users-dialog'; import { ImportUsersDialog } from './import-users-dialog';
@@ -22,6 +23,7 @@ import { CreateUserForm } from './user-form';
interface DataTableToolbarProps<TData> { interface DataTableToolbarProps<TData> {
data: TData[]; data: TData[];
setDataTable: (data: TData[]) => void; setDataTable: (data: TData[]) => void;
selectedUsers: UserType[];
table?: Table<TData>; table?: Table<TData>;
} }
@@ -58,6 +60,7 @@ function useDebouncedValue<T>(value: T, delay: number): T {
export function DataTableToolbar<TData>({ export function DataTableToolbar<TData>({
data, data,
selectedUsers,
setDataTable, setDataTable,
table, table,
}: DataTableToolbarProps<TData>) { }: DataTableToolbarProps<TData>) {
@@ -158,9 +161,7 @@ export function DataTableToolbar<TData>({
{table && ( {table && (
<ExportUsersDialog <ExportUsersDialog
users={table users={selectedUsers}
.getFilteredSelectedRowModel()
.rows.map(row => row.original as UserType)}
open={exportDialogOpen} open={exportDialogOpen}
onOpenChange={setExportDialogOpen} onOpenChange={setExportDialogOpen}
/> />

View File

@@ -18,6 +18,7 @@ import {
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'; import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import type { UserType } from '../schema';
import { DataTablePagination } from './data-table-pagination'; import { DataTablePagination } from './data-table-pagination';
import { DataTableToolbar } from './data-table-toolbar'; import { DataTableToolbar } from './data-table-toolbar';
import { useUserCount } from './use-user-management'; import { useUserCount } from './use-user-management';
@@ -26,6 +27,7 @@ interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
pagination: PaginationState; pagination: PaginationState;
selectedUsers: UserType[];
onPaginationChange: Dispatch< onPaginationChange: Dispatch<
SetStateAction<{ SetStateAction<{
pageIndex: number; pageIndex: number;
@@ -34,10 +36,11 @@ interface DataTableProps<TData, TValue> {
>; >;
} }
export function DataTable<TData, TValue>({ export function DataTable<TData extends { id: string }, TValue>({
columns, columns,
data, data,
pagination, pagination,
selectedUsers,
onPaginationChange, onPaginationChange,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const usersCount = useUserCount(); const usersCount = useUserCount();
@@ -50,6 +53,7 @@ export function DataTable<TData, TValue>({
data: tableData, data: tableData,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getRowId: row => row.id,
manualPagination: true, manualPagination: true,
rowCount: usersCount, rowCount: usersCount,
enableFilters: true, enableFilters: true,
@@ -70,7 +74,12 @@ export function DataTable<TData, TValue>({
return ( return (
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto"> <div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto">
<DataTableToolbar setDataTable={setTableData} data={data} table={table} /> <DataTableToolbar
setDataTable={setTableData}
data={data}
table={table}
selectedUsers={selectedUsers}
/>
<div className="rounded-md border h-full flex flex-col overflow-auto"> <div className="rounded-md border h-full flex flex-col overflow-auto">
<Table> <Table>
<TableHeader> <TableHeader>

View File

@@ -9,11 +9,11 @@ import {
} from '@affine/admin/components/ui/dialog'; } from '@affine/admin/components/ui/dialog';
import { Label } from '@affine/admin/components/ui/label'; import { Label } from '@affine/admin/components/ui/label';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import type { UserType } from '@affine/graphql';
import { CopyIcon } from '@blocksuite/icons/rc'; import { CopyIcon } from '@blocksuite/icons/rc';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { UserType } from '../schema';
import { type ExportField, useExportUsers } from './use-user-management'; import { type ExportField, useExportUsers } from './use-user-management';
interface ExportUsersDialogProps { interface ExportUsersDialogProps {

View File

@@ -1,20 +1,39 @@
import { useEffect, useMemo, useState } from 'react';
import { Header } from '../header'; import { Header } from '../header';
import { columns } from './components/columns'; import { useColumns } from './components/columns';
import { DataTable } from './components/data-table'; import { DataTable } from './components/data-table';
import type { UserType } from './schema';
import { useUserList } from './use-user-list'; import { useUserList } from './use-user-list';
export function AccountPage() { export function AccountPage() {
const { users, pagination, setPagination } = useUserList(); const { users, pagination, setPagination } = useUserList();
// Remember the user temporarily, because userList is paginated on the server side,can't get all users at once.
const [memoUsers, setMemoUsers] = useState<UserType[]>([]);
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(
new Set<string>()
);
const columns = useColumns({ setSelectedUserIds });
useEffect(() => {
setMemoUsers(prev => [...new Set([...prev, ...users])]);
}, [users]);
const selectedUsers = useMemo(() => {
return memoUsers.filter(user => selectedUserIds.has(user.id));
}, [selectedUserIds, memoUsers]);
return ( return (
<div className=" h-screen flex-1 flex-col flex"> <div className=" h-screen flex-1 flex-col flex">
<Header title="Accounts" /> <Header title="Accounts" />
<DataTable <DataTable
data={users} data={users}
// @ts-expect-error do not complains
columns={columns} columns={columns}
pagination={pagination} pagination={pagination}
onPaginationChange={setPagination} onPaginationChange={setPagination}
selectedUsers={selectedUsers}
/> />
</div> </div>
); );