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 (