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",