From 21d4ec4ef37f08502f083f77af1fb80ce60581d7 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 1 Feb 2021 22:51:36 -0300 Subject: [PATCH] [IMPROVE] Add visual validation on users admin forms (#20308) Co-authored-by: Gabriel Henriques --- client/views/admin/users/AddUser.js | 34 ++++++++++++++++++++++----- client/views/admin/users/EditUser.js | 35 ++++++++++++++++++++++------ client/views/admin/users/UserForm.js | 30 +++++++++++++++++------- client/views/admin/users/UserInfo.js | 3 +-- 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/client/views/admin/users/AddUser.js b/client/views/admin/users/AddUser.js index d0299b11f41..4ac0662b3dd 100644 --- a/client/views/admin/users/AddUser.js +++ b/client/views/admin/users/AddUser.js @@ -1,5 +1,6 @@ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { Field, Box, Button } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useEndpointData } from '../../../hooks/useEndpointData'; @@ -15,6 +16,18 @@ export function AddUser({ roles, ...props }) { const router = useRoute('admin-users'); const { value: roleData } = useEndpointData('roles.list', ''); + const [errors, setErrors] = useState({}); + + const validationKeys = { + name: (name) => setErrors((errors) => ({ ...errors, name: !name.trim().length ? t('The_field_is_required', t('name')) : undefined })), + username: (username) => setErrors((errors) => ({ ...errors, username: !username.trim().length ? t('The_field_is_required', t('username')) : undefined })), + email: (email) => setErrors((errors) => ({ ...errors, email: !email.trim().length ? t('The_field_is_required', t('email')) : undefined })), + password: (password) => setErrors((errors) => ({ ...errors, password: !password.trim().length ? t('The_field_is_required', t('password')) : undefined })), + }; + + const validateForm = ({ key, value }) => { + validationKeys[key] && validationKeys[key](value); + }; const { values, @@ -36,21 +49,30 @@ export function AddUser({ roles, ...props }) { sendWelcomeEmail: true, joinDefaultChannels: true, customFields: {}, - }); + }, validateForm); const goToUser = useCallback((id) => router.push({ context: 'info', id, }), [router]); - const saveAction = useEndpointAction('POST', 'users.create', values, t('User_created_successfully')); + const saveAction = useEndpointAction('POST', 'users.create', values, t('User_created_successfully!')); + + const handleSave = useMutableCallback(async () => { + Object.entries(values).forEach(([key, value]) => { + validateForm({ key, value }); + }); + + const { name, username, password, email } = values; + if (name === '' || username === '' || password === '' || email === '') { + return false; + } - const handleSave = useCallback(async () => { const result = await saveAction(); if (result.success) { goToUser(result.user._id); } - }, [goToUser, saveAction]); + }); const availableRoles = useMemo(() => roleData?.roles?.map(({ _id, description }) => [_id, description || _id]) ?? [], [roleData]); @@ -63,5 +85,5 @@ export function AddUser({ roles, ...props }) { , [hasUnsavedChanges, reset, t, handleSave]); - return ; + return ; } diff --git a/client/views/admin/users/EditUser.js b/client/views/admin/users/EditUser.js index 5d97ec5ad51..c4da7d9001c 100644 --- a/client/views/admin/users/EditUser.js +++ b/client/views/admin/users/EditUser.js @@ -1,5 +1,6 @@ import React, { useMemo, useState, useCallback } from 'react'; import { Box, Field, Margins, Button, Callout } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; @@ -18,7 +19,7 @@ export function EditUserWithData({ uid, ...props }) { const { value: data, phase: state, error } = useEndpointData('users.info', useMemo(() => ({ userId: uid }), [uid])); if ([state, roleState].includes(AsyncStatePhase.LOADING)) { - return ; + return ; } if (error || roleError) { @@ -48,8 +49,19 @@ export function EditUser({ data, roles, ...props }) { const t = useTranslation(); const [avatarObj, setAvatarObj] = useState(); + const [errors, setErrors] = useState({}); - const { values, handlers, reset, hasUnsavedChanges } = useForm(getInitialValue(data)); + const validationKeys = { + name: (name) => setErrors((errors) => ({ ...errors, name: !name.trim().length ? t('The_field_is_required', t('name')) : undefined })), + username: (username) => setErrors((errors) => ({ ...errors, username: !username.trim().length ? t('The_field_is_required', t('username')) : undefined })), + email: (email) => setErrors((errors) => ({ ...errors, email: !email.trim().length ? t('The_field_is_required', t('email')) : undefined })), + }; + + const validateForm = ({ key, value }) => { + validationKeys[key] && validationKeys[key](value); + }; + + const { values, handlers, reset, hasUnsavedChanges } = useForm(getInitialValue(data), validateForm); const router = useRoute('admin-users'); @@ -88,7 +100,16 @@ export function EditUser({ data, roles, ...props }) { return saveAvatarAction(avatarObj); }, [avatarObj, resetAvatarAction, saveAvatarAction, saveAvatarUrlAction, data._id]); - const handleSave = useCallback(async () => { + const handleSave = useMutableCallback(async () => { + Object.entries(values).forEach(([key, value]) => { + validationKeys[key] && validationKeys[key](value); + }); + + const { name, username, email } = values; + if (name === '' || username === '' || email === '') { + return false; + } + const result = await saveAction(); if (result.success) { if (avatarObj) { @@ -96,7 +117,7 @@ export function EditUser({ data, roles, ...props }) { } goToUser(data._id); } - }, [avatarObj, data._id, goToUser, saveAction, updateAvatar]); + }, [avatarObj, data._id, goToUser, saveAction, updateAvatar, values, errors, validationKeys]); const availableRoles = roles.map(({ _id, description }) => [_id, description || _id]); @@ -109,11 +130,11 @@ export function EditUser({ data, roles, ...props }) { - + - , [handleSave, canSaveOrReset, reset, t, values]); + , [handleSave, canSaveOrReset, reset, t]); - return ; + return ; } diff --git a/client/views/admin/users/UserForm.js b/client/views/admin/users/UserForm.js index 2bbb32a3e7e..a065c6fc508 100644 --- a/client/views/admin/users/UserForm.js +++ b/client/views/admin/users/UserForm.js @@ -6,7 +6,7 @@ import { isEmail } from '../../../../app/utils/lib/isEmail.js'; import VerticalBar from '../../../components/VerticalBar'; import CustomFieldsForm from '../../../components/CustomFieldsForm'; -export default function UserForm({ formValues, formHandlers, availableRoles, append, prepend, ...props }) { +export default function UserForm({ formValues, formHandlers, availableRoles, append, prepend, errors, ...props }) { const t = useTranslation(); const [hasCustomFields, setHasCustomFields] = useState(false); @@ -52,26 +52,35 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app {useMemo(() => {t('Name')} - + - , [t, name, handleName])} + {errors && errors.name && + {errors.name} + } + , [t, name, handleName, errors])} {useMemo(() => {t('Username')} - }/> + }/> - , [t, username, handleUsername])} + {errors && errors.username && + {errors.username} + } + , [t, username, handleUsername, errors])} {useMemo(() => {t('Email')} - 0 ? 'error' : undefined} onChange={handleEmail} addon={}/> + 0 ? 'error' : undefined} onChange={handleEmail} addon={}/> + {errors && errors.email && + {errors.email} + } {t('Verified')} - , [t, email, handleEmail, verified, handleVerified])} + , [t, email, handleEmail, verified, handleVerified, errors])} {useMemo(() => {t('StatusMessage')} @@ -93,9 +102,12 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app {useMemo(() => {t('Password')} - }/> + }/> - , [t, password, handlePassword])} + {errors && errors.password && + {errors.password} + } + , [t, password, handlePassword, errors])} {useMemo(() => diff --git a/client/views/admin/users/UserInfo.js b/client/views/admin/users/UserInfo.js index 137c45e3006..5cda5b92be3 100644 --- a/client/views/admin/users/UserInfo.js +++ b/client/views/admin/users/UserInfo.js @@ -1,4 +1,3 @@ - import React, { useMemo } from 'react'; import { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -59,7 +58,7 @@ export function UserInfoWithData({ uid, username, ...props }) { }, [approveManuallyUsers, data, showRealNames]); if (state === AsyncStatePhase.LOADING) { - return ; + return ; } if (error) {