diff --git a/.changeset/serious-poets-smash.md b/.changeset/serious-poets-smash.md new file mode 100644 index 000000000000..0bbc770cc7ce --- /dev/null +++ b/.changeset/serious-poets-smash.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +"@rocket.chat/core-typings": patch +"@rocket.chat/ui-client": patch +--- + +New admin user panel to manage users in different status, such as: All, Pending, Active, Deactivated diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 7b6c964a50bb..b8c5c06b2eba 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -27,6 +27,7 @@ import { passwordPolicy } from '../../../lib/server'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; +import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; import { API } from '../api'; import { getLoggedInUser } from '../helpers/getLoggedInUser'; @@ -634,9 +635,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); - const isSMTPConfigured = Boolean(settings.get('SMTP_Host')) || isMailURLSet; - return API.v1.success({ isSMTPConfigured }); + return API.v1.success({ isSMTPConfigured: isSMTPConfigured() }); }, }, ); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 10ea2f0b5ac2..8ae9501bbe41 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -6,6 +6,8 @@ import { isUserSetActiveStatusParamsPOST, isUserDeactivateIdleParamsPOST, isUsersInfoParamsGetProps, + isUsersListStatusProps, + isUsersSendWelcomeEmailProps, isUserRegisterParamsPOST, isUserLogoutParamsPOST, isUsersListTeamsProps, @@ -17,6 +19,7 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -25,6 +28,7 @@ import type { Filter } from 'mongodb'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; +import { sendWelcomeEmail } from '../../../../server/methods/sendWelcomeEmail'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -555,6 +559,130 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'users.listByStatus', + { + authRequired: true, + validateParams: isUsersListStatusProps, + permissionsRequired: ['view-d-room', 'view-outside-room'], + }, + { + async get() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields } = await this.parseJsonQuery(); + const { status, roles, searchTerm } = this.queryParams; + + const projection = { + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + active: 1, + avatarETag: 1, + lastLogin: 1, + type: 1, + reason: 0, + ...fields, + }; + + const actualSort: Record = sort || { username: 1 }; + + if (sort?.status) { + actualSort.active = sort.status; + } + + if (sort?.name) { + actualSort.nameInsensitive = sort.name; + } + + let match: Filter; + + switch (status) { + case 'active': + match = { + active: true, + lastLogin: { $exists: true }, + }; + break; + case 'all': + match = {}; + break; + case 'deactivated': + match = { + active: false, + lastLogin: { $exists: true }, + }; + break; + case 'pending': + match = { + lastLogin: { $exists: false }, + type: { $nin: ['bot', 'app'] }, + }; + projection.reason = 1; + break; + default: + throw new Meteor.Error('invalid-params', 'Invalid status parameter'); + } + + const canSeeAllUserInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); + + match = { + ...match, + $or: [ + ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } }] : []), + { username: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + { name: { $regex: escapeRegExp(searchTerm), $options: 'i' } }, + ], + }; + + if (roles?.length && !roles.includes('all')) { + match = { + ...match, + roles: { $in: roles }, + }; + } + + const { cursor, totalCount } = await Users.findPaginated( + { + ...match, + }, + { + sort: actualSort, + skip: offset, + limit: count, + projection, + }, + ); + + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + users, + count: users.length, + offset, + total, + }); + }, + }, +); + +API.v1.addRoute( + 'users.sendWelcomeEmail', + { + authRequired: true, + validateParams: isUsersSendWelcomeEmailProps, + }, + { + async post() { + const { email } = this.bodyParams; + await sendWelcomeEmail(email); + + return API.v1.success(); + }, + }, +); + API.v1.addRoute( 'users.register', { diff --git a/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts new file mode 100644 index 000000000000..fa300cb37ee2 --- /dev/null +++ b/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts @@ -0,0 +1,6 @@ +import { settings } from '../../../settings/server'; + +export const isSMTPConfigured = (): boolean => { + const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined); + return Boolean(settings.get('SMTP_Host')) || isMailURLSet; +}; diff --git a/apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx b/apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx index 7ea4de6d9867..615f6762efd1 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx @@ -5,15 +5,15 @@ import React from 'react'; type InfoPanelTitleProps = { title: string; - icon: ReactNode; + icon?: ReactNode; }; const isValidIcon = (icon: ReactNode): icon is IconName => typeof icon === 'string'; const InfoPanelTitle: FC = ({ title, icon }) => ( - + {isValidIcon(icon) ? : icon} - + {title} diff --git a/apps/meteor/client/components/UserCard/UserCardInfo.tsx b/apps/meteor/client/components/UserCard/UserCardInfo.tsx index 8e235670a3dc..2afcf6a37f2c 100644 --- a/apps/meteor/client/components/UserCard/UserCardInfo.tsx +++ b/apps/meteor/client/components/UserCard/UserCardInfo.tsx @@ -3,7 +3,7 @@ import type { ReactElement, ComponentProps } from 'react'; import React from 'react'; const UserCardInfo = (props: ComponentProps): ReactElement => ( - + ); export default UserCardInfo; diff --git a/apps/meteor/client/components/UserCard/UserCardRoles.tsx b/apps/meteor/client/components/UserCard/UserCardRoles.tsx index 9cd22ebdff35..f7a977d4a4dc 100644 --- a/apps/meteor/client/components/UserCard/UserCardRoles.tsx +++ b/apps/meteor/client/components/UserCard/UserCardRoles.tsx @@ -6,7 +6,7 @@ import UserCardInfo from './UserCardInfo'; const UserCardRoles = ({ children }: { children: ReactNode }): ReactElement => ( - + {children} diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 72722e8e9156..4ad79d886e36 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -10,6 +10,12 @@ import { useUserCustomFields } from '../../hooks/useUserCustomFields'; import { useUserDisplayName } from '../../hooks/useUserDisplayName'; import { ContextualbarScrollableContent } from '../Contextualbar'; import InfoPanel from '../InfoPanel'; +import InfoPanelAvatar from '../InfoPanel/InfoPanelAvatar'; +import InfoPanelField from '../InfoPanel/InfoPanelField'; +import InfoPanelLabel from '../InfoPanel/InfoPanelLabel'; +import InfoPanelSection from '../InfoPanel/InfoPanelSection'; +import InfoPanelText from '../InfoPanel/InfoPanelText'; +import InfoPanelTitle from '../InfoPanel/InfoPanelTitle'; import MarkdownText from '../MarkdownText'; import UTCClock from '../UTCClock'; import { UserCardRoles } from '../UserCard'; @@ -39,6 +45,7 @@ type UserInfoProps = UserInfoDataProps & { verified?: boolean; actions: ReactElement; roles: ReactElement[]; + reason?: string; }; const UserInfo = ({ @@ -57,8 +64,9 @@ const UserInfo = ({ status, statusText, customFields, - canViewAllInfo, actions, + reason, + canViewAllInfo, ...props }: UserInfoProps): ReactElement => { const t = useTranslation(); @@ -70,118 +78,124 @@ const UserInfo = ({ {username && ( - + - + )} - {actions && {actions}} + {actions && {actions}} - - {userDisplayName && } + + {userDisplayName && } {statusText && ( - - - + + + + )} + + + + {reason && ( + + {t('Reason_for_joining')} + {reason} + + )} + {nickname && ( + + {t('Nickname')} + {nickname} + )} - - {roles.length !== 0 && ( - - {t('Roles')} + + {t('Roles')} {roles} - + )} {Number.isInteger(utcOffset) && ( - - {t('Local_Time')} - {utcOffset && } - + + {t('Local_Time')} + {utcOffset && } + )} {username && username !== name && ( - - {t('Username')} - {username} - - )} - - {canViewAllInfo && ( - - {t('Last_login')} - {lastLogin ? timeAgo(lastLogin) : t('Never')} - + + {t('Username')} + {username} + )} - {name && ( - - {t('Full_Name')} - {name} - - )} - - {nickname && ( - - {t('Nickname')} - {nickname} - + {Number.isInteger(utcOffset) && ( + + {t('Local_Time')} + {utcOffset && } + )} {bio && ( - - {t('Bio')} - + + {t('Bio')} + - - + + + )} + + {Number.isInteger(utcOffset) && canViewAllInfo && ( + + {t('Last_login')} + {lastLogin ? timeAgo(lastLogin) : t('Never')} + )} {phone && ( - - {t('Phone')} - + + {t('Phone')} + {phone} - - + + )} {email && ( - - {t('Email')} - + + {t('Email')} + {email} {verified ? t('Verified') : t('Not_verified')} - - + + )} {userCustomFields?.map( (customField) => customField?.value && ( - - {t(customField.label as TranslationKey)} - + + {t(customField.label as TranslationKey)} + - - + + ), )} {createdAt && ( - - {t('Created_at')} - {timeAgo(createdAt)} - + + {t('Created_at')} + {timeAgo(createdAt)} + )} - + ); diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index fcdaa29c9dff..456e66c3abf4 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -9,7 +9,6 @@ const roomTypeFilterStructure = [ { id: 'filter_by_room', text: 'Filter_by_room', - isGroupTitle: true, }, { id: 'd', @@ -89,7 +88,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch { + const { t } = useTranslation(); + const router = useRouter(); + + return ( + <> + + + + + + + + + ); +}; + +export default AdminUserCreated; diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index a85754705090..12167729830e 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -1,26 +1,24 @@ -import type { AvatarObject, IUser, Serialized } from '@rocket.chat/core-typings'; +import type { AvatarObject, IRole, IUser, Serialized } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; import { Field, - FieldLabel, - FieldRow, - FieldError, - FieldHint, TextInput, TextAreaInput, - PasswordInput, MultiSelectFiltered, Box, ToggleSwitch, - Icon, - Divider, FieldGroup, ContextualbarFooter, - ButtonGroup, Button, Callout, + FieldLabel, + FieldRow, + FieldError, + FieldHint, + Icon, } from '@rocket.chat/fuselage'; -import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { UserCreateParamsPOST } from '@rocket.chat/rest-typings'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, @@ -30,8 +28,8 @@ import { useToastMessageDispatch, useTranslation, } from '@rocket.chat/ui-contexts'; -import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useCallback } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -41,22 +39,32 @@ import UserAvatarEditor from '../../../components/avatar/UserAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useUpdateAvatar } from '../../../hooks/useUpdateAvatar'; import { USER_STATUS_TEXT_MAX_LENGTH, BIO_TEXT_MAX_LENGTH } from '../../../lib/constants'; +import AdminUserSetRandomPasswordContent from './AdminUserSetRandomPasswordContent'; +import AdminUserSetRandomPasswordRadios from './AdminUserSetRandomPasswordRadios'; import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { userData?: Serialized; onReload: () => void; + context: string; + refetchUserFormData?: () => void; + roleData: { roles: IRole[] } | undefined; + roleError: unknown; }; +export type userFormProps = Omit; + const getInitialValue = ({ data, defaultUserRoles, isSmtpEnabled, + isNewUserPage, }: { data?: Serialized; defaultUserRoles?: IUser['roles']; isSmtpEnabled?: boolean; -}) => ({ + isNewUserPage?: boolean; +}): userFormProps => ({ roles: data?.roles ?? defaultUserRoles, name: data?.name ?? '', password: '', @@ -65,50 +73,49 @@ const getInitialValue = ({ nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: false, - requirePasswordChange: data?.requirePasswordChange || false, + setRandomPassword: isNewUserPage && isSmtpEnabled, + requirePasswordChange: isNewUserPage && (data?.requirePasswordChange ?? true), customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', joinDefaultChannels: true, sendWelcomeEmail: isSmtpEnabled, avatar: '' as AvatarObject, + passwordConfirmation: '', }); -const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { +const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleData, roleError, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); + const queryClient = useQueryClient(); const customFieldsMetadata = useAccountsCustomFields(); const defaultRoles = useSetting('Accounts_Registration_Users_Default_Roles') || ''; + const isVerificationNeeded = useSetting('Accounts_EmailVerification'); const defaultUserRoles = parseCSV(defaultRoles); - const { data } = useSmtpQuery(); + const { data, isSuccess: isSmtpStatusAvailable } = useSmtpQuery(); const isSmtpEnabled = data?.isSMTPConfigured; - - const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); - const updateUserAction = useEndpoint('POST', '/v1/users.update'); - const createUserAction = useEndpoint('POST', '/v1/users.create'); - - const getRoles = useEndpoint('GET', '/v1/roles.list'); - const { data: roleData, error: roleError } = useQuery(['roles'], async () => getRoles()); - - const availableRoles: SelectOption[] = roleData?.roles.map(({ _id, name, description }) => [_id, description || name]) || []; - - const goToUser = useCallback((id) => router.navigate(`/admin/users/info/${id}`), [router]); + const isNewUserPage = context === 'new'; const { control, watch, handleSubmit, - reset, formState: { errors, isDirty }, + setValue, } = useForm({ - defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled }), + defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isNewUserPage }), mode: 'onBlur', }); - const { avatar, username, setRandomPassword } = watch(); + const { avatar, username, setRandomPassword, password } = watch(); + + const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); + const updateUserAction = useEndpoint('POST', '/v1/users.update'); + const createUserAction = useEndpoint('POST', '/v1/users.create'); + + const availableRoles: SelectOption[] = roleData?.roles.map(({ _id, name, description }) => [_id, description || name]) || []; const updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); @@ -119,6 +126,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { await updateAvatar(); router.navigate(`/admin/users/info/${_id}`); onReload(); + refetchUserFormData?.(); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); @@ -127,12 +135,15 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const handleCreateUser = useMutation({ mutationFn: createUserAction, - onSuccess: async (data) => { + onSuccess: async ({ user: { _id } }) => { dispatchToastMessage({ type: 'success', message: t('User_created_successfully!') }); await eventStats({ params: [{ eventName: 'updateCounter', settingsId: 'Manual_Entry_User_Count' }], }); - goToUser(data.user._id); + queryClient.invalidateQueries(['pendingUsersCount'], { + refetchType: 'all', + }); + router.navigate(`/admin/users/created/${_id}`); onReload(); }, onError: (error) => { @@ -140,12 +151,14 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { }, }); - const handleSaveUser = useMutableCallback(async (userFormPayload) => { - const { avatar, ...userFormData } = userFormPayload; - if (userData?._id) { + const handleSaveUser = useMutableCallback(async (userFormPayload: userFormProps) => { + const { avatar, passwordConfirmation, ...userFormData } = userFormPayload; + + if (!isNewUserPage && userData?._id) { return handleUpdateUser.mutateAsync({ userId: userData?._id, data: userFormData }); } - return handleCreateUser.mutateAsync(userFormData); + + return handleCreateUser.mutateAsync({ ...userFormData, fields: '' }); }); const nameId = useUniqueId(); @@ -156,17 +169,22 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const bioId = useUniqueId(); const nicknameId = useUniqueId(); const passwordId = useUniqueId(); - const requirePasswordChangeId = useUniqueId(); - const setRandomPasswordId = useUniqueId(); const rolesId = useUniqueId(); const joinDefaultChannelsId = useUniqueId(); const sendWelcomeEmailId = useUniqueId(); + const setRandomPasswordId = useUniqueId(); + + const [showCustomFields, setShowCustomFields] = useState(true); + + if (!context) { + return null; + } return ( <> - {userData?._id && ( + {!isNewUserPage && ( { /> )} + {isNewUserPage && {t('Manually_created_users_briefing')}} + + {t('Email')} + + (validateEmail(email) ? undefined : t('ensure_email_address_valid')), + }} + render={({ field }) => ( + + )} + /> + + {errors?.email && ( + + {errors.email.message} + + )} + + + + {t('Mark_email_as_verified')} + + + + ( + + )} + /> + + {isVerificationNeeded && !isSmtpEnabled && ( + + )} + {!isVerificationNeeded && ( + + )} + {t('Name')} @@ -222,7 +296,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-describedby={`${usernameId}-error`} error={errors.username?.message} flexGrow={1} - addon={} /> )} /> @@ -234,43 +307,97 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - {t('Email')} - - + {t('Password')} + + + {!setRandomPassword && ( + (validateEmail(email) ? undefined : t('error-invalid-email-address')), - }} - render={({ field }) => ( - } - /> - )} + setRandomPassword={setRandomPassword} + isNewUserPage={isNewUserPage} + passwordId={passwordId} + errors={errors} + password={password} /> - - {errors?.email && ( - - {errors.email.message} - )} + {t('Roles')} - {t('Verified')} - } - /> + {roleError && {roleError}} + {!roleError && ( + ( + + )} + /> + )} + {errors?.roles && {errors.roles.message}} + + + + {t('Join_default_channels')} + + ( + + )} + /> + + + + + + + {t('Send_welcome_email')} + + + {isSmtpStatusAvailable && ( + ( + + )} + /> + )} + + + {!isSmtpEnabled && ( + + )} {t('StatusMessage')} @@ -287,7 +414,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-invalid={errors.statusText ? 'true' : 'false'} aria-describedby={`${statusTextId}-error`} flexGrow={1} - addon={} /> )} /> @@ -314,7 +440,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-invalid={errors.bio ? 'true' : 'false'} aria-describedby={`${bioId}-error`} flexGrow={1} - addon={} /> )} /> @@ -328,173 +453,34 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { {t('Nickname')} - ( - } /> - )} - /> - - - - - {!setRandomPassword && ( - - {t('Password')} - - ( - } - /> - )} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - - )} - - - {t('Require_password_change')} - ( - - )} - /> + } /> - - - {t('Set_random_password_and_send_by_email')} - ( - - )} - /> - - {!isSmtpEnabled && ( - - )} - - - {t('Roles')} - - {roleError && {roleError}} - {!roleError && ( - ( - - )} - /> - )} - - {errors?.roles && {errors.roles.message}} - - - - {t('Join_default_channels')} - ( - - )} - /> - - - - - {t('Send_welcome_email')} - ( - - )} - /> - - {!isSmtpEnabled && ( - - )} - {Boolean(customFieldsMetadata.length) && ( <> - - {t('Custom_Fields')} - + + {showCustomFields && } )} - - - - + ); }; -export default UserForm; +export default AdminUserForm; diff --git a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx index 63fe2691d972..83cf07347789 100644 --- a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx @@ -1,4 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IRole, IUser } from '@rocket.chat/core-typings'; import { isUserFederated } from '@rocket.chat/core-typings'; import { Box, Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -12,11 +12,14 @@ import AdminUserForm from './AdminUserForm'; type AdminUserFormWithDataProps = { uid: IUser['_id']; onReload: () => void; + context: string; + roleData: { roles: IRole[] } | undefined; + roleError: unknown; }; -const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): ReactElement => { +const AdminUserFormWithData = ({ uid, onReload, context, roleData, roleError }: AdminUserFormWithDataProps): ReactElement => { const t = useTranslation(); - const { data, isLoading, isError } = useUserInfoQuery({ userId: uid }); + const { data, isLoading, isError, refetch } = useUserInfoQuery({ userId: uid }); if (isLoading) { return ( @@ -34,7 +37,7 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - if (data?.user && isUserFederated(data?.user as unknown as IUser)) { + if (data?.user && isUserFederated(data?.user)) { return ( {t('Edit_Federated_User_Not_Allowed')} @@ -42,7 +45,18 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - return ; + return ( + { + refetch(); + }} + roleData={roleData} + roleError={roleError} + /> + ); }; export default AdminUserFormWithData; diff --git a/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx b/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx index 258e8dd8be47..f816e273a31c 100644 --- a/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useMemo } from 'react'; import UserInfo from '../../../components/UserInfo'; import { useActionSpread } from '../../hooks/useActionSpread'; +import type { IAdminUserTabs } from './IAdminUserTabs'; import { useChangeAdminStatusAction } from './hooks/useChangeAdminStatusAction'; import { useChangeUserStatusAction } from './hooks/useChangeUserStatusAction'; import { useDeleteUserAction } from './hooks/useDeleteUserAction'; @@ -20,6 +21,7 @@ type AdminUserInfoActionsProps = { isAdmin: boolean; onChange: () => void; onReload: () => void; + tab: IAdminUserTabs; }; // TODO: Replace menu @@ -31,6 +33,7 @@ const AdminUserInfoActions = ({ isAdmin, onChange, onReload, + tab, }: AdminUserInfoActionsProps): ReactElement => { const t = useTranslation(); const directRoute = useRoute('direct'); @@ -81,24 +84,25 @@ const AdminUserInfoActions = ({ disabled: isFederatedUser, }, }), - ...(changeAdminStatusAction && !isFederatedUser && { makeAdmin: changeAdminStatusAction }), - ...(resetE2EKeyAction && !isFederatedUser && { resetE2EKey: resetE2EKeyAction }), - ...(resetTOTPAction && !isFederatedUser && { resetTOTP: resetTOTPAction }), - ...(deleteUserAction && { delete: deleteUserAction }), + ...(changeAdminStatusAction && !isFederatedUser && tab !== 'deactivated' && { makeAdmin: changeAdminStatusAction }), + ...(resetE2EKeyAction && !isFederatedUser && tab !== 'deactivated' && { resetE2EKey: resetE2EKeyAction }), + ...(resetTOTPAction && !isFederatedUser && tab !== 'deactivated' && { resetTOTP: resetTOTPAction }), ...(changeUserStatusAction && !isFederatedUser && { changeActiveStatus: changeUserStatusAction }), + ...(deleteUserAction && { delete: deleteUserAction }), }), [ - t, canDirectMessage, + t, directMessageClick, canEditOtherUserInfo, + isFederatedUser, editUserClick, changeAdminStatusAction, - changeUserStatusAction, - deleteUserAction, + tab, resetE2EKeyAction, resetTOTPAction, - isFederatedUser, + changeUserStatusAction, + deleteUserAction, ], ); @@ -117,7 +121,13 @@ const AdminUserInfoActions = ({ secondary flexShrink={0} key='menu' - renderItem={({ label: { label, icon }, ...props }): ReactElement =>