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,
AvatarImage,
} from '@affine/admin/components/ui/avatar';
import type { UserType } from '@affine/graphql';
import { FeatureType } from '@affine/graphql';
import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc';
import type { ColumnDef } from '@tanstack/react-table';
import { cssVarV2 } from '@toeverything/theme/v2';
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 type { UserType } from '../schema';
import { DataTableColumnHeader } from './data-table-column-header';
import { DataTableRowActions } from './data-table-row-actions';
@@ -47,8 +52,13 @@ const StatusItem = ({
)}
</div>
);
export const columns: ColumnDef<UserType>[] = [
export const useColumns = ({
setSelectedUserIds,
}: {
setSelectedUserIds: Dispatch<SetStateAction<Set<string>>>;
}) => {
const columns: ColumnDef<UserType>[] = useMemo(() => {
return [
{
id: 'select',
header: ({ table }) => (
@@ -57,7 +67,34 @@ export const columns: ColumnDef<UserType>[] = [
table.getIsAllPageRowsSelected() ||
(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"
className="translate-y-[2px]"
/>
@@ -65,7 +102,17 @@ export const columns: ColumnDef<UserType>[] = [
cell: ({ row }) => (
<Checkbox
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"
className="translate-y-[2px]"
/>
@@ -76,7 +123,11 @@ export const columns: ColumnDef<UserType>[] = [
{
accessorKey: 'info',
header: ({ column }) => (
<DataTableColumnHeader className="text-xs" column={column} title="Name" />
<DataTableColumnHeader
className="text-xs"
column={column}
title="Name"
/>
),
cell: ({ row }) => (
<div className="flex gap-4 items-center max-w-[50vw] overflow-hidden">
@@ -180,6 +231,11 @@ export const columns: ColumnDef<UserType>[] = [
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 { Input } from '@affine/admin/components/ui/input';
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 type { Table } from '@tanstack/react-table';
import {
@@ -14,6 +14,7 @@ import {
} from 'react';
import { useRightPanel } from '../../panel/context';
import type { UserType } from '../schema';
import { DiscardChanges } from './discard-changes';
import { ExportUsersDialog } from './export-users-dialog';
import { ImportUsersDialog } from './import-users-dialog';
@@ -22,6 +23,7 @@ import { CreateUserForm } from './user-form';
interface DataTableToolbarProps<TData> {
data: TData[];
setDataTable: (data: TData[]) => void;
selectedUsers: UserType[];
table?: Table<TData>;
}
@@ -58,6 +60,7 @@ function useDebouncedValue<T>(value: T, delay: number): T {
export function DataTableToolbar<TData>({
data,
selectedUsers,
setDataTable,
table,
}: DataTableToolbarProps<TData>) {
@@ -158,9 +161,7 @@ export function DataTableToolbar<TData>({
{table && (
<ExportUsersDialog
users={table
.getFilteredSelectedRowModel()
.rows.map(row => row.original as UserType)}
users={selectedUsers}
open={exportDialogOpen}
onOpenChange={setExportDialogOpen}
/>

View File

@@ -18,6 +18,7 @@ import {
} from '@tanstack/react-table';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import type { UserType } from '../schema';
import { DataTablePagination } from './data-table-pagination';
import { DataTableToolbar } from './data-table-toolbar';
import { useUserCount } from './use-user-management';
@@ -26,6 +27,7 @@ interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pagination: PaginationState;
selectedUsers: UserType[];
onPaginationChange: Dispatch<
SetStateAction<{
pageIndex: number;
@@ -34,10 +36,11 @@ interface DataTableProps<TData, TValue> {
>;
}
export function DataTable<TData, TValue>({
export function DataTable<TData extends { id: string }, TValue>({
columns,
data,
pagination,
selectedUsers,
onPaginationChange,
}: DataTableProps<TData, TValue>) {
const usersCount = useUserCount();
@@ -50,6 +53,7 @@ export function DataTable<TData, TValue>({
data: tableData,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: row => row.id,
manualPagination: true,
rowCount: usersCount,
enableFilters: true,
@@ -70,7 +74,12 @@ export function DataTable<TData, TValue>({
return (
<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">
<Table>
<TableHeader>

View File

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

View File

@@ -1,20 +1,39 @@
import { useEffect, useMemo, useState } from 'react';
import { Header } from '../header';
import { columns } from './components/columns';
import { useColumns } from './components/columns';
import { DataTable } from './components/data-table';
import type { UserType } from './schema';
import { useUserList } from './use-user-list';
export function AccountPage() {
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 (
<div className=" h-screen flex-1 flex-col flex">
<Header title="Accounts" />
<DataTable
data={users}
// @ts-expect-error do not complains
columns={columns}
pagination={pagination}
onPaginationChange={setPagination}
selectedUsers={selectedUsers}
/>
</div>
);