From eb8fe915259804020fecc2747ae4ad381ca17fa1 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Wed, 26 Mar 2025 10:16:18 +0000 Subject: [PATCH] fix(admin): unable to select all users in current page (#11155) close AF-2380 --- .../modules/accounts/components/columns.tsx | 324 ++++++++++-------- .../components/data-table-toolbar.tsx | 9 +- .../accounts/components/data-table.tsx | 13 +- .../components/export-users-dialog.tsx | 2 +- .../admin/src/modules/accounts/index.tsx | 23 +- 5 files changed, 228 insertions(+), 143 deletions(-) diff --git a/packages/frontend/admin/src/modules/accounts/components/columns.tsx b/packages/frontend/admin/src/modules/accounts/components/columns.tsx index 67e5937154..ae65be0636 100644 --- a/packages/frontend/admin/src/modules/accounts/components/columns.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/columns.tsx @@ -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,139 +52,190 @@ const StatusItem = ({ )} ); +export const useColumns = ({ + setSelectedUserIds, +}: { + setSelectedUserIds: Dispatch>>; +}) => { + const columns: ColumnDef[] = useMemo(() => { + return [ + { + id: 'select', + header: ({ table }) => ( + { + 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) + ) + ) + ); + } -export const columns: ColumnDef[] = [ - { - id: 'select', - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: 'info', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( -
- - - - - - -
-
- {row.original.name} - {row.original.features.includes(FeatureType.Admin) && ( - - Admin - - )} - {row.original.disabled && ( - - Disabled - - )} -
-
- {row.original.email} -
-
-
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: 'property', - header: ({ column }) => ( - - ), - cell: ({ row: { original: user } }) => ( -
-
-
{user.id}
-
- + aria-label="Select all" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + { + if (value) { + setSelectedUserIds(prev => new Set([...prev, row.original.id])); + } else { + setSelectedUserIds( + prev => + new Set([...prev].filter(id => id !== row.original.id)) + ); } - IconFalse={} - textTrue="Password Set" - textFalse="No Password" - /> - - } - IconFalse={} - textTrue="Email Verified" - textFalse="Email Not Verified" - /> + row.toggleSelected(!!value); + }} + aria-label="Select row" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'info', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ + + + + + +
+
+ {row.original.name} + {row.original.features.includes(FeatureType.Admin) && ( + + Admin + + )} + {row.original.disabled && ( + + Disabled + + )} +
+
+ {row.original.email} +
+
-
-
- ), - }, - { - id: 'actions', - header: ({ column }) => ( - - ), - cell: ({ row: { original: user } }) => , - }, -]; + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'property', + header: ({ column }) => ( + + ), + cell: ({ row: { original: user } }) => ( +
+
+
{user.id}
+
+ + } + IconFalse={} + textTrue="Password Set" + textFalse="No Password" + /> + + } + IconFalse={} + textTrue="Email Verified" + textFalse="Email Not Verified" + /> +
+
+
+ ), + }, + { + id: 'actions', + header: ({ column }) => ( + + ), + cell: ({ row: { original: user } }) => ( + + ), + }, + ]; + }, [setSelectedUserIds]); + return columns; +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx index c55910256c..82a9b2d0aa 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx @@ -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 { data: TData[]; setDataTable: (data: TData[]) => void; + selectedUsers: UserType[]; table?: Table; } @@ -58,6 +60,7 @@ function useDebouncedValue(value: T, delay: number): T { export function DataTableToolbar({ data, + selectedUsers, setDataTable, table, }: DataTableToolbarProps) { @@ -158,9 +161,7 @@ export function DataTableToolbar({ {table && ( row.original as UserType)} + users={selectedUsers} open={exportDialogOpen} onOpenChange={setExportDialogOpen} /> diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table.tsx index 0b924b2b75..a323eff7ce 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table.tsx @@ -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 { columns: ColumnDef[]; data: TData[]; pagination: PaginationState; + selectedUsers: UserType[]; onPaginationChange: Dispatch< SetStateAction<{ pageIndex: number; @@ -34,10 +36,11 @@ interface DataTableProps { >; } -export function DataTable({ +export function DataTable({ columns, data, pagination, + selectedUsers, onPaginationChange, }: DataTableProps) { const usersCount = useUserCount(); @@ -50,6 +53,7 @@ export function DataTable({ data: tableData, columns, getCoreRowModel: getCoreRowModel(), + getRowId: row => row.id, manualPagination: true, rowCount: usersCount, enableFilters: true, @@ -70,7 +74,12 @@ export function DataTable({ return (
- +
diff --git a/packages/frontend/admin/src/modules/accounts/components/export-users-dialog.tsx b/packages/frontend/admin/src/modules/accounts/components/export-users-dialog.tsx index 5db42721e9..4b3c6e3fea 100644 --- a/packages/frontend/admin/src/modules/accounts/components/export-users-dialog.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/export-users-dialog.tsx @@ -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 { diff --git a/packages/frontend/admin/src/modules/accounts/index.tsx b/packages/frontend/admin/src/modules/accounts/index.tsx index f269daa733..0bcef51233 100644 --- a/packages/frontend/admin/src/modules/accounts/index.tsx +++ b/packages/frontend/admin/src/modules/accounts/index.tsx @@ -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([]); + + const [selectedUserIds, setSelectedUserIds] = useState>( + new Set() + ); + 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 (
);