Skip to content

fix: prevent save button appearing for whitespace-only changes#38244

Open
Anshumancanrock wants to merge 3 commits intoRocketChat:developfrom
Anshumancanrock:space
Open

fix: prevent save button appearing for whitespace-only changes#38244
Anshumancanrock wants to merge 3 commits intoRocketChat:developfrom
Anshumancanrock:space

Conversation

@Anshumancanrock
Copy link
Contributor

@Anshumancanrock Anshumancanrock commented Jan 18, 2026

Proposed changes (including videos or screenshots)

This PR fixes an issue where the "Save Changes" button appears in the profile settings when users add only whitespace (spaces/tabs) to text fields, even though no meaningful changes have been made.

Changes:

  • Added isTrulyDirty logic in AccountProfilePage.tsx that compares trimmed values of form fields instead of raw values
  • Added trimming of string fields in handleSave in AccountProfileForm.tsx to ensure clean data is saved and the form state is reset with trimmed values
  • Prevents unnecessary API calls and improves UX by only showing the Save button when actual content changes occur

Issue(s)

Closes #38243

Steps to test or reproduce

  1. Navigate to Account Settings → Profile
  2. Click on any text field (Name, Username, Nickname, Bio, Status Message, Email)
  3. Add spaces at the beginning or end of the existing value
  4. Verify the "Save Changes" button does NOT appear
  5. Make an actual change (e.g., change "John" to "Jane")
  6. Verify the "Save Changes" button does appear
  7. Click Save and verify the values are saved correctly without leading/trailing spaces

Before Fix:

Recording.2026-01-18.220530.mp4

After Fix:

Recording.2026-01-30.005625.mp4

Summary by CodeRabbit

  • Bug Fixes
    • Prevented saving whitespace-only profile values (nickname, bio) by trimming input before validation and save.
  • Improvements
    • After saving profile changes, the form now refreshes using the backend's updated values while preserving custom fields.
    • The form no longer forcibly resets on error/exit, avoiding unwanted loss of user edits.
  • Chores
    • Added a changeset entry describing the patch for whitespace handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@Anshumancanrock Anshumancanrock requested a review from a team as a code owner January 18, 2026 16:58
@changeset-bot
Copy link

changeset-bot bot commented Jan 18, 2026

🦋 Changeset detected

Latest commit: f2b4d95

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 40 packages
Name Type
@rocket.chat/meteor Patch
@rocket.chat/core-typings Patch
@rocket.chat/rest-typings Patch
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/fuselage-ui-kit Patch
@rocket.chat/gazzodown Patch
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Patch
@rocket.chat/ui-client Patch
@rocket.chat/ui-contexts Patch
@rocket.chat/ui-voip Patch
@rocket.chat/web-ui-registration Patch
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/models Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Patch
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Jan 18, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Walkthrough

Trim whitespace from nickname and bio on the server and update the client form to reset using backend-provided, trimmed profile values after a successful save; add a changeset documenting the patch.

Changes

Cohort / File(s) Summary
Client: profile form
apps/meteor/client/views/account/profile/AccountProfileForm.tsx
Import getProfileInitialValues; after successful updateOwnBasicInfo, reset the form using getProfileInitialValues(updatedUser) (preserving customFields) instead of raw fields; removed previous finally-based reset.
Server: profile save
apps/meteor/server/methods/saveUserProfile.ts
Trim bio and nickname before length validation and before persisting, preventing whitespace-only changes from being treated as modifications.
Release note
.changeset/prevent-whitespace-saving.md
Add changeset entry for patch release describing prevention of saving whitespace-only profile field changes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

stat: ready to merge, stat: QA assured

Suggested reviewers

  • dougfabris
  • ricardogarim

Poem

🐰 I nibbled spaces from the name and bio,
Pulled stray gaps where they used to grow,
Now saves mean something, tidy and bright,
Hops of joy in profile light! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preventing the save button from appearing for whitespace-only changes, which is the core objective of the PR.
Linked Issues check ✅ Passed The PR addresses the requirements from issue #38243 by trimming profile fields and implementing dirty detection based on trimmed values to prevent unnecessary saves.
Out of Scope Changes check ✅ Passed All changes are directly related to the whitespace-only prevention objective. The trimming in handleSave, the changeset documentation, and server-side validation align with the PR's stated scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Jan 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.41%. Comparing base (eb366e7) to head (f2b4d95).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #38244      +/-   ##
===========================================
+ Coverage    70.39%   70.41%   +0.01%     
===========================================
  Files         3161     3161              
  Lines       110654   110648       -6     
  Branches     19892    19885       -7     
===========================================
+ Hits         77895    77910      +15     
+ Misses       30731    30713      -18     
+ Partials      2028     2025       -3     
Flag Coverage Δ
unit 71.38% <ø> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

112-140: Remove inline implementation comment.

Line 113 adds a comment that restates the code and isn’t needed. Please remove it.

🧹 Suggested cleanup
-		// Trim string values before saving
As per coding guidelines, avoid code comments in the implementation.
🤖 Fix all issues with AI agents
In `@apps/meteor/client/views/account/profile/AccountProfilePage.tsx`:
- Around line 47-75: Remove the two inline explanatory comments inside the
AccountProfilePage component: drop the comment above the useWatch call ("//
Watch all form values to compare trimmed versions") and the comment before
stringFields ("// Check if form is truly dirty by comparing trimmed string
values"), leaving the logic for watchedValues, defaultValues, and the
isTrulyDirty useMemo (including stringFields, avatar/statusType/customFields
checks) intact; ensure only the code around useWatch, getProfileInitialValues,
and the isTrulyDirty computation is preserved without those comments.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/meteor/client/views/account/profile/AccountProfileForm.tsx`:
- Line 113: Remove the inline explanatory comment "// Trim string values before
saving" inside the AccountProfileForm component (look for the AccountProfileForm
function/JSX where trimming is performed) so the implementation contains no
in-code comments; leave the trimming code intact but delete that comment to
comply with the coding guideline against inline comments.
🧹 Nitpick comments (1)
apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

139-141: Consider including avatar in the reset and improve line formatting.

The reset omits avatar, which could leave stale state if the avatar was changed. Also, this line exceeds typical length limits, making it hard to read.

♻️ Suggested refactor
 		} finally {
-			reset({ email: trimmedEmail, name: trimmedName, username: trimmedUsername, statusType, statusText: trimmedStatusText, nickname: trimmedNickname, bio: trimmedBio, customFields });
+			reset({
+				email: trimmedEmail,
+				name: trimmedName,
+				username: trimmedUsername,
+				statusType,
+				statusText: trimmedStatusText,
+				nickname: trimmedNickname,
+				bio: trimmedBio,
+				customFields,
+				avatar: undefined,
+			});
 		}

Verify that resetting avatar to undefined (or its appropriate default) aligns with the expected behavior after a successful save.

@Anshumancanrock Anshumancanrock force-pushed the space branch 2 times, most recently from 78fa457 to d15fc31 Compare January 18, 2026 18:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

299-313: Broken JSX structure around Nickname/Bio fields.

From Line 302 onward, the bio props are dangling (no <TextAreaInput ...> opener) and the bio errors are rendered under the Nickname label. This will fail to compile or render correctly. Please restore a dedicated Bio controller/TextAreaInput and close the Nickname field row before it.

🛠️ Suggested fix
-				<FieldRow>
-				<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} id={nicknameId} flexGrow={1} />} />
-									id={bioId}
-									error={errors.bio?.message}
-									rows={3}
-									flexGrow={1}
-									addon={<Icon name='edit' size='x20' alignSelf='center' />}
-									aria-invalid={errors.statusText ? 'true' : 'false'}
-									aria-describedby={`${bioId}-error`}
-								/>
-							)}
-						/>
-					</FieldRow>
-					{errors?.bio && (
-						<FieldError aria-live='assertive' id={`${bioId}-error`}>
-							{errors.bio.message}
-						</FieldError>
-					)}
-				</Field>
+				<FieldRow>
+					<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} id={nicknameId} flexGrow={1} />} />
+				</FieldRow>
+			</Field>
+			<Field>
+				<FieldLabel htmlFor={bioId}>{t('Bio')}</FieldLabel>
+				<FieldRow>
+					<Controller
+						control={control}
+						name='bio'
+						render={({ field }) => (
+							<TextAreaInput
+								{...field}
+								id={bioId}
+								error={errors.bio?.message}
+								rows={3}
+								flexGrow={1}
+								aria-invalid={errors.bio ? 'true' : 'false'}
+								aria-describedby={`${bioId}-error`}
+							/>
+						)}
+					/>
+				</FieldRow>
+				{errors?.bio && (
+					<FieldError aria-live='assertive' id={`${bioId}-error`}>
+						{errors.bio.message}
+					</FieldError>
+				)}
+			</Field>

…file

Trims field values on blur to prevent whitespace-only changes from triggering the dirty state.
This is more performant than watching all form values since it only runs when user leaves a field.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

299-313: Fix the broken JSX in the nickname/bio section.

Line 302 closes the nickname Controller, but the subsequent bio props (id={bioId}, rows, etc.) are now outside any JSX element, which will break rendering/compilation. The bio field needs its own Controller/TextAreaInput block after the nickname field is closed.

🐛 Proposed fix
 				<FieldLabel htmlFor={nicknameId}>{t('Nickname')}</FieldLabel>
 				<FieldRow>
-				<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} id={nicknameId} flexGrow={1} />} />
-									id={bioId}
-									error={errors.bio?.message}
-									rows={3}
-									flexGrow={1}
-									addon={<Icon name='edit' size='x20' alignSelf='center' />}
-									aria-invalid={errors.statusText ? 'true' : 'false'}
-									aria-describedby={`${bioId}-error`}
-								/>
-							)}
-						/>
-					</FieldRow>
-					{errors?.bio && (
-						<FieldError aria-live='assertive' id={`${bioId}-error`}>
-							{errors.bio.message}
-						</FieldError>
-					)}
-				</Field>
+					<Controller
+						control={control}
+						name='nickname'
+						render={({ field }) => <TextInput {...field} id={nicknameId} flexGrow={1} />}
+					/>
+				</FieldRow>
+			</Field>
+			<Field>
+				<FieldLabel htmlFor={bioId}>{t('Bio')}</FieldLabel>
+				<FieldRow>
+					<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}
+								id={bioId}
+								error={errors.bio?.message}
+								rows={3}
+								flexGrow={1}
+								aria-invalid={errors.bio ? 'true' : 'false'}
+								aria-describedby={`${bioId}-error`}
+							/>
+						)}
+					/>
+				</FieldRow>
+				{errors?.bio && (
+					<FieldError aria-live='assertive' id={`${bioId}-error`}>
+						{errors.bio.message}
+					</FieldError>
+				)}
+			</Field>

Copy link
Member

@MartinSchoeler MartinSchoeler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR tries to do 2 things that cancel each other out, you either trim the data or disable the button, if you do both one of the cases will never happen.

But all the other forms do not concern themselves with trimming data, since most of the endpoints already do that, so maybe a backend solution would be more appropriate in this case

const watchedValues = useWatch({ control });
const defaultValues = useMemo(() => getProfileInitialValues(user), [user]);

const isTrulyDirty = useMemo(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are basically re implementing the validations done in react-hook-form

Please check it's documentation https://www.react-hook-form.com/api/useform/register/

It would better to try to create a validation callback. But before that check the main review comment

const updateAvatar = useUpdateAvatar(avatar, user?._id || '');

const handleSave = async ({ email, name, username, statusType, statusText, nickname, bio, customFields }: AccountProfileFormValues) => {
const trimmedName = name.trim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't create a bunch of variables just for the trimmed data, either trim it directly or iterate and filter the data beforehand

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

113-133: Remove inline comment at line 131.

The inline comment violates the coding guideline: avoid code comments in the implementation. The reset(getProfileInitialValues(updatedUser)) call correctly preserves customFields since getProfileInitialValues explicitly includes customFields: user?.customFields ?? {} and updateOwnBasicInfo returns the complete user object from the backend.

Diff
-			// Reset form with backend response (which has trimmed whitespace)
 			reset(getProfileInitialValues(updatedUser));

@Anshumancanrock Anshumancanrock force-pushed the space branch 4 times, most recently from a56dfa4 to e9580d8 Compare January 29, 2026 18:59
@Anshumancanrock
Copy link
Contributor Author

Anshumancanrock commented Jan 29, 2026

@MartinSchoeler Thanks for the feedback! . I've simplified the approach to fix this on the backend like you suggested. I also moved the length validation to check the trimmed value instead of the raw input, which actually fixes a bug where users could bypass the max length by padding with spaces. The form resets with the API response now, so the save button behavior works correctly. Much cleaner this way!

Let me know if it looks good or you would like any changes.

Recording.2026-01-30.005625.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui Touches the code on client side

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save button appears when adding only whitespace to profile fields

3 participants