diff --git a/.changeset/prevent-whitespace-saving.md b/.changeset/prevent-whitespace-saving.md new file mode 100644 index 0000000000000..8dabee58dbd17 --- /dev/null +++ b/.changeset/prevent-whitespace-saving.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Prevent saving whitespace-only changes in nickname and bio and other fields in profile section diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 74eef8f84754b..69537e886c4ad 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -35,7 +35,7 @@ import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; import UserStatusMenu from '../../../components/UserStatusMenu'; import UserAvatarEditor from '../../../components/avatar/UserAvatarEditor'; import { useUpdateAvatar } from '../../../hooks/useUpdateAvatar'; -import { USER_STATUS_TEXT_MAX_LENGTH, BIO_TEXT_MAX_LENGTH } from '../../../lib/constants'; +import { USER_STATUS_TEXT_MAX_LENGTH } from '../../../lib/constants'; const AccountProfileForm = (props: AllHTMLAttributes): ReactElement => { const t = useTranslation(); @@ -110,16 +110,23 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle const updateAvatar = useUpdateAvatar(avatar, user?._id || ''); const handleSave = async ({ email, name, username, statusType, statusText, nickname, bio, customFields }: AccountProfileFormValues) => { + const trimmedName = name.trim(); + const trimmedUsername = username.trim(); + const trimmedEmail = email.trim(); + const trimmedStatusText = statusText?.trim() ?? ''; + const trimmedNickname = nickname?.trim() ?? ''; + const trimmedBio = bio?.trim() ?? ''; + try { await updateOwnBasicInfo({ data: { - name, - ...(user ? getUserEmailAddress(user) !== email && { email } : {}), - username, - statusText, + name: trimmedName, + ...(user ? getUserEmailAddress(user) !== trimmedEmail && { email: trimmedEmail } : {}), + username: trimmedUsername, + statusText: trimmedStatusText, statusType, - nickname, - bio, + nickname: trimmedNickname, + bio: trimmedBio, }, customFields, }); @@ -129,7 +136,16 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } finally { - reset({ email, name, username, statusType, statusText, nickname, bio, customFields }); + reset({ + email: trimmedEmail, + name: trimmedName, + username: trimmedUsername, + statusType, + statusText: trimmedStatusText, + nickname: trimmedNickname, + bio: trimmedBio, + customFields, + }); } }; @@ -283,13 +299,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('Nickname')} - ( - } /> - )} - /> + } /> @@ -298,7 +308,6 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle ( ): ReactEle rows={3} flexGrow={1} addon={} - aria-invalid={errors.statusText ? 'true' : 'false'} + aria-invalid={errors.bio ? 'true' : 'false'} aria-describedby={`${bioId}-error`} /> )} diff --git a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx index eda29d780c3d9..24d0278f0d66f 100644 --- a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx @@ -13,8 +13,8 @@ import { useLayout, } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { useId, useState, useCallback } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { useId, useState, useCallback, useMemo } from 'react'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; import AccountProfileForm from './AccountProfileForm'; import ActionConfirmModal from './ActionConfirmModal'; @@ -44,9 +44,33 @@ const AccountProfilePage = (): ReactElement => { const { reset, + control, formState: { isDirty, isSubmitting }, } = methods; + const watchedValues = useWatch({ control }); + const defaultValues = useMemo(() => getProfileInitialValues(user), [user]); + + const isTrulyDirty = useMemo(() => { + if (!isDirty || !watchedValues) return false; + + const stringFields = ['name', 'username', 'statusText', 'nickname', 'bio', 'email'] as const; + + for (const field of stringFields) { + const currentValue = (watchedValues[field] ?? '').toString().trim(); + const defaultValue = (defaultValues[field] ?? '').toString().trim(); + if (currentValue !== defaultValue) { + return true; + } + } + + if (watchedValues.avatar !== defaultValues.avatar) return true; + if (watchedValues.statusType !== defaultValues.statusType) return true; + if (JSON.stringify(watchedValues.customFields) !== JSON.stringify(defaultValues.customFields)) return true; + + return false; + }, [isDirty, watchedValues, defaultValues]); + const logoutOtherClients = useEndpoint('POST', '/v1/users.logoutOtherClients'); const deleteOwnAccount = useEndpoint('POST', '/v1/users.deleteOwnAccount'); @@ -134,12 +158,12 @@ const AccountProfilePage = (): ReactElement => { - + - -