From f8e2e088b420a62e295e75f8269c8070137825d7 Mon Sep 17 00:00:00 2001 From: rique223 Date: Thu, 5 Oct 2023 15:41:27 -0300 Subject: [PATCH 01/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 210 +++++++++++------- .../rocketchat-i18n/i18n/en.i18n.json | 4 +- 2 files changed, 130 insertions(+), 84 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 334aca68b8f8..e21e61eb8bf3 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -14,6 +14,7 @@ import { ButtonGroup, Button, Callout, + RadioButton, } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -61,7 +62,8 @@ const getInitialValue = ({ nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: false, + setRandomPassword: true, + setRandomPasswordManually: false, requirePasswordChange: data?.requirePasswordChange || false, customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', @@ -99,6 +101,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { handleSubmit, reset, formState: { errors, isDirty }, + setValue, } = useForm({ defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled }), mode: 'onBlur', @@ -154,6 +157,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const passwordId = useUniqueId(); const requirePasswordChangeId = useUniqueId(); const setRandomPasswordId = useUniqueId(); + const setRandomPasswordManuallyId = useUniqueId(); const rolesId = useUniqueId(); const joinDefaultChannelsId = useUniqueId(); const sendWelcomeEmailId = useUniqueId(); @@ -178,6 +182,45 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { /> )} + + {t('Email')} + + (validateEmail(email) ? undefined : t('error-invalid-email-address')), + }} + render={({ field }) => ( + } + /> + )} + /> + + {errors?.email && ( + + {errors.email.message} + + )} + + + {t('Mark_email_as_verified')} + + } + /> + + {t('Name')} @@ -229,35 +272,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - - {t('Email')} - - (validateEmail(email) ? undefined : t('error-invalid-email-address')), - }} - render={({ field }) => ( - } - /> - )} - /> - - {errors?.email && ( - - {errors.email.message} - - )} - {t('Verified')} @@ -337,79 +351,109 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { - {!setRandomPassword && ( - - {t('Password')} - - ( - } - /> - )} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - - )} - - {t('Require_password_change')} - + + {t('Password')} + + + ( - { + setValue('setRandomPasswordManually', false); + onChange(true); + }} + disabled={!isSmtpEnabled} /> )} /> + + {t('Set_randomly_and_send_by_email')} + - - - - {t('Set_random_password_and_send_by_email')} - + + ( - { + setValue('setRandomPassword', false); + onChange(true); + }} disabled={!isSmtpEnabled} /> )} /> + + {t('Set_manually')} + - {!isSmtpEnabled && ( - + {/* {!isSmtpEnabled && ( + + )} */} + {!setRandomPassword && ( + <> + + {t('Require_password_change')} + + ( + + )} + /> + + + + ( + } + placeholder='Password' + /> + )} + /> + + {errors?.password && ( + + {errors.password.message} + + )} + )} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index cd8b88fe4058..70be77296a05 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3296,6 +3296,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", @@ -4666,9 +4667,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", From f32a3f4adf6120f50c6b44ca80a6a95214b9f11c Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 6 Oct 2023 18:06:01 -0300 Subject: [PATCH 02/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 153 ++++++++---------- .../users/AdminUserSetRandomPassword.tsx | 83 ++++++++++ packages/core-typings/src/IUser.ts | 4 +- .../src/v1/users/UserCreateParamsPOST.ts | 2 +- .../PasswordVerifier/PasswordVerifier.tsx | 11 +- .../PasswordVerifier/PasswordVerifierItem.tsx | 5 +- 6 files changed, 168 insertions(+), 90 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index e21e61eb8bf3..bfb294711168 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -14,11 +14,11 @@ import { ButtonGroup, Button, Callout, - RadioButton, } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { CustomFieldsForm } from '@rocket.chat/ui-client'; +import type { UserCreateParamsPOST } from '@rocket.chat/rest-typings'; +import { CustomFieldsForm, PasswordVerifier } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, useSetting, @@ -38,6 +38,7 @@ 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 AdminUserSetRandomPassword from './AdminUserSetRandomPassword'; import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { @@ -45,6 +46,11 @@ type AdminUserFormProps = { onReload: () => void; }; +export type userFormProps = Omit< + UserCreateParamsPOST & { setPasswordManually: boolean; avatar: AvatarObject; passwordConfirmation: string }, + 'fields' +>; + const getInitialValue = ({ data, defaultUserRoles, @@ -53,7 +59,7 @@ const getInitialValue = ({ data?: Serialized; defaultUserRoles?: IUser['roles']; isSmtpEnabled?: boolean; -}) => ({ +}): userFormProps => ({ roles: data?.roles ?? defaultUserRoles, name: data?.name ?? '', password: '', @@ -63,13 +69,14 @@ const getInitialValue = ({ email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, setRandomPassword: true, - setRandomPasswordManually: false, + setPasswordManually: false, requirePasswordChange: data?.requirePasswordChange || false, customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', joinDefaultChannels: true, sendWelcomeEmail: isSmtpEnabled, avatar: '' as AvatarObject, + passwordConfirmation: '', }); const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { @@ -79,6 +86,9 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const customFieldsMetadata = useAccountsCustomFields(); const defaultRoles = useSetting('Accounts_Registration_Users_Default_Roles') || ''; + const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); + const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); + const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); const defaultUserRoles = parseCSV(defaultRoles); const { data } = useSmtpQuery(); @@ -99,7 +109,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { control, watch, handleSubmit, - reset, + // reset, formState: { errors, isDirty }, setValue, } = useForm({ @@ -107,7 +117,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { mode: 'onBlur', }); - const { avatar, username, setRandomPassword } = watch(); + const { avatar, username, setRandomPassword, password } = watch(); const updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); @@ -139,12 +149,12 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { }, }); - const handleSaveUser = useMutableCallback(async (userFormPayload) => { - const { avatar, ...userFormData } = userFormPayload; + const handleSaveUser = useMutableCallback(async (userFormPayload: userFormProps) => { + const { avatar, setPasswordManually, passwordConfirmation, ...userFormData } = userFormPayload; if (userData?._id) { return handleUpdateUser.mutateAsync({ userId: userData?._id, data: userFormData }); } - return handleCreateUser.mutateAsync(userFormData); + return handleCreateUser.mutateAsync({ ...userFormData, fields: '' }); }); const nameId = useUniqueId(); @@ -155,12 +165,13 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const bioId = useUniqueId(); const nicknameId = useUniqueId(); const passwordId = useUniqueId(); + const passwordConfirmationId = useUniqueId(); const requirePasswordChangeId = useUniqueId(); - const setRandomPasswordId = useUniqueId(); - const setRandomPasswordManuallyId = useUniqueId(); const rolesId = useUniqueId(); const joinDefaultChannelsId = useUniqueId(); const sendWelcomeEmailId = useUniqueId(); + const setRandomPasswordId = useUniqueId(); + const passwordVerifierId = useUniqueId(); return ( <> @@ -272,18 +283,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - - - {t('Verified')} - - } - /> - - - {t('StatusMessage')} @@ -355,60 +354,19 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { {t('Password')} - - - ( - { - setValue('setRandomPasswordManually', false); - onChange(true); - }} - disabled={!isSmtpEnabled} - /> - )} - /> - - - {t('Set_randomly_and_send_by_email')} - - - - - ( - { - setValue('setRandomPassword', false); - onChange(true); - }} - disabled={!isSmtpEnabled} - /> - )} - /> - - - {t('Set_manually')} - - - {/* {!isSmtpEnabled && ( - - )} */} + + {!isSmtpEnabled && ( + + )} {!setRandomPassword && ( <> @@ -442,8 +400,8 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-describedby={`${passwordId}-error`} error={errors.password?.message} flexGrow={1} - addon={} - placeholder='Password' + addon={} + placeholder={passwordPlaceholder || t('Password')} /> )} /> @@ -453,6 +411,37 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { {errors.password.message} )} + {requiresPasswordConfirmation && ( + + (watch('password') === val ? true : t('Invalid_confirm_pass')), + }} + render={({ field }) => ( + } + placeholder={passwordConfirmationPlaceholder || t('Confirm_password')} + /> + )} + /> + + )} + {errors?.passwordConfirmation && ( + + {errors.passwordConfirmation.message} + + )} + )} @@ -531,15 +520,15 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { - + */} diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx new file mode 100644 index 000000000000..ef1eeedf13b9 --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx @@ -0,0 +1,83 @@ +import { Box, Field, 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 = { + control: Control; + isSmtpEnabled: boolean; + setRandomPassword: boolean; + setValue: UseFormSetValue; + setRandomPasswordId: string; +}; + +const AdminUserSetRandomPassword = ({ + control, + isSmtpEnabled, + setRandomPassword, + setValue, + setRandomPasswordId, +}: AdminUserSetRandomPasswordProps) => { + const t = useTranslation(); + + const setPasswordManuallyId = useUniqueId(); + + return ( + <> + + + ( + { + setValue('setPasswordManually', false); + onChange(true); + }} + disabled={!isSmtpEnabled} + /> + )} + /> + + + {t('Set_randomly_and_send_by_email')} + + + + + ( + { + setValue('setRandomPassword', false); + onChange(true); + }} + disabled={!isSmtpEnabled} + /> + )} + /> + + + {t('Set_manually')} + + + + ); +}; + +export default AdminUserSetRandomPassword; 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 ( Date: Mon, 9 Oct 2023 13:48:47 -0300 Subject: [PATCH 03/20] 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. --- apps/meteor/client/views/admin/users/AdminUserForm.tsx | 2 +- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index bfb294711168..789b40b3b41c 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -193,6 +193,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { /> )} + {t('Manually_created_users_briefing')} {t('Email')} @@ -211,7 +212,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { aria-describedby={`${emailId}-error`} error={errors.email?.message} flexGrow={1} - addon={} /> )} /> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 70be77296a05..be348b485db5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3288,6 +3288,7 @@ "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", "Manual_Selection": "Manual Selection", + "Manually_created_users_briefing": "Manually created users will initially be listed under the 'Pending' tab. Once they log in for the first time, they will be moved to the 'Active' tab.", "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.", From e3304ba901ad2a5f51b244949102b596749da0f4 Mon Sep 17 00:00:00 2001 From: rique223 Date: Mon, 9 Oct 2023 14:39:30 -0300 Subject: [PATCH 04/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 136 +++++++++--------- .../views/admin/users/AdminUsersPage.tsx | 8 +- .../rocketchat-i18n/i18n/en.i18n.json | 6 +- 3 files changed, 74 insertions(+), 76 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 789b40b3b41c..73b31788b639 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -202,7 +202,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { name='email' rules={{ required: t('The_field_is_required', t('Email')), - validate: (email) => (validateEmail(email) ? undefined : t('error-invalid-email-address')), + validate: (email) => (validateEmail(email) ? undefined : t('ensure_email_address_valid')), }} render={({ field }) => ( { aria-describedby={`${usernameId}-error`} error={errors.username?.message} flexGrow={1} - addon={} /> )} /> @@ -283,73 +282,6 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} - - {t('StatusMessage')} - - ( - } - /> - )} - /> - - {errors?.statusText && ( - - {errors.statusText.message} - - )} - - - {t('Bio')} - - ( - } - /> - )} - /> - - {errors?.bio && ( - - {errors.bio.message} - - )} - - - {t('Nickname')} - - ( - } /> - )} - /> - - - - {t('Password')} @@ -460,7 +392,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { value={value} onChange={onChange} flexGrow={1} - placeholder={t('Select_an_option')} + placeholder={t('Select_role')} options={availableRoles} /> )} @@ -509,10 +441,70 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { /> )} + + {t('StatusMessage')} + + ( + + )} + /> + + {errors?.statusText && ( + + {errors.statusText.message} + + )} + + + {t('Bio')} + + ( + + )} + /> + + {errors?.bio && ( + + {errors.bio.message} + + )} + + + {t('Nickname')} + + } /> + + + + {Boolean(customFieldsMetadata.length) && ( <> - - {t('Custom_Fields')} + )} diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index e0e88bedcd90..1829766aa5cc 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 } 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'; @@ -90,7 +90,11 @@ const UsersPage = (): ReactElement => { {context === 'info' && t('User_Info')} {context === 'edit' && t('Edit_User')} - {context === 'new' && t('Add_User')} + {context === 'new' && ( + <> + {t('New_user')} + + )} {context === 'invite' && t('Invite_Users')} router.navigate('/admin/users')} /> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index be348b485db5..b1135a97a849 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -313,7 +313,6 @@ "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", @@ -1888,6 +1887,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", @@ -2449,6 +2449,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\"?", @@ -3655,6 +3656,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", @@ -4589,7 +4591,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", From 33b3ff643960d3c95a3e191753f51d976d8ae79a Mon Sep 17 00:00:00 2001 From: rique223 Date: Mon, 9 Oct 2023 15:01:34 -0300 Subject: [PATCH 05/20] 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 --- .../client/views/admin/users/AdminUserForm.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 73b31788b639..beff65ba56a2 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -8,7 +8,6 @@ import { Box, ToggleSwitch, Icon, - Divider, FieldGroup, ContextualbarFooter, ButtonGroup, @@ -28,7 +27,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -173,6 +172,8 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const setRandomPasswordId = useUniqueId(); const passwordVerifierId = useUniqueId(); + const [showCustomFields, setShowCustomFields] = useState(true); + return ( <> @@ -502,10 +503,18 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { {Boolean(customFieldsMetadata.length) && ( <> - - + {showCustomFields && } )} From a1f91839787e6a62a1257cea504b416d5e85afda Mon Sep 17 00:00:00 2001 From: rique223 Date: Mon, 9 Oct 2023 18:40:32 -0300 Subject: [PATCH 06/20] 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. --- .../views/admin/users/AdminUserCreated.tsx | 32 ++++++++++ .../views/admin/users/AdminUserForm.tsx | 59 +++++++++---------- .../views/admin/users/AdminUsersPage.tsx | 4 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 + 4 files changed, 64 insertions(+), 33 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/AdminUserCreated.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..f69a0911e0ac --- /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 }: { uid: string }) => { + const t = useTranslation(); + const router = useRouter(); + + const goToUser = useCallback((id) => router.navigate(`/admin/users/info/${id}`), [router]); + + return ( + <> + + {t('You_have_created_one_user')} + + + + + + + + + ); +}; + +export default AdminUserCreated; diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index beff65ba56a2..09386a093e59 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -10,7 +10,6 @@ import { Icon, FieldGroup, ContextualbarFooter, - ButtonGroup, Button, Callout, } from '@rocket.chat/fuselage'; @@ -27,7 +26,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useCallback, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -54,10 +53,12 @@ const getInitialValue = ({ data, defaultUserRoles, isSmtpEnabled, + isNewUserPage, }: { data?: Serialized; defaultUserRoles?: IUser['roles']; isSmtpEnabled?: boolean; + isNewUserPage?: boolean; }): userFormProps => ({ roles: data?.roles ?? defaultUserRoles, name: data?.name ?? '', @@ -67,8 +68,8 @@ const getInitialValue = ({ nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: true, - setPasswordManually: false, + setRandomPassword: isNewUserPage, + setPasswordManually: !isNewUserPage, requirePasswordChange: data?.requirePasswordChange || false, customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', @@ -93,29 +94,32 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { const { data } = 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 { control, watch, handleSubmit, - // reset, formState: { errors, isDirty }, setValue, + resetField, } = useForm({ - defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled }), + defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isNewUserPage: !userData?._id }), mode: 'onBlur', }); + useEffect(() => { + resetField('sendWelcomeEmail', { defaultValue: isSmtpEnabled }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSmtpEnabled]); + + 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 { avatar, username, setRandomPassword, password } = watch(); const updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); @@ -135,12 +139,12 @@ 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); + router.navigate(`/admin/users/created/${_id}`); onReload(); }, onError: (error) => { @@ -194,7 +198,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { /> )} - {t('Manually_created_users_briefing')} + {!userData?._id && {t('Manually_created_users_briefing')}} {t('Email')} @@ -520,18 +524,9 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { - - {/* */} - - + ); diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 1829766aa5cc..284e31561f81 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -8,6 +8,7 @@ import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; import AdminInviteUsers from './AdminInviteUsers'; +import AdminUserCreated from './AdminUserCreated'; import AdminUserForm from './AdminUserForm'; import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; @@ -90,7 +91,7 @@ const UsersPage = (): ReactElement => { {context === 'info' && t('User_Info')} {context === 'edit' && t('Edit_User')} - {context === 'new' && ( + {(context === 'new' || context === 'created') && ( <> {t('New_user')} @@ -102,6 +103,7 @@ const UsersPage = (): ReactElement => { {context === 'info' && id && } {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 b1135a97a849..7d7440bdec82 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -307,6 +307,7 @@ "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", @@ -5769,6 +5770,7 @@ "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_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.", From 28ececa90324aff64f560f937efff5941ccb36f5 Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 11 Oct 2023 15:46:31 -0300 Subject: [PATCH 07/20] 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. --- .../views/admin/users/AdminUserCreated.tsx | 4 +- .../views/admin/users/AdminUserForm.tsx | 131 +++++++++--------- .../users/AdminUserSetRandomPassword.tsx | 20 +-- .../views/admin/users/AdminUsersPage.tsx | 5 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 + 5 files changed, 83 insertions(+), 79 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserCreated.tsx b/apps/meteor/client/views/admin/users/AdminUserCreated.tsx index f69a0911e0ac..ac793c1ac756 100644 --- a/apps/meteor/client/views/admin/users/AdminUserCreated.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserCreated.tsx @@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; -const AdminUserCreated = ({ uid }: { uid: string }) => { +const AdminUserCreated = ({ uid, createdUsersCount }: { uid: string; createdUsersCount: number }) => { const t = useTranslation(); const router = useRouter(); @@ -13,7 +13,7 @@ const AdminUserCreated = ({ uid }: { uid: string }) => { return ( <> - {t('You_have_created_one_user')} + {createdUsersCount === 1 ? t('You_have_created_one_user') : t('You_have_created_users', { count: createdUsersCount })} diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 09386a093e59..935bd0118945 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -7,11 +7,14 @@ import { MultiSelectFiltered, Box, ToggleSwitch, - Icon, FieldGroup, ContextualbarFooter, Button, Callout, + FieldLabel, + FieldRow, + FieldError, + FieldHint, } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -42,6 +45,7 @@ import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { userData?: Serialized; onReload: () => void; + setCreatedUsersCount: React.Dispatch>; }; export type userFormProps = Omit< @@ -79,7 +83,7 @@ const getInitialValue = ({ passwordConfirmation: '', }); -const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { +const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); @@ -144,6 +148,7 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { await eventStats({ params: [{ eventName: 'updateCounter', settingsId: 'Manual_Entry_User_Count' }], }); + setCreatedUsersCount((prevUsersCount) => prevUsersCount + 1); router.navigate(`/admin/users/created/${_id}`); onReload(); }, @@ -200,8 +205,8 @@ const UserForm = ({ userData, onReload, ...props }: AdminUserFormProps) => { )} {!userData?._id && {t('Manually_created_users_briefing')}} - {t('Email')} - + {t('Email')} + { /> )} /> - + {errors?.email && ( - + {errors.email.message} - + )} - - + + {t('Mark_email_as_verified')} - + } /> - + - {t('Name')} - + {t('Name')} + { /> )} /> - + {errors?.name && ( - + {errors.name.message} - + )} - {t('Username')} - + {t('Username')} + { /> )} /> - + {errors?.username && ( - + {errors.username.message} - + )} - + {t('Password')} - + { setValue={setValue} /> {!isSmtpEnabled && ( - )} {!setRandomPassword && ( <> - - {t('Require_password_change')} - + + {t('Require_password_change')} + { /> )} /> - + - + { aria-describedby={`${passwordId}-error`} error={errors.password?.message} flexGrow={1} - addon={} placeholder={passwordPlaceholder || t('Password')} /> )} /> - + {errors?.password && ( - + {errors.password.message} - + )} {requiresPasswordConfirmation && ( - + { aria-describedby={`${passwordConfirmationId}-error`} error={errors.passwordConfirmation?.message} flexGrow={1} - addon={} placeholder={passwordConfirmationPlaceholder || t('Confirm_password')} /> )} /> - + )} {errors?.passwordConfirmation && ( - + {errors.passwordConfirmation.message} - + )} )} - {t('Roles')} - + {t('Roles')} + {roleError && {roleError}} {!roleError && ( { )} /> )} - - {errors?.roles && {errors.roles.message}} + + {errors?.roles && {errors.roles.message}} - {t('Join_default_channels')} - + {t('Join_default_channels')} + { )} /> - + - {t('Send_welcome_email')} - + {t('Send_welcome_email')} + { /> )} /> - + {!isSmtpEnabled && ( - )} - {t('StatusMessage')} - + {t('StatusMessage')} + { /> )} /> - + {errors?.statusText && ( - + {errors.statusText.message} - + )} - {t('Bio')} - + {t('Bio')} + { /> )} /> - + {errors?.bio && ( - + {errors.bio.message} - + )} - {t('Nickname')} - + {t('Nickname')} + } /> - + - - {Boolean(customFieldsMetadata.length) && ( <> {showCustomFields && } diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx index ef1eeedf13b9..38cd38b36800 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx @@ -1,4 +1,4 @@ -import { Box, Field, RadioButton } from '@rocket.chat/fuselage'; +import { Box, 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'; @@ -29,7 +29,7 @@ const AdminUserSetRandomPassword = ({ return ( <> - + )} /> - - + + {t('Set_randomly_and_send_by_email')} - + - - + + )} /> - - + + {t('Set_manually')} - + ); diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 284e31561f81..d3259ed48f5c 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -27,6 +27,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) { @@ -102,8 +103,8 @@ const UsersPage = (): ReactElement => { {context === 'info' && id && } {context === 'edit' && id && } - {context === 'new' && } - {context === 'created' && 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 7d7440bdec82..dc49bd247d12 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4701,6 +4701,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", @@ -5771,6 +5772,7 @@ "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.", From 35a1e496953aa12b209f2c3ad2105f121bde3f09 Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 11 Oct 2023 18:22:23 -0300 Subject: [PATCH 08/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 59 ++++++++++++------- .../users/AdminUserSetRandomPassword.tsx | 39 +++++------- .../rocketchat-i18n/i18n/en.i18n.json | 4 +- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 935bd0118945..f6e5867bf20e 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -15,6 +15,7 @@ import { FieldRow, FieldError, FieldHint, + Icon, } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useUniqueId, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -48,10 +49,7 @@ type AdminUserFormProps = { setCreatedUsersCount: React.Dispatch>; }; -export type userFormProps = Omit< - UserCreateParamsPOST & { setPasswordManually: boolean; avatar: AvatarObject; passwordConfirmation: string }, - 'fields' ->; +export type userFormProps = Omit; const getInitialValue = ({ data, @@ -72,8 +70,7 @@ const getInitialValue = ({ nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: isNewUserPage, - setPasswordManually: !isNewUserPage, + setRandomPassword: isNewUserPage && isSmtpEnabled, requirePasswordChange: data?.requirePasswordChange || false, customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', @@ -93,6 +90,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); + const isVerificationNeeded = useSetting('Accounts_EmailVerification'); const defaultUserRoles = parseCSV(defaultRoles); const { data } = useSmtpQuery(); @@ -103,7 +101,6 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU watch, handleSubmit, formState: { errors, isDirty }, - setValue, resetField, } = useForm({ defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isNewUserPage: !userData?._id }), @@ -115,6 +112,11 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSmtpEnabled]); + useEffect(() => { + resetField('setRandomPassword', { defaultValue: !userData?._id && isSmtpEnabled }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSmtpEnabled, userData?._id]); + const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); const updateUserAction = useEndpoint('POST', '/v1/users.update'); const createUserAction = useEndpoint('POST', '/v1/users.create'); @@ -158,7 +160,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU }); const handleSaveUser = useMutableCallback(async (userFormPayload: userFormProps) => { - const { avatar, setPasswordManually, passwordConfirmation, ...userFormData } = userFormPayload; + const { avatar, passwordConfirmation, ...userFormData } = userFormPayload; if (userData?._id) { return handleUpdateUser.mutateAsync({ userId: userData?._id, data: userFormData }); } @@ -232,15 +234,33 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU )} - - {t('Mark_email_as_verified')} - + + + {t('Mark_email_as_verified')} + + + } + render={({ field: { onChange, value } }) => ( + + )} /> + {/* */} + {isVerificationNeeded && !isSmtpEnabled && ( + + )} + {!isVerificationNeeded && ( + + )} {t('Name')} @@ -300,15 +320,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU setRandomPasswordId={setRandomPasswordId} control={control} isSmtpEnabled={isSmtpEnabled || false} - setRandomPassword={setRandomPassword || false} - setValue={setValue} /> - {!isSmtpEnabled && ( - - )} {!setRandomPassword && ( <> @@ -424,8 +436,10 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU - - {t('Send_welcome_email')} + + + {t('Send_welcome_email')} + )} diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx index 38cd38b36800..147fe3cbd93c 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx @@ -1,8 +1,8 @@ -import { Box, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/fuselage'; +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 type { Control } from 'react-hook-form'; import { Controller } from 'react-hook-form'; import type { userFormProps } from './AdminUserForm'; @@ -10,18 +10,10 @@ import type { userFormProps } from './AdminUserForm'; type AdminUserSetRandomPasswordProps = { control: Control; isSmtpEnabled: boolean; - setRandomPassword: boolean; - setValue: UseFormSetValue; setRandomPasswordId: string; }; -const AdminUserSetRandomPassword = ({ - control, - isSmtpEnabled, - setRandomPassword, - setValue, - setRandomPasswordId, -}: AdminUserSetRandomPasswordProps) => { +const AdminUserSetRandomPassword = ({ control, isSmtpEnabled, setRandomPasswordId }: AdminUserSetRandomPasswordProps) => { const t = useTranslation(); const setPasswordManuallyId = useUniqueId(); @@ -39,35 +31,36 @@ const AdminUserSetRandomPassword = ({ id={setRandomPasswordId} aria-describedby={`${setRandomPasswordId}-hint`} checked={value} - onChange={() => { - setValue('setPasswordManually', false); - onChange(true); - }} + onChange={() => onChange(true)} disabled={!isSmtpEnabled} /> )} /> - + {t('Set_randomly_and_send_by_email')} + {!isSmtpEnabled && ( + + )} ( { - setValue('setRandomPassword', false); - onChange(true); - }} - disabled={!isSmtpEnabled} + checked={!value} + onChange={() => onChange(false)} /> )} /> diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index dc49bd247d12..0cb52873da1d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -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", + "Activate_to_bypass_email_verification": "Activate to bypass email verification", "Active": "Active", "Active_users": "Active users", "Activity": "Activity", @@ -1843,6 +1844,7 @@ "Email_subject": "Email Subject", "Email_verified": "Email verified", "Email_sent": "Email sent", + "Email_verification_isnt_required": "Email verification is not required. Change this registration option in accounts settings to enable.", "Emoji": "Emoji", "Emoji_picker": "Emoji picker", "EmojiCustomFilesystem": "Custom Emoji Filesystem", @@ -4613,7 +4615,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.", From ff44564a40049f89b3c7733f046ce903c1cf0cc5 Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 11 Oct 2023 18:26:52 -0300 Subject: [PATCH 09/20] 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" --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0cb52873da1d..4fd9f3076ba0 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. ", From 48f3e333256b32b1351179e43a718b9d6777fd5e Mon Sep 17 00:00:00 2001 From: rique223 Date: Mon, 16 Oct 2023 17:13:05 -0300 Subject: [PATCH 10/20] 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. --- .../client/views/admin/users/AdminUserForm.tsx | 12 +++++++++--- .../packages/rocketchat-i18n/i18n/en.i18n.json | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index f6e5867bf20e..5b576bba65b1 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -107,6 +107,8 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU mode: 'onBlur', }); + const { avatar, username, setRandomPassword, password } = watch(); + useEffect(() => { resetField('sendWelcomeEmail', { defaultValue: isSmtpEnabled }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -117,6 +119,12 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSmtpEnabled, userData?._id]); + useEffect(() => { + resetField('requirePasswordChange', { defaultValue: setRandomPassword }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setRandomPassword]); + const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); const updateUserAction = useEndpoint('POST', '/v1/users.update'); const createUserAction = useEndpoint('POST', '/v1/users.create'); @@ -126,8 +134,6 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU const availableRoles: SelectOption[] = roleData?.roles.map(({ _id, name, description }) => [_id, description || name]) || []; - const { avatar, username, setRandomPassword, password } = watch(); - const updateAvatar = useUpdateAvatar(avatar, userData?._id || ''); const handleUpdateUser = useMutation({ @@ -238,7 +244,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU {t('Mark_email_as_verified')} - +
accounts settings to enable.", + "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", @@ -3292,7 +3292,7 @@ "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", "Manual_Selection": "Manual Selection", - "Manually_created_users_briefing": "Manually created users will initially be listed under the 'Pending' tab. Once they log in for the first time, they will be moved to the 'Active' tab.", + "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.", From 7ca48e486d7752bc285f337e7add760b7b42e38c Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 18 Oct 2023 11:16:11 -0300 Subject: [PATCH 11/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 91 +++++++++---------- .../admin/users/AdminUserFormWithData.tsx | 5 +- .../users/AdminUserSetRandomPassword.tsx | 32 ++++++- .../views/admin/users/AdminUsersPage.tsx | 4 +- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 5b576bba65b1..999c89e6bd84 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -30,7 +30,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useQuery, useMutation } from '@tanstack/react-query'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -46,7 +46,8 @@ import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { userData?: Serialized; onReload: () => void; - setCreatedUsersCount: React.Dispatch>; + setCreatedUsersCount?: React.Dispatch>; + context: string; }; export type userFormProps = Omit; @@ -70,8 +71,8 @@ const getInitialValue = ({ nickname: data?.nickname ?? '', email: (data?.emails?.length && data.emails[0].address) || '', verified: (data?.emails?.length && data.emails[0].verified) || false, - setRandomPassword: isNewUserPage && isSmtpEnabled, - requirePasswordChange: data?.requirePasswordChange || false, + setRandomPassword: (isNewUserPage && isSmtpEnabled) ?? true, + requirePasswordChange: isNewUserPage && (data?.requirePasswordChange ?? true), customFields: data?.customFields ?? {}, statusText: data?.statusText ?? '', joinDefaultChannels: true, @@ -80,7 +81,7 @@ const getInitialValue = ({ passwordConfirmation: '', }); -const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminUserFormProps) => { +const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); @@ -93,38 +94,23 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU const isVerificationNeeded = useSetting('Accounts_EmailVerification'); const defaultUserRoles = parseCSV(defaultRoles); - const { data } = useSmtpQuery(); + const { data, isSuccess: isSmtpStatusAvailable } = useSmtpQuery(); const isSmtpEnabled = data?.isSMTPConfigured; + const isNewUserPage = context === 'new'; const { control, watch, handleSubmit, formState: { errors, isDirty }, - resetField, + setValue, } = useForm({ - defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isNewUserPage: !userData?._id }), + defaultValues: getInitialValue({ data: userData, defaultUserRoles, isSmtpEnabled, isNewUserPage }), mode: 'onBlur', }); const { avatar, username, setRandomPassword, password } = watch(); - useEffect(() => { - resetField('sendWelcomeEmail', { defaultValue: isSmtpEnabled }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSmtpEnabled]); - - useEffect(() => { - resetField('setRandomPassword', { defaultValue: !userData?._id && isSmtpEnabled }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSmtpEnabled, userData?._id]); - - useEffect(() => { - resetField('requirePasswordChange', { defaultValue: setRandomPassword }); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setRandomPassword]); - const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); const updateUserAction = useEndpoint('POST', '/v1/users.update'); const createUserAction = useEndpoint('POST', '/v1/users.create'); @@ -156,7 +142,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU await eventStats({ params: [{ eventName: 'updateCounter', settingsId: 'Manual_Entry_User_Count' }], }); - setCreatedUsersCount((prevUsersCount) => prevUsersCount + 1); + setCreatedUsersCount?.((prevUsersCount) => prevUsersCount + 1); router.navigate(`/admin/users/created/${_id}`); onReload(); }, @@ -167,9 +153,11 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU const handleSaveUser = useMutableCallback(async (userFormPayload: userFormProps) => { const { avatar, passwordConfirmation, ...userFormData } = userFormPayload; - if (userData?._id) { + + if (!isNewUserPage && userData?._id) { return handleUpdateUser.mutateAsync({ userId: userData?._id, data: userFormData }); } + return handleCreateUser.mutateAsync({ ...userFormData, fields: '' }); }); @@ -191,11 +179,15 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU const [showCustomFields, setShowCustomFields] = useState(true); + if (!context) { + return null; + } + return ( <> - {userData?._id && ( + {!isNewUserPage && ( )} - {!userData?._id && {t('Manually_created_users_briefing')}} + {isNewUserPage && {t('Manually_created_users_briefing')}} {t('Email')} @@ -254,7 +246,6 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU )} /> - {/* */} {isVerificationNeeded && !isSmtpEnabled && ( {!setRandomPassword && ( <> @@ -340,7 +334,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU ref={ref} id={requirePasswordChangeId} disabled={setRandomPassword} - checked={setRandomPassword || value} + checked={value} onChange={onChange} /> )} @@ -351,7 +345,7 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU ( (watch('password') === val ? true : t('Invalid_confirm_pass')), }} @@ -447,19 +441,22 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU {t('Send_welcome_email')}
- ( - - )} - /> + {isSmtpStatusAvailable && ( + ( + + )} + /> + )} {!isSmtpEnabled && ( @@ -554,4 +551,4 @@ const UserForm = ({ userData, onReload, setCreatedUsersCount, ...props }: AdminU ); }; -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..83b64d580511 100644 --- a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx @@ -12,9 +12,10 @@ 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 }); @@ -42,7 +43,7 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R ); } - return ; + return ; }; export default AdminUserFormWithData; diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx index 147fe3cbd93c..f7033fcc0e9e 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx @@ -2,22 +2,42 @@ import { Box, FieldHint, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { Control } from 'react-hook-form'; +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; - isSmtpEnabled: boolean; + isSmtpStatusAvailable: boolean; + isSmtpEnabled: boolean | undefined; setRandomPasswordId: string; + setValue: UseFormSetValue; }; -const AdminUserSetRandomPassword = ({ control, isSmtpEnabled, setRandomPasswordId }: AdminUserSetRandomPasswordProps) => { +const AdminUserSetRandomPassword = ({ + 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 ( <> @@ -25,13 +45,14 @@ const AdminUserSetRandomPassword = ({ control, isSmtpEnabled, setRandomPasswordI ( onChange(true)} + onChange={() => handleSetRandomPasswordChange(onChange, true)} disabled={!isSmtpEnabled} /> )} @@ -54,13 +75,14 @@ const AdminUserSetRandomPassword = ({ control, isSmtpEnabled, setRandomPasswordI ( onChange(false)} + onChange={() => handleSetRandomPasswordChange(onChange, false)} /> )} /> diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index d3259ed48f5c..8f7b7649d4bc 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -102,8 +102,8 @@ const UsersPage = (): ReactElement => { router.navigate('/admin/users')} /> {context === 'info' && id && } - {context === 'edit' && id && } - {context === 'new' && } + {context === 'edit' && id && } + {context === 'new' && } {context === 'created' && id && } {context === 'invite' && } From e0825be97422953a21ebf80d5ee122f03960950d Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 18 Oct 2023 11:43:18 -0300 Subject: [PATCH 12/20] 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. --- .../views/admin/users/AdminUserForm.tsx | 95 ++------------- .../AdminUserSetRandomPasswordContent.tsx | 109 ++++++++++++++++++ ...x => AdminUserSetRandomPasswordRadios.tsx} | 4 +- 3 files changed, 123 insertions(+), 85 deletions(-) create mode 100644 apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordContent.tsx rename apps/meteor/client/views/admin/users/{AdminUserSetRandomPassword.tsx => AdminUserSetRandomPasswordRadios.tsx} (96%) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 999c89e6bd84..5fbe122ff68e 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -3,7 +3,6 @@ import { Field, TextInput, TextAreaInput, - PasswordInput, MultiSelectFiltered, Box, ToggleSwitch, @@ -20,7 +19,7 @@ import { 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, PasswordVerifier } from '@rocket.chat/ui-client'; +import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, useSetting, @@ -40,7 +39,8 @@ 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 AdminUserSetRandomPassword from './AdminUserSetRandomPassword'; +import AdminUserSetRandomPasswordContent from './AdminUserSetRandomPasswordContent'; +import AdminUserSetRandomPasswordRadios from './AdminUserSetRandomPasswordRadios'; import { useSmtpQuery } from './hooks/useSmtpQuery'; type AdminUserFormProps = { @@ -88,9 +88,6 @@ const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...p const customFieldsMetadata = useAccountsCustomFields(); const defaultRoles = useSetting('Accounts_Registration_Users_Default_Roles') || ''; - const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); - const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); - const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); const isVerificationNeeded = useSetting('Accounts_EmailVerification'); const defaultUserRoles = parseCSV(defaultRoles); @@ -169,13 +166,10 @@ const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...p const bioId = useUniqueId(); const nicknameId = useUniqueId(); const passwordId = useUniqueId(); - const passwordConfirmationId = useUniqueId(); - const requirePasswordChangeId = useUniqueId(); const rolesId = useUniqueId(); const joinDefaultChannelsId = useUniqueId(); const sendWelcomeEmailId = useUniqueId(); const setRandomPasswordId = useUniqueId(); - const passwordVerifierId = useUniqueId(); const [showCustomFields, setShowCustomFields] = useState(true); @@ -313,7 +307,7 @@ const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...p {t('Password')} - {!setRandomPassword && ( - <> - - {t('Require_password_change')} - - ( - - )} - /> - - - - ( - - )} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - {requiresPasswordConfirmation && ( - - (watch('password') === val ? true : t('Invalid_confirm_pass')), - }} - render={({ field }) => ( - - )} - /> - - )} - {errors?.passwordConfirmation && ( - - {errors.passwordConfirmation.message} - - )} - - + )} 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/AdminUserSetRandomPassword.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx similarity index 96% rename from apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx rename to apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx index f7033fcc0e9e..1eea93639edb 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPassword.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx @@ -16,7 +16,7 @@ type AdminUserSetRandomPasswordProps = { setValue: UseFormSetValue; }; -const AdminUserSetRandomPassword = ({ +const AdminUserSetRandomPasswordRadios = ({ isNewUserPage, control, isSmtpStatusAvailable, @@ -95,4 +95,4 @@ const AdminUserSetRandomPassword = ({ ); }; -export default AdminUserSetRandomPassword; +export default AdminUserSetRandomPasswordRadios; From 6eb334d100e5c189c75b1ada9b9b0dc10ac4baf2 Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 18 Oct 2023 12:54:54 -0300 Subject: [PATCH 13/20] 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. --- .../client/views/admin/users/AdminUserForm.tsx | 4 +++- .../views/admin/users/AdminUserFormWithData.tsx | 13 +++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 5fbe122ff68e..3071ccb8c63a 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -48,6 +48,7 @@ type AdminUserFormProps = { onReload: () => void; setCreatedUsersCount?: React.Dispatch>; context: string; + refetchUserFormData?: () => void; }; export type userFormProps = Omit; @@ -81,7 +82,7 @@ const getInitialValue = ({ passwordConfirmation: '', }); -const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...props }: AdminUserFormProps) => { +const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, refetchUserFormData, ...props }: AdminUserFormProps) => { const t = useTranslation(); const router = useRouter(); const dispatchToastMessage = useToastMessageDispatch(); @@ -126,6 +127,7 @@ const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, ...p await updateAvatar(); router.navigate(`/admin/users/info/${_id}`); onReload(); + refetchUserFormData?.(); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx index 83b64d580511..a584cf284c0f 100644 --- a/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx @@ -17,7 +17,7 @@ type AdminUserFormWithDataProps = { 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 ( @@ -43,7 +43,16 @@ const AdminUserFormWithData = ({ uid, onReload, context }: AdminUserFormWithData ); } - return ; + return ( + { + refetch(); + }} + /> + ); }; export default AdminUserFormWithData; From af3ddaf7ec702b5c5719e108fb9980c96fb256c2 Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 18 Oct 2023 15:46:28 -0300 Subject: [PATCH 14/20] Move tabs component to Page instead of Page content --- .../views/admin/users/AdminUsersPage.tsx | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 8f7b7649d4bc..1f8c3abd93f8 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, Icon, 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'; @@ -14,6 +14,9 @@ import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; import UsersTable from './UsersTable'; +import PageHeader from '/client/components/Page/PageHeader'; +import PageContent from '/client/components/Page/PageContent'; + const UsersPage = (): ReactElement => { const t = useTranslation(); const seatsCap = useSeatsCap(); @@ -47,7 +50,7 @@ const UsersPage = (): ReactElement => { return ( - + {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( ) : ( @@ -64,27 +67,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 && ( From f7caac0f7c7b8adb1a36174e6fab291ed8c1a4ba Mon Sep 17 00:00:00 2001 From: rique223 Date: Wed, 18 Oct 2023 22:18:35 -0300 Subject: [PATCH 15/20] Fix imports --- apps/meteor/client/views/admin/users/AdminUsersPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 1f8c3abd93f8..ab07134e0794 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -7,6 +7,8 @@ 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'; @@ -14,9 +16,6 @@ import AdminUserFormWithData from './AdminUserFormWithData'; import AdminUserInfoWithData from './AdminUserInfoWithData'; import UsersTable from './UsersTable'; -import PageHeader from '/client/components/Page/PageHeader'; -import PageContent from '/client/components/Page/PageContent'; - const UsersPage = (): ReactElement => { const t = useTranslation(); const seatsCap = useSeatsCap(); From b3705ff8e56d7012fc527047236d655df64a576f Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 20 Oct 2023 14:48:17 -0300 Subject: [PATCH 16/20] First test fix --- .../tests/e2e/page-objects/fragments/admin-flextab-users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..507253625335 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 @@ -36,7 +36,7 @@ export class AdminFlextabUsers { } get checkboxVerified(): Locator { - return this.page.locator('//label[text()="Verified"]'); + return this.page.locator('//label[text()="Mark email as verified"]'); } get userRole(): Locator { From 8b0ec3af13b48d72e480da501a3c7234a9fefa53 Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 20 Oct 2023 16:29:18 -0300 Subject: [PATCH 17/20] Fix create user test --- apps/meteor/tests/e2e/administration.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index b439258429f8..8b2728317552 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -42,7 +42,6 @@ test.describe.parallel('administration', () => { 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.inputPassword.type('any_password'); await expect(poAdmin.tabs.users.userRole).toBeVisible(); await poAdmin.tabs.users.btnSave.click(); From 8473b81da7b164f7adf539a92233ca85e37252c4 Mon Sep 17 00:00:00 2001 From: rique223 Date: Fri, 20 Oct 2023 18:39:59 -0300 Subject: [PATCH 18/20] Fix tests --- apps/meteor/tests/e2e/administration.spec.ts | 4 +++- .../e2e/page-objects/fragments/admin-flextab-users.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 8b2728317552..85a016daaf6c 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -39,10 +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.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 507253625335..a38c43699cbc 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 @@ -31,8 +31,16 @@ 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 randomly and send by email"]'); + } + 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 { From 5547b297b5ef211dcea1c749d76ed0f0708f8eee Mon Sep 17 00:00:00 2001 From: rique223 Date: Sun, 22 Oct 2023 22:21:43 -0300 Subject: [PATCH 19/20] Fix test typo --- .../tests/e2e/page-objects/fragments/admin-flextab-users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a38c43699cbc..7fd253290e5b 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 @@ -32,7 +32,7 @@ export class AdminFlextabUsers { } get inputSetManually(): Locator { - return this.page.locator('//label[text()="Set randomly and send by email"]'); + return this.page.locator('//label[text()="Set manually"]'); } get inputPassword(): Locator { From 2ceae99ca5b2942101997f705fac13a96afe88b9 Mon Sep 17 00:00:00 2001 From: rique223 Date: Sun, 22 Oct 2023 23:33:00 -0300 Subject: [PATCH 20/20] Test fix --- .../tests/e2e/page-objects/fragments/admin-flextab-users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7fd253290e5b..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 {