From df67715b26293b1ca5654b3a230e7641e75ff2af Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 16 Jul 2026 22:32:32 +0000 Subject: [PATCH 1/5] UILD-780: Incorporate preferred profile settings --- src/common/api/profiles.api.ts | 33 +++++ src/common/constants/api.constants.ts | 1 + src/common/helpers/navigation.helper.ts | 6 - .../DefaultProfileSettingsOption.scss | 7 + .../DefaultProfileSettingsOption.test.tsx | 137 ++++++++++++++++++ .../DefaultProfileSettingsOption.tsx | 55 +++++++ .../DefaultProfileSettingsOption/index.ts | 1 + .../ModalCloseProfileSettings.test.tsx | 8 +- .../ModalCloseProfileSettings.tsx | 4 + .../ProfileSettingsEditor.tsx | 8 + .../ProfileSettingsList.test.tsx | 7 + .../ProfileSettingsList.tsx | 3 + .../hooks/useResetSettings.ts | 2 + .../hooks/useSaveProfileSettings.test.tsx | 76 ++++++++-- .../hooks/useSaveProfileSettings.ts | 39 ++++- .../manageProfileSettings/utils/index.ts | 2 +- .../utils/profileSave.test.ts | 108 +++++++++++++- .../utils/profileSave.ts | 47 +++++- .../ModalChooseProfile.test.tsx | 69 +-------- .../ModalChooseProfile/ModalChooseProfile.tsx | 62 +------- src/features/profiles/hooks/index.ts | 6 + .../usePreferredProfileSettings.test.tsx | 72 +++++++++ .../hooks/usePreferredProfileSettings.ts | 17 +++ ...PreferredProfileSettingsMutations.test.tsx | 94 ++++++++++++ .../usePreferredProfileSettingsMutations.ts | 55 +++++++ .../hooks/useProfileSelectionActions.test.ts | 36 ++--- .../hooks/useProfileSelectionActions.ts | 8 +- .../helpers/buildProcessedResource.ts | 18 ++- .../hooks/useResourceProcessing.test.tsx | 5 +- .../resources/hooks/useResourceProcessing.ts | 15 +- src/store/stores/manageProfileSettings.ts | 4 + .../__tests__/common/api/profiles.api.test.ts | 63 ++++++++ translations/ui-linked-data/en.json | 2 +- 33 files changed, 875 insertions(+), 195 deletions(-) create mode 100644 src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.scss create mode 100644 src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.test.tsx create mode 100644 src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx create mode 100644 src/features/manageProfileSettings/components/DefaultProfileSettingsOption/index.ts create mode 100644 src/features/profiles/hooks/usePreferredProfileSettings.test.tsx create mode 100644 src/features/profiles/hooks/usePreferredProfileSettings.ts create mode 100644 src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx create mode 100644 src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts diff --git a/src/common/api/profiles.api.ts b/src/common/api/profiles.api.ts index 70e8ad8d1..94e50b269 100644 --- a/src/common/api/profiles.api.ts +++ b/src/common/api/profiles.api.ts @@ -1,4 +1,5 @@ import { + PREFERRED_PROFILE_SETTINGS_PATH, PROFILE_API_ENDPOINT, PROFILE_METADATA_API_ENDPOINT, PROFILE_PREFERRED_API_ENDPOINT, @@ -99,3 +100,35 @@ export const saveProfileSettings = ( }, }); }; + +export const fetchPreferredProfileSettings = (profileId: string | number) => + baseApi.getJson({ + url: `${PROFILE_API_ENDPOINT}/${profileId}/${PREFERRED_PROFILE_SETTINGS_PATH}`, + }) as Promise; + +export const savePreferredProfileSettings = (profileId: string | number, profileSettingsId: number) => { + const url = `${PROFILE_API_ENDPOINT}/${profileId}/${PREFERRED_PROFILE_SETTINGS_PATH}`; + const body = JSON.stringify({ profileSettingsId }); + + return baseApi.request({ + url, + requestParams: { + method: 'POST', + body, + headers: { + 'content-type': 'application/json', + }, + }, + }); +}; + +export const deletePreferredProfileSettings = (profileId: string | number) => { + const url = `${PROFILE_API_ENDPOINT}/${profileId}/${PREFERRED_PROFILE_SETTINGS_PATH}`; + + return baseApi.request({ + url, + requestParams: { + method: 'DELETE', + }, + }); +}; diff --git a/src/common/constants/api.constants.ts b/src/common/constants/api.constants.ts index 259a99826..56a479b67 100644 --- a/src/common/constants/api.constants.ts +++ b/src/common/constants/api.constants.ts @@ -10,6 +10,7 @@ export const PROFILE_API_ENDPOINT = '/linked-data/profile'; export const PROFILE_METADATA_API_ENDPOINT = '/linked-data/profile/metadata'; export const PROFILE_PREFERRED_API_ENDPOINT = '/linked-data/profile/preferred'; export const PROFILE_SETTINGS_PATH = 'settings'; +export const PREFERRED_PROFILE_SETTINGS_PATH = `preferred`; export const AUTHORITY_ASSIGNMENT_CHECK_API_ENDPOINT = '/linked-data/authority-assignment-check'; export const IMPORT_JSON_FILE_API_ENDPOINT = '/linked-data/import/file'; export const IMPORT_JSON_URL_API_ENDPOINT = '/linked-data/import/url'; diff --git a/src/common/helpers/navigation.helper.ts b/src/common/helpers/navigation.helper.ts index c84bd8c0d..2ecfdeae3 100644 --- a/src/common/helpers/navigation.helper.ts +++ b/src/common/helpers/navigation.helper.ts @@ -41,12 +41,10 @@ export const generatePageURL = ({ url, queryParams, profileId, - profileSettingsId, }: { url: string; queryParams: Record; profileId: string | number; - profileSettingsId?: string | number; }) => { const urlParams = new URLSearchParams(); @@ -62,10 +60,6 @@ export const generatePageURL = ({ urlParams.set(QueryParams.ProfileId, profileId.toString()); } - if (profileSettingsId) { - urlParams.set(QueryParams.ProfileSettingsId, profileSettingsId.toString()); - } - const paramString = urlParams.toString(); return paramString ? `${url}?${paramString}` : url; diff --git a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.scss b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.scss new file mode 100644 index 000000000..1a023be37 --- /dev/null +++ b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.scss @@ -0,0 +1,7 @@ +.default-profile-settings { + padding: 3px 15px 15px; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; +} diff --git a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.test.tsx b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.test.tsx new file mode 100644 index 000000000..58914547a --- /dev/null +++ b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.test.tsx @@ -0,0 +1,137 @@ +import { setInitialGlobalState } from '@/test/__mocks__/store'; + +import { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import { fetchPreferredProfileSettings } from '@/common/api/profiles.api'; +import { PROFILE_SETTINGS_DEFAULT_OPTION } from '@/common/constants/profileSettings.constants'; + +import { useManageProfileSettingsState } from '@/store'; + +import { DefaultProfileSettingsOption } from './DefaultProfileSettingsOption'; + +jest.mock('@/common/api/profiles.api', () => ({ + fetchPreferredProfileSettings: jest.fn(), +})); + +describe('DefaultProfileSettingsOption', () => { + const mockSetIsModified = jest.fn(); + + let queryClient: QueryClient; + + const createWrapper = () => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return Wrapper; + }; + + const mockProfileId = 3; + const mockProfileSettingsId = 24; + const mockPreferredProfileSettings = [ + { + id: mockProfileSettingsId, + profileId: mockProfileId, + name: 'mock settings', + }, + ]; + const mockAlternatePreferredProfileSettings = [ + { + id: 52, + profileId: mockProfileId, + name: 'alternate settings', + }, + ]; + + const renderComponent = (profileId: string | number | null, profileSettingsId: string | number) => { + setInitialGlobalState([ + { + store: useManageProfileSettingsState, + state: { + setIsModified: mockSetIsModified, + }, + }, + ]); + + return render( + + + , + { wrapper: createWrapper() }, + ); + }; + + afterEach(() => { + queryClient?.clear(); + jest.clearAllMocks(); + }); + + it('checked when current profile settings match preferred profile settings', async () => { + (fetchPreferredProfileSettings as jest.Mock).mockReturnValue(mockPreferredProfileSettings); + + renderComponent(mockProfileId, mockProfileSettingsId); + + await waitFor(() => { + expect(screen.getByTestId('default-profile-settings-control')).toBeChecked(); + }); + }); + + it('disabled when profile is undefined', async () => { + (fetchPreferredProfileSettings as jest.Mock).mockReturnValue(mockPreferredProfileSettings); + + renderComponent(null, PROFILE_SETTINGS_DEFAULT_OPTION); + + await waitFor(() => { + expect(screen.getByTestId('default-profile-settings-control')).toBeDisabled(); + }); + }); + + it('unchecked when selected profile settings is not preferred', async () => { + (fetchPreferredProfileSettings as jest.Mock).mockReturnValue(mockAlternatePreferredProfileSettings); + + renderComponent(mockProfileId, mockProfileSettingsId); + + await waitFor(() => { + expect(screen.getByTestId('default-profile-settings-control')).not.toBeChecked(); + }); + }); + + it('unchecked when no preferred profle settings', async () => { + (fetchPreferredProfileSettings as jest.Mock).mockReturnValue([]); + + renderComponent(mockProfileId, mockProfileSettingsId); + + await waitFor(() => { + expect(screen.getByTestId('default-profile-settings-control')).not.toBeChecked(); + }); + }); + + it('updates preferred value through interaction', async () => { + (fetchPreferredProfileSettings as jest.Mock).mockReturnValue(mockPreferredProfileSettings); + + renderComponent(mockProfileId, mockProfileSettingsId); + + await waitFor(() => { + expect(screen.getByTestId('default-profile-settings-control')).toBeChecked(); + }); + + fireEvent.click(screen.getByTestId('default-profile-settings-control')); + + await waitFor(() => { + expect(mockSetIsModified).toHaveBeenCalledWith(true); + expect(screen.getByTestId('default-profile-settings-control')).not.toBeChecked(); + }); + }); +}); diff --git a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx new file mode 100644 index 000000000..b9d9027bf --- /dev/null +++ b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx @@ -0,0 +1,55 @@ +import { FC, useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { usePreferredProfileSettings } from '@/features/profiles'; + +import { useManageProfileSettingsState } from '@/store'; + +import './DefaultProfileSettingsOption.scss'; + +type DefaultProfileSettingsOptionProps = { + selectedProfileId: string | number | null; + selectedProfileSettingsId?: string | number; +}; + +export const DefaultProfileSettingsOption: FC = ({ + selectedProfileId, + selectedProfileSettingsId, +}) => { + const { data: defaultProfileSettings } = usePreferredProfileSettings(selectedProfileId); + const { isPreferredProfileSettings, setIsModified, setIsPreferredProfileSettings } = useManageProfileSettingsState([ + 'isPreferredProfileSettings', + 'setIsModified', + 'setIsPreferredProfileSettings', + ]); + + useEffect(() => { + if (defaultProfileSettings) { + const exists = defaultProfileSettings.find( + pref => pref.id === selectedProfileSettingsId && pref.profileId === selectedProfileId, + ); + setIsPreferredProfileSettings(!!exists); + } + }, [selectedProfileId, selectedProfileSettingsId, defaultProfileSettings, setIsPreferredProfileSettings]); + + const handleChange = () => { + setIsModified(true); + setIsPreferredProfileSettings(prev => !prev); + }; + + return ( +
+ +
+ ); +}; diff --git a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/index.ts b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/index.ts new file mode 100644 index 000000000..e6ba3e6f5 --- /dev/null +++ b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/index.ts @@ -0,0 +1 @@ +export { DefaultProfileSettingsOption } from './DefaultProfileSettingsOption'; diff --git a/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.test.tsx b/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.test.tsx index d69716b1f..be2e1282f 100644 --- a/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.test.tsx +++ b/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.test.tsx @@ -32,6 +32,8 @@ describe('ModalCloseProfileSettings', () => { const mockSetSelectedProfile = jest.fn(); const mockSetIsCreating = jest.fn(); const mockSetSelectedProfileSettingsMeta = jest.fn(); + const mockSetIsPreferredProfileSettings = jest.fn(); + const mockSetSettingsName = jest.fn(); const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -190,6 +192,7 @@ describe('ModalCloseProfileSettings', () => { isCreatingSettingsNext: true, setIsCreating: mockSetIsCreating, setSelectedProfileSettingsMeta: mockSetSelectedProfileSettingsMeta, + setIsPreferredProfileSettings: mockSetIsPreferredProfileSettings, }, }, ]); @@ -201,11 +204,12 @@ describe('ModalCloseProfileSettings', () => { await waitFor(() => { expect(mockSetIsCreating).toHaveBeenCalledWith(true); expect(mockSetSelectedProfileSettingsMeta).toHaveBeenCalledWith(null); + expect(mockSetIsPreferredProfileSettings).toHaveBeenCalledWith(false); }); }); it('when editing existing settings next, move to editing mode with next settings meta selected ', async () => { - const settingsMeta = { id: 'meta' }; + const settingsMeta = { id: 'meta', name: 'edit-name' }; setInitialGlobalState([ { store: useManageProfileSettingsState, @@ -217,6 +221,7 @@ describe('ModalCloseProfileSettings', () => { nextSelectedSettingsMeta: settingsMeta, setIsCreating: mockSetIsCreating, setSelectedProfileSettingsMeta: mockSetSelectedProfileSettingsMeta, + setSettingsName: mockSetSettingsName, }, }, ]); @@ -228,6 +233,7 @@ describe('ModalCloseProfileSettings', () => { await waitFor(() => { expect(mockSetIsCreating).toHaveBeenCalledWith(false); expect(mockSetSelectedProfileSettingsMeta).toHaveBeenCalledWith(settingsMeta); + expect(mockSetSettingsName).toHaveBeenCalledWith('edit-name'); }); }); }); diff --git a/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.tsx b/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.tsx index e40f96536..697dc500f 100644 --- a/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.tsx +++ b/src/features/manageProfileSettings/components/ModalCloseProfileSettings/ModalCloseProfileSettings.tsx @@ -36,6 +36,7 @@ export const ModalCloseProfileSettings: FC = ({ setNextSelectedSettingsMeta, setSelectedProfileSettingsMeta, setSettingsName, + setIsPreferredProfileSettings, } = useManageProfileSettingsState([ 'isClosingNext', 'setIsClosingNext', @@ -53,6 +54,7 @@ export const ModalCloseProfileSettings: FC = ({ 'setNextSelectedSettingsMeta', 'setSelectedProfileSettingsMeta', 'setSettingsName', + 'setIsPreferredProfileSettings', ]); const { resetSettings } = useResetSettings(); const { setIsManageProfileSettingsShowProfiles, setIsManageProfileSettingsShowEditor } = useUIState([ @@ -75,11 +77,13 @@ export const ModalCloseProfileSettings: FC = ({ setIsCreating(true); setSelectedProfileSettingsMeta(null); setSettingsName(''); + setIsPreferredProfileSettings(false); setIsCreatingSettingsNext(false); } else if (isEditingSettingsNext) { setIsCreating(false); resetSettings(); setSelectedProfileSettingsMeta(nextSelectedSettingsMeta); + setSettingsName(nextSelectedSettingsMeta?.name ?? ''); setIsEditingSettingsNext(false); setNextSelectedSettingsMeta(null); } diff --git a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx index eb0842d1f..7449d2080 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx @@ -27,6 +27,7 @@ import { getProfileChildren, getSettingsChildren, } from '../../utils'; +import { DefaultProfileSettingsOption } from '../DefaultProfileSettingsOption'; import { ComponentType } from './BaseComponent'; import { ComponentList } from './ComponentList'; import { DraggingComponent } from './DraggingComponent'; @@ -47,6 +48,7 @@ export const ProfileSettingsEditor = () => { const { fullProfile, + selectedProfile, profileSettings, unusedComponents, selectedComponents, @@ -60,6 +62,7 @@ export const ProfileSettingsEditor = () => { setIsModified, } = useManageProfileSettingsState([ 'fullProfile', + 'selectedProfile', 'profileSettings', 'unusedComponents', 'selectedComponents', @@ -176,6 +179,11 @@ export const ProfileSettingsEditor = () => { + +
diff --git a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx index 66f4a7ac4..79d173f72 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx @@ -10,6 +10,7 @@ import { ProfileSettingsList } from './ProfileSettingsList'; describe('ProfileSettingsList', () => { const mockSetIsManageProfileSettingsUnsavedModalOpen = jest.fn(); const mockSetIsCreating = jest.fn(); + const mockSetIsPreferredProfileSettings = jest.fn(); const renderComponent = (modified: boolean) => { const queryClient = new QueryClient({ defaultOptions: { @@ -40,6 +41,7 @@ describe('ProfileSettingsList', () => { }, isModified: modified, setIsCreating: mockSetIsCreating, + setIsPreferredProfileSettings: mockSetIsPreferredProfileSettings, }, }, { @@ -71,6 +73,7 @@ describe('ProfileSettingsList', () => { waitFor(() => { expect(mockSetIsManageProfileSettingsUnsavedModalOpen).toHaveBeenCalledWith(true); expect(mockSetIsCreating).not.toHaveBeenCalled(); + expect(mockSetIsPreferredProfileSettings).not.toHaveBeenCalled(); }); }); @@ -82,6 +85,7 @@ describe('ProfileSettingsList', () => { waitFor(() => { expect(mockSetIsManageProfileSettingsUnsavedModalOpen).not.toHaveBeenCalled(); expect(mockSetIsCreating).toHaveBeenCalledWith(true); + expect(mockSetIsPreferredProfileSettings).toHaveBeenCalledWith(false); }); }); @@ -94,6 +98,7 @@ describe('ProfileSettingsList', () => { waitFor(() => { expect(mockSetIsManageProfileSettingsUnsavedModalOpen).not.toHaveBeenCalled(); expect(mockSetIsCreating).not.toHaveBeenCalled(); + expect(mockSetIsPreferredProfileSettings).not.toHaveBeenCalled(); }); }); @@ -105,6 +110,7 @@ describe('ProfileSettingsList', () => { waitFor(() => { expect(mockSetIsManageProfileSettingsUnsavedModalOpen).toHaveBeenCalledWith(true); expect(mockSetIsCreating).not.toHaveBeenCalled(); + expect(mockSetIsPreferredProfileSettings).not.toHaveBeenCalled(); }); }); @@ -116,6 +122,7 @@ describe('ProfileSettingsList', () => { waitFor(() => { expect(mockSetIsManageProfileSettingsUnsavedModalOpen).not.toHaveBeenCalled(); expect(mockSetIsCreating).toHaveBeenCalledWith(false); + expect(mockSetIsPreferredProfileSettings).not.toHaveBeenCalled(); }); }); }); diff --git a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx index bc870353a..3114ba3a2 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx @@ -30,6 +30,7 @@ export const ProfileSettingsList = () => { setIsCreating, setIsCreatingSettingsNext, setIsEditingSettingsNext, + setIsPreferredProfileSettings, setNextSelectedSettingsMeta, } = useManageProfileSettingsState([ 'isModified', @@ -40,6 +41,7 @@ export const ProfileSettingsList = () => { 'setIsCreating', 'setIsCreatingSettingsNext', 'setIsEditingSettingsNext', + 'setIsPreferredProfileSettings', 'setNextSelectedSettingsMeta', ]); @@ -79,6 +81,7 @@ export const ProfileSettingsList = () => { setIsCreating(true); setSelectedProfileSettingsMeta(null); setSettingsName(''); + setIsPreferredProfileSettings(false); } }; diff --git a/src/features/manageProfileSettings/hooks/useResetSettings.ts b/src/features/manageProfileSettings/hooks/useResetSettings.ts index 9951be70e..5a176d7a6 100644 --- a/src/features/manageProfileSettings/hooks/useResetSettings.ts +++ b/src/features/manageProfileSettings/hooks/useResetSettings.ts @@ -8,6 +8,7 @@ export const useResetSettings = () => { resetProfileSettings, resetSettingsName, resetNextSelectedProfile, + resetIsPreferredProfileSettings, } = useManageProfileSettingsState(); const resetSettings = () => { @@ -17,6 +18,7 @@ export const useResetSettings = () => { const resetSettingsExceptModified = () => { resetIsSettingsActive(); + resetIsPreferredProfileSettings(); resetSettingsName(); resetSelectedProfileSettingsMeta(); resetProfileSettings(); diff --git a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.test.tsx b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.test.tsx index 105ac9410..a1ed03ac9 100644 --- a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.test.tsx +++ b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.test.tsx @@ -5,7 +5,12 @@ import { ReactNode } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook } from '@testing-library/react'; -import { createProfileSettings, savePreferredProfile, saveProfileSettings } from '@/common/api/profiles.api'; +import { + createProfileSettings, + savePreferredProfile, + savePreferredProfileSettings, + saveProfileSettings, +} from '@/common/api/profiles.api'; import { StatusType } from '@/common/constants/status.constants'; import { UserNotificationFactory } from '@/common/services/userNotification'; @@ -18,6 +23,7 @@ jest.mock('@/common/api/profiles.api', () => ({ savePreferredProfile: jest.fn(), saveProfileSettings: jest.fn(), createProfileSettings: jest.fn(), + savePreferredProfileSettings: jest.fn(), })); jest.mock('@/common/services/userNotification', () => ({ UserNotificationFactory: { @@ -34,9 +40,12 @@ const createWrapper = () => { }, }); - return ({ children }: { children: ReactNode }) => ( - {children} - ); + return { + queryClient, + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }; }; describe('useSaveProfileSettings', () => { @@ -79,7 +88,8 @@ describe('useSaveProfileSettings', () => { }, ]); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); @@ -117,7 +127,8 @@ describe('useSaveProfileSettings', () => { }, ]); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); @@ -155,7 +166,8 @@ describe('useSaveProfileSettings', () => { }, ]); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); @@ -197,7 +209,8 @@ describe('useSaveProfileSettings', () => { }, ]); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); @@ -236,7 +249,8 @@ describe('useSaveProfileSettings', () => { }, ]); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); @@ -245,9 +259,50 @@ describe('useSaveProfileSettings', () => { active: false, children: [], name: 'settings-thing', + profileId: 'another', }); }); + it('updates preferred profile setting', async () => { + setInitialGlobalState([ + { + store: useProfileState, + state: { + preferredProfiles: [], + }, + }, + { + store: useManageProfileSettingsState, + state: { + selectedProfile: { + id: 'another', + name: 'another', + resourceType: 'for-type', + }, + isTypeDefaultProfile: true, + isCreating: false, + isPreferredProfileSettings: true, + selectedProfileSettingsMeta: { + id: 44, + profileId: 'another', + name: 'settings-thing', + }, + settingsName: 'settings-thing', + }, + }, + ]); + + const { queryClient, wrapper } = createWrapper(); + queryClient.setQueryData(['preferredProfileSettings', 'another'], []); + + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); + await act(async () => { + await result.current.saveSettings(); + }); + + expect(savePreferredProfileSettings).toHaveBeenCalledWith('another', 44); + }); + it('shows loading at start and removes it at end', async () => { setInitialGlobalState([ { @@ -276,7 +331,8 @@ describe('useSaveProfileSettings', () => { ]); (createProfileSettings as jest.Mock).mockResolvedValue({ id: 'meta-one' }); - const { result } = renderHook(() => useSaveProfileSettings(), { wrapper: createWrapper() }); + const { wrapper } = createWrapper(); + const { result } = renderHook(() => useSaveProfileSettings(), { wrapper }); await act(async () => { await result.current.saveSettings(); }); diff --git a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts index 85c0a790a..684e6d3bb 100644 --- a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts +++ b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts @@ -7,9 +7,11 @@ import { checkHasErrorOfCodeType } from '@/common/helpers/api.helper'; import { logger } from '@/common/services/logger'; import { UserNotificationFactory } from '@/common/services/userNotification'; +import { usePreferredProfileSettings, usePreferredProfileSettingsMutations } from '@/features/profiles'; + import { useLoadingState, useManageProfileSettingsState, useProfileState, useStatusState } from '@/store'; -import { determinePreferredAction, generateSettings } from '../utils'; +import { determinePreferredAction, determinePreferredSettingsAction, generateSettings } from '../utils'; export const useSaveProfileSettings = () => { const queryClient = useQueryClient(); @@ -18,6 +20,7 @@ export const useSaveProfileSettings = () => { const { addStatusMessagesItem } = useStatusState(['addStatusMessagesItem']); const { isCreating, + isPreferredProfileSettings, isSettingsActive, isTypeDefaultProfile, selectedProfile, @@ -31,6 +34,7 @@ export const useSaveProfileSettings = () => { setSelectedProfileSettingsMeta, } = useManageProfileSettingsState([ 'isCreating', + 'isPreferredProfileSettings', 'isSettingsActive', 'isTypeDefaultProfile', 'selectedProfile', @@ -43,6 +47,8 @@ export const useSaveProfileSettings = () => { 'setProfileSettings', 'setSelectedProfileSettingsMeta', ]); + const { data: preferredProfileSettings } = usePreferredProfileSettings(selectedProfile?.id); + const { removePreferredProfileSettings, updatePreferredProfileSettings } = usePreferredProfileSettingsMutations(); const saveAndSetPreferred = async () => { const preferredAction = determinePreferredAction(selectedProfile, preferredProfiles, isTypeDefaultProfile); @@ -60,7 +66,13 @@ export const useSaveProfileSettings = () => { }; const saveAndSetSettings = async () => { - const settingsToSave = generateSettings(selectedComponents, unusedComponents, isSettingsActive, settingsName); + const settingsToSave = generateSettings( + selectedProfile.id, + selectedComponents, + unusedComponents, + isSettingsActive, + settingsName, + ); let settingsMeta: ProfileSettingsMeta; try { @@ -71,10 +83,11 @@ export const useSaveProfileSettings = () => { settingsMeta = await createProfileSettings(selectedProfile.id, settingsToSave); setIsCreating(false); } - setProfileSettings({ ...settingsToSave, missingFromSettings: [] }); + setProfileSettings({ ...settingsToSave, missingFromSettings: [], id: settingsMeta.id }); queryClient.refetchQueries({ queryKey: ['profileSettingsMeta', String(selectedProfile.id)] }); queryClient.refetchQueries({ queryKey: ['profileSettings', String(selectedProfile.id), settingsMeta.id] }); setSelectedProfileSettingsMeta(settingsMeta); + return settingsMeta; } catch (error) { logger.error('Failed to set profile settings:', error); let errKey = 'ld.error.saveProfileSettings'; @@ -87,13 +100,31 @@ export const useSaveProfileSettings = () => { } addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, errKey)); } + return; + }; + + const saveAndSetPreferredProfileSetting = async (settingsMeta: ProfileSettingsMeta | undefined) => { + if (settingsMeta) { + determinePreferredSettingsAction({ + profileId: selectedProfile.id, + profileSettingsId: settingsMeta.id, + preferredProfileSettings, + isPreferredProfileSettings, + remove: removePreferredProfileSettings, + update: updatePreferredProfileSettings, + }); + } + // If an undefined settingsMeta is being passed in, there was an error preceding it, + // so no additional error message should be shown. Do nothing, the user needs to + // fix something else before this will be tried again. }; const saveSettings = async () => { try { setIsLoading(true); await saveAndSetPreferred(); - await saveAndSetSettings(); + const settingsMeta = await saveAndSetSettings(); + await saveAndSetPreferredProfileSetting(settingsMeta); setIsModified(false); } finally { setIsLoading(false); diff --git a/src/features/manageProfileSettings/utils/index.ts b/src/features/manageProfileSettings/utils/index.ts index 314f823cc..3630b4329 100644 --- a/src/features/manageProfileSettings/utils/index.ts +++ b/src/features/manageProfileSettings/utils/index.ts @@ -1,4 +1,4 @@ export { FilteredPointerSensor, FilteredKeyboardSensor } from './filteredSensors'; export { componentFromId, getProfileChildren, getSettingsChildren, childrenDifference } from './children'; export { chooseModifiers } from './modifiers'; -export { determinePreferredAction, generateSettings } from './profileSave'; +export { determinePreferredAction, determinePreferredSettingsAction, generateSettings } from './profileSave'; diff --git a/src/features/manageProfileSettings/utils/profileSave.test.ts b/src/features/manageProfileSettings/utils/profileSave.test.ts index 94bd6d908..2babb8c23 100644 --- a/src/features/manageProfileSettings/utils/profileSave.test.ts +++ b/src/features/manageProfileSettings/utils/profileSave.test.ts @@ -1,6 +1,7 @@ import { deletePreferredProfile, savePreferredProfile } from '@/common/api/profiles.api'; +import { PROFILE_SETTINGS_DEFAULT_OPTION } from '@/common/constants/profileSettings.constants'; -import { determinePreferredAction, generateSettings } from './profileSave'; +import { determinePreferredAction, determinePreferredSettingsAction, generateSettings } from './profileSave'; jest.mock('@/common/api/profiles.api', () => ({ deletePreferredProfile: jest.fn(), @@ -163,6 +164,108 @@ describe('profileSave', () => { }); }); + describe('determinePreferredSettingsAction', () => { + const mockUpdate = jest.fn(); + const mockRemove = jest.fn(); + const mockProfileId = 100; + const mockProfileSettingsId = 24; + const mockPreferredProfileSettings = [ + { + id: mockProfileSettingsId, + profileId: mockProfileId, + name: 'preferred', + }, + ]; + + it('skips any action when preferred profile settings remains the same', async () => { + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + preferredProfileSettings: mockPreferredProfileSettings, + isPreferredProfileSettings: true, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockRemove).not.toHaveBeenCalled(); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('skips any action when no preferred profile settings and none set', async () => { + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + preferredProfileSettings: [], + isPreferredProfileSettings: false, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockRemove).not.toHaveBeenCalled(); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('removes preferred profile setting when previously set but now unset', async () => { + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + preferredProfileSettings: mockPreferredProfileSettings, + isPreferredProfileSettings: false, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockRemove).toHaveBeenCalledWith({ profileId: mockProfileId }); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('removes preferred profile setting when trying to set to default', async () => { + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: PROFILE_SETTINGS_DEFAULT_OPTION, + preferredProfileSettings: mockPreferredProfileSettings, + isPreferredProfileSettings: true, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockRemove).toHaveBeenCalledWith({ profileId: mockProfileId }); + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it('updates preferred profile setting when given a new profile setting', async () => { + const alternateProfileSettingsId = 846; + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: alternateProfileSettingsId, + preferredProfileSettings: mockPreferredProfileSettings, + isPreferredProfileSettings: true, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockUpdate).toHaveBeenCalledWith({ + profileId: mockProfileId, + profileSettingsId: alternateProfileSettingsId, + }); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + it('updates preferred profile setting when none previously set', async () => { + determinePreferredSettingsAction({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + preferredProfileSettings: [], + isPreferredProfileSettings: true, + remove: mockRemove, + update: mockUpdate, + }); + + expect(mockUpdate).toHaveBeenCalledWith({ profileId: mockProfileId, profileSettingsId: mockProfileSettingsId }); + expect(mockRemove).not.toHaveBeenCalled(); + }); + }); + describe('generateSettings', () => { it('generates expected settings from selected and unused components', async () => { const unusedComponents = [ @@ -185,9 +288,10 @@ describe('profileSave', () => { }, ]; - const result = generateSettings(selectedComponents, unusedComponents, true, 'some-name'); + const result = generateSettings(22, selectedComponents, unusedComponents, true, 'some-name'); expect(result).toEqual({ + profileId: 22, active: true, name: 'some-name', children: [ diff --git a/src/features/manageProfileSettings/utils/profileSave.ts b/src/features/manageProfileSettings/utils/profileSave.ts index 49f875600..943a0cee4 100644 --- a/src/features/manageProfileSettings/utils/profileSave.ts +++ b/src/features/manageProfileSettings/utils/profileSave.ts @@ -1,8 +1,19 @@ import { deletePreferredProfile, savePreferredProfile } from '@/common/api/profiles.api'; import { createUpdatedPreferredProfiles } from '@/common/helpers/profileActions.helper'; +import type { DeletePreferredProfileSettingsParams, SavePreferredProfileSettingsParams } from '@/features/profiles'; + import { type PreferredProfiles } from '@/store'; +interface DeterminePreferredSettingsParams { + profileId: string | number; + profileSettingsId: string | number; + preferredProfileSettings: ProfileSettingsMetaList | undefined; + isPreferredProfileSettings: boolean; + remove: (params: DeletePreferredProfileSettingsParams) => void; + update: (params: SavePreferredProfileSettingsParams) => void; +} + const saveAndUpdatePreferredProfiles = async ( selectedProfile: ProfileDTO, preferredProfiles: ProfileDTO[], @@ -54,7 +65,40 @@ export const determinePreferredAction = ( return null; }; +export const determinePreferredSettingsAction = ({ + profileId, + profileSettingsId, + preferredProfileSettings, + isPreferredProfileSettings, + remove, + update, +}: DeterminePreferredSettingsParams) => { + const preferred = preferredProfileSettings?.find(p => p.profileId === profileId); + + if (preferred) { + if (preferred.id === profileSettingsId) { + if (!isPreferredProfileSettings) { + // This was preferred but is now not; delete the preferred settings for this profile. + remove({ profileId }); + } + } else if (isPreferredProfileSettings) { + // This was not preferred but now is; update to set it as the preferred settings. + if (typeof profileSettingsId === 'number') { + update({ profileId, profileSettingsId }); + } else { + // Unless profileSettingsId is PROFILE_SETTINGS_DEFAULT_OPTION (a string), + // in which case remove preferred settings for this profile. + remove({ profileId }); + } + } + } else if (isPreferredProfileSettings) { + // There was no preferred settings, but now this is; update to set it as the preferred settings. + if (typeof profileSettingsId === 'number') update({ profileId, profileSettingsId }); + } +}; + export const generateSettings = ( + profileId: string | number, selectedComponents: ProfileSettingComponent[], unusedComponents: ProfileSettingComponent[], isSettingsActive: boolean, @@ -79,7 +123,8 @@ export const generateSettings = ( }); return { - name: name, + profileId, + name, active: isSettingsActive, children: settingsChildren, } as ProfileSettings; diff --git a/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.test.tsx b/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.test.tsx index 4c1463e47..0b1d645d6 100644 --- a/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.test.tsx +++ b/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.test.tsx @@ -1,6 +1,5 @@ import { createModalContainer } from '@/test/__mocks__/common/misc/createModalContainer.mock'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { ModalChooseProfile } from './ModalChooseProfile'; @@ -15,33 +14,6 @@ const mockProfileSelectionType = { resourceTypeURL: 'http://bibfra.me/vocab/lite/Work' as ResourceTypeURL, } as ProfileSelectionType; -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); - - queryClient.setQueryData( - ['profileSettingsMeta', 'profile_1'], - [ - { - id: 1, - name: 'one', - }, - { - id: 2, - name: 'two', - }, - ], - ); - - return ({ children }: { children: React.ReactNode }) => { - return {children}; - }; -}; - describe('ModalChooseProfile', () => { const onCancel = jest.fn(); const onSubmit = jest.fn(); @@ -65,7 +37,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); // Check that modal content is rendered @@ -93,7 +64,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); expect(screen.getByTestId('select-profile')).toHaveValue('profile_1'); @@ -109,7 +79,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); const selectElement = screen.getByTestId('select-profile'); @@ -128,7 +97,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); const checkbox = screen.getByRole('checkbox'); @@ -155,7 +123,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); const selectElement = screen.getByTestId('select-profile'); @@ -163,7 +130,7 @@ describe('ModalChooseProfile', () => { fireEvent.click(screen.getByTestId('modal-button-submit')); await waitFor(() => { - expect(onSubmit).toHaveBeenCalledWith('profile_2', 'default', false); + expect(onSubmit).toHaveBeenCalledWith('profile_2', false); }); }); @@ -177,7 +144,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); fireEvent.click(screen.getByTestId('modal-button-cancel')); @@ -194,7 +160,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); fireEvent.click(screen.getByTestId('modal-overlay')); @@ -211,7 +176,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={mockProfiles} />, - { wrapper: createWrapper() }, ); expect(container.firstChild).toBeNull(); @@ -227,7 +191,6 @@ describe('ModalChooseProfile', () => { onClose={onClose} profiles={[]} />, - { wrapper: createWrapper() }, ); rerender( @@ -244,7 +207,7 @@ describe('ModalChooseProfile', () => { fireEvent.click(screen.getByTestId('modal-button-submit')); await waitFor(() => { - expect(onSubmit).toHaveBeenCalledWith('profile_1', 'default', false); + expect(onSubmit).toHaveBeenCalledWith('profile_1', false); expect(onSubmit).not.toHaveBeenCalledWith('0'); expect(onSubmit).not.toHaveBeenCalledWith(undefined); }); @@ -267,7 +230,6 @@ describe('ModalChooseProfile', () => { preferredProfiles={preferredProfiles} resourceTypeURL="http://bibfra.me/vocab/lite/Work" />, - { wrapper: createWrapper() }, ); const checkbox = screen.getByRole('checkbox'); @@ -291,7 +253,6 @@ describe('ModalChooseProfile', () => { preferredProfiles={preferredProfiles} resourceTypeURL="http://bibfra.me/vocab/lite/Work" />, - { wrapper: createWrapper() }, ); const checkbox = screen.getByRole('checkbox'); @@ -314,7 +275,6 @@ describe('ModalChooseProfile', () => { preferredProfiles={preferredProfiles} resourceTypeURL="http://bibfra.me/vocab/lite/Work" />, - { wrapper: createWrapper() }, ); const selectElement = screen.getByTestId('select-profile'); @@ -346,7 +306,6 @@ describe('ModalChooseProfile', () => { profiles={mockProfiles} selectedProfileId="profile_1" />, - { wrapper: createWrapper() }, ); const checkbox = screen.getByRole('checkbox'); @@ -369,33 +328,9 @@ describe('ModalChooseProfile', () => { selectedProfileId="profile_1" preferredProfiles={preferredProfiles} />, - { wrapper: createWrapper() }, ); const checkbox = screen.getByRole('checkbox'); expect(checkbox).not.toBeChecked(); }); - - test('changing the selected settings changes the submitted settings ID', async () => { - render( - , - { wrapper: createWrapper() }, - ); - - fireEvent.change(screen.getByTestId('select-profile-settings'), { target: { value: '2' } }); - - fireEvent.click(screen.getByTestId('modal-button-submit')); - - await waitFor(() => { - expect(onSubmit).toHaveBeenCalledWith('profile_1', '2', false); - }); - }); }); diff --git a/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.tsx b/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.tsx index a0a73296b..b377c6dff 100644 --- a/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.tsx +++ b/src/features/profiles/components/ModalChooseProfile/ModalChooseProfile.tsx @@ -1,7 +1,6 @@ -import { FC, memo, useEffect, useMemo, useState } from 'react'; +import { FC, memo, useEffect, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { PROFILE_SETTINGS_DEFAULT_OPTION } from '@/common/constants/profileSettings.constants'; import { PROFILE_SELECTION_LABEL_IDS, getProfileSelectionMessageIds, @@ -11,23 +10,15 @@ import { import { Modal } from '@/components/Modal'; import { Select, SelectValue } from '@/components/Select'; -import { useLoadProfileSettingsMeta } from '../../hooks'; import { WarningMessages } from './WarningMessages'; import './ModalChooseProfile.scss'; -const metaToOption = (meta: ProfileSettingsMeta) => { - return { - label: meta.name, - value: String(meta.id), - }; -}; - interface ModalChooseProfileProps { isOpen: boolean; profileSelectionType: ProfileSelectionType; onCancel: VoidFunction; - onSubmit: (profileId: string | number, profileSettingsId: string | number, isDefault?: boolean) => void; + onSubmit: (profileId: string | number, isDefault?: boolean) => void; onClose: VoidFunction; profiles: ProfileDTO[]; selectedProfileId?: string | number | null; @@ -49,25 +40,11 @@ export const ModalChooseProfile: FC = memo( }) => { const { formatMessage } = useIntl(); const [selectedValue, setSelectedValue] = useState(selectedProfileId ?? profiles?.[0]?.id); - const [selectedSettingsValue, setSelectedSettingsValue] = useState( - PROFILE_SETTINGS_DEFAULT_OPTION, - ); const [isDefault, setIsDefault] = useState(() => isProfilePreferred({ profileId: selectedProfileId ?? profiles?.[0]?.id, preferredProfiles, resourceTypeURL }), ); - const SETTINGS_OPTION_DEFAULT = [ - { - label: formatMessage({ id: 'ld.profileSettings.defaultSettings' }), - value: PROFILE_SETTINGS_DEFAULT_OPTION, - }, - ]; - const { data: settingsMeta } = useLoadProfileSettingsMeta(selectedValue); - const settingsMetaOptions = useMemo(() => { - return settingsMeta ? SETTINGS_OPTION_DEFAULT.concat(settingsMeta.map(metaToOption)) : SETTINGS_OPTION_DEFAULT; - }, [SETTINGS_OPTION_DEFAULT, settingsMeta]); - useEffect(() => { if (profiles && profiles.length > 0 && !selectedValue) { setSelectedValue(profiles[0].id); @@ -88,24 +65,14 @@ export const ModalChooseProfile: FC = memo( setIsDefault(isProfilePreferred({ profileId: newValue, preferredProfiles, resourceTypeURL })); }; - const onSettingsChange = (selected: SelectValue) => { - const newValue = selected.value; - - setSelectedSettingsValue(newValue); - }; - const handleSubmit = () => { - onSubmit(selectedValue, selectedSettingsValue, isDefault); + onSubmit(selectedValue, isDefault); }; const getProfileById = (id: string | number) => { return profiles.find(profile => profile.id === id); }; - const getProfileSettingById = (id: string | number) => { - return settingsMeta?.find(settingMeta => settingMeta.id === id); - }; - const profileIdToSelectValue = (id: string | number) => { return { value: id, @@ -113,13 +80,6 @@ export const ModalChooseProfile: FC = memo( } as SelectValue; }; - const profileSettingsIdToSelectValue = (id: string | number) => { - return { - value: id.toString(), - label: getProfileSettingById(id)?.name, - } as SelectValue; - }; - const profilesAsOptions = () => { return profiles?.map(({ id, name }) => { return { @@ -136,7 +96,6 @@ export const ModalChooseProfile: FC = memo( const labelSelect = formatMessage({ id: PROFILE_SELECTION_LABEL_IDS.select }, { type: typeLabel }); const labelSetAsDefault = formatMessage({ id: PROFILE_SELECTION_LABEL_IDS.setAsDefault }, { type: typeLabel }); const labelSubmit = formatMessage({ id: submitId }); - const labelSettingsSelect = formatMessage({ id: 'ld.savedSettings' }); return ( = memo(
- {profileSelectionType.action === 'set' && ( -
-
{labelSettingsSelect}
- -
- )} - {profileSelectionType.action === 'change' && ( { + let queryClient: QueryClient; + + const createWrapper = () => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return Wrapper; + }; + + const mockFetchPreferredProfileSettings = profilesApi.fetchPreferredProfileSettings as jest.MockedFunction< + typeof profilesApi.fetchPreferredProfileSettings + >; + + afterEach(() => { + queryClient?.clear(); + }); + + describe('loadPreferredProfileSettings', () => { + it('returns the preferred profile settings for a given profile', async () => { + const mockProfileId = 3; + const mockResult = [ + { + id: 25, + profileId: mockProfileId, + name: 'settings name', + }, + ]; + mockFetchPreferredProfileSettings.mockResolvedValue(mockResult); + + const { result } = renderHook(() => usePreferredProfileSettings(mockProfileId), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(mockFetchPreferredProfileSettings).toHaveBeenCalledWith(mockProfileId); + expect(result.current.data).toEqual(mockResult); + }); + }); + + it('does not fetch when profile ID is null', async () => { + const { result } = renderHook(() => usePreferredProfileSettings(null), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(mockFetchPreferredProfileSettings).not.toHaveBeenCalled(); + expect(result.current.data).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/features/profiles/hooks/usePreferredProfileSettings.ts b/src/features/profiles/hooks/usePreferredProfileSettings.ts new file mode 100644 index 000000000..2d0c31f2b --- /dev/null +++ b/src/features/profiles/hooks/usePreferredProfileSettings.ts @@ -0,0 +1,17 @@ +import { queryOptions, useQuery } from '@tanstack/react-query'; + +import { fetchPreferredProfileSettings } from '@/common/api/profiles.api'; + +export const preferredProfileSettingsOptions = (profileId: string | number | null) => + queryOptions({ + queryKey: ['preferredProfileSettings', String(profileId)], + queryFn: async () => { + return fetchPreferredProfileSettings(profileId!); + }, + staleTime: Infinity, + enabled: !!profileId, + }); + +export const usePreferredProfileSettings = (profileId: string | number | null) => { + return useQuery(preferredProfileSettingsOptions(profileId)); +}; diff --git a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx new file mode 100644 index 000000000..be7a4d005 --- /dev/null +++ b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx @@ -0,0 +1,94 @@ +import { ReactNode } from 'react'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; + +import * as profilesApi from '@/common/api/profiles.api'; + +import { usePreferredProfileSettingsMutations } from './usePreferredProfileSettingsMutations'; + +jest.mock('@/common/api/profiles.api'); + +describe('usePreferredProfileSettings', () => { + let queryClient: QueryClient; + + const createWrapper = () => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return Wrapper; + }; + + const mockSavePreferredProfileSettings = profilesApi.savePreferredProfileSettings as jest.MockedFunction< + typeof profilesApi.savePreferredProfileSettings + >; + const mockDeletePreferredProfileSettings = profilesApi.deletePreferredProfileSettings as jest.MockedFunction< + typeof profilesApi.deletePreferredProfileSettings + >; + + afterEach(() => { + queryClient?.clear(); + jest.clearAllMocks(); + }); + + describe('updatePreferredProfileSettings', () => { + it('calls the correct API method', async () => { + const mockProfileId = 993; + const mockProfileSettingsId = 21; + const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 201 }); + mockSavePreferredProfileSettings.mockResolvedValue(mockResponse); + + const { result } = renderHook(() => usePreferredProfileSettingsMutations(), { + wrapper: createWrapper(), + }); + const invalidateSpy = jest.spyOn(queryClient, 'invalidateQueries'); + + result.current.updatePreferredProfileSettings({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + }); + + await waitFor(() => { + expect(mockSavePreferredProfileSettings).toHaveBeenCalledWith(mockProfileId, mockProfileSettingsId); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['preferredProfileSettings', String(mockProfileId)] }); + }); + }); + + it('handles errors with an error message', async () => { + // TODO + }); + }); + + describe('removePreferredProfileSettings', () => { + it('calls the correct API method', async () => { + const mockProfileId = 744; + const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 204 }); + mockDeletePreferredProfileSettings.mockResolvedValue(mockResponse); + + const { result } = renderHook(() => usePreferredProfileSettingsMutations(), { + wrapper: createWrapper(), + }); + const setQueryDataSpy = jest.spyOn(queryClient, 'setQueryData'); + + result.current.removePreferredProfileSettings({ profileId: mockProfileId }); + + await waitFor(() => { + expect(mockDeletePreferredProfileSettings).toHaveBeenCalledWith(mockProfileId); + expect(setQueryDataSpy).toHaveBeenCalledWith(['preferredProfileSettings', String(mockProfileId)], []); + }); + }); + + it('handles errors with an error message', async () => { + // TODO + }); + }); +}); diff --git a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts new file mode 100644 index 000000000..ce2507316 --- /dev/null +++ b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts @@ -0,0 +1,55 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { deletePreferredProfileSettings, savePreferredProfileSettings } from '@/common/api/profiles.api'; + +export interface SavePreferredProfileSettingsParams { + profileId: string | number; + profileSettingsId: number; +} + +export interface DeletePreferredProfileSettingsParams { + profileId: string | number; +} + +export const usePreferredProfileSettingsMutations = () => { + const queryClient = useQueryClient(); + + const updateMutation = useMutation({ + mutationFn: async ({ profileId, profileSettingsId }) => { + return savePreferredProfileSettings(profileId, profileSettingsId); + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: ['preferredProfileSettings', String(variables.profileId)], + }); + }, + onError: () => { + // handle error + }, + }); + + const deleteMutation = useMutation({ + mutationFn: async ({ profileId }) => { + return deletePreferredProfileSettings(profileId); + }, + onSuccess: (_data, variables) => { + queryClient.setQueryData(['preferredProfileSettings', String(variables.profileId)], []); + }, + onError: () => { + // TODO handle error + }, + }); + + const updatePreferredProfileSettings = (params: SavePreferredProfileSettingsParams) => { + return updateMutation.mutate(params); + }; + + const removePreferredProfileSettings = (params: DeletePreferredProfileSettingsParams) => { + return deleteMutation.mutate(params); + }; + + return { + updatePreferredProfileSettings, + removePreferredProfileSettings, + }; +}; diff --git a/src/features/profiles/hooks/useProfileSelectionActions.test.ts b/src/features/profiles/hooks/useProfileSelectionActions.test.ts index 4dae4f373..7d8573a52 100644 --- a/src/features/profiles/hooks/useProfileSelectionActions.test.ts +++ b/src/features/profiles/hooks/useProfileSelectionActions.test.ts @@ -53,7 +53,6 @@ describe('useProfileSelectionActions', () => { const mockSetPreferredProfiles = jest.fn(); const mockQueryParams = { param: 'value' }; const mockProfileId = 'profile-123'; - const mockProfileSettingsId = 'settings-456'; const mockResourceTypeURL = 'test-resource-type' as ResourceTypeURL; const mockGeneratedURL = '/generated-url'; const mockProfileName = 'Test Profile'; @@ -132,7 +131,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(savePreferredProfile).toHaveBeenCalledWith(mockProfileId, mockResourceTypeURL); @@ -150,7 +149,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(getProfileNameById).toHaveBeenCalledWith({ @@ -181,7 +180,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(mockSetIsLoading).toHaveBeenCalledWith(true); @@ -197,7 +196,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(savePreferredProfile).not.toHaveBeenCalled(); @@ -219,7 +218,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(savePreferredProfile).toHaveBeenCalledWith(mockProfileId, mockResourceTypeURL); @@ -242,14 +241,13 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(generatePageURL).toHaveBeenCalledWith({ url: ROUTES.RESOURCE_CREATE.uri, queryParams: mockQueryParams, profileId: mockProfileId, - profileSettingsId: mockProfileSettingsId, }); expect(mockNavigateToEditPage).toHaveBeenCalledWith(mockGeneratedURL); expect(mockResetModalState).toHaveBeenCalled(); @@ -267,7 +265,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(mockChangeRecordProfile).toHaveBeenCalledWith({ profileId: mockProfileId }); @@ -287,7 +285,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(mockChangeRecordProfile).toHaveBeenCalledWith({ profileId: mockProfileId }); @@ -307,7 +305,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(savePreferredProfile).toHaveBeenCalledWith(mockProfileId, mockResourceTypeURL); @@ -327,7 +325,6 @@ describe('useProfileSelectionActions', () => { url: ROUTES.RESOURCE_CREATE.uri, queryParams: mockQueryParams, profileId: mockProfileId, - profileSettingsId: mockProfileSettingsId, }); expect(mockNavigateToEditPage).toHaveBeenCalledWith(mockGeneratedURL); expect(mockResetModalState).toHaveBeenCalled(); @@ -346,7 +343,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, true); + await result.current.handleSubmit(mockProfileId, true); }); expect(savePreferredProfile).toHaveBeenCalledWith(mockProfileId, mockResourceTypeURL); @@ -380,7 +377,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(deletePreferredProfile).toHaveBeenCalledWith(mockResourceTypeURL); @@ -400,7 +397,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(deletePreferredProfile).not.toHaveBeenCalled(); @@ -420,7 +417,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(mockSetIsLoading).toHaveBeenCalledWith(true); @@ -441,7 +438,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(deletePreferredProfile).toHaveBeenCalledWith(mockResourceTypeURL); @@ -470,7 +467,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(deletePreferredProfile).toHaveBeenCalledWith(mockResourceTypeURL); @@ -481,7 +478,6 @@ describe('useProfileSelectionActions', () => { url: ROUTES.RESOURCE_CREATE.uri, queryParams: mockQueryParams, profileId: mockProfileId, - profileSettingsId: mockProfileSettingsId, }); expect(mockNavigateToEditPage).toHaveBeenCalledWith(mockGeneratedURL); expect(mockResetModalState).toHaveBeenCalled(); @@ -502,7 +498,7 @@ describe('useProfileSelectionActions', () => { ); await act(async () => { - await result.current.handleSubmit(mockProfileId, mockProfileSettingsId, false); + await result.current.handleSubmit(mockProfileId, false); }); expect(deletePreferredProfile).toHaveBeenCalledWith(mockResourceTypeURL); diff --git a/src/features/profiles/hooks/useProfileSelectionActions.ts b/src/features/profiles/hooks/useProfileSelectionActions.ts index a7676acab..f1b81167f 100644 --- a/src/features/profiles/hooks/useProfileSelectionActions.ts +++ b/src/features/profiles/hooks/useProfileSelectionActions.ts @@ -84,12 +84,12 @@ export const useProfileSelectionActions = ({ } }; - const handleCreateResource = (profileId: string | number, profileSettingsId: string | number) => { - const url = generatePageURL({ url: ROUTES.RESOURCE_CREATE.uri, queryParams, profileId, profileSettingsId }); + const handleCreateResource = (profileId: string | number) => { + const url = generatePageURL({ url: ROUTES.RESOURCE_CREATE.uri, queryParams, profileId }); navigateToEditPage(url); }; - const handleSubmit = async (profileId: string | number, profileSettingsId: string | number, isDefault?: boolean) => { + const handleSubmit = async (profileId: string | number, isDefault?: boolean) => { // If "set as default" is checked, save preferred profile if (isDefault) { await handleSetProfileAsDefault(profileId); @@ -100,7 +100,7 @@ export const useProfileSelectionActions = ({ resetModalState(); if (action === 'set') { - handleCreateResource(profileId, profileSettingsId); + handleCreateResource(profileId); } else { try { await changeRecordProfile({ profileId }); diff --git a/src/features/resources/helpers/buildProcessedResource.ts b/src/features/resources/helpers/buildProcessedResource.ts index 2cbf4df36..2351e051c 100644 --- a/src/features/resources/helpers/buildProcessedResource.ts +++ b/src/features/resources/helpers/buildProcessedResource.ts @@ -1,3 +1,4 @@ +import { QueryClient } from '@tanstack/react-query'; import { v4 as uuidv4 } from 'uuid'; import { PROFILE_SETTINGS_DEFAULT_OPTION } from '@/common/constants/profileSettings.constants'; @@ -11,6 +12,8 @@ import { import { getReferenceIdsRaw } from '@/common/helpers/recordFormatting.helper'; import { getUri } from '@/configs/resourceTypes'; +import { preferredProfileSettingsOptions } from '@/features/profiles'; + import type { ProcessedResource } from '../types'; import { extractProfileParams } from './resourceParams'; @@ -18,10 +21,11 @@ type BuildProcessedResourceParams = { pipeline: SchemaPipelineServices; record?: RecordEntry; profileIdParam?: string | null; - profileSettingsIdParam?: string | null; + profileSettingsId?: string | null; typeParam?: string | null; asClone?: boolean; templateMetadata?: ResourceTemplateMetadata[]; + queryClient?: QueryClient | null; loadProfile: (id: string | number) => Promise; loadProfileSettings: ( id: string | number, @@ -35,10 +39,11 @@ export const buildProcessedResource = async ({ pipeline, record, profileIdParam = null, - profileSettingsIdParam = null, + profileSettingsId = null, typeParam = null, asClone = false, templateMetadata, + queryClient = null, loadProfile, loadProfileSettings, }: BuildProcessedResourceParams): Promise => { @@ -63,9 +68,14 @@ export const buildProcessedResource = async ({ if (!selectedProfile) return null; + const selectedProfileId = String(profileConfig.ids?.[0]); + const preferredProfileSettings = profileSettingsId + ? [] + : await queryClient?.ensureQueryData(preferredProfileSettingsOptions(selectedProfileId)); + const profileSettings = await loadProfileSettings( - profileSettingsIdParam ?? PROFILE_SETTINGS_DEFAULT_OPTION, - String(profileConfig.ids?.[0]), + profileSettingsId ?? preferredProfileSettings?.[0]?.id ?? PROFILE_SETTINGS_DEFAULT_OPTION, + selectedProfileId, selectedProfile, getUri(resourceType), ); diff --git a/src/features/resources/hooks/useResourceProcessing.test.tsx b/src/features/resources/hooks/useResourceProcessing.test.tsx index 36b390303..c4bbefebc 100644 --- a/src/features/resources/hooks/useResourceProcessing.test.tsx +++ b/src/features/resources/hooks/useResourceProcessing.test.tsx @@ -9,7 +9,7 @@ import * as buildProcessedResourceModule from '../helpers/buildProcessedResource import { useResourceProcessing } from './useResourceProcessing'; jest.mock('react-router-dom', () => ({ - useSearchParams: () => [new URLSearchParams('type=work&profileId=123&profileSettingsId=456'), jest.fn()], + useSearchParams: () => [new URLSearchParams('type=work&profileId=123'), jest.fn()], })); jest.mock('../helpers/buildProcessedResource', () => ({ @@ -66,7 +66,7 @@ describe('useResourceProcessing', () => { (getEditingRecordBlocks as jest.Mock).mockReturnValue(undefined); }); - it('calls buildProcessedResource with typeParam, profileIdParam, and profileSettingsIdParam from URL', async () => { + it('calls buildProcessedResource with typeParam, profileIdParam from URL', async () => { const { result } = renderHook(() => useResourceProcessing(), { wrapper }); await result.current.processResource({}); @@ -75,7 +75,6 @@ describe('useResourceProcessing', () => { expect.objectContaining({ typeParam: 'work', profileIdParam: '123', - profileSettingsIdParam: '456', }), ); }); diff --git a/src/features/resources/hooks/useResourceProcessing.ts b/src/features/resources/hooks/useResourceProcessing.ts index 2cf406b0e..6117b006f 100644 --- a/src/features/resources/hooks/useResourceProcessing.ts +++ b/src/features/resources/hooks/useResourceProcessing.ts @@ -27,7 +27,6 @@ export const useResourceProcessing = () => { const [searchParams] = useSearchParams(); const typeParam = searchParams.get(QueryParams.Type); const profileIdParam = searchParams.get(QueryParams.ProfileId); - const profileSettingsIdParam = searchParams.get(QueryParams.ProfileSettingsId); const sharedInfra = useContext(SharedInfraContext); const queryClient = useQueryClient(); @@ -59,24 +58,16 @@ export const useResourceProcessing = () => { pipeline, record, profileIdParam, - profileSettingsIdParam: profileSettingsId ?? profileSettingsIdParam, + profileSettingsId, typeParam, asClone, templateMetadata, + queryClient, loadProfile, loadProfileSettings, }); }, - [ - typeParam, - profileIdParam, - profileSettingsIdParam, - sharedInfra, - queryClient, - loadProfile, - loadProfileSettings, - formatMessage, - ], + [typeParam, profileIdParam, sharedInfra, queryClient, loadProfile, loadProfileSettings, formatMessage], ); return { processResource }; diff --git a/src/store/stores/manageProfileSettings.ts b/src/store/stores/manageProfileSettings.ts index d1ce8b6d6..948295e32 100644 --- a/src/store/stores/manageProfileSettings.ts +++ b/src/store/stores/manageProfileSettings.ts @@ -20,6 +20,7 @@ export type ManageProfileSettingsState = SliceState<'selectedProfile', ProfileDT SliceState<'isSettingsActive', boolean> & SliceState<'profileSettings', ProfileSettingsWithDrift> & SliceState<'isTypeDefaultProfile', boolean> & + SliceState<'isPreferredProfileSettings', boolean> & SliceState<'isModified', boolean> & SliceState<'settingsName', string> & SliceState<'isCreating', boolean>; @@ -66,6 +67,9 @@ const sliceConfigs: SliceConfigs = { isTypeDefaultProfile: { initialValue: false, }, + isPreferredProfileSettings: { + initialValue: false, + }, isModified: { initialValue: false, }, diff --git a/src/test/__tests__/common/api/profiles.api.test.ts b/src/test/__tests__/common/api/profiles.api.test.ts index 46e8b4895..70bed62aa 100644 --- a/src/test/__tests__/common/api/profiles.api.test.ts +++ b/src/test/__tests__/common/api/profiles.api.test.ts @@ -2,11 +2,14 @@ import baseApi from '@/common/api/base.api'; import { createProfileSettings, deletePreferredProfile, + deletePreferredProfileSettings, fetchAllSettingsForProfile, + fetchPreferredProfileSettings, fetchProfile, fetchProfileSettings, fetchProfiles, savePreferredProfile, + savePreferredProfileSettings, saveProfileSettings, } from '@/common/api/profiles.api'; @@ -260,4 +263,64 @@ describe('profiles.api', () => { }); expect(result).toEqual(mockResponse); }); + + test('fetchPreferredProfileSettings', async () => { + const profileId = 105; + const testResult = [ + { + profileId, + profileSettingsId: 2003, + name: 'fetch-test-name', + }, + ]; + + jest.spyOn(baseApi, 'getJson').mockResolvedValue(testResult); + + const result = await fetchPreferredProfileSettings(profileId); + + expect(baseApi.getJson).toHaveBeenCalledWith({ + url: `/linked-data/profile/${profileId}/preferred`, + }); + + expect(result).toEqual(testResult); + }); + + test('savePreferredProfileSettings', async () => { + const profileId = 98; + const profileSettingsId = 22; + const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 201 }); + + jest.spyOn(baseApi, 'request').mockResolvedValue(mockResponse); + + const result = await savePreferredProfileSettings(profileId, profileSettingsId); + + expect(baseApi.request).toHaveBeenCalledWith({ + url: `/linked-data/profile/${profileId}/preferred`, + requestParams: { + method: 'POST', + body: JSON.stringify({ profileSettingsId }), + headers: { + 'content-type': 'application/json', + }, + }, + }); + expect(result).toEqual(mockResponse); + }); + + test('deletePreferredProfileSettings', async () => { + const profileId = '341'; + const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 204 }); + + jest.spyOn(baseApi, 'request').mockResolvedValue(mockResponse); + + const result = await deletePreferredProfileSettings(profileId); + + expect(baseApi.request).toHaveBeenCalledWith({ + url: `/linked-data/profile/${profileId}/preferred`, + requestParams: { + method: 'DELETE', + }, + }); + expect(result).toEqual(mockResponse); + }); }); diff --git a/translations/ui-linked-data/en.json b/translations/ui-linked-data/en.json index ac7e13f31..4e81a9142 100644 --- a/translations/ui-linked-data/en.json +++ b/translations/ui-linked-data/en.json @@ -359,6 +359,7 @@ "ld.selectedComponents": "Selected components", "ld.selectedComponents.description": "Drag or use move buttons to place components in the desired order.", "ld.setDefaultTypeProfile": "Set as my default {type} profile", + "ld.setDefaultProfileSettings": "Set as my default profile settings for this profile", "ld.aria.nudgeComponentUp": "Move component up", "ld.aria.nudgeComponentDown": "Move component down", "ld.aria.moveComponentOptions": "Move component options", @@ -385,6 +386,5 @@ "ld.visibleByDrift": "This component is shown because it is missing from your settings. Manage profile settings for this profile and save your update to clear this message.", "ld.types.books": "Books", "ld.types.continuingResources": "Serials Work", - "ld.savedSettings": "Saved settings for this profile", "ld.profileDefaults": "Profile defaults" } From f5677612b8f65273043518da790cf1b7d2f69087 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 16 Jul 2026 23:15:14 +0000 Subject: [PATCH 2/5] Address lint, test issues --- .../DefaultProfileSettingsOption.tsx | 4 +- .../ProfileSettingsEditor.test.tsx | 43 +++++++++---------- .../ProfileSettingsEditor.tsx | 4 +- .../hooks/useSaveProfileSettings.ts | 1 - 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx index b9d9027bf..d3c9792e7 100644 --- a/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx +++ b/src/features/manageProfileSettings/components/DefaultProfileSettingsOption/DefaultProfileSettingsOption.tsx @@ -25,10 +25,10 @@ export const DefaultProfileSettingsOption: FC useEffect(() => { if (defaultProfileSettings) { - const exists = defaultProfileSettings.find( + const exists = defaultProfileSettings.some( pref => pref.id === selectedProfileSettingsId && pref.profileId === selectedProfileId, ); - setIsPreferredProfileSettings(!!exists); + setIsPreferredProfileSettings(exists); } }, [selectedProfileId, selectedProfileSettingsId, defaultProfileSettings, setIsPreferredProfileSettings]); diff --git a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.test.tsx b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.test.tsx index 491f0803f..c465a9677 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.test.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.test.tsx @@ -2,6 +2,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { MemoryRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { AdvancedFieldType } from '@/common/constants/uiControls.constants'; @@ -13,12 +14,26 @@ import { ProfileSettingsEditor } from './ProfileSettingsEditor'; describe('ProfileSettingsEditor', () => { const mockSetSettingsName = jest.fn(); - it('renders with no state', async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + const renderComponent = () => { render( - + + + , ); + }; + + afterEach(() => { + queryClient.clear(); + }); + + it('renders with no state', async () => { + renderComponent(); expect(screen.getByTestId('profile-settings-editor')).toBeInTheDocument(); }); @@ -49,11 +64,7 @@ describe('ProfileSettingsEditor', () => { }, ]); - render( - - - , - ); + renderComponent(); const section = screen.getByTestId('selected-component-list'); expect(within(section).getByText('1. Child')).toBeInTheDocument(); @@ -91,11 +102,7 @@ describe('ProfileSettingsEditor', () => { }, ]); - render( - - - , - ); + renderComponent(); const section = screen.getByTestId('unused-component-list'); expect(within(section).getByText('Child')).toBeInTheDocument(); @@ -144,11 +151,7 @@ describe('ProfileSettingsEditor', () => { }, ]); - render( - - - , - ); + renderComponent(); const unused = screen.getByTestId('unused-component-list'); const selected = screen.getByTestId('selected-component-list'); @@ -170,11 +173,7 @@ describe('ProfileSettingsEditor', () => { }, ]); - render( - - - , - ); + renderComponent(); const nameInput = screen.getByTestId('settings-name'); expect(nameInput).toHaveValue(initialName); diff --git a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx index 7449d2080..77ff7d9a2 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsEditor/ProfileSettingsEditor.tsx @@ -180,8 +180,8 @@ export const ProfileSettingsEditor = () => { diff --git a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts index 684e6d3bb..2303299dc 100644 --- a/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts +++ b/src/features/manageProfileSettings/hooks/useSaveProfileSettings.ts @@ -100,7 +100,6 @@ export const useSaveProfileSettings = () => { } addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, errKey)); } - return; }; const saveAndSetPreferredProfileSetting = async (settingsMeta: ProfileSettingsMeta | undefined) => { From 9b840cdf9f4170c69b6473bc5b336413e5044068 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 17 Jul 2026 22:27:10 +0000 Subject: [PATCH 3/5] Add various indicators to settings menu --- .../components/EditSection/EditSection.scss | 15 ++++ .../ProfileSettingsSelector.test.tsx | 69 ++++++++++++++----- .../EditSection/ProfileSettingsSelector.tsx | 51 ++++++++++++-- src/features/edit/hooks/useEditPage.test.ts | 2 + src/features/edit/hooks/useEditPage.ts | 4 +- .../helpers/buildProcessedResource.ts | 5 +- .../resources/types/resource.types.ts | 1 + translations/ui-linked-data/en.json | 3 +- 8 files changed, 125 insertions(+), 25 deletions(-) diff --git a/src/features/edit/components/EditSection/EditSection.scss b/src/features/edit/components/EditSection/EditSection.scss index 5fa2bd8cd..90227f240 100644 --- a/src/features/edit/components/EditSection/EditSection.scss +++ b/src/features/edit/components/EditSection/EditSection.scss @@ -98,6 +98,21 @@ padding-bottom: 5px; } + .setting-current { + font-weight: 700; + } + + .setting-default { + font-style: italic; + } + + .setting-preferred { + span { + text-transform: uppercase; + font-size: 90%; + } + } + &-menu { position: absolute; list-style: none; diff --git a/src/features/edit/components/EditSection/ProfileSettingsSelector.test.tsx b/src/features/edit/components/EditSection/ProfileSettingsSelector.test.tsx index 011118148..ee2e5b1bb 100644 --- a/src/features/edit/components/EditSection/ProfileSettingsSelector.test.tsx +++ b/src/features/edit/components/EditSection/ProfileSettingsSelector.test.tsx @@ -19,7 +19,11 @@ describe('ProfileSettingsSelector', () => { const mockProfileId = 'profile-id'; const mockSetSelectedProfileSettingsId = jest.fn(); - const renderComponent = () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + const renderComponent = (withOptions: boolean) => { setInitialGlobalState([ { store: useProfileStore, @@ -30,23 +34,24 @@ describe('ProfileSettingsSelector', () => { }, ]); - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); queryClient.setQueryData( ['profileSettingsMeta', mockProfileId], - [ - { - id: 15, - name: 'fifteen', - }, - { - id: 32, - name: 'thirty-two', - }, - ], + withOptions + ? [ + { + id: 15, + name: 'fifteen', + }, + { + id: 32, + name: 'thirty-two', + }, + ] + : [], ); + queryClient.setQueryData(['preferredProfileSettings', mockProfileId], [{ profileId: mockProfileId, id: 15 }]); + return render( @@ -56,14 +61,30 @@ describe('ProfileSettingsSelector', () => { ); }; + afterEach(() => { + queryClient?.clear(); + }); + it('renders the component', () => { - renderComponent(); + mockGetRecordProfileId.mockReturnValue(mockProfileId); + + renderComponent(true); expect(screen.getByTestId('profile-settings-selector-button')).toBeInTheDocument(); }); + it('does not render the component when no options', () => { + mockGetRecordProfileId.mockReturnValue(mockProfileId); + + renderComponent(false); + + expect(screen.queryByTestId('profile-settings-selector-button')).not.toBeInTheDocument(); + }); + it('clicking button shows menu', () => { - renderComponent(); + mockGetRecordProfileId.mockReturnValue(mockProfileId); + + renderComponent(true); fireEvent.click(screen.getByTestId('profile-settings-selector-button')); @@ -73,7 +94,7 @@ describe('ProfileSettingsSelector', () => { it('profile setting currently in use is disabled', async () => { mockGetRecordProfileId.mockReturnValue(mockProfileId); - renderComponent(); + renderComponent(true); fireEvent.click(screen.getByTestId('profile-settings-selector-button')); @@ -82,10 +103,22 @@ describe('ProfileSettingsSelector', () => { }); }); + it('preferred profile setting shows a label', async () => { + mockGetRecordProfileId.mockReturnValue(mockProfileId); + + renderComponent(true); + + fireEvent.click(screen.getByTestId('profile-settings-selector-button')); + + await waitFor(() => { + expect(screen.getByText('ld.preferred')).toBeInTheDocument(); + }); + }); + it('clicking a setting selects it', async () => { mockGetRecordProfileId.mockReturnValue(mockProfileId); - renderComponent(); + renderComponent(true); fireEvent.click(screen.getByTestId('profile-settings-selector-button')); fireEvent.click(screen.getByText('fifteen')); diff --git a/src/features/edit/components/EditSection/ProfileSettingsSelector.tsx b/src/features/edit/components/EditSection/ProfileSettingsSelector.tsx index 7faa08fea..25051997c 100644 --- a/src/features/edit/components/EditSection/ProfileSettingsSelector.tsx +++ b/src/features/edit/components/EditSection/ProfileSettingsSelector.tsx @@ -8,7 +8,7 @@ import { getRecordProfileId } from '@/common/helpers/record.helper'; import { useDismissMenu } from '@/common/hooks/useDismissMenu'; import { Button, ButtonType } from '@/components/Button'; -import { useLoadProfileSettingsMeta } from '@/features/profiles'; +import { useLoadProfileSettingsMeta, usePreferredProfileSettings } from '@/features/profiles'; import { useInputsState, useProfileState } from '@/store'; @@ -33,10 +33,16 @@ export const ProfileSettingsSelector = () => { }, ] as ProfileSettingsMetaList; const { data: profileSettings } = useLoadProfileSettingsMeta(selectedProfileId); + const { data: preferredProfileSettings } = usePreferredProfileSettings(selectedProfileId); + const profileSettingsOptions = useMemo(() => { return profileSettings ? basicOption.concat(profileSettings) : basicOption; }, [basicOption, profileSettings]); + const hasOptions = useMemo(() => { + return (profileSettings?.length ?? 0) > 0; + }, [profileSettings]); + const { isOpen: isMenuEnabled, setIsOpen: setIsMenuEnabled, toggle: toggleIsMenuEnabled } = useDismissMenu(ref); const handleSettingClick = async (profileSettingsId: string | number) => { @@ -44,7 +50,40 @@ export const ProfileSettingsSelector = () => { setSelectedProfileSettingsId(profileSettingsId.toString()); }; - return ( + const isPreferred = (profileSetting: ProfileSettingsMeta) => { + return preferredProfileSettings?.some(p => p.id === profileSetting.id); + }; + + const getClassNames = (profileSetting: ProfileSettingsMeta) => { + const classNames = []; + if (profileSetting.id === PROFILE_SETTINGS_DEFAULT_OPTION) { + classNames.push('setting-default'); + } + if (profileSetting.id === selectedProfileSettingsId) { + classNames.push('setting-current'); + } + if (isPreferred(profileSetting)) { + classNames.push('setting-preferred'); + } + return classNames.join(' '); + }; + + const getLabel = (profileSetting: ProfileSettingsMeta) => { + return ( + <> + {profileSetting.name} + {isPreferred(profileSetting) ? ( + + + + ) : ( + '' + )} + + ); + }; + + return hasOptions ? (
); })} )}
+ ) : ( + <> ); }; diff --git a/src/features/edit/hooks/useEditPage.test.ts b/src/features/edit/hooks/useEditPage.test.ts index 445b58732..eb40b0381 100644 --- a/src/features/edit/hooks/useEditPage.test.ts +++ b/src/features/edit/hooks/useEditPage.test.ts @@ -65,6 +65,7 @@ jest.mock('@/common/hooks/useSchemaPipeline', () => ({ const mockSetIsLoading = jest.fn(); const mockSetSelectedProfile = jest.fn(); +const mockSetSelectedProfileSettingsId = jest.fn(); const mockSetInitialSchemaKey = jest.fn(); const mockSetSchema = jest.fn(); const mockSetUserValues = jest.fn(); @@ -86,6 +87,7 @@ const setupStores = () => { store: useProfileStore, state: { setSelectedProfile: mockSetSelectedProfile, + setSelectedProfileSettingsId: mockSetSelectedProfileSettingsId, setInitialSchemaKey: mockSetInitialSchemaKey, setSchema: mockSetSchema, }, diff --git a/src/features/edit/hooks/useEditPage.ts b/src/features/edit/hooks/useEditPage.ts index 093453c60..42e974989 100644 --- a/src/features/edit/hooks/useEditPage.ts +++ b/src/features/edit/hooks/useEditPage.ts @@ -61,8 +61,9 @@ export const useEditPage = () => { const { selectedEntriesService } = useSchemaPipeline(); const { setIsLoading } = useLoadingState(['setIsLoading']); - const { setSelectedProfile, setInitialSchemaKey, setSchema } = useProfileState([ + const { setSelectedProfile, setSelectedProfileSettingsId, setInitialSchemaKey, setSchema } = useProfileState([ 'setSelectedProfile', + 'setSelectedProfileSettingsId', 'setInitialSchemaKey', 'setSchema', ]); @@ -110,6 +111,7 @@ export const useEditPage = () => { const applyToProfileAndInputStores = useCallback( ({ result }: ApplyToProfileAndInputStoresProps) => { setSelectedProfile(result.selectedProfile ?? null); + setSelectedProfileSettingsId(result.selectedProfileSettingsId?.toString() ?? null); setSchema(result.schema); setInitialSchemaKey(result.initKey); setUserValues(result.userValues); diff --git a/src/features/resources/helpers/buildProcessedResource.ts b/src/features/resources/helpers/buildProcessedResource.ts index 2351e051c..b9adb5071 100644 --- a/src/features/resources/helpers/buildProcessedResource.ts +++ b/src/features/resources/helpers/buildProcessedResource.ts @@ -72,9 +72,11 @@ export const buildProcessedResource = async ({ const preferredProfileSettings = profileSettingsId ? [] : await queryClient?.ensureQueryData(preferredProfileSettingsOptions(selectedProfileId)); + const selectedProfileSettingsId = + profileSettingsId ?? preferredProfileSettings?.[0]?.id ?? PROFILE_SETTINGS_DEFAULT_OPTION; const profileSettings = await loadProfileSettings( - profileSettingsId ?? preferredProfileSettings?.[0]?.id ?? PROFILE_SETTINGS_DEFAULT_OPTION, + selectedProfileSettingsId, selectedProfileId, selectedProfile, getUri(resourceType), @@ -127,6 +129,7 @@ export const buildProcessedResource = async ({ selectedEntries: pipeline.selectedEntriesService.get(), selectedRecordBlocks, selectedProfile, + selectedProfileSettingsId, title: getRecordTitle(recordData as RecordEntry), entities: record ? getPrimaryEntitiesFromRecord(record) : undefined, referenceIds: record ? getReferenceIdsRaw(record) : undefined, diff --git a/src/features/resources/types/resource.types.ts b/src/features/resources/types/resource.types.ts index 859b98756..707fa05e7 100644 --- a/src/features/resources/types/resource.types.ts +++ b/src/features/resources/types/resource.types.ts @@ -5,6 +5,7 @@ export type ProcessedResource = { selectedEntries: string[]; selectedRecordBlocks?: SelectedRecordBlocks; selectedProfile?: Profile; + selectedProfileSettingsId?: string | number; title?: string; entities?: string[]; referenceIds?: { id: unknown }[]; diff --git a/translations/ui-linked-data/en.json b/translations/ui-linked-data/en.json index 4e81a9142..be4de2994 100644 --- a/translations/ui-linked-data/en.json +++ b/translations/ui-linked-data/en.json @@ -386,5 +386,6 @@ "ld.visibleByDrift": "This component is shown because it is missing from your settings. Manage profile settings for this profile and save your update to clear this message.", "ld.types.books": "Books", "ld.types.continuingResources": "Serials Work", - "ld.profileDefaults": "Profile defaults" + "ld.profileDefaults": "Profile Defaults", + "ld.preferred": "preferred" } From 45cd9500f73f679df72e1eb9bc2276368b6c8b33 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 17 Jul 2026 23:09:21 +0000 Subject: [PATCH 4/5] Add preferred indicator to settings list --- .../ProfileSettingsList.test.tsx | 20 +++++++++++++++++++ .../ProfileSettingsList.tsx | 19 ++++++++++++++---- translations/ui-linked-data/en.json | 2 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx index 79d173f72..a48d2ed6b 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.test.tsx @@ -32,6 +32,16 @@ describe('ProfileSettingsList', () => { }, ], ); + queryClient.setQueryData( + ['preferredProfileSettings', '3'], + [ + { + id: 18, + profileId: 3, + name: 'eighteen', + }, + ], + ); setInitialGlobalState([ { store: useManageProfileSettingsStore, @@ -65,6 +75,16 @@ describe('ProfileSettingsList', () => { expect(screen.getByTestId('profile-settings-select-section')).toBeInTheDocument(); }); + it('labels the preferred setting in the options list', async () => { + renderComponent(false); + + const selectInput = screen.getByTestId('profile-settings-select'); + + await fireEvent.change(selectInput, { target: { value: '18' } }); + + expect(screen.getByRole('option', { selected: true })).toHaveTextContent(/ld\.preferred/); + }); + it('opens a confirmation modal when attempting to create while modified', async () => { renderComponent(true); diff --git a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx index 3114ba3a2..df65ff5ae 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx @@ -1,11 +1,11 @@ import { useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { BASE_SETTINGS_OPTIONS } from '@/common/constants/profileSettings.constants'; import { Button, ButtonType } from '@/components/Button'; import { Select, SelectValue } from '@/components/Select'; -import { useLoadProfileSettingsMeta } from '@/features/profiles'; +import { useLoadProfileSettingsMeta, usePreferredProfileSettings } from '@/features/profiles'; import { useManageProfileSettingsState, useUIState } from '@/store'; @@ -49,11 +49,22 @@ export const ProfileSettingsList = () => { const { setIsManageProfileSettingsUnsavedModalOpen } = useUIState(['setIsManageProfileSettingsUnsavedModalOpen']); + const { formatMessage } = useIntl(); + const { data: settingsMeta } = useLoadProfileSettingsMeta(selectedProfile.id); + const { data: preferredProfileSettings } = usePreferredProfileSettings(selectedProfile.id); const settingsMetaOptions = useMemo(() => { - return settingsMeta ? BASE_SETTINGS_OPTIONS.concat(settingsMeta.map(metaToOption)) : BASE_SETTINGS_OPTIONS; - }, [settingsMeta]); + const settingsWithDefault = settingsMeta + ? BASE_SETTINGS_OPTIONS.concat(settingsMeta.map(metaToOption)) + : BASE_SETTINGS_OPTIONS; + settingsWithDefault.map(option => { + if (option.value === preferredProfileSettings?.find(p => p.profileId === selectedProfile.id)?.id.toString()) { + option.label += ' (' + formatMessage({ id: 'ld.preferred' }) + ')'; + } + }); + return settingsWithDefault; + }, [settingsMeta, preferredProfileSettings, selectedProfile]); const handleChange = (selected: SelectValue) => { if (selected.value !== '') { diff --git a/translations/ui-linked-data/en.json b/translations/ui-linked-data/en.json index be4de2994..40944e4be 100644 --- a/translations/ui-linked-data/en.json +++ b/translations/ui-linked-data/en.json @@ -359,7 +359,7 @@ "ld.selectedComponents": "Selected components", "ld.selectedComponents.description": "Drag or use move buttons to place components in the desired order.", "ld.setDefaultTypeProfile": "Set as my default {type} profile", - "ld.setDefaultProfileSettings": "Set as my default profile settings for this profile", + "ld.setDefaultProfileSettings": "Set as my preferred profile settings for this profile", "ld.aria.nudgeComponentUp": "Move component up", "ld.aria.nudgeComponentDown": "Move component down", "ld.aria.moveComponentOptions": "Move component options", From 99a7e9fec490263e23d8326d1d6198b914596499 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 17 Jul 2026 23:38:38 +0000 Subject: [PATCH 5/5] Add mutation error handling, address lint issue --- .../ProfileSettingsList.tsx | 4 +- ...PreferredProfileSettingsMutations.test.tsx | 62 +++++++++++++++++-- .../usePreferredProfileSettingsMutations.ts | 20 ++++-- translations/ui-linked-data/en.json | 1 + 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx index df65ff5ae..54a6cd59b 100644 --- a/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx +++ b/src/features/manageProfileSettings/components/ProfileSettingsList/ProfileSettingsList.tsx @@ -58,12 +58,12 @@ export const ProfileSettingsList = () => { const settingsWithDefault = settingsMeta ? BASE_SETTINGS_OPTIONS.concat(settingsMeta.map(metaToOption)) : BASE_SETTINGS_OPTIONS; - settingsWithDefault.map(option => { + return settingsWithDefault.map(option => { if (option.value === preferredProfileSettings?.find(p => p.profileId === selectedProfile.id)?.id.toString()) { option.label += ' (' + formatMessage({ id: 'ld.preferred' }) + ')'; } + return option; }); - return settingsWithDefault; }, [settingsMeta, preferredProfileSettings, selectedProfile]); const handleChange = (selected: SelectValue) => { diff --git a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx index be7a4d005..3b87e3e80 100644 --- a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx +++ b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.test.tsx @@ -1,3 +1,5 @@ +import { setInitialGlobalState } from '@/test/__mocks__/store'; + import { ReactNode } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -5,11 +7,14 @@ import { renderHook, waitFor } from '@testing-library/react'; import * as profilesApi from '@/common/api/profiles.api'; +import { useStatusStore } from '@/store'; + import { usePreferredProfileSettingsMutations } from './usePreferredProfileSettingsMutations'; jest.mock('@/common/api/profiles.api'); describe('usePreferredProfileSettings', () => { + const mockAddStatusMessagesItem = jest.fn(); let queryClient: QueryClient; const createWrapper = () => { @@ -41,9 +46,10 @@ describe('usePreferredProfileSettings', () => { }); describe('updatePreferredProfileSettings', () => { + const mockProfileId = 993; + const mockProfileSettingsId = 21; + it('calls the correct API method', async () => { - const mockProfileId = 993; - const mockProfileSettingsId = 21; const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 201 }); mockSavePreferredProfileSettings.mockResolvedValue(mockResponse); @@ -64,13 +70,38 @@ describe('usePreferredProfileSettings', () => { }); it('handles errors with an error message', async () => { - // TODO + const mockResponse = new Response(JSON.stringify({ ok: false }), { status: 400 }); + mockSavePreferredProfileSettings.mockRejectedValue(mockResponse); + + setInitialGlobalState([ + { + store: useStatusStore, + state: { + addStatusMessagesItem: mockAddStatusMessagesItem, + }, + }, + ]); + + const { result } = renderHook(() => usePreferredProfileSettingsMutations(), { + wrapper: createWrapper(), + }); + + result.current.updatePreferredProfileSettings({ + profileId: mockProfileId, + profileSettingsId: mockProfileSettingsId, + }); + + await waitFor(() => { + expect(mockSavePreferredProfileSettings).toHaveBeenCalledWith(mockProfileId, mockProfileSettingsId); + expect(mockAddStatusMessagesItem).toHaveBeenCalled(); + }); }); }); describe('removePreferredProfileSettings', () => { + const mockProfileId = 744; + it('calls the correct API method', async () => { - const mockProfileId = 744; const mockResponse = new Response(JSON.stringify({ ok: true }), { status: 204 }); mockDeletePreferredProfileSettings.mockResolvedValue(mockResponse); @@ -88,7 +119,28 @@ describe('usePreferredProfileSettings', () => { }); it('handles errors with an error message', async () => { - // TODO + const mockResponse = new Response(JSON.stringify({ ok: false }), { status: 400 }); + mockDeletePreferredProfileSettings.mockRejectedValue(mockResponse); + + setInitialGlobalState([ + { + store: useStatusStore, + state: { + addStatusMessagesItem: mockAddStatusMessagesItem, + }, + }, + ]); + + const { result } = renderHook(() => usePreferredProfileSettingsMutations(), { + wrapper: createWrapper(), + }); + + result.current.removePreferredProfileSettings({ profileId: mockProfileId }); + + await waitFor(() => { + expect(mockDeletePreferredProfileSettings).toHaveBeenCalledWith(mockProfileId); + expect(mockAddStatusMessagesItem).toHaveBeenCalled(); + }); }); }); }); diff --git a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts index ce2507316..1df22e274 100644 --- a/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts +++ b/src/features/profiles/hooks/usePreferredProfileSettingsMutations.ts @@ -1,6 +1,11 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { deletePreferredProfileSettings, savePreferredProfileSettings } from '@/common/api/profiles.api'; +import { StatusType } from '@/common/constants/status.constants'; +import { logger } from '@/common/services/logger'; +import { UserNotificationFactory } from '@/common/services/userNotification'; + +import { useStatusState } from '@/store'; export interface SavePreferredProfileSettingsParams { profileId: string | number; @@ -13,6 +18,7 @@ export interface DeletePreferredProfileSettingsParams { export const usePreferredProfileSettingsMutations = () => { const queryClient = useQueryClient(); + const { addStatusMessagesItem } = useStatusState(['addStatusMessagesItem']); const updateMutation = useMutation({ mutationFn: async ({ profileId, profileSettingsId }) => { @@ -23,8 +29,11 @@ export const usePreferredProfileSettingsMutations = () => { queryKey: ['preferredProfileSettings', String(variables.profileId)], }); }, - onError: () => { - // handle error + onError: error => { + logger.error('Failed to save preferred profile settings', error); + addStatusMessagesItem?.( + UserNotificationFactory.createMessage(StatusType.error, 'ld.error.updatePreferredProfileSettings'), + ); }, }); @@ -35,8 +44,11 @@ export const usePreferredProfileSettingsMutations = () => { onSuccess: (_data, variables) => { queryClient.setQueryData(['preferredProfileSettings', String(variables.profileId)], []); }, - onError: () => { - // TODO handle error + onError: error => { + logger.error('Failed to delete preferred profile settings', error); + addStatusMessagesItem?.( + UserNotificationFactory.createMessage(StatusType.error, 'ld.error.updatePreferredProfileSettings'), + ); }, }); diff --git a/translations/ui-linked-data/en.json b/translations/ui-linked-data/en.json index 40944e4be..7748ee78d 100644 --- a/translations/ui-linked-data/en.json +++ b/translations/ui-linked-data/en.json @@ -381,6 +381,7 @@ "ld.authorities": "Authorities", "ld.error.saveProfileSettings": "Error saving profile settings", "ld.error.profileSettingsNameNotBlank": "Profile settings name should not be blank, please provide a name", + "ld.error.updatePreferredProfileSettings": "Error updating preferred profile settings", "ld.profileComponentsDeselected": "Profile components deselected", "ld.removedProfileComponentsResult": "You have removed components from your selected list for this profile. Only the selected components will be visible for resources using this profile. Would you like to proceed?", "ld.visibleByDrift": "This component is shown because it is missing from your settings. Manage profile settings for this profile and save your update to clear this message.",