Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/prevent-whitespace-saving.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Prevent saving whitespace-only changes in nickname and bio and other fields in profile section
43 changes: 26 additions & 17 deletions apps/meteor/client/views/account/profile/AccountProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLFormElement>): ReactElement => {
const t = useTranslation();
Expand Down Expand Up @@ -110,16 +110,23 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): 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,
});
Expand All @@ -129,7 +136,16 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): 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,
});
}
};

Expand Down Expand Up @@ -283,13 +299,7 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
<Field>
<FieldLabel htmlFor={nicknameId}>{t('Nickname')}</FieldLabel>
<FieldRow>
<Controller
control={control}
name='nickname'
render={({ field }) => (
<TextInput {...field} id={nicknameId} flexGrow={1} addon={<Icon name='edit' size='x20' alignSelf='center' />} />
)}
/>
<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} id={nicknameId} flexGrow={1} />} />
</FieldRow>
</Field>
<Field>
Expand All @@ -298,7 +308,6 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
<Controller
control={control}
name='bio'
rules={{ maxLength: { value: BIO_TEXT_MAX_LENGTH, message: t('Max_length_is', BIO_TEXT_MAX_LENGTH) } }}
render={({ field }) => (
<TextAreaInput
{...field}
Expand All @@ -307,7 +316,7 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
rows={3}
flexGrow={1}
addon={<Icon name='edit' size='x20' alignSelf='center' />}
aria-invalid={errors.statusText ? 'true' : 'false'}
aria-invalid={errors.bio ? 'true' : 'false'}
aria-describedby={`${bioId}-error`}
/>
)}
Expand Down
34 changes: 29 additions & 5 deletions apps/meteor/client/views/account/profile/AccountProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -134,12 +158,12 @@ const AccountProfilePage = (): ReactElement => {
</Box>
</Box>
</PageScrollableContentWithShadow>
<PageFooter isDirty={isDirty}>
<PageFooter isDirty={isTrulyDirty}>
<ButtonGroup>
<Button disabled={!isDirty} onClick={() => reset(getProfileInitialValues(user))}>
<Button disabled={!isTrulyDirty} onClick={() => reset(getProfileInitialValues(user))}>
{t('Cancel')}
</Button>
<Button form={profileFormId} primary disabled={!isDirty || loggingOut} loading={isSubmitting} type='submit'>
<Button form={profileFormId} primary disabled={!isTrulyDirty || loggingOut} loading={isSubmitting} type='submit'>
{t('Save_changes')}
</Button>
</ButtonGroup>
Expand Down
Loading