From 809ca6c265b53d76baae34d41f1e00525c1627bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Mon, 23 Oct 2023 14:00:48 -0300 Subject: [PATCH] feat: New create user contextual bar (#30582) * feat: :sparkles: New SetRandomPassword UI/ux Created a new ui logic for the SetRandomPassword form with 2 radio buttons instead of a toggle and changed the email field to be the first one in the create user contextual bar. * feat: :sparkles: Implement password confirmation and password verification Implemented the password verification and confirmation flows in the users page contextual bar, changed some types to follow the changes, removed the old Verified toggle and componentized the setRandomPassword radios. * feat: :sparkles: Create briefing field in new users contextual bar Created a briefing text warning users of the new tab layout functionality in the new user contextual bar of the admin users page. * feat: :sparkles: Create visual part of hide custom fields button Created a button that will hide or show the custom fields of the new users form on click. Also added an icon to the user form title, changed the password field position in the form, removed the addon icons from some fields and created a new briefing message in the top of the form. * feat: :sparkles: Create logic for 'hide additional fields' button Created the logic that hides the custom fields of the new user form when the 'hide additional fields' button is clicked * feat: :sparkles: Implement multiple user creation flow Added a new screen to enhance the user creation experience. This screen appears immediately after you create a new user. On this page, you have two options: you can either complete the process and view the user you just created, or you can return to the form to create another user without exiting the contextual bar. Additionally, I've made some minor logic improvements and removed commented-out code. * feat: :sparkles: Make user created contextual bar page count dynamic Implemented logic to the user created contextual bar, now for each user created without exiting or refreshing the page the number of the message will be incremented by 1. Also stopped using the old Field.Component components in favor of their standalone versions and made the message of the Hide Additional Fields button change to Show additional fields when the fields are hidden. * feat: :sparkles: Implement label tooltip in the user creation page email field Implemented a tooltip in the email verification field of the user creation contextual bar that explains what toggling on this field will do. Also implemented a way to use a single value in order to controll the setRandomPassword radios instead of two and added the necessary entries in the i18n to follow the aforementioned changes. * refactor: :recycle: Change "Only allow verified users to login" setting copy Changed the title of the "Only allow verified users to login" setting to "Require email verification to login" and its description from "Make sure you have correct SMTP settings to use this feature" to "Ensure SMTP is configured to enable this feature" * refactor: :recycle: Ensure requirePasswordChange is true when setRandomPassword is true Created some logic to make sure the requirePasswordChange field will be true even if the user changed it to false and then changed setRandomPassword to true. Also changed some strings and sizes to follow figma specs. * refactor: :recycle: Remove useEffect + reset usage of new user creation form Removed un-optimal usage of the useEffect hook together with RHF's reset function, implemented a function that changes the requirePasswordChange value based on which setRandomPassword radio is checked, switched resetField for setValue to make sure the value is always correct, changed some rendering logic to make sure the first state of some fields is correct and optimized some parts of the getInitialValues function return. * refactor: :recycle: Componentize setRandomPassword field content Changed the name of the AdminUserSetRandomPassword component to AdminUserSetRandomPasswordRadios and created a new component called AdminUserSetRandomPasswordContent to organize and clean the AdminUserForm component. * fix: :bug: Refetch user form data after editing user Implemented a refecthUserData function for the AdminuserInfoWithData component that will run when the handleUpdateUser mutation is succesful to refetch the user data of the edited user. This fixes a bug in which the user form would show the old data instead of the updated one. * Move tabs component to Page instead of Page content * Fix imports * First test fix * Fix create user test * Fix tests * Fix test typo * Test fix --- .../views/admin/users/AdminUserCreated.tsx | 32 ++ .../views/admin/users/AdminUserForm.tsx | 437 +++++++++--------- .../admin/users/AdminUserFormWithData.tsx | 16 +- .../AdminUserSetRandomPasswordContent.tsx | 109 +++++ .../AdminUserSetRandomPasswordRadios.tsx | 98 ++++ .../views/admin/users/AdminUsersPage.tsx | 59 ++- .../rocketchat-i18n/i18n/en.i18n.json | 23 +- apps/meteor/tests/e2e/administration.spec.ts | 5 +- .../fragments/admin-flextab-users.ts | 14 +- packages/core-typings/src/IUser.ts | 4 +- .../src/v1/users/UserCreateParamsPOST.ts | 2 +- .../PasswordVerifier/PasswordVerifier.tsx | 11 +- .../PasswordVerifier/PasswordVerifierItem.tsx | 5 +- 13 files changed, 537 insertions(+), 278 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/AdminUserCreated.tsx create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx diff --git a/apps/meteor/client/views/admin/users/AdminUserCreated.tsx b/apps/meteor/client/views/admin/users/AdminUserCreated.tsx new file mode 100644 index 000000000000..ac793c1ac756 --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserCreated.tsx @@ -0,0 +1,32 @@ +import { Button, ButtonGroup, ContextualbarFooter } from '@rocket.chat/fuselage'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; + +const AdminUserCreated = ({ uid, createdUsersCount }: { uid: string; createdUsersCount: number }) => { + const t = useTranslation(); + const router = useRouter(); + + const goToUser = useCallback((id) => router.navigate(`/admin/users/info/${id}`), [router]); + + return ( + <> + + {createdUsersCount === 1 ? t('You_have_created_one_user') : t('You_have_created_users', { count: createdUsersCount })} + + + + + + + + + ); +}; + +export default AdminUserCreated; diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 1912150b4a48..3071ccb8c63a 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 { 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, @@ -31,7 +29,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useCallback } from 'react'; +import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -41,22 +39,31 @@ 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; + setCreatedUsersCount?: React.Dispatch>; + context: string; + refetchUserFormData?: () => void; }; +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 +72,51 @@ 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) ?? true, + 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, setCreatedUsersCount, context, refetchUserFormData, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); 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 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 updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); @@ -119,6 +127,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 +136,13 @@ 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); + setCreatedUsersCount?.((prevUsersCount) => prevUsersCount + 1); + router.navigate(`/admin/users/created/${_id}`); onReload(); }, onError: (error) => { @@ -140,12 +150,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 +168,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 +295,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-describedby={`${usernameId}-error`} error={errors.username?.message} flexGrow={1} - addon={} /> )} /> @@ -234,46 +306,98 @@ 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')} + + {roleError && {roleError}} + {!roleError && ( + ( + + )} + /> + )} + + {errors?.roles && {errors.roles.message}} + - {t('Verified')} + {t('Join_default_channels')} } + name='joinDefaultChannels' + render={({ field: { ref, onChange, value } }) => ( + + )} /> + + + + {t('Send_welcome_email')} + + + {isSmtpStatusAvailable && ( + ( + + )} + /> + )} + + + {!isSmtpEnabled && ( + + )} + {t('StatusMessage')} @@ -289,7 +413,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-invalid={errors.statusText ? 'true' : 'false'} aria-describedby={`${statusTextId}-error`} flexGrow={1} - addon={} /> )} /> @@ -316,7 +439,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-invalid={errors.bio ? 'true' : 'false'} aria-describedby={`${bioId}-error`} flexGrow={1} - addon={} /> )} /> @@ -330,181 +452,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..a584cf284c0f 100644 --- a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx @@ -12,11 +12,12 @@ import AdminUserForm from './AdminUserForm'; type AdminUserFormWithDataProps = { uid: IUser['_id']; onReload: () => void; + context: string; }; -const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): ReactElement => { +const AdminUserFormWithData = ({ uid, onReload, context }: AdminUserFormWithDataProps): ReactElement => { const t = useTranslation(); - const { data, isLoading, isError } = useUserInfoQuery({ userId: uid }); + const { data, isLoading, isError, refetch } = useUserInfoQuery({ userId: uid }); if (isLoading) { return ( @@ -42,7 +43,16 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - return ; + return ( + { + refetch(); + }} + /> + ); }; export default AdminUserFormWithData; diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx new file mode 100644 index 000000000000..282515b71068 --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx @@ -0,0 +1,109 @@ +import { Box, FieldError, FieldLabel, FieldRow, PasswordInput, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { PasswordVerifier } from '@rocket.chat/ui-client'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { Control, FieldErrors } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import type { userFormProps } from './AdminUserForm'; + +type AdminUserSetRandomPasswordContentProps = { + control: Control; + setRandomPassword: boolean | undefined; + isNewUserPage: boolean; + passwordId: string; + errors: FieldErrors; + password: string; +}; + +const AdminUserSetRandomPasswordContent = ({ + control, + setRandomPassword, + isNewUserPage, + passwordId, + errors, + password, +}: AdminUserSetRandomPasswordContentProps) => { + const t = useTranslation(); + + const passwordConfirmationId = useUniqueId(); + const requirePasswordChangeId = useUniqueId(); + const passwordVerifierId = useUniqueId(); + + const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); + const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); + const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); + + return ( + <> + + {t('Require_password_change')} + + ( + + )} + /> + + + + ( + + )} + /> + + {errors?.password && ( + + {errors.password.message} + + )} + {requiresPasswordConfirmation && ( + + (password === val ? true : t('Invalid_confirm_pass')), + }} + render={({ field }) => ( + + )} + /> + + )} + {errors?.passwordConfirmation && ( + + {errors.passwordConfirmation.message} + + )} + + + ); +}; + +export default AdminUserSetRandomPasswordContent; diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx new file mode 100644 index 000000000000..1eea93639edb --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx @@ -0,0 +1,98 @@ +import { Box, FieldHint, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { Control, UseFormSetValue } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import type { userFormProps } from './AdminUserForm'; + +type AdminUserSetRandomPasswordProps = { + isNewUserPage: boolean | undefined; + control: Control; + isSmtpStatusAvailable: boolean; + isSmtpEnabled: boolean | undefined; + setRandomPasswordId: string; + setValue: UseFormSetValue; +}; + +const AdminUserSetRandomPasswordRadios = ({ + isNewUserPage, + control, + isSmtpStatusAvailable, + isSmtpEnabled, + setRandomPasswordId, + setValue, +}: AdminUserSetRandomPasswordProps) => { + const t = useTranslation(); + + const setPasswordManuallyId = useUniqueId(); + + const handleSetRandomPasswordChange = (onChange: (...event: any[]) => void, value: boolean) => { + setValue('requirePasswordChange', value); + + onChange(value); + }; + + if (!isSmtpStatusAvailable || isNewUserPage === undefined) { + return null; + } + + return ( + <> + + + ( + handleSetRandomPasswordChange(onChange, true)} + disabled={!isSmtpEnabled} + /> + )} + /> + + + {t('Set_randomly_and_send_by_email')} + + + {!isSmtpEnabled && ( + + )} + + + ( + handleSetRandomPasswordChange(onChange, false)} + /> + )} + /> + + + {t('Set_manually')} + + + + ); +}; + +export default AdminUserSetRandomPasswordRadios; diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index e0e88bedcd90..ab07134e0794 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonGroup, Tabs } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Icon, Tabs, TabsItem } from '@rocket.chat/fuselage'; import { usePermission, useRouteParameter, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useEffect, useRef, useState } from 'react'; @@ -7,7 +7,10 @@ import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap'; import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; +import PageContent from '../../../components/Page/PageContent'; +import PageHeader from '../../../components/Page/PageHeader'; import AdminInviteUsers from './AdminInviteUsers'; +import AdminUserCreated from './AdminUserCreated'; import AdminUserForm from './AdminUserForm'; import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; @@ -26,6 +29,7 @@ const UsersPage = (): ReactElement => { const canBulkCreateUser = usePermission('bulk-register-user'); const [tab, setTab] = useState<'all' | 'invited' | 'new' | 'active' | 'deactivated'>('all'); + const [createdUsersCount, setCreatedUsersCount] = useState(0); useEffect(() => { if (!context || !seatsCap) { @@ -45,7 +49,7 @@ const UsersPage = (): ReactElement => { return ( - + {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( ) : ( @@ -62,27 +66,27 @@ const UsersPage = (): ReactElement => { )} )} - - - - setTab('all')}> - {t('All')} - - setTab('invited')}> - {t('Invited')} - - setTab('new')}> - {t('New_users')} - - setTab('active')}> - {t('Active')} - - setTab('deactivated')}> - {t('Deactivated')} - - + + + setTab('all')}> + {t('All')} + + setTab('invited')}> + {t('Invited')} + + setTab('new')}> + {t('New_users')} + + setTab('active')}> + {t('Active')} + + setTab('deactivated')}> + {t('Deactivated')} + + + - + {context && ( @@ -90,14 +94,19 @@ const UsersPage = (): ReactElement => { {context === 'info' && t('User_Info')} {context === 'edit' && t('Edit_User')} - {context === 'new' && t('Add_User')} + {(context === 'new' || context === 'created') && ( + <> + {t('New_user')} + + )} {context === 'invite' && t('Invite_Users')} router.navigate('/admin/users')} /> {context === 'info' && id && } - {context === 'edit' && id && } - {context === 'new' && } + {context === 'edit' && id && } + {context === 'new' && } + {context === 'created' && id && } {context === 'invite' && } )} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 056f7f003727..5da4b1da20f1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -113,8 +113,8 @@ "Accounts_Email_Approved_Subject": "Account approved", "Accounts_Email_Deactivated": "[name]

Your account was deactivated.

", "Accounts_Email_Deactivated_Subject": "Account deactivated", - "Accounts_EmailVerification": "Only allow verified users to login", - "Accounts_EmailVerification_Description": "Make sure you have correct SMTP settings to use this feature", + "Accounts_EmailVerification": "Require email verification to login", + "Accounts_EmailVerification_Description": "Ensure SMTP is configured to enable this feature", "Accounts_Enrollment_Email": "Enrollment Email", "Accounts_Enrollment_Email_Default": "

Welcome to [Site_Name]

Go to [Site_URL] and try the best open source chat solution available today!

", "Accounts_Enrollment_Email_Description": "You may use the following placeholders: \n - `[name]`, `[fname]`, `[lname]` for the user's full name, first name or last name, respectively. \n - `[email]` for the user's email. \n - `[Site_Name]` and `[Site_URL]` for the Application Name and URL respectively. ", @@ -295,6 +295,7 @@ "Action_Available_After_Custom_Content_Added": "This action will become available after the custom content has been added", "Action_Available_After_Custom_Content_Added_And_Visible": "This action will become available after the custom content has been added and made visible to everyone", "Activate": "Activate", + "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Active": "Active", "Active_users": "Active users", "Activity": "Activity", @@ -307,13 +308,13 @@ "Add_files_from": "Add files from", "Add_manager": "Add manager", "Add_monitor": "Add monitor", + "Add_more_users": "Add more users", "Add_Reaction": "Add reaction", "Add_Role": "Add Role", "Add_Sender_To_ReplyTo": "Add Sender to Reply-To", "Add_Server": "Add Server", "Add_URL": "Add URL", "Add_user": "Add user", - "Add_User": "Add User", "Add_users": "Add users", "Add_members": "Add Members", "add-all-to-room": "Add all users to a room", @@ -1862,6 +1863,7 @@ "Email_subject": "Email Subject", "Email_verified": "Email verified", "Email_sent": "Email sent", + "Email_verification_isnt_required": "Email verification to login is not required. To require, enable setting in Accounts > Registration", "Emoji": "Emoji", "Emoji_picker": "Emoji picker", "EmojiCustomFilesystem": "Custom Emoji Filesystem", @@ -1908,6 +1910,7 @@ "Engagement_Dashboard": "Engagement dashboard", "Enrich_your_workspace": "Enrich your workspace perspective with the engagement dashboard. Analyze practical usage statistics about your users, messages and channels. Included with Rocket.Chat Enterprise.", "Ensure_secure_workspace_access": "Ensure secure workspace access", + "ensure_email_address_valid": "Please ensure the email address is valid", "Enter": "Enter", "Enter_a_custom_message": "Enter a custom message", "Enter_a_department_name": "Enter a department name", @@ -2469,6 +2472,7 @@ "Hi_username": "Hi [name]", "Hidden": "Hidden", "Hide": "Hide", + "Hide_additional_fields": "Hide additional fields", "Hide_counter": "Hide counter", "Hide_flextab": "Hide Contextual Bar by clicking outside of it", "Hide_Group_Warning": "Are you sure you want to hide the group \"%s\"?", @@ -3315,6 +3319,7 @@ "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", "Manual_Selection": "Manual Selection", + "Manually_created_users_briefing": "Manually created users will initially be shown as pending. Once they log in for the first time, they will be shown as active.", "Manufacturing": "Manufacturing", "MapView_Enabled": "Enable Mapview", "MapView_Enabled_Description": "Enabling mapview will display a location share button on the right of the chat input field.", @@ -3323,6 +3328,7 @@ "Mark_all_as_read": "`%s` - Mark all messages (in all channels) as read", "Mark_as_read": "Mark As Read", "Mark_as_unread": "Mark As Unread", + "Mark_email_as_verified": "Mark email as verified", "Mark_read": "Mark Read", "Mark_unread": "Mark Unread", "Marketplace": "Marketplace", @@ -3677,6 +3683,7 @@ "New_Tag": "New Tag", "New_Trigger": "New Trigger", "New_Unit": "New Unit", + "New_user": "New user", "New_users": "New users", "New_version_available_(s)": "New version available (%s)", "New_videocall_request": "New Video Call Request", @@ -4613,7 +4620,7 @@ "Select_at_least_two_users": "Select at least two users", "Select_department": "Select a department", "Select_file": "Select file", - "Select_role": "Select a Role", + "Select_role": "Select a role", "Select_service_to_login": "Select a service to login to load your picture or upload one directly from your computer", "Select_tag": "Select a tag", "Select_the_channels_you_want_the_user_to_be_removed_from": "Select the channels you want the user to be removed from", @@ -4634,7 +4641,7 @@ "Send_confirmation_email": "Send confirmation email", "Send_data_into_RocketChat_in_realtime": "Send data into Rocket.Chat in real-time.", "Send_email": "Send Email", - "Send_Email_SMTP_Warning": "To send this email you need to setup SMTP emailing server", + "Send_Email_SMTP_Warning": "Set up the SMTP server in email settings to enable", "Send_invitation_email": "Send invitation email", "Send_invitation_email_error": "You haven't provided any valid email address.", "Send_invitation_email_info": "You can send multiple email invitations at once.", @@ -4692,9 +4699,10 @@ "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", "Upload_app": "Upload App", - "Set_random_password_and_send_by_email": "Set random password and send by email", + "Set_randomly_and_send_by_email": "Set randomly and send by email", "set-leader": "Set Leader", "set-leader_description": "Permission to set other users as leader of a channel", + "Set_manually": "Set manually", "set-moderator": "Set Moderator", "set-moderator_description": "Permission to set other users as moderator of a channel", "set-owner": "Set Owner", @@ -4721,6 +4729,7 @@ "shortcut_name": "shortcut name", "Should_be_a_URL_of_an_image": "Should be a URL of an image.", "Should_exists_a_user_with_this_username": "The user must already exist.", + "Show_additional_fields": "Show additional fields", "Show_agent_email": "Show agent email", "Show_agent_info": "Show agent information", "Show_all": "Show All", @@ -5794,6 +5803,8 @@ "You_have_a_new_message": "You have a new message", "You_have_been_muted": "You have been muted and cannot speak in this room", "You_have_been_removed_from__roomName_": "You've been removed from the room {{roomName}}", + "You_have_created_one_user": "You’ve created 1 user", + "You_have_created_users": "You’ve created {{count}} users", "You_have_joined_a_new_call_with": "You have joined a new call with", "You_have_n_codes_remaining": "You have {{number}} codes remaining.", "You_have_not_verified_your_email": "You have not verified your email.", diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index b439258429f8..85a016daaf6c 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -39,11 +39,12 @@ test.describe.parallel('administration', () => { test('expect create a user', async () => { await poAdmin.tabs.users.btnNew.click(); + await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); await poAdmin.tabs.users.inputName.type(faker.person.firstName()); await poAdmin.tabs.users.inputUserName.type(faker.internet.userName()); - await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); - await poAdmin.tabs.users.checkboxVerified.click(); + await poAdmin.tabs.users.inputSetManually.click(); await poAdmin.tabs.users.inputPassword.type('any_password'); + await poAdmin.tabs.users.inputConfirmPassword.type('any_password'); await expect(poAdmin.tabs.users.userRole).toBeVisible(); await poAdmin.tabs.users.btnSave.click(); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts index bd46de6ea00f..2e9c73b9e37d 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts @@ -12,7 +12,7 @@ export class AdminFlextabUsers { } get btnSave(): Locator { - return this.page.locator('role=button[name="Save"]'); + return this.page.locator('role=button[name="Add user"]'); } get btnInvite(): Locator { @@ -31,12 +31,20 @@ export class AdminFlextabUsers { return this.page.locator('//label[text()="Email"]/following-sibling::span//input').first(); } + get inputSetManually(): Locator { + return this.page.locator('//label[text()="Set manually"]'); + } + get inputPassword(): Locator { - return this.page.locator('//label[text()="Password"]/following-sibling::span//input'); + return this.page.locator('input[placeholder="Password"]'); + } + + get inputConfirmPassword(): Locator { + return this.page.locator('input[placeholder="Confirm password"]'); } get checkboxVerified(): Locator { - return this.page.locator('//label[text()="Verified"]'); + return this.page.locator('//label[text()="Mark email as verified"]'); } get userRole(): Locator { diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index ce14c4020d6f..7e5a08899f57 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -150,9 +150,7 @@ export interface IUser extends IRocketChatRecord { public_key: string; }; requirePasswordChange?: boolean; - customFields?: { - [key: string]: any; - }; + customFields?: Record; settings?: IUserSettings; defaultRoom?: string; ldap?: boolean; diff --git a/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts index 347498999011..49fb8b2f6912 100644 --- a/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts @@ -19,7 +19,7 @@ export type UserCreateParamsPOST = { setRandomPassword?: boolean; sendWelcomeEmail?: boolean; verified?: boolean; - customFields?: object; + customFields?: Record; /* @deprecated */ fields: string; }; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx index bf53360a2351..c2ed984512fa 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx @@ -8,6 +8,7 @@ import { PasswordVerifierItem } from './PasswordVerifierItem'; type PasswordVerifierProps = { password: string | undefined; id?: string; + vertical?: boolean; }; type PasswordVerificationProps = { @@ -16,7 +17,7 @@ type PasswordVerificationProps = { limit?: number; }[]; -export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => { +export const PasswordVerifier = ({ password, id, vertical }: PasswordVerifierProps) => { const { t } = useTranslation(); const uniqueId = useUniqueId(); @@ -37,7 +38,13 @@ export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => { {passwordVerifications.map(({ isValid, limit, name }) => ( - + ))} diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx index 97499c0eaf73..c622fc74e6c8 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx @@ -20,13 +20,14 @@ const variants: { export const PasswordVerifierItem = ({ text, isValid, + vertical, ...props -}: { text: string; isValid: boolean } & Omit, 'is'>) => { +}: { text: string; isValid: boolean; vertical: boolean } & Omit, 'is'>) => { const { icon, color } = variants[isValid ? 'success' : 'error']; return (