From 5f95c4ec6b5e6417ea46db3ef33db89930fe0a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Thu, 20 Jun 2024 23:54:29 -0300 Subject: [PATCH] feat: New users page pending tab (#31987) Co-authored-by: Tasso --- .changeset/metal-candles-float.md | 13 ++ apps/meteor/app/api/server/lib/users.ts | 1 + .../client/components/Page/PageHeader.tsx | 12 +- .../client/components/UserInfo/UserInfo.tsx | 49 +++--- .../hooks/useLicenseLimitsByBehavior.ts | 63 ++++++++ .../SubscriptionCalloutLimits.tsx | 68 ++------ .../admin/users/AdminUserInfoWithData.tsx | 5 +- .../views/admin/users/AdminUsersPage.tsx | 58 ++++++- .../UserPageHeaderContentWithSeatsCap.tsx | 16 +- .../admin/users/UsersTable/UsersTable.tsx | 57 +++---- .../admin/users/UsersTable/UsersTableRow.tsx | 146 ++++++++++++++---- .../admin/users/hooks/usePendingUsersCount.ts | 25 +++ .../hooks/useSendWelcomeEmailMutation.ts | 29 ++++ packages/core-typings/src/IUser.ts | 3 +- packages/i18n/src/locales/en.i18n.json | 7 + packages/rest-typings/src/v1/Ajv.ts | 7 + packages/rest-typings/src/v1/users.ts | 2 +- .../users/UsersSendWelcomeEmailParamsPOST.ts | 2 +- 18 files changed, 408 insertions(+), 155 deletions(-) create mode 100644 .changeset/metal-candles-float.md create mode 100644 apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts create mode 100644 apps/meteor/client/views/admin/users/hooks/usePendingUsersCount.ts create mode 100644 apps/meteor/client/views/admin/users/hooks/useSendWelcomeEmailMutation.ts diff --git a/.changeset/metal-candles-float.md b/.changeset/metal-candles-float.md new file mode 100644 index 000000000000..256e6c3ac7d2 --- /dev/null +++ b/.changeset/metal-candles-float.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": patch +"@rocket.chat/i18n": patch +--- + +Implemented a new "Pending Users" tab on the users page to list users who have not yet been activated and/or have not logged in for the first time. +Additionally, added a "Pending Action" column to aid administrators in identifying necessary actions for each user. Incorporated a "Reason for Joining" field +into the user info contextual bar, along with a callout for exceeding the seats cap in the users page header. Finally, introduced a new logic to disable user creation buttons upon surpassing the seats cap. + + + + diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index f80d662771df..a912043fc656 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -154,6 +154,7 @@ export async function findPaginatedUsersByStatus({ lastLogin: 1, type: 1, reason: 1, + federated: 1, }; const actualSort: Record = sort || { username: 1 }; diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 197592d3e98c..4549c69dccec 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -24,12 +24,20 @@ const PageHeader: FC = ({ children = undefined, title, onClickB - + {isMobile && ( diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 025889edc7cd..a4656373b488 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -39,6 +39,7 @@ type UserInfoProps = UserInfoDataProps & { verified?: boolean; actions: ReactElement; roles: ReactElement[]; + reason?: string; }; const UserInfo = ({ @@ -59,6 +60,7 @@ const UserInfo = ({ customFields, canViewAllInfo, actions, + reason, ...props }: UserInfoProps): ReactElement => { const t = useTranslation(); @@ -79,53 +81,47 @@ const UserInfo = ({ {userDisplayName && } + {statusText && ( - + )} - {roles.length !== 0 && ( + {reason && ( - {t('Roles')} - {roles} + {t('Reason_for_joining')} + {reason} )} - {Number.isInteger(utcOffset) && ( - - {t('Local_Time')} - {utcOffset && } - - )} - - {username && username !== name && ( + {nickname && ( - {t('Username')} - {username} + {t('Nickname')} + {nickname} )} - {canViewAllInfo && ( + {roles.length !== 0 && ( - {t('Last_login')} - {lastLogin ? timeAgo(lastLogin) : t('Never')} + {t('Roles')} + {roles} )} - {name && ( + {username && username !== name && ( - {t('Full_Name')} - {name} + {t('Username')} + {username} )} - {nickname && ( + {Number.isInteger(utcOffset) && ( - {t('Nickname')} - {nickname} + {t('Local_Time')} + {utcOffset && } )} @@ -138,6 +134,13 @@ const UserInfo = ({ )} + {Number.isInteger(utcOffset) && canViewAllInfo && ( + + {t('Last_login')} + {lastLogin ? timeAgo(lastLogin) : t('Never')} + + )} + {phone && ( {t('Phone')} diff --git a/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts new file mode 100644 index 000000000000..7ccea2dd4bd4 --- /dev/null +++ b/apps/meteor/client/hooks/useLicenseLimitsByBehavior.ts @@ -0,0 +1,63 @@ +import type { LicenseBehavior, LicenseLimitKind } from '@rocket.chat/core-typings'; +import { validateWarnLimit } from '@rocket.chat/license/src/validation/validateLimit'; + +import { useLicense } from './useLicense'; + +type LicenseLimitsByBehavior = Record; + +export const useLicenseLimitsByBehavior = () => { + const result = useLicense({ loadValues: true }); + + if (result.isLoading || result.isError) { + return null; + } + + const { license, limits } = result.data; + + if (!license || !limits) { + return null; + } + + const keyLimits = Object.keys(limits) as Array; + + // Get the rule with the highest limit that applies to this key + const rules = keyLimits + .map((key) => { + const rule = license.limits[key] + ?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior)) + .reduce<{ max: number; behavior: LicenseBehavior } | null>( + (maxLimit, currentLimit) => (!maxLimit || currentLimit.max > maxLimit.max ? currentLimit : maxLimit), + null, + ); + + if (!rule) { + return undefined; + } + + if (rule.max === 0) { + return undefined; + } + + if (rule.max === -1) { + return undefined; + } + + return [key, rule.behavior]; + }) + .filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>; + + if (!rules.length) { + return null; + } + + // Group by behavior + return rules.reduce((acc, [key, behavior]) => { + if (!acc[behavior]) { + acc[behavior] = []; + } + + acc[behavior].push(key); + + return acc; + }, {} as LicenseLimitsByBehavior); +}; diff --git a/apps/meteor/client/views/admin/subscription/SubscriptionCalloutLimits.tsx b/apps/meteor/client/views/admin/subscription/SubscriptionCalloutLimits.tsx index 2adc3d306034..c5de629e5582 100644 --- a/apps/meteor/client/views/admin/subscription/SubscriptionCalloutLimits.tsx +++ b/apps/meteor/client/views/admin/subscription/SubscriptionCalloutLimits.tsx @@ -1,80 +1,33 @@ -import type { LicenseBehavior } from '@rocket.chat/core-typings'; +import type { LicenseInfo } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; -import { validateWarnLimit } from '@rocket.chat/license/src/validation/validateLimit'; import { ExternalLink } from '@rocket.chat/ui-client'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useLicense } from '../../../hooks/useLicense'; +import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBehavior'; import { useCheckoutUrl } from './hooks/useCheckoutUrl'; export const SubscriptionCalloutLimits = () => { const manageSubscriptionUrl = useCheckoutUrl(); const { t } = useTranslation(); - const result = useLicense({ loadValues: true }); - if (result.isLoading || result.isError) { - return null; - } - - const { license, limits } = result.data; + const licenseLimits = useLicenseLimitsByBehavior(); - if (!license || !limits) { + if (!licenseLimits) { return null; } - const keyLimits = Object.keys(limits) as Array; - - // Get the rule with the highest limit that applies to this key - - const rules = keyLimits - .map((key) => { - const rule = license.limits[key] - ?.filter((limit) => validateWarnLimit(limit.max, limits[key].value ?? 0, limit.behavior)) - .sort((a, b) => b.max - a.max)[0]; + const { prevent_action, disable_modules, invalidate_license, start_fair_policy } = licenseLimits; - if (!rule) { - return undefined; - } - - if (rule.max === 0) { - return undefined; - } - - if (rule.max === -1) { - return undefined; - } - - return [key, rule.behavior]; - }) - .filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>; - - if (!rules.length) { - return null; - } - - // Group by behavior - const groupedRules = rules.reduce((acc, [key, behavior]) => { - if (!acc[behavior]) { - acc[behavior] = []; - } - - acc[behavior].push(key); - - return acc; - }, {} as Record); - - const { prevent_action, disable_modules, invalidate_license, start_fair_policy } = groupedRules; - - const map = (key: keyof typeof limits) => t(`subscription.callout.${key}`); + const toTranslationKey = (key: keyof LicenseInfo['limits']) => t(`subscription.callout.${key}`); return ( <> {start_fair_policy && ( - Your workspace reached the <>{{ val: start_fair_policy.map(map) }} limit. + Your workspace reached the <>{{ val: start_fair_policy.map(toTranslationKey) }} limit. { )} + {prevent_action && ( - Your workspace exceeded the <>{{ val: prevent_action.map(map) }} license limit. + Your workspace exceeded the <>{{ val: prevent_action.map(toTranslationKey) }} license limit. { {disable_modules && ( - Your workspace exceeded the <>{{ val: disable_modules.map(map) }} license limit. + Your workspace exceeded the <>{{ val: disable_modules.map(toTranslationKey) }} license limit. { {invalidate_license && ( - Your workspace exceeded the <>{{ val: invalidate_license.map(map) }} license limit. + Your workspace exceeded the <>{{ val: invalidate_license.map(toTranslationKey) }} license limit. , statusText, nickname, + reason, }; }, [approveManuallyUsers, data, getRoles]); @@ -119,7 +120,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R isAdmin={data?.user.roles?.includes('admin')} userId={data?.user._id} username={user.username} - isFederatedUser={isUserFederated(data?.user as unknown as IUser)} + isFederatedUser={!!data.user.federated} onChange={onChange} onReload={onReload} /> diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 1ec27c28a3bb..d49f3bdafa7f 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -1,9 +1,11 @@ -import type { IAdminUserTabs } from '@rocket.chat/core-typings'; -import { Button, ButtonGroup, ContextualbarIcon, Tabs, TabsItem } from '@rocket.chat/fuselage'; +import type { IAdminUserTabs, LicenseInfo } from '@rocket.chat/core-typings'; +import { Button, ButtonGroup, Callout, ContextualbarIcon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { ExternalLink } from '@rocket.chat/ui-client'; import { usePermission, useRouteParameter, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Trans } from 'react-i18next'; import { Contextualbar, @@ -15,7 +17,9 @@ import { import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; import { Page, PageHeader, PageContent } from '../../../components/Page'; +import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBehavior'; import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction'; +import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl'; import AdminInviteUsers from './AdminInviteUsers'; import AdminUserForm from './AdminUserForm'; import AdminUserFormWithData from './AdminUserFormWithData'; @@ -24,19 +28,24 @@ import AdminUserUpgrade from './AdminUserUpgrade'; import UserPageHeaderContentWithSeatsCap from './UserPageHeaderContentWithSeatsCap'; import UsersTable from './UsersTable'; import useFilteredUsers from './hooks/useFilteredUsers'; +import usePendingUsersCount from './hooks/usePendingUsersCount'; import { useSeatsCap } from './useSeatsCap'; export type UsersFilters = { text: string; }; -export type UsersTableSortingOptions = 'name' | 'username' | 'emails.address' | 'status'; +export type UsersTableSortingOptions = 'name' | 'username' | 'emails.address' | 'status' | 'active'; const AdminUsersPage = (): ReactElement => { const t = useTranslation(); const seatsCap = useSeatsCap(); + const isSeatsCapExceeded = useShouldPreventAction('activeUsers'); + const { prevent_action: preventAction } = useLicenseLimitsByBehavior() ?? {}; + const manageSubscriptionUrl = useCheckoutUrl(); + const router = useRouter(); const context = useRouteParameter('context'); const id = useRouteParameter('id'); @@ -47,7 +56,7 @@ const AdminUsersPage = (): ReactElement => { const isCreateUserDisabled = useShouldPreventAction('activeUsers'); const paginationData = usePagination(); - const sortData = useSort<'name' | 'username' | 'emails.address' | 'status'>('name'); + const sortData = useSort('name'); const [tab, setTab] = useState('all'); const [userFilters, setUserFilters] = useState({ text: '' }); @@ -63,11 +72,19 @@ const AdminUsersPage = (): ReactElement => { tab, }); + const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users); + const handleReload = (): void => { seatsCap?.reload(); filteredUsersQueryResult?.refetch(); }; + const handleTabChangeAndSort = (tab: IAdminUserTabs) => { + setTab(tab); + + sortData.setSort(tab === 'pending' ? 'active' : 'name', 'asc'); + }; + useEffect(() => { prevSearchTerm.current = searchTerm; }, [searchTerm]); @@ -77,31 +94,55 @@ const AdminUsersPage = (): ReactElement => { [context, isCreateUserDisabled], ); + const toTranslationKey = (key: keyof LicenseInfo['limits']) => t(`subscription.callout.${key}`); + return ( {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( - + ) : ( {canBulkCreateUser && ( - )} {canCreateUser && ( - )} )} + {preventAction?.includes('activeUsers') && ( + + + Your workspace exceeded the <>{{ val: preventAction.map(toTranslationKey) }} license limit. + + Manage your subscription + + to increase limits. + + + )} - setTab('all')}> + handleTabChangeAndSort('all')}> {t('All')} + handleTabChangeAndSort('pending')} display='flex' flexDirection='row'> + {`${t('Pending')} `} + {pendingUsersCount.isLoading && } + {pendingUsersCount.isSuccess && `(${pendingUsersCount.data})`} + { paginationData={paginationData} sortData={sortData} tab={tab} + isSeatsCapExceeded={isSeatsCapExceeded} /> diff --git a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx b/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx index c4642d8baefb..3000b4f51a5a 100644 --- a/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx +++ b/apps/meteor/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx @@ -4,18 +4,20 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useExternalLink } from '../../../hooks/useExternalLink'; -import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction'; import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl'; import SeatsCapUsage from './SeatsCapUsage'; type UserPageHeaderContentWithSeatsCapProps = { activeUsers: number; maxActiveUsers: number; + isSeatsCapExceeded: boolean; }; -const UserPageHeaderContentWithSeatsCap = ({ activeUsers, maxActiveUsers }: UserPageHeaderContentWithSeatsCapProps): ReactElement => { - const isCreateUserDisabled = useShouldPreventAction('activeUsers'); - +const UserPageHeaderContentWithSeatsCap = ({ + isSeatsCapExceeded, + activeUsers, + maxActiveUsers, +}: UserPageHeaderContentWithSeatsCapProps): ReactElement => { const t = useTranslation(); const router = useRouter(); @@ -36,13 +38,13 @@ const UserPageHeaderContentWithSeatsCap = ({ activeUsers, maxActiveUsers }: User - - - {isCreateUserDisabled && ( + {isSeatsCapExceeded && ( diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 93762f973c62..fa35df715fc5 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,6 +1,6 @@ import type { IAdminUserTabs, Serialized } from '@rocket.chat/core-typings'; import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; -import { useMediaQuery, useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; @@ -28,9 +28,9 @@ type UsersTableProps = { filteredUsersQueryResult: UseQueryResult[] }>>; paginationData: ReturnType; sortData: ReturnType>; + isSeatsCapExceeded: boolean; }; -// TODO: Missing error state const UsersTable = ({ filteredUsersQueryResult, setUserFilters, @@ -38,10 +38,14 @@ const UsersTable = ({ onReload, paginationData, sortData, + isSeatsCapExceeded, }: UsersTableProps): ReactElement | null => { const t = useTranslation(); const router = useRouter(); - const mediaQuery = useMediaQuery('(min-width: 1024px)'); + const breakpoints = useBreakpoints(); + + const isMobile = !breakpoints.includes('xl'); + const isLaptop = !breakpoints.includes('xxl'); const { data, isLoading, isError, isSuccess } = filteredUsersQueryResult; @@ -76,24 +80,14 @@ const UsersTable = ({ const headers = useMemo( () => [ - + {t('Name')} , - mediaQuery && ( - - {t('Username')} - - ), - mediaQuery && ( + + {t('Username')} + , + !isLaptop && ( ), - mediaQuery && ( - - {t('Roles')} + !isLaptop && {t('Roles')}, + tab === 'all' && !isMobile && ( + + {t('Registration_status')} ), - tab === 'all' && ( - - {t('Registration_status')} + tab === 'pending' && !isMobile && ( + + {t('Pending_action')} ), + , ], - [mediaQuery, setSort, sortBy, sortDirection, t, tab], + [isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab], ); const handleSearchTextChange = useCallback( @@ -153,7 +149,16 @@ const UsersTable = ({ {headers} {data.users.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 86a69dda1dbb..727edbc7a188 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,6 +1,6 @@ import { UserStatus as Status } from '@rocket.chat/core-typings'; import type { IAdminUserTabs, IRole, IUser, Serialized } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, Button, Menu, Option } from '@rocket.chat/fuselage'; import type { DefaultUserInfo } from '@rocket.chat/rest-typings'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -10,16 +10,26 @@ import React, { useMemo } from 'react'; import { Roles } from '../../../../../app/models/client'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import { UserStatus } from '../../../../components/UserStatus'; +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'; +import { useSendWelcomeEmailMutation } from '../hooks/useSendWelcomeEmailMutation'; type UsersTableRowProps = { user: Serialized; onClick: (id: IUser['_id'], e: React.MouseEvent | React.KeyboardEvent) => void; - mediaQuery: boolean; + isMobile: boolean; + isLaptop: boolean; + onReload: () => void; tab: IAdminUserTabs; + isSeatsCapExceeded: boolean; }; -const UsersTableRow = ({ user, onClick, mediaQuery, tab }: UsersTableRowProps): ReactElement => { +const UsersTableRow = ({ user, onClick, onReload, isMobile, isLaptop, tab, isSeatsCapExceeded }: UsersTableRowProps): ReactElement => { const t = useTranslation(); + const { _id, emails, username, name, roles, status, active, avatarETag, lastLogin, type } = user; const registrationStatusText = useMemo(() => { const usersExcludedFromPending = ['bot', 'app']; @@ -42,6 +52,49 @@ const UsersTableRow = ({ user, onClick, mediaQuery, tab }: UsersTableRowProps): .filter((roleName): roleName is string => !!roleName) .join(', '); + const userId = user._id; + const isAdmin = user.roles?.includes('admin'); + const isActive = user.active; + const isFederatedUser = !!user.federated; + + const changeAdminStatusAction = useChangeAdminStatusAction(userId, isAdmin, onReload); + const changeUserStatusAction = useChangeUserStatusAction(userId, isActive, onReload); + const deleteUserAction = useDeleteUserAction(userId, onReload, onReload); + const resetTOTPAction = useResetTOTPAction(userId); + const resetE2EKeyAction = useResetE2EEKeyAction(userId); + const resendWelcomeEmail = useSendWelcomeEmailMutation(); + + const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser; + const menuOptions = { + ...(isNotPendingDeactivatedNorFederated && + changeAdminStatusAction && { + makeAdmin: { + label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon }, + action: changeAdminStatusAction.action, + }, + }), + ...(isNotPendingDeactivatedNorFederated && + resetE2EKeyAction && { + resetE2EKey: { label: { label: resetE2EKeyAction.label, icon: resetE2EKeyAction.icon }, action: resetE2EKeyAction.action }, + }), + ...(isNotPendingDeactivatedNorFederated && + resetTOTPAction && { + resetTOTP: { label: { label: resetTOTPAction.label, icon: resetTOTPAction.icon }, action: resetTOTPAction.action }, + }), + ...(changeUserStatusAction && + !isFederatedUser && { + changeActiveStatus: { + label: { label: changeUserStatusAction.label, icon: changeUserStatusAction.icon }, + action: changeUserStatusAction.action, + }, + }), + ...(deleteUserAction && { + delete: { label: { label: deleteUserAction.label, icon: deleteUserAction.icon }, action: deleteUserAction.action }, + }), + }; + + const handleResendWelcomeEmail = () => resendWelcomeEmail.mutateAsync({ email: emails?.[0].address }); + return ( onClick(_id, e)} @@ -53,39 +106,78 @@ const UsersTableRow = ({ user, onClick, mediaQuery, tab }: UsersTableRowProps): > - {username && } - - - - - - - {name || username} - - {!mediaQuery && name && ( - - {`@${username}`} - - )} + {username && } + + + + + + {name || username} - {mediaQuery && ( - - - {username} - - + + + + {username} + + + + {!isLaptop && {emails?.length && emails[0].address}} + + {!isLaptop && {roleNames}} + + {tab === 'all' && !isMobile && ( + + {registrationStatusText} )} - {mediaQuery && {emails?.length && emails[0].address}} - {mediaQuery && {roleNames}} - {tab === 'all' && ( + + {tab === 'pending' && !isMobile && ( - {registrationStatusText} + + {active ? t('User_first_log_in') : t('Activation')} + )} + + { + e.stopPropagation(); + }} + > + {tab === 'pending' && ( + <> + {active ? ( + + ) : ( + + )} + + )} + + + label === 'Delete' ? ( +