Skip to content

Commit b25d7da

Browse files
committed
Update manage settings feature to accommodate creating new settings; update existing tests to run correctly
1 parent 72d8b77 commit b25d7da

51 files changed

Lines changed: 700 additions & 420 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
* Refactor services providers and simple lookup data loading. Refs [UILD-816].
99
* Refactor imports and add import boundary rules. Refs [UILD-816].
1010
* Fix regression issues that appeared after refactoring. Refs [UILD-816], [UILD-827].
11+
* Fix multiple settings per profile. Refs [UILD-780].
1112

1213
[UILD-744]:https://folio-org.atlassian.net/browse/UILD-744
1314
[UILD-816]:https://folio-org.atlassian.net/browse/UILD-816
1415
[UILD-821]:https://folio-org.atlassian.net/browse/UILD-821
1516
[UILD-827]:https://folio-org.atlassian.net/browse/UILD-827
17+
[UILD-780]:https://folio-org.atlassian.net/browse/UILD-780
1618

1719
## 2.0.4 (2026-06-03)
1820
* Fix default profile type persistence across edit form and profile settings. Fixes [UILD-820].

src/common/api/profiles.api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export const fetchProfileSettings = (profileId: string | number, profileSettings
6262
url: `${PROFILE_API_ENDPOINT}/${profileId}/${PROFILE_SETTINGS_PATH}/${profileSettingsId}`,
6363
}) as Promise<ProfileSettings>;
6464

65-
export const createProfileSettings = (profileId: string | number, settings: ProfileSettings) => {
65+
export const createProfileSettings = async (profileId: string | number, settings: ProfileSettings) => {
6666
const url = `${PROFILE_API_ENDPOINT}/${profileId}/${PROFILE_SETTINGS_PATH}`;
6767
const body = JSON.stringify(settings);
6868

69-
return baseApi.request({
69+
const response = await baseApi.request({
7070
url,
7171
requestParams: {
7272
method: 'POST',
@@ -76,6 +76,8 @@ export const createProfileSettings = (profileId: string | number, settings: Prof
7676
},
7777
},
7878
});
79+
80+
return response?.json();
7981
};
8082

8183
export const saveProfileSettings = (

src/common/constants/api.constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export enum ApiErrorCodes {
4949
RequiredPrimaryMainTitle = 'required_primary_main_title',
5050
LccnDoesNotMatchPattern = 'lccn_does_not_match_pattern',
5151
LccnNotUnique = 'lccn_not_unique',
52+
ProfileSettingsNameNotUnique = 'profile_settings_name_not_unique',
53+
NotBlank = 'must not be blank',
5254
FailedDependency = 'failed_dependency',
5355
NotFound = 'not_found',
5456
Mapping = 'mapping',

src/common/constants/profileSettings.constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@ export const DEFAULT_INACTIVE_SETTINGS: ProfileSettingsWithDrift = {
33
children: [],
44
missingFromSettings: [],
55
};
6+
7+
export const PROFILE_SETTINGS_DEFAULT_OPTION = 'default';
8+
9+
export const BASE_SETTINGS_OPTIONS = [
10+
{
11+
label: '',
12+
value: '',
13+
},
14+
];

src/common/constants/routes.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export enum QueryParams {
5757
Source = 'source',
5858
SourceUri = 'sourceUri',
5959
ProfileId = 'profileId',
60+
ProfileSettingsId = 'profileSettingsId',
6061
}
6162

6263
export enum SearchQueryParams {

src/common/helpers/navigation.helper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ export const generatePageURL = ({
4141
url,
4242
queryParams,
4343
profileId,
44+
profileSettingsId,
4445
}: {
4546
url: string;
4647
queryParams: Record<QueryParams, string>;
4748
profileId: string | number;
49+
profileSettingsId?: string | number;
4850
}) => {
4951
const urlParams = new URLSearchParams();
5052

@@ -60,6 +62,10 @@ export const generatePageURL = ({
6062
urlParams.set(QueryParams.ProfileId, profileId.toString());
6163
}
6264

65+
if (profileSettingsId) {
66+
urlParams.set(QueryParams.ProfileSettingsId, profileSettingsId.toString());
67+
}
68+
6369
const paramString = urlParams.toString();
6470

6571
return paramString ? `${url}?${paramString}` : url;

src/common/hooks/useDismissMenu.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { RefObject, useEffect, useState } from 'react';
2+
3+
export const useDismissMenu = (ref: RefObject<HTMLElement | null>) => {
4+
const [isOpen, setIsOpen] = useState(false);
5+
6+
useEffect(() => {
7+
const handleClickOutside = (event: MouseEvent) => {
8+
if (ref.current && !ref.current.contains(event.target as Node)) {
9+
setIsOpen(false);
10+
}
11+
};
12+
13+
const handleFocusOutside = (event: FocusEvent) => {
14+
if (!event.relatedTarget || (ref.current && !ref.current.contains(event.relatedTarget as Node))) {
15+
setIsOpen(false);
16+
}
17+
};
18+
19+
const handleEscape = (event: KeyboardEvent) => {
20+
if (ref.current && event.key === 'Escape') {
21+
setIsOpen(false);
22+
ref.current.focus();
23+
}
24+
};
25+
26+
document.addEventListener('pointerdown', handleClickOutside);
27+
document.addEventListener('focusout', handleFocusOutside);
28+
document.addEventListener('keydown', handleEscape);
29+
30+
return () => {
31+
document.removeEventListener('pointerdown', handleClickOutside);
32+
document.removeEventListener('focusout', handleFocusOutside);
33+
document.removeEventListener('keydown', handleEscape);
34+
};
35+
}, [isOpen]);
36+
37+
const toggle = () => {
38+
setIsOpen(prev => !prev);
39+
};
40+
41+
return {
42+
isOpen,
43+
setIsOpen,
44+
toggle,
45+
};
46+
};

src/features/edit/components/EditSection/DrawComponent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getPlaceholderForProperty } from '@/features/search/ui';
1616

1717
import { FieldWithMetadataAndControls } from '../FieldWithMetadataAndControls';
1818
import { BlockActions } from './BlockActions';
19+
import { ProfileSettingsSelector } from './ProfileSettingsSelector';
1920
import { EditSectionDataProps } from './renderDrawComponent';
2021

2122
export type IDrawComponent = {
@@ -67,6 +68,7 @@ export const DrawComponent: FC<IDrawComponent & EditSectionDataProps> = ({
6768
<FormattedMessage id={collapsedEntries.size ? 'ld.expandAll' : 'ld.collapseAll'} />
6869
</Button>
6970
)}
71+
<ProfileSettingsSelector />
7072
<BlockActions entry={entry} />
7173
</div>
7274
</FieldWithMetadataAndControls>

src/features/edit/components/EditSection/EditSection.scss

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,40 @@
8484
width: 20rem;
8585
}
8686
}
87+
88+
.profile-settings-select {
89+
display: inline-block;
90+
position: relative;
91+
margin: 0;
92+
padding: 0;
93+
94+
#selector-title {
95+
font-size: $font-md;
96+
font-weight: 700;
97+
color: $buttonIcon;
98+
padding-bottom: 5px;
99+
}
100+
101+
&-menu {
102+
position: absolute;
103+
list-style: none;
104+
z-index: 2;
105+
top: 10px;
106+
right: 0;
107+
width: max-content;
108+
padding: 15px 10px;
109+
border: 1px solid $gray-350;
110+
background-color: $base;
111+
box-shadow: $menu-drop-shadow;
112+
113+
button {
114+
font-size: $font-md;
115+
font-weight: 400;
116+
width: 100%;
117+
justify-content: flex-start;
118+
}
119+
}
120+
}
87121
}
88122

89123
.edit-section-passive {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useRef } from 'react';
2+
import { FormattedMessage } from 'react-intl';
3+
import { useSearchParams } from 'react-router-dom';
4+
5+
import { PROFILE_SETTINGS_DEFAULT_OPTION } from '@/common/constants/profileSettings.constants';
6+
import { QueryParams } from '@/common/constants/routes.constants';
7+
import { getRecordProfileId } from '@/common/helpers/record.helper';
8+
import { useDismissMenu } from '@/common/hooks/useDismissMenu';
9+
import { Button, ButtonType } from '@/components/Button';
10+
11+
import { useLoadProfileSettingsMeta } from '@/features/profiles';
12+
13+
import { useInputsState, useProfileState } from '@/store';
14+
15+
import Settings from '@/assets/settings.svg?react';
16+
17+
export const ProfileSettingsSelector = () => {
18+
const ref = useRef<HTMLDivElement>(null);
19+
const [searchParams] = useSearchParams();
20+
const { record } = useInputsState(['record']);
21+
const { setSelectedProfileSettingsId } = useProfileState(['setSelectedProfileSettingsId']);
22+
const recordProfileId = getRecordProfileId(record);
23+
const profileIdParam = searchParams.get(QueryParams.ProfileId);
24+
const selectedProfileId = recordProfileId ?? profileIdParam;
25+
const { data: profileSettings } = useLoadProfileSettingsMeta(selectedProfileId);
26+
27+
const { isOpen: isMenuEnabled, setIsOpen: setIsMenuEnabled, toggle: toggleIsMenuEnabled } = useDismissMenu(ref);
28+
29+
const handleSettingClick = async (profileSettingsId: string | number) => {
30+
setIsMenuEnabled(false);
31+
setSelectedProfileSettingsId(profileSettingsId.toString());
32+
};
33+
34+
return (
35+
<div className="profile-settings-select" ref={ref}>
36+
<Button ariaHaspopup="menu" ariaExpanded={isMenuEnabled} onClick={toggleIsMenuEnabled}>
37+
<Settings />
38+
</Button>
39+
{isMenuEnabled && (
40+
<ul
41+
data-testid="profile-settings-select-menu"
42+
className="profile-settings-select-menu"
43+
role="menu" // NOSONAR
44+
aria-labelledby="selector-title"
45+
>
46+
<li id="selector-title" role="none">
47+
<FormattedMessage id="ld.applyProfileSettings" />
48+
</li>
49+
<li role="none">
50+
<Button
51+
type={ButtonType.Text}
52+
label={<FormattedMessage id="ld.profileDefaults" />}
53+
role="menuitem"
54+
onClick={() => handleSettingClick(PROFILE_SETTINGS_DEFAULT_OPTION)}
55+
tabbable={isMenuEnabled}
56+
/>
57+
</li>
58+
{profileSettings?.map(profileSetting => {
59+
return (
60+
<li key={profileSetting.id} role="none">
61+
<Button
62+
type={ButtonType.Text}
63+
label={profileSetting.name}
64+
role="menuitem"
65+
onClick={() => handleSettingClick(profileSetting.id)}
66+
tabbable={isMenuEnabled}
67+
/>
68+
</li>
69+
);
70+
})}
71+
</ul>
72+
)}
73+
</div>
74+
);
75+
};

0 commit comments

Comments
 (0)