Skip to content

Commit 93c0c6a

Browse files
authored
UILD-451: Authority warning message displayed on save (#93)
1 parent df50307 commit 93c0c6a

File tree

14 files changed

+387
-30
lines changed

14 files changed

+387
-30
lines changed

src/common/hooks/useModalControls.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@ export const useModalControls = () => {
77
!isModalOpen && setIsModalOpen(true);
88
};
99

10-
return { isModalOpen, setIsModalOpen, openModal };
10+
const closeModal = () => {
11+
isModalOpen && setIsModalOpen(false);
12+
};
13+
14+
return { isModalOpen, setIsModalOpen, openModal, closeModal };
1115
};

src/common/hooks/useSaveRecord.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useSearchParams } from 'react-router-dom';
2+
import { useRecordStatus } from '@common/hooks/useRecordStatus';
3+
import { useRecordControls } from '@common/hooks/useRecordControls';
4+
import { useModalControls } from '@common/hooks/useModalControls';
5+
import { QueryParams } from '@common/constants/routes.constants';
6+
import { useStatusState } from '@src/store';
7+
8+
export const useSaveRecord = (primary: boolean) => {
9+
const { isRecordEdited } = useStatusState();
10+
const { hasBeenSaved } = useRecordStatus();
11+
const { saveRecord } = useRecordControls();
12+
const { isModalOpen, openModal, closeModal } = useModalControls();
13+
const [searchParams] = useSearchParams();
14+
15+
const isButtonDisabled = !searchParams.get(QueryParams.CloneOf) && !hasBeenSaved && !isRecordEdited;
16+
17+
return {
18+
isButtonDisabled,
19+
isModalOpen,
20+
openModal,
21+
closeModal,
22+
saveRecord: () => saveRecord({ isNavigatingBack: primary }),
23+
};
24+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useUIState, useInputsState } from '@src/store';
2+
import { TYPE_URIS } from '@common/constants/bibframe.constants';
3+
4+
const hasSelectedUncontrolledAuthority = (userValues: UserValues) =>
5+
Object.values(userValues).some((value: UserValue) =>
6+
value.contents.some(content => content?.meta?.isPreferred !== undefined && !content?.meta?.isPreferred),
7+
);
8+
9+
export const useSaveRecordWarning = () => {
10+
const { hasShownAuthorityWarning, setHasShownAuthorityWarning } = useUIState();
11+
const { userValues, selectedRecordBlocks } = useInputsState();
12+
const isWorkEditPage = selectedRecordBlocks?.block === TYPE_URIS.WORK;
13+
const shouldDisplayWarningMessage =
14+
isWorkEditPage && !hasShownAuthorityWarning && hasSelectedUncontrolledAuthority(userValues);
15+
16+
return {
17+
shouldDisplayWarningMessage,
18+
setHasShownAuthorityWarning,
19+
};
20+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.uncontrolled-authorities-contents {
2+
padding: 1.5rem 0;
3+
white-space: pre-line;
4+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { FC } from 'react';
2+
import { FormattedMessage, useIntl } from 'react-intl';
3+
import { Modal } from '@components/Modal';
4+
import './ModalUncontrolledAuthorities.scss';
5+
6+
type ModalUncontrolledAuthoritiesProps = {
7+
isOpen: boolean;
8+
onCancel: VoidFunction;
9+
onSubmit: VoidFunction;
10+
onClose: VoidFunction;
11+
};
12+
13+
export const ModalUncontrolledAuthorities: FC<ModalUncontrolledAuthoritiesProps> = ({
14+
isOpen,
15+
onCancel,
16+
onSubmit,
17+
onClose,
18+
}) => {
19+
const { formatMessage } = useIntl();
20+
21+
return (
22+
<Modal
23+
isOpen={isOpen}
24+
title={formatMessage({ id: 'ld.modal.uncontrolledAuthoritiesWarning.title' })}
25+
submitButtonLabel={formatMessage({ id: 'ld.continue' })}
26+
cancelButtonLabel={formatMessage({ id: 'ld.cancel' })}
27+
onClose={onClose}
28+
onSubmit={onSubmit}
29+
onCancel={onCancel}
30+
className="modal-uncontrolled-authorities-warning"
31+
>
32+
<div className="uncontrolled-authorities-contents" data-testid="modal-uncontrolled-authorities-warning">
33+
<FormattedMessage id="ld.modal.uncontrolledAuthoritiesWarning.body" />
34+
</div>
35+
</Modal>
36+
);
37+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ModalUncontrolledAuthorities } from './ModalUncontrolledAuthorities';

src/components/RecordControls/RecordControls.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { SaveRecord } from '@components/SaveRecord';
33
import { CloseRecord } from '@components/CloseRecord';
44
import './RecordControls.scss';
55

6-
export const RecordControls = memo(() => (
7-
<div className="record-controls">
8-
<CloseRecord />
9-
<SaveRecord primary />
10-
<SaveRecord />
11-
</div>
12-
));
6+
export const RecordControls = memo(() => {
7+
return (
8+
<div className="record-controls">
9+
<CloseRecord />
10+
<SaveRecord primary />
11+
<SaveRecord />
12+
</div>
13+
);
14+
});

src/components/SaveRecord/SaveRecord.tsx

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,55 @@
1-
import { memo } from 'react';
1+
import { FC, memo } from 'react';
22
import { FormattedMessage } from 'react-intl';
3-
import { useSearchParams } from 'react-router-dom';
4-
import { useRecordControls } from '@common/hooks/useRecordControls';
53
import { Button, ButtonType } from '@components/Button';
6-
import { useRecordStatus } from '@common/hooks/useRecordStatus';
7-
import { QueryParams } from '@common/constants/routes.constants';
8-
import { useStatusState } from '@src/store';
4+
import { ModalUncontrolledAuthorities } from '@components/ModalUncontrolledAuthorities';
5+
import { useSaveRecordWarning } from '@common/hooks/useSaveRecordWarning';
6+
import { useSaveRecord } from '@common/hooks/useSaveRecord';
97

10-
const SaveRecord = ({ primary = false }) => {
11-
const { isRecordEdited } = useStatusState();
12-
const { saveRecord } = useRecordControls();
13-
const { hasBeenSaved } = useRecordStatus();
14-
const [searchParams] = useSearchParams();
8+
type SaveRecordProps = {
9+
primary?: boolean;
10+
};
11+
12+
const SaveRecord: FC<SaveRecordProps> = ({ primary = false }) => {
13+
const { shouldDisplayWarningMessage, setHasShownAuthorityWarning } = useSaveRecordWarning();
14+
const { isButtonDisabled, isModalOpen, openModal, closeModal, saveRecord } = useSaveRecord(primary);
15+
16+
const handleButtonClick = () => {
17+
if (shouldDisplayWarningMessage) {
18+
openModal();
19+
} else {
20+
handleSave();
21+
}
22+
};
23+
24+
const handleSave = () => {
25+
saveRecord();
26+
handleCloseModal();
27+
};
28+
29+
const handleCloseModal = () => {
30+
if (shouldDisplayWarningMessage) {
31+
setHasShownAuthorityWarning(true);
32+
}
33+
closeModal();
34+
};
1535

1636
return (
17-
<Button
18-
data-testid={`save-record${primary ? '-and-close' : '-and-keep-editing'}`}
19-
type={primary ? ButtonType.Primary : ButtonType.Highlighted}
20-
onClick={() => saveRecord({ isNavigatingBack: primary })}
21-
disabled={!searchParams.get(QueryParams.CloneOf) && !hasBeenSaved && !isRecordEdited}
22-
>
23-
<FormattedMessage id={!primary ? 'ld.saveAndKeepEditing' : 'ld.saveAndClose'} />
24-
</Button>
37+
<>
38+
<Button
39+
data-testid={`save-record${primary ? '-and-close' : '-and-keep-editing'}`}
40+
type={primary ? ButtonType.Primary : ButtonType.Highlighted}
41+
onClick={handleButtonClick}
42+
disabled={isButtonDisabled}
43+
>
44+
<FormattedMessage id={primary ? 'ld.saveAndClose' : 'ld.saveAndKeepEditing'} />
45+
</Button>
46+
<ModalUncontrolledAuthorities
47+
isOpen={isModalOpen}
48+
onCancel={handleCloseModal}
49+
onSubmit={handleSave}
50+
onClose={handleCloseModal}
51+
/>
52+
</>
2553
);
2654
};
2755

src/store/stores/ui.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export type UIState = SliceState<'isAdvancedSearchOpen', boolean> &
1212
SliceState<'collapsibleEntries', UIEntries> &
1313
SliceState<'currentlyEditedEntityBfid', UIEntries> &
1414
SliceState<'fullDisplayComponentType', FullDisplayType> &
15-
SliceState<'currentlyPreviewedEntityBfid', UIEntries>;
15+
SliceState<'currentlyPreviewedEntityBfid', UIEntries> &
16+
SliceState<'hasShownAuthorityWarning', boolean>;
1617

1718
const STORE_NAME = 'UI';
1819

@@ -44,6 +45,9 @@ const sliceConfigs: SliceConfigs = {
4445
currentlyPreviewedEntityBfid: {
4546
initialValue: new Set(),
4647
},
48+
hasShownAuthorityWarning: {
49+
initialValue: false,
50+
},
4751
};
4852

4953
export const useUIStore = createStoreFactory<UIState, SliceConfigs>(sliceConfigs, STORE_NAME);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { useSearchParams } from 'react-router-dom';
3+
import { useSaveRecord } from '@common/hooks/useSaveRecord';
4+
import { useStatusStore } from '@src/store';
5+
import { setInitialGlobalState } from '@src/test/__mocks__/store';
6+
7+
const mockSaveRecord = jest.fn();
8+
const mockOpenModal = jest.fn();
9+
const mockCloseModal = jest.fn();
10+
11+
jest.mock('react-router-dom', () => ({
12+
useSearchParams: jest.fn(),
13+
}));
14+
15+
jest.mock('@common/hooks/useRecordStatus', () => ({
16+
useRecordStatus: () => ({
17+
hasBeenSaved: false,
18+
}),
19+
}));
20+
21+
jest.mock('@common/hooks/useRecordControls', () => ({
22+
useRecordControls: () => ({
23+
saveRecord: mockSaveRecord,
24+
}),
25+
}));
26+
27+
jest.mock('@common/hooks/useModalControls', () => ({
28+
useModalControls: () => ({
29+
isModalOpen: false,
30+
openModal: mockOpenModal,
31+
closeModal: mockCloseModal,
32+
}),
33+
}));
34+
35+
describe('useSaveRecord', () => {
36+
const renderUseSaveRecordHook = (isRecordEdited = false, searchParamsCloneOf?: string) => {
37+
(useSearchParams as jest.Mock).mockReturnValue([{ get: () => searchParamsCloneOf }]);
38+
39+
setInitialGlobalState([
40+
{
41+
store: useStatusStore,
42+
state: { isRecordEdited },
43+
},
44+
]);
45+
46+
return renderHook(() => useSaveRecord(true));
47+
};
48+
49+
test('isButtonDisabled is false when record is edited', () => {
50+
const { result } = renderUseSaveRecordHook(true);
51+
52+
expect(result.current.isButtonDisabled).toBeFalsy();
53+
});
54+
55+
test('isButtonDisabled is false when cloning record', () => {
56+
const { result } = renderUseSaveRecordHook(false, 'cloneOfId');
57+
58+
expect(result.current.isButtonDisabled).toBeFalsy();
59+
});
60+
61+
test('saveRecord calls useRecordControls.saveRecord with correct params', () => {
62+
const { result } = renderUseSaveRecordHook(true);
63+
64+
result.current.saveRecord();
65+
66+
expect(mockSaveRecord).toHaveBeenCalledWith({ isNavigatingBack: true });
67+
});
68+
69+
test('modal controls are exposed correctly', () => {
70+
const { result } = renderUseSaveRecordHook();
71+
72+
result.current.openModal();
73+
expect(mockOpenModal).toHaveBeenCalled();
74+
75+
result.current.closeModal();
76+
expect(mockCloseModal).toHaveBeenCalled();
77+
});
78+
});

0 commit comments

Comments
 (0)