diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c772d7a6..14423a26f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@
* Show loading icon until profile picture loads on user detail screen. Refs - UIU-3043.
* Show loading icon until profile picture loads on user edit screen. Refs - UIU-3044.
* Delete profile picture. Refs UIU-3004.
+* Changing user type confirmation modal for ECS-enabled environment. Refs UIU-2969.
## [10.0.4](https://github.com/folio-org/ui-users/tree/v10.0.4) (2023-11-10)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.0.3...v10.0.4)
diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.js b/src/components/EditSections/EditUserInfo/EditUserInfo.js
index febe6fc7e..d1439d282 100644
--- a/src/components/EditSections/EditUserInfo/EditUserInfo.js
+++ b/src/components/EditSections/EditUserInfo/EditUserInfo.js
@@ -1,12 +1,11 @@
-import _ from 'lodash';
-import React from 'react';
+import get from 'lodash/get';
+import moment from 'moment-timezone';
import PropTypes from 'prop-types';
+import React from 'react';
import { Field } from 'react-final-form';
-import { FormattedMessage, injectIntl } from 'react-intl';
-import moment from 'moment-timezone';
import { OnChange } from 'react-final-form-listeners';
+import { FormattedMessage, injectIntl } from 'react-intl';
-import { ViewMetaData } from '@folio/stripes/smart-components';
import {
Button,
Select,
@@ -19,14 +18,16 @@ import {
Modal,
ModalFooter,
} from '@folio/stripes/components';
+import { ViewMetaData } from '@folio/stripes/smart-components';
import { USER_TYPES, USER_TYPE_FIELD } from '../../../constants';
import { isConsortiumEnabled } from '../../util';
import asyncValidateField from '../../validators/asyncValidateField';
import validateMinDate from '../../validators/validateMinDate';
+import { ChangeUserTypeModal, ProfilePicture } from './components';
+
import css from './EditUserInfo.css';
-import ProfilePicture from './components/ProfilePicture';
class EditUserInfo extends React.Component {
static propTypes = {
@@ -56,6 +57,7 @@ class EditUserInfo extends React.Component {
const { initialValues: { patronGroup } } = props;
this.state = {
showRecalculateModal: false,
+ showUserTypeModal: false,
selectedPatronGroup: patronGroup,
};
}
@@ -78,6 +80,12 @@ class EditUserInfo extends React.Component {
this.setState({ showRecalculateModal: false });
}
+ setChangedUserType = (userType) => {
+ const { form: { change } } = this.props;
+ change(USER_TYPE_FIELD, userType);
+ this.setState({ showUserTypeModal: false });
+ }
+
calculateNewExpirationDate = (startCalcToday) => {
const { initialValues } = this.props;
const expirationDate = new Date(initialValues.expirationDate);
@@ -94,7 +102,7 @@ class EditUserInfo extends React.Component {
getPatronGroupOffset = () => {
const selectedPatronGroup = this.props.patronGroups.find(i => i.id === this.state.selectedPatronGroup);
- return _.get(selectedPatronGroup, 'expirationOffsetInDays', '');
+ return get(selectedPatronGroup, 'expirationOffsetInDays', '');
};
parseExpirationDate = (expirationDate) => {
@@ -207,7 +215,7 @@ class EditUserInfo extends React.Component {
].filter(o => o.visible);
const offset = this.getPatronGroupOffset();
- const group = _.get(this.props.patronGroups.find(i => i.id === this.state.selectedPatronGroup), 'group', '');
+ const group = get(this.props.patronGroups.find(i => i.id === this.state.selectedPatronGroup), 'group', '');
const date = moment(this.calculateNewExpirationDate(true)).format('LL');
const modalFooter = (
@@ -407,7 +415,18 @@ class EditUserInfo extends React.Component {
dataOptions={typeOptions}
aria-required={isConsortium}
required={isConsortium}
- />
+ >
+
+ {(selectedUserType) => {
+ if (isConsortium
+ && initialValues.type === USER_TYPES.STAFF
+ && selectedUserType === USER_TYPES.PATRON
+ ) {
+ this.setState({ showUserTypeModal: true });
+ }
+ }}
+
+
@@ -425,6 +444,11 @@ class EditUserInfo extends React.Component {
/>
+
>
);
}
diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js
index 3d7f7bf7f..857e80b43 100644
--- a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js
+++ b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js
@@ -37,7 +37,21 @@ jest.mock('../../util', () => ({
isConsortiumEnabled: jest.fn(() => true),
}));
-jest.mock('./components/ProfilePicture', () => jest.fn(() => 'Profile Picture'));
+jest.mock('./components', () => ({
+ ProfilePicture : jest.fn(() => 'Profile Picture'),
+ ChangeUserTypeModal: jest.fn(({ onChange, initialUserType, open }) => {
+ if (!open) {
+ return null;
+ }
+
+ return (
+
+
ChangeUserTypeModal
+
+
+ );
+ }),
+}));
const onSubmit = jest.fn();
@@ -195,6 +209,31 @@ describe('Render Edit User Information component', () => {
expect(screen.getByText('Profile Picture')).toBeInTheDocument();
});
+ it('should not change user type onClick `Cancel` button', async () => {
+ isConsortiumEnabled.mockClear().mockReturnValue(true);
+ renderEditUserInfo({
+ ...props,
+ initialValues: {
+ ...props.initialValues,
+ type: USER_TYPES.STAFF,
+ },
+ });
+
+ await userEvent.selectOptions(screen.getByRole('combobox', { name: 'ui-users.information.userType' }), USER_TYPES.PATRON);
+
+ const option = screen.getByRole('option', { name: 'ui-users.information.userType.patron' });
+
+
+ await userEvent.click(option);
+
+ await screen.findByText('ChangeUserTypeModal');
+
+ const cancelButton = screen.getByText('Cancel');
+
+ await userEvent.click(cancelButton);
+ expect(changeMock).toHaveBeenCalledWith('type', USER_TYPES.STAFF);
+ });
+
describe('when profilePicture configuration is not enabled', () => {
it('should not render profile picture', () => {
renderEditUserInfo({ ...props, areProfilePicturesEnabled: false });
diff --git a/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.js b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.js
new file mode 100644
index 000000000..dece42bc5
--- /dev/null
+++ b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.js
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+import {
+ Button,
+ Modal,
+ ModalFooter,
+} from '@folio/stripes/components';
+
+import { USER_TYPES } from '../../../../../constants';
+
+const ChangeUserTypeModal = ({ onChange, initialUserType, open }) => {
+ const userTypeModalFooter = (
+
+
+
+
+ );
+
+ return (
+ }
+ open={open}
+ >
+
+
+
+
+ );
+};
+
+ChangeUserTypeModal.propTypes = {
+ onChange: PropTypes.func.isRequired,
+ initialUserType: PropTypes.string.isRequired,
+ open: PropTypes.bool,
+};
+
+ChangeUserTypeModal.defaultProps = {
+ open: false,
+};
+
+export default ChangeUserTypeModal;
diff --git a/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.test.js b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.test.js
new file mode 100644
index 000000000..967a2c804
--- /dev/null
+++ b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/ChangeUserTypeModal.test.js
@@ -0,0 +1,41 @@
+import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
+import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
+
+import { USER_TYPES } from '../../../../../constants';
+import ChangeUserTypeModal from './ChangeUserTypeModal';
+
+describe('ChangeUserTypeModal', () => {
+ it('should cancel modal confirmation', async () => {
+ const onChange = jest.fn();
+ render();
+
+ expect(screen.getByText('ui-users.information.change.userType.modal.label')).toBeInTheDocument();
+
+ const cancelButton = screen.getByText('ui-users.cancel');
+
+ await userEvent.click(cancelButton);
+ expect(onChange).toHaveBeenCalledWith(USER_TYPES.STAFF);
+ });
+
+ it('should confirm modal confirmation with `patron` user type', async () => {
+ const onChange = jest.fn();
+ const initialUserType = USER_TYPES.STAFF;
+
+ render();
+
+ expect(screen.getByText('ui-users.information.change.userType.modal.label')).toBeInTheDocument();
+
+ const cancelButton = screen.getByText('ui-users.information.change.userType.modal.confirm');
+
+ await userEvent.click(cancelButton);
+ expect(onChange).toHaveBeenCalledWith(USER_TYPES.PATRON);
+ });
+});
diff --git a/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/index.js b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/index.js
new file mode 100644
index 000000000..d1699e951
--- /dev/null
+++ b/src/components/EditSections/EditUserInfo/components/ChangeUserTypeModal/index.js
@@ -0,0 +1 @@
+export { default } from './ChangeUserTypeModal';
diff --git a/src/components/EditSections/EditUserInfo/components/index.js b/src/components/EditSections/EditUserInfo/components/index.js
index fd887f2a1..8a8df9d1e 100644
--- a/src/components/EditSections/EditUserInfo/components/index.js
+++ b/src/components/EditSections/EditUserInfo/components/index.js
@@ -1,4 +1,4 @@
-/* eslint-disable import/prefer-default-export */
-export { default as ProfilePicture } from './ProfilePicture';
-export { default as ExternalLinkModal } from './ExternalLinkModal';
+export { default as ChangeUserTypeModal } from './ChangeUserTypeModal';
export { default as DeleteProfilePictureModal } from './DeleteProfilePictureModal';
+export { default as ExternalLinkModal } from './ExternalLinkModal';
+export { default as ProfilePicture } from './ProfilePicture';
diff --git a/translations/ui-users/en.json b/translations/ui-users/en.json
index 7d8aa45e8..007e3bde6 100644
--- a/translations/ui-users/en.json
+++ b/translations/ui-users/en.json
@@ -340,6 +340,9 @@
"information.recalculate.modal.button": "Set",
"information.recalculate.modal.text": "Library accounts with patron group {group} expire in {offset} days. Do you want to set this user’s account to expire on {date}?",
"information.recalculate.modal.label": "Set expiration date?",
+ "information.change.userType.modal.label": "Changing user type?",
+ "information.change.userType.modal.confirm": "Confirm",
+ "information.change.userType.modal.text": "Making this change will update the user's affiliations and the permissions they are granted for those affiliations when clicking Save & close. This action cannot easily be reversed, you would need to manually update the user's affiliations and permissions to reverse the resulting changes. Would you like to proceed?",
"information.recalculate.will.reactivate.user": "User will reactivate after saving",
"lostItems.message.noAccessToActualCostPage": "User does not have permission to access \"Lost items needing actual cost\" processing page",