From 656e543c56ded76af29cd6209864e6d5c5c6f38c Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 29 Apr 2025 14:24:24 -0300 Subject: [PATCH 1/4] fix: Save Preferences being called multiple times --- .../account/preferences/AccountPreferencesPage.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index fcec450b607e1..7c0e4025fac9a 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -28,22 +28,22 @@ const AccountPreferencesPage = (): ReactElement => { const { handleSubmit, reset, - watch, formState: { isDirty, dirtyFields }, } = methods; - const currentData = watch(); - const setPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); const setPreferencesAction = useMutation({ - mutationFn: setPreferencesEndpoint, + mutationFn: ({ + data, + }: { + data: AccountPreferencesData & { dontAskAgainList?: { action: string; label: string }[] } & { highlights?: string[] }; + }) => setPreferencesEndpoint({ data }), onSuccess: () => { dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); }, - onSettled: () => reset(currentData), }); const handleSaveData = async (formData: AccountPreferencesData) => { @@ -66,7 +66,6 @@ const AccountPreferencesPage = (): ReactElement => { : []; Object.assign(data, { dontAskAgainList: list }); } - setPreferencesAction.mutateAsync({ data }); }; From 8cc8566ce11a862db78a6e8971ccda638ace6ad8 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 29 Apr 2025 15:28:56 -0300 Subject: [PATCH 2/4] fix: SavePreferences being called multiple times and causing issues --- .../hooks/account/useSavePreferences.spec.ts | 21 ++++++++ .../hooks/account/useSavePreferences.ts | 53 +++++++++++++++++++ .../preferences/AccountPreferencesPage.tsx | 45 ++-------------- 3 files changed, 77 insertions(+), 42 deletions(-) create mode 100644 apps/meteor/client/hooks/account/useSavePreferences.spec.ts create mode 100644 apps/meteor/client/hooks/account/useSavePreferences.ts diff --git a/apps/meteor/client/hooks/account/useSavePreferences.spec.ts b/apps/meteor/client/hooks/account/useSavePreferences.spec.ts new file mode 100644 index 0000000000000..2218093e7b2c9 --- /dev/null +++ b/apps/meteor/client/hooks/account/useSavePreferences.spec.ts @@ -0,0 +1,21 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useSavePreferences } from './useSavePreferences'; +import type { AccountPreferencesData } from '../../views/account/preferences/useAccountPreferencesValues'; + +const mockSetPreferencesEndpoint = jest.fn(); + +describe('useSavePreferences', () => { + it('should call setPreferencesEndpoint with correct data', async () => { + const dirtyFields = { language: true }; + const { result } = renderHook(() => useSavePreferences({ dirtyFields }), { + wrapper: mockAppRoot().withEndpoint('POST', '/v1/users.setPreferences', mockSetPreferencesEndpoint).build(), + }); + + const formData: AccountPreferencesData = { language: 'en' }; + await waitFor(() => result.current(formData)); + + expect(mockSetPreferencesEndpoint).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/meteor/client/hooks/account/useSavePreferences.ts b/apps/meteor/client/hooks/account/useSavePreferences.ts new file mode 100644 index 0000000000000..555e2490cc375 --- /dev/null +++ b/apps/meteor/client/hooks/account/useSavePreferences.ts @@ -0,0 +1,53 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import { getDirtyFields } from '../../lib/getDirtyFields'; +import { dispatchToastMessage } from '../../lib/toast'; +import type { AccountPreferencesData } from '../../views/account/preferences/useAccountPreferencesValues'; + +type useSavePreferencesProps = { + dirtyFields: Partial>; +}; + +export const useSavePreferences = ({ dirtyFields }: useSavePreferencesProps) => { + const setPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); + const { t } = useTranslation(); + + const setPreferencesAction = useMutation({ + mutationFn: ({ + data, + }: { + data: AccountPreferencesData & { dontAskAgainList?: { action: string; label: string }[] } & { highlights?: string[] }; + }) => setPreferencesEndpoint({ data }), + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + }); + + return async (formData: AccountPreferencesData) => { + const { highlights, dontAskAgainList, ...data } = getDirtyFields(formData, dirtyFields); + if (highlights || highlights === '') { + Object.assign(data, { + highlights: + typeof highlights === 'string' && + highlights + .split(/,|\n/) + .map((val) => val.trim()) + .filter(Boolean), + }); + } + + if (dontAskAgainList) { + const list = + Array.isArray(dontAskAgainList) && dontAskAgainList.length > 0 + ? dontAskAgainList.map(([action, label]) => ({ action, label })) + : []; + Object.assign(data, { dontAskAgainList: list }); + } + setPreferencesAction.mutateAsync({ data }); + }; +}; diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index 7c0e4025fac9a..72cf317c9c665 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -1,6 +1,5 @@ import { ButtonGroup, Button, Box, Accordion } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useSetting, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import { useMutation } from '@tanstack/react-query'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import { useId } from 'react'; import type { ReactElement } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; @@ -13,14 +12,12 @@ import PreferencesMyDataSection from './PreferencesMyDataSection'; import PreferencesNotificationsSection from './PreferencesNotificationsSection'; import PreferencesSoundSection from './PreferencesSoundSection'; import PreferencesUserPresenceSection from './PreferencesUserPresenceSection'; -import type { AccountPreferencesData } from './useAccountPreferencesValues'; import { useAccountPreferencesValues } from './useAccountPreferencesValues'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; -import { getDirtyFields } from '../../../lib/getDirtyFields'; +import { useSavePreferences } from '../../../hooks/account/useSavePreferences'; const AccountPreferencesPage = (): ReactElement => { const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); const dataDownloadEnabled = useSetting('UserData_EnableDownload'); const preferencesValues = useAccountPreferencesValues(); @@ -31,43 +28,7 @@ const AccountPreferencesPage = (): ReactElement => { formState: { isDirty, dirtyFields }, } = methods; - const setPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); - const setPreferencesAction = useMutation({ - mutationFn: ({ - data, - }: { - data: AccountPreferencesData & { dontAskAgainList?: { action: string; label: string }[] } & { highlights?: string[] }; - }) => setPreferencesEndpoint({ data }), - onSuccess: () => { - dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); - }, - onError: (error) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }); - - const handleSaveData = async (formData: AccountPreferencesData) => { - const { highlights, dontAskAgainList, ...data } = getDirtyFields(formData, dirtyFields); - if (highlights || highlights === '') { - Object.assign(data, { - highlights: - typeof highlights === 'string' && - highlights - .split(/,|\n/) - .map((val) => val.trim()) - .filter(Boolean), - }); - } - - if (dontAskAgainList) { - const list = - Array.isArray(dontAskAgainList) && dontAskAgainList.length > 0 - ? dontAskAgainList.map(([action, label]) => ({ action, label })) - : []; - Object.assign(data, { dontAskAgainList: list }); - } - setPreferencesAction.mutateAsync({ data }); - }; + const handleSaveData = useSavePreferences({ dirtyFields }); const preferencesFormId = useId(); From ec7c7f5cc5dba43ba0201b632ad719e4e3ed30bc Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 29 Apr 2025 16:02:07 -0300 Subject: [PATCH 3/4] fix: missing reset --- apps/meteor/client/hooks/account/useSavePreferences.spec.ts | 2 +- apps/meteor/client/hooks/account/useSavePreferences.ts | 6 +++++- .../views/account/preferences/AccountPreferencesPage.tsx | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/hooks/account/useSavePreferences.spec.ts b/apps/meteor/client/hooks/account/useSavePreferences.spec.ts index 2218093e7b2c9..c90495ab7b2a2 100644 --- a/apps/meteor/client/hooks/account/useSavePreferences.spec.ts +++ b/apps/meteor/client/hooks/account/useSavePreferences.spec.ts @@ -9,7 +9,7 @@ const mockSetPreferencesEndpoint = jest.fn(); describe('useSavePreferences', () => { it('should call setPreferencesEndpoint with correct data', async () => { const dirtyFields = { language: true }; - const { result } = renderHook(() => useSavePreferences({ dirtyFields }), { + const { result } = renderHook(() => useSavePreferences({ dirtyFields, reset: jest.fn(), currentData: {} }), { wrapper: mockAppRoot().withEndpoint('POST', '/v1/users.setPreferences', mockSetPreferencesEndpoint).build(), }); diff --git a/apps/meteor/client/hooks/account/useSavePreferences.ts b/apps/meteor/client/hooks/account/useSavePreferences.ts index 555e2490cc375..39c9b3cebd1f6 100644 --- a/apps/meteor/client/hooks/account/useSavePreferences.ts +++ b/apps/meteor/client/hooks/account/useSavePreferences.ts @@ -1,5 +1,6 @@ import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import type { UseFormReset } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { getDirtyFields } from '../../lib/getDirtyFields'; @@ -8,9 +9,11 @@ import type { AccountPreferencesData } from '../../views/account/preferences/use type useSavePreferencesProps = { dirtyFields: Partial>; + reset: UseFormReset; + currentData: AccountPreferencesData; }; -export const useSavePreferences = ({ dirtyFields }: useSavePreferencesProps) => { +export const useSavePreferences = ({ dirtyFields, currentData, reset }: useSavePreferencesProps) => { const setPreferencesEndpoint = useEndpoint('POST', '/v1/users.setPreferences'); const { t } = useTranslation(); @@ -26,6 +29,7 @@ export const useSavePreferences = ({ dirtyFields }: useSavePreferencesProps) => onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); }, + onSettled: () => reset(currentData, { keepValues: true }), }); return async (formData: AccountPreferencesData) => { diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index 72cf317c9c665..851fb9a7df539 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -25,10 +25,13 @@ const AccountPreferencesPage = (): ReactElement => { const { handleSubmit, reset, + watch, formState: { isDirty, dirtyFields }, } = methods; - const handleSaveData = useSavePreferences({ dirtyFields }); + const currentData = watch(); + + const handleSaveData = useSavePreferences({ dirtyFields, currentData, reset }); const preferencesFormId = useId(); From 9df1b67c97674261233dd06152c7e3fc3e152201 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 29 Apr 2025 16:33:06 -0300 Subject: [PATCH 4/4] Create short-pumpkins-fail.md --- .changeset/short-pumpkins-fail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/short-pumpkins-fail.md diff --git a/.changeset/short-pumpkins-fail.md b/.changeset/short-pumpkins-fail.md new file mode 100644 index 0000000000000..b017b5632d7f1 --- /dev/null +++ b/.changeset/short-pumpkins-fail.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issue when trying to save preferences in the user preferences page