From 72bacc5a2eca67827df6e5d6f123a61c65f33380 Mon Sep 17 00:00:00 2001 From: rique223 Date: Thu, 31 Aug 2023 16:51:01 -0300 Subject: [PATCH 01/10] feat: :sparkles: Filter users list by active users Implemented the necessary resources to filter the list of users and to return only those who are active, when the active tab is enabled. --- .../views/admin/users/AdminUsersPage.tsx | 12 ++-- .../admin/users/UsersTable/UsersTable.tsx | 71 +++++-------------- .../admin/users/hooks/useFilterActiveUsers.ts | 7 ++ .../views/admin/users/hooks/useListUsers.ts | 69 ++++++++++++++++++ 4 files changed, 100 insertions(+), 59 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts create mode 100644 apps/meteor/client/views/admin/users/hooks/useListUsers.ts diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 838a383d4fc9..c16e6b6a43b2 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -25,7 +25,7 @@ const UsersPage = (): ReactElement => { const canCreateUser = usePermission('create-user'); const canBulkCreateUser = usePermission('bulk-register-user'); - const [tab, setTab] = useState('all'); + const [tab, setTab] = useState<'all' | 'invited' | 'new' | 'active' | 'deactivated'>('all'); useEffect(() => { if (!context || !seatsCap) { @@ -68,20 +68,20 @@ const UsersPage = (): ReactElement => { setTab('all')}> {t('All')} - setTab('tab-invited')}> + setTab('invited')}> {t('Invited')} - setTab('tab-new')}> + setTab('new')}> {t('New_users')} - setTab('tab-active')}> + setTab('active')}> {t('Active')} - setTab('tab-deactivated')}> + setTab('deactivated')}> {t('Deactivated')} - + {context && ( diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 44175ec47de7..035f716721c5 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,8 +1,6 @@ import { Pagination } from '@rocket.chat/fuselage'; import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { useEndpoint, useRoute, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; +import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, MutableRefObject } from 'react'; import React, { useRef, useMemo, useState, useEffect } from 'react'; @@ -17,14 +15,17 @@ import { } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; +import { useFilterActiveUsers } from '../hooks/useFilterActiveUsers'; +import { useListUsers } from '../hooks/useListUsers'; import UsersTableRow from './UsersTableRow'; type UsersTableProps = { reload: MutableRefObject<() => void>; + tab: string; }; // TODO: Missing error state -const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => { +const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { const t = useTranslation(); const usersRoute = useRoute('admin-users'); const mediaQuery = useMediaQuery('(min-width: 1024px)'); @@ -34,55 +35,19 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => { const { sortBy, sortDirection, setSort } = useSort<'name' | 'username' | 'emails.address' | 'status'>('name'); const searchTerm = useDebouncedValue(text, 500); - const prevSearchTerm = useRef(''); - - const query = useDebouncedValue( - useMemo(() => { - if (searchTerm !== prevSearchTerm.current) { - setCurrent(0); - } - - return { - fields: JSON.stringify({ - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - avatarETag: 1, - active: 1, - }), - query: JSON.stringify({ - $or: [ - { 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } }, - { username: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, - { name: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, - ], - }), - sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, - count: itemsPerPage, - offset: searchTerm === prevSearchTerm.current ? current : 0, - }; - }, [searchTerm, sortBy, sortDirection, itemsPerPage, current, setCurrent]), - 500, + const prevSearchTerm = useRef(''); + + const { data, isLoading, isSuccess, error, refetch } = useListUsers( + searchTerm, + prevSearchTerm, + setCurrent, + sortBy, + sortDirection, + itemsPerPage, + current, ); - const getUsers = useEndpoint('GET', '/v1/users.list'); - - const dispatchToastMessage = useToastMessageDispatch(); - - const { data, isLoading, error, isSuccess, refetch } = useQuery( - ['users', query], - async () => { - const users = await getUsers(query); - return users; - }, - { - onError: (error) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }, - ); + const filteredUsers = [...useFilterActiveUsers(data?.users, tab)]; useEffect(() => { reload.current = refetch; @@ -151,12 +116,12 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => { {isLoading && } )} - {data?.users && data.count > 0 && isSuccess && ( + {isSuccess && !!data && !!filteredUsers && data.count > 0 && ( <> {headers} - {data?.users.map((user) => ( + {(tab === 'all' ? data.users : filteredUsers).map((user) => ( ))} diff --git a/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts new file mode 100644 index 000000000000..86c97245d745 --- /dev/null +++ b/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts @@ -0,0 +1,7 @@ +import type { UsersEndpoints } from '@rocket.chat/rest-typings'; + +export const useFilterActiveUsers = (users: ReturnType['users'] | undefined, tab: string) => { + if (!users || tab !== 'active') return []; + + return users.filter((currentUser) => currentUser.active); +}; diff --git a/apps/meteor/client/views/admin/users/hooks/useListUsers.ts b/apps/meteor/client/views/admin/users/hooks/useListUsers.ts new file mode 100644 index 000000000000..57b263f2f360 --- /dev/null +++ b/apps/meteor/client/views/admin/users/hooks/useListUsers.ts @@ -0,0 +1,69 @@ +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { MutableRefObject } from 'react'; +import { useMemo } from 'react'; + +import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import type { useSort } from '../../../../components/GenericTable/hooks/useSort'; + +export const useListUsers = ( + searchTerm: string, + prevSearchTerm: MutableRefObject, + setCurrent: ReturnType['setCurrent'], + sortBy: ReturnType['sortBy'], + sortDirection: ReturnType['sortDirection'], + itemsPerPage: ReturnType['itemsPerPage'], + current: ReturnType['current'], +) => { + const query = useDebouncedValue( + useMemo(() => { + if (searchTerm !== prevSearchTerm.current) { + setCurrent(0); + } + + return { + fields: JSON.stringify({ + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + avatarETag: 1, + active: 1, + }), + query: JSON.stringify({ + $or: [ + { 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + { username: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + { name: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + ], + }), + sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, + count: itemsPerPage, + offset: searchTerm === prevSearchTerm.current ? current : 0, + }; + }, [searchTerm, prevSearchTerm, sortBy, sortDirection, itemsPerPage, current, setCurrent]), + 500, + ); + + const getUsers = useEndpoint('GET', '/v1/users.list'); + + const dispatchToastMessage = useToastMessageDispatch(); + + const usersListQueryResult = useQuery( + ['users', query], + async () => { + const users = await getUsers(query); + return users; + }, + { + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + }, + ); + + return usersListQueryResult; +}; From 444c93016a7310d40ad3efd74eaaf69dca67f50e Mon Sep 17 00:00:00 2001 From: rique223 Date: Tue, 5 Sep 2023 19:25:58 -0300 Subject: [PATCH 02/10] feat: :sparkles: Implement new roles filter in the admin users page Implemented a new filter on the users' page that retrieves the roles list and creates a dropdown menu. When any of its options is selected, it will filter the users' list to display only those with the selected roles. Additionally, I made some minor adjustments to the MultiSelectCustom component and introduced some new hooks. --- .../admin/users/UsersTable/UsersTable.tsx | 61 ++++++++++++++++--- .../admin/users/hooks/useFilterActiveUsers.ts | 7 ++- .../admin/users/hooks/useFilterUsersByrole.ts | 12 ++++ .../MultiSelectCustom/MultiSelectCustom.tsx | 4 +- .../MultiSelectCustomAnchor.tsx | 9 ++- .../MultiSelectCustomList.tsx | 4 +- 6 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/hooks/useFilterUsersByrole.ts diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 035f716721c5..7fe4688c9735 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,6 +1,9 @@ import { Pagination } from '@rocket.chat/fuselage'; import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import type { OptionProp } from '@rocket.chat/ui-client'; +import { MultiSelectCustom } from '@rocket.chat/ui-client'; +import { useEndpoint, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement, MutableRefObject } from 'react'; import React, { useRef, useMemo, useState, useEffect } from 'react'; @@ -16,6 +19,7 @@ import { import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import { useFilterActiveUsers } from '../hooks/useFilterActiveUsers'; +import { useFilterUsersByRole } from '../hooks/useFilterUsersByrole'; import { useListUsers } from '../hooks/useListUsers'; import UsersTableRow from './UsersTableRow'; @@ -29,7 +33,34 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { const t = useTranslation(); const usersRoute = useRoute('admin-users'); const mediaQuery = useMediaQuery('(min-width: 1024px)'); + const getRoles = useEndpoint('GET', '/v1/roles.list'); + const { data: roleData, isSuccess: hasRoleData } = useQuery(['roles'], async () => getRoles()); + + const roleFilterStructure = useMemo( + () => + [ + { + id: 'filter_by_role', + text: 'Filter_by_role', + isGroupTitle: true, + }, + { + id: 'all', + text: 'All_roles', + isGroupTitle: false, + }, + ...((hasRoleData && roleData.roles) || []).map((currentRole) => ({ + id: currentRole._id, + text: currentRole.name, + isGroupTitle: false, + })), + ] as OptionProp[], + [hasRoleData, roleData?.roles], + ); + const [text, setText] = useState(''); + const [roleFilterOptions, setRoleFilterOptions] = useState([]); + const [roleFilterSelectedOptions, setRoleFilterSelectedOptions] = useState([]); const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); const { sortBy, sortDirection, setSort } = useSort<'name' | 'username' | 'emails.address' | 'status'>('name'); @@ -47,15 +78,22 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { current, ); - const filteredUsers = [...useFilterActiveUsers(data?.users, tab)]; + const useAllUsers = () => (tab === 'all' && isSuccess ? data?.users : []); + + const currentTabUsers = [...useAllUsers(), ...useFilterActiveUsers(data?.users, tab)]; + const filteredUsers = useFilterUsersByRole( + currentTabUsers, + roleFilterSelectedOptions.map((currentRole) => currentRole.id), + ); useEffect(() => { reload.current = refetch; - }, [reload, refetch]); + prevSearchTerm.current = searchTerm; + }, [reload, refetch, searchTerm]); useEffect(() => { - prevSearchTerm.current = searchTerm; - }, [searchTerm]); + setRoleFilterOptions(roleFilterStructure); + }, [roleFilterStructure]); const handleClick = useMutableCallback((id): void => usersRoute.push({ @@ -109,7 +147,16 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { return ( <> - setText(text)} /> + setText(text)}> + + {isLoading && ( {headers} @@ -121,7 +168,7 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { {headers} - {(tab === 'all' ? data.users : filteredUsers).map((user) => ( + {filteredUsers.map((user) => ( ))} diff --git a/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts index 86c97245d745..a8dd6acfa3b7 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilterActiveUsers.ts @@ -1,6 +1,9 @@ -import type { UsersEndpoints } from '@rocket.chat/rest-typings'; +import type { IUser } from '@rocket.chat/core-typings'; -export const useFilterActiveUsers = (users: ReturnType['users'] | undefined, tab: string) => { +export const useFilterActiveUsers = ( + users: Pick[] | undefined, + tab: string, +) => { if (!users || tab !== 'active') return []; return users.filter((currentUser) => currentUser.active); diff --git a/apps/meteor/client/views/admin/users/hooks/useFilterUsersByrole.ts b/apps/meteor/client/views/admin/users/hooks/useFilterUsersByrole.ts new file mode 100644 index 000000000000..44b6c6ae1217 --- /dev/null +++ b/apps/meteor/client/views/admin/users/hooks/useFilterUsersByrole.ts @@ -0,0 +1,12 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +export const useFilterUsersByRole = ( + users: Pick[], + selectedRoles: string[], +) => { + if (!users.length || !selectedRoles.length || selectedRoles.includes('all')) return users; + + return users.filter((currentUser) => { + return currentUser.roles.some((currentRole) => selectedRoles.includes(currentRole)); + }); +}; diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index 1420a62346d6..a81d93f8c2f5 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -53,12 +53,12 @@ export type OptionProp = TitleOptionProp | CheckboxOptionProp; */ type DropDownProps = { dropdownOptions: OptionProp[]; - defaultTitle: TranslationKey; + defaultTitle: string; selectedOptionsTitle: TranslationKey; selectedOptions: OptionProp[]; setSelectedOptions: Dispatch>; customSetSelected: Dispatch>; - searchBarText?: TranslationKey; + searchBarText?: string; }; export const MultiSelectCustom = ({ diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx index 9f6a15d38ba1..d06e6d881f92 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx @@ -1,15 +1,14 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, Icon, Palette } from '@rocket.chat/fuselage'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { forwardRef } from 'react'; type MultiSelectCustomAnchorProps = { onClick?: (value: boolean) => void; collapsed: boolean; - defaultTitle: TranslationKey; - selectedOptionsTitle: TranslationKey; + defaultTitle: string; + selectedOptionsTitle: string; selectedOptionsCount: number; maxCount: number; } & ComponentProps; @@ -59,7 +58,7 @@ const MultiSelectCustomAnchor = forwardRef - {isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)} + {isDirty ? `${t(selectedOptionsTitle as TranslationKey)} (${selectedOptionsCount})` : t(defaultTitle as TranslationKey)} ); diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index 7e6bfdb9fee1..85ee17f1751d 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -14,7 +14,7 @@ const MultiSelectCustomList = ({ }: { options: OptionProp[]; onSelected: (item: OptionProp, e?: FormEvent) => void; - searchBarText?: TranslationKey; + searchBarText?: string; }) => { const t = useTranslation(); @@ -33,7 +33,7 @@ const MultiSelectCustomList = ({ - + {context && ( diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 48ede4678d66..55fd15a195bd 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -22,10 +22,11 @@ import UsersTableRow from './UsersTableRow'; type UsersTableProps = { reload: MutableRefObject<() => void>; tab: string; + onReload: () => void; }; // TODO: Missing error state -const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { +const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement | null => { const t = useTranslation(); const router = useRouter(); const mediaQuery = useMediaQuery('(min-width: 1024px)'); @@ -57,15 +58,17 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { prevSearchTerm.current = searchTerm; }, [reload, refetch, searchTerm]); - const handleClick = useMutableCallback((id): void => + const handleClick = useMutableCallback((id, e: React.MouseEvent | React.KeyboardEvent): void => { + e.stopPropagation(); + router.navigate({ name: 'admin-users', params: { context: 'info', id, }, - }), - ); + }); + }); const headers = useMemo( () => [ @@ -102,6 +105,7 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { {t('Registration_status')} , + , ], [mediaQuery, setSort, sortBy, sortDirection, t], ); @@ -121,7 +125,14 @@ const UsersTable = ({ reload, tab }: UsersTableProps): ReactElement | null => { {headers} {filteredUsers.map((user) => ( - + ))} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index 64e3f4858ffa..495e8112ab5e 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,7 +1,9 @@ -import { UserStatus as Status } from '@rocket.chat/core-typings'; +import { UserStatus as Status, isUserFederated } from '@rocket.chat/core-typings'; import type { IRole, IUser } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, Menu, Option } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; @@ -9,14 +11,21 @@ import { Roles } from '../../../../../app/models/client'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import { UserStatus } from '../../../../components/UserStatus'; import UserAvatar from '../../../../components/avatar/UserAvatar'; +import { useChangeAdminStatusAction } from '../hooks/useChangeAdminStatusAction'; +import { useChangeUserStatusAction } from '../hooks/useChangeUserStatusAction'; +import { useDeleteUserAction } from '../hooks/useDeleteUserAction'; +import { useResetE2EEKeyAction } from '../hooks/useResetE2EEKeyAction'; +import { useResetTOTPAction } from '../hooks/useResetTOTPAction'; type UsersTableRowProps = { user: Pick; - onClick: (id: IUser['_id']) => void; + onClick: (id: IUser['_id'], e: React.MouseEvent | React.KeyboardEvent) => void; mediaQuery: boolean; + refetchUsers: ReturnType['refetch']; + onReload: () => void; }; -const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): ReactElement => { +const UsersTableRow = ({ user, onClick, mediaQuery, refetchUsers, onReload }: UsersTableRowProps): ReactElement => { const t = useTranslation(); const { _id, emails, username, name, status, roles, active, avatarETag } = user; const registrationStatusText = active ? t('Active') : t('Deactivated'); @@ -26,10 +35,54 @@ const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): React .filter((roleName): roleName is string => !!roleName) .join(', '); + const userId = user._id; + const isAdmin = user.roles?.includes('admin'); + const isActive = user.active; + const isFederatedUser = isUserFederated(user); + + const onChange = useMutableCallback(() => { + onReload(); + refetchUsers(); + }); + + const changeAdminStatusAction = useChangeAdminStatusAction(userId, isAdmin, onChange); + const changeUserStatusAction = useChangeUserStatusAction(userId, isActive, onChange); + const deleteUserAction = useDeleteUserAction(userId, onChange, onReload); + const resetTOTPAction = useResetTOTPAction(userId); + const resetE2EKeyAction = useResetE2EEKeyAction(userId); + + const menuOptions = { + ...(changeAdminStatusAction && + !isFederatedUser && { + makeAdmin: { + label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon }, + action: changeAdminStatusAction.action, + }, + }), + ...(resetE2EKeyAction && + !isFederatedUser && { + resetE2EKey: { label: { label: resetE2EKeyAction.label, icon: resetE2EKeyAction.icon }, action: resetE2EKeyAction.action }, + }), + ...(resetTOTPAction && + !isFederatedUser && { + resetTOTP: { label: { label: resetTOTPAction.label, icon: resetTOTPAction.icon }, action: resetTOTPAction.action }, + }), + ...(deleteUserAction && { + delete: { label: { label: deleteUserAction.label, icon: deleteUserAction.icon }, action: deleteUserAction.action }, + }), + ...(changeUserStatusAction && + !isFederatedUser && { + changeActiveStatus: { + label: { label: changeUserStatusAction.label, icon: changeUserStatusAction.icon }, + action: changeUserStatusAction.action, + }, + }), + }; + return ( onClick(_id)} - onClick={(): void => onClick(_id)} + onKeyDown={(e): void => onClick(_id, e)} + onClick={(e): void => onClick(_id, e)} tabIndex={0} role='link' action @@ -69,6 +122,16 @@ const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): React {registrationStatusText} + +