diff --git a/CHANGELOG.md b/CHANGELOG.md index 442e556a8..1fe6a1cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ * Add auto focus to textarea on staff and patron info modal. Fixes UIU-2932. * ECS - Filter users by "User Type". Refs UIU-2943. * Users App: Consume {{FormattedDate}} and {{FormattedTime}} via stripes-component. Refs UIU-1860. +* ECS - Prevent editing of specific shadow user data. Refs UIU-2951. * Relabel "Users: Can create new user" to "Users: Can create and edit users". Refs UIU-2955. ## [9.0.0](https://github.com/folio-org/ui-users/tree/v9.0.0) (2023-02-20) diff --git a/src/components/EditSections/EditContactInfo/EditContactInfo.js b/src/components/EditSections/EditContactInfo/EditContactInfo.js index 4dbcda651..9c793b684 100644 --- a/src/components/EditSections/EditContactInfo/EditContactInfo.js +++ b/src/components/EditSections/EditContactInfo/EditContactInfo.js @@ -25,6 +25,7 @@ const EditContactInfo = ({ addressTypes, preferredContactTypeId, intl, + disabled, }) => { const contactTypeOptions = (contactTypes || []).map(g => { return ( @@ -64,6 +65,7 @@ const EditContactInfo = ({ component={TextField} required fullWidth + disabled={disabled} /> @@ -73,6 +75,7 @@ const EditContactInfo = ({ id="adduser_phone" component={TextField} fullWidth + disabled={disabled} /> @@ -82,6 +85,7 @@ const EditContactInfo = ({ id="adduser_mobilePhone" component={TextField} fullWidth + disabled={disabled} /> @@ -93,6 +97,7 @@ const EditContactInfo = ({ fullWidth aria-required="true" required + disabled={disabled} defaultValue={selectedContactTypeId} > @@ -108,6 +113,7 @@ const EditContactInfo = ({ fieldComponents={addressFields} canDelete formType="final-form" + disabled={disabled} /> ); @@ -120,6 +126,7 @@ EditContactInfo.propTypes = { addressTypes: PropTypes.arrayOf(PropTypes.object), preferredContactTypeId: PropTypes.string, intl: PropTypes.object.isRequired, + disabled: PropTypes.bool, }; export default injectIntl(EditContactInfo); diff --git a/src/components/EditSections/EditContactInfo/EditContactInfo.test.js b/src/components/EditSections/EditContactInfo/EditContactInfo.test.js index 53a58a9a4..fd0c44be7 100644 --- a/src/components/EditSections/EditContactInfo/EditContactInfo.test.js +++ b/src/components/EditSections/EditContactInfo/EditContactInfo.test.js @@ -1,12 +1,13 @@ -import { screen } from '@folio/jest-config-stripes/testing-library/react'; -import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import { Form } from 'react-final-form'; -import '__mock__/stripesComponents.mock'; +import { screen } from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import renderWithRouter from 'helpers/renderWithRouter'; import EditContactInfo from './EditContactInfo'; +jest.unmock('@folio/stripes/components'); + const onSubmit = jest.fn(); const arrayMutators = { @@ -28,7 +29,8 @@ const renderEditContactInfo = (props) => { ); - renderWithRouter( + + return renderWithRouter(
{ renderEditContactInfo(props); expect(screen.getByText('AddressEditList')).toBeInTheDocument(); }); + it('Must be rendered', async () => { renderEditContactInfo(props); - await userEvent.type(document.querySelector('[id="adduser_email"]'), 'Test@gmail.com'); - expect(document.querySelector('[id="adduser_email"]').value).toBe('Test@gmail.com'); + + const emailInput = screen.getByRole('textbox', { name: /email/i }); + + await userEvent.type(emailInput, 'Test@gmail.com'); + expect(emailInput.value).toBe('Test@gmail.com'); + }); + + it('should render with disabled fields', () => { + renderEditContactInfo({ ...props, disabled: true }); + + expect(screen.getByRole('textbox', { name: /email/i })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /mobilePhone/i })).toBeDisabled(); + expect(screen.getByLabelText('ui-users.contact.phone')).toBeDisabled(); }); }); diff --git a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.css b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.css index dc0b77654..1295a307c 100644 --- a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.css +++ b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.css @@ -10,4 +10,9 @@ &:focus { outline: none; } + + &:disabled { + color: var(--checkable-disabled-fill); + cursor: not-allowed; + } } diff --git a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.js b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.js index 1af75ba29..d91750b65 100644 --- a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.js +++ b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.js @@ -23,6 +23,7 @@ class CreateResetPasswordControl extends React.Component { POST: PropTypes.func.isRequired, }).isRequired, }).isRequired, + disabled: PropTypes.bool, }; static manifest = Object.freeze({ @@ -111,6 +112,8 @@ class CreateResetPasswordControl extends React.Component { }; render() { + const { disabled } = this.props; + return ( diff --git a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.test.js b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.test.js index 180d3560d..97e112ac7 100644 --- a/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.test.js +++ b/src/components/EditSections/EditExtendedInfo/CreateResetPasswordControl/CreateResetPasswordControl.test.js @@ -12,7 +12,7 @@ jest.unmock('@folio/stripes/smart-components'); const renderCreateResetPasswordControl = (props) => renderWithRouter(); -const propData = (postMock) => { +const propData = (postMock, disabled = false) => { return { email: 'testemail@email.com', name: 'sample', @@ -22,6 +22,7 @@ const propData = (postMock) => { POST: postMock, } }, + disabled, }; }; @@ -42,6 +43,13 @@ describe('CreateResetPasswordControl component', () => { await waitFor(() => userEvent.click(screen.getByText('ui-users.extended.sendResetPassword'))); await waitFor(() => expect(screen.getByText('ui-users.extended.copyLink')).toBeInTheDocument()); }); + it('should link be disabled', () => { + const mockFunc = jest.fn(() => new Promise((resolve, _) => { + resolve({ ok: true, link: 'bl-users/password-reset/link' }); + })); + renderCreateResetPasswordControl(propData(mockFunc, true)); + expect(screen.getByText('ui-users.extended.sendResetPassword')).toBeDisabled(); + }); /* Can be uncommented after the createResetpasswordControl modal logic is reworked. Should add an assertion at the end after the results */ // it('If it redirects after POST fails', async () => { // const mockFunc = jest.fn(() => new Promise((_, reject) => { diff --git a/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.js b/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.js index 750636ee9..e4564ff7a 100644 --- a/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.js +++ b/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.js @@ -3,6 +3,7 @@ import { FormattedMessage, useIntl, } from 'react-intl'; +import PropTypes from 'prop-types'; import { Field } from 'react-final-form'; import { FieldArray } from 'react-final-form-arrays'; @@ -15,7 +16,7 @@ import { departmentsShape } from '../../../../shapes'; import css from './DepartmentsNameEdit.css'; -const DepartmentsNameEdit = ({ departments }) => { +const DepartmentsNameEdit = ({ departments, disabled }) => { const { formatMessage } = useIntl(); const defaultDepartment = { label: formatMessage({ id: 'ui-users.extended.department.default' }), @@ -35,10 +36,13 @@ const DepartmentsNameEdit = ({ departments }) => { component={RepeatableField} name="departments" onAdd={fields => fields.push()} + canRemove={!disabled} + canAdd={!disabled} renderField={field => ( )} @@ -47,6 +51,9 @@ const DepartmentsNameEdit = ({ departments }) => { ); }; -DepartmentsNameEdit.propTypes = { departments: departmentsShape }; +DepartmentsNameEdit.propTypes = { + departments: departmentsShape, + disabled: PropTypes.bool +}; export default DepartmentsNameEdit; diff --git a/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.test.js b/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.test.js index 8396de678..b68034c4c 100644 --- a/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.test.js +++ b/src/components/EditSections/EditExtendedInfo/DepartmentsNameEdit/DepartmentsNameEdit.test.js @@ -8,9 +8,9 @@ import { import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import renderWithRouter from 'helpers/renderWithRouter'; import DepartmentsNameEdit from './DepartmentsNameEdit'; -import '__mock__/stripesSmartComponent.mock'; jest.unmock('@folio/stripes/components'); +jest.unmock('@folio/stripes/smart-components'); const onSubmit = jest.fn(); @@ -63,3 +63,10 @@ describe('Given DepartmentsNameEdit', () => { expect(screen.queryByPlaceholderText(/ui-users.extended.department.default/i)); }); }); + +describe('Given DepartmentsNameEdit with disabled: true', () => { + it('should add button to be disabled', async () => { + renderDepartmentsNameEdit({ ...props, disabled: true }); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); diff --git a/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.js b/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.js index b6d5f4b92..8829ce510 100644 --- a/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.js +++ b/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.js @@ -40,6 +40,7 @@ class EditExtendedInfo extends Component { change: PropTypes.func.isRequired, values: PropTypes.object, uniquenessValidator: PropTypes.object, + disabled: PropTypes.bool, }; buildAccordionHeader = () => { @@ -83,6 +84,7 @@ class EditExtendedInfo extends Component { departments, change, uniquenessValidator, + disabled, } = this.props; const accordionHeader = this.buildAccordionHeader(); @@ -110,6 +112,7 @@ class EditExtendedInfo extends Component { name="enrollmentDate" id="adduser_enrollmentdate" validate={validateMinDate('ui-users.errors.extended.dateEnrolled')} + disabled={disabled} /> @@ -161,6 +166,7 @@ class EditExtendedInfo extends Component { defaultDeliveryAddressTypeId={defaultDeliveryAddressTypeId} deliveryAvailable={deliveryAvailable} setFieldValue={change} + disabled={disabled} /> @@ -170,7 +176,7 @@ class EditExtendedInfo extends Component { xs={12} md={3} > - + ) : null @@ -188,6 +194,7 @@ class EditExtendedInfo extends Component { component={TextField} fullWidth validStylesEnabled + disabled={disabled} validate={asyncValidateField('username', username, uniquenessValidator)} /> @@ -199,6 +206,7 @@ class EditExtendedInfo extends Component { email={userEmail} name={userFirstName} username={username} + disabled={disabled} /> ) } diff --git a/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.test.js b/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.test.js index a1b58c2a4..37cb243c6 100644 --- a/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.test.js +++ b/src/components/EditSections/EditExtendedInfo/EditExtendedInfo.test.js @@ -2,11 +2,11 @@ import { screen } from '@folio/jest-config-stripes/testing-library/react'; import { Form } from 'react-final-form'; import PropTypes from 'prop-types'; -import '__mock__/stripesComponents.mock'; - import renderWithRouter from 'helpers/renderWithRouter'; import EditExtendedInfo from './EditExtendedInfo'; +jest.unmock('@folio/stripes/components'); + const onSubmit = jest.fn(); const arrayMutators = { @@ -28,7 +28,8 @@ const renderEditExtendedInfo = (props) => { ); - renderWithRouter( + + return renderWithRouter( { renderEditExtendedInfo(props); expect(screen.getByText('test@test.ccom')).toBeInTheDocument(); }); + it('should fields to be disabled', () => { + renderEditExtendedInfo({ ...props, disabled: true }); + expect(screen.getAllByRole('textbox')[0]).toBeDisabled(); + }); }); diff --git a/src/components/EditSections/EditExtendedInfo/RequestPreferencesEdit/RequestPreferencesEdit.js b/src/components/EditSections/EditExtendedInfo/RequestPreferencesEdit/RequestPreferencesEdit.js index b37d72573..ae55d9cdf 100644 --- a/src/components/EditSections/EditExtendedInfo/RequestPreferencesEdit/RequestPreferencesEdit.js +++ b/src/components/EditSections/EditExtendedInfo/RequestPreferencesEdit/RequestPreferencesEdit.js @@ -35,6 +35,7 @@ class RequestPreferencesEdit extends Component { setFieldValue: PropTypes.func.isRequired, defaultDeliveryAddressTypeId: nullOrStringIsRequiredTypeValidator, intl: PropTypes.object.isRequired, + disabled: PropTypes.bool, } componentDidUpdate(prevProps) { @@ -76,7 +77,7 @@ class RequestPreferencesEdit extends Component { } renderServicePointSelect() { - const { servicePoints, intl } = this.props; + const { servicePoints, intl, disabled } = this.props; const options = servicePoints.map(servicePoint => ({ value: servicePoint.id, @@ -92,6 +93,7 @@ class RequestPreferencesEdit extends Component { label={} dataOptions={resultOptions} component={Select} + disabled={disabled} parse={this.defaultServicePointFieldParser} /> ); @@ -174,6 +176,7 @@ class RequestPreferencesEdit extends Component { render() { const { deliveryAvailable, + disabled, } = this.props; return ( @@ -209,6 +212,7 @@ class RequestPreferencesEdit extends Component { label={} component={Checkbox} type="checkbox" + disabled={disabled} /> diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.js b/src/components/EditSections/EditUserInfo/EditUserInfo.js index a4db5e7dd..a78efdd04 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.js @@ -43,6 +43,7 @@ class EditUserInfo extends React.Component { }), }).isRequired, form: PropTypes.object, + disabled: PropTypes.bool, uniquenessValidator: PropTypes.object, }; @@ -115,6 +116,7 @@ class EditUserInfo extends React.Component { intl, stripes, uniquenessValidator, + disabled, } = this.props; const isConsortium = isConsortiumEnabled(stripes); @@ -170,7 +172,7 @@ class EditUserInfo extends React.Component { const isShadowUser = initialValues.type === USER_TYPES.SHADOW; const isSystemUser = initialValues.type === USER_TYPES.SYSTEM; - const isUserTypeDisabled = isShadowUser || isSystemUser; + const isUserTypeDisabled = isShadowUser || isSystemUser || disabled; const typeOptions = [ { @@ -242,6 +244,7 @@ class EditUserInfo extends React.Component { required fullWidth autoFocus + disabled={disabled} /> @@ -251,6 +254,7 @@ class EditUserInfo extends React.Component { id="adduser_firstname" component={TextField} fullWidth + disabled={disabled} /> @@ -260,6 +264,7 @@ class EditUserInfo extends React.Component { id="adduser_middlename" component={TextField} fullWidth + disabled={disabled} /> @@ -269,6 +274,7 @@ class EditUserInfo extends React.Component { id="adduser_preferredname" component={TextField} fullWidth + disabled={disabled} /> @@ -286,6 +292,7 @@ class EditUserInfo extends React.Component { defaultValue={initialValues.patronGroup} aria-required="true" required + disabled={disabled} /> {(selectedPatronGroup) => { @@ -304,7 +311,7 @@ class EditUserInfo extends React.Component { id="useractive" component={Select} fullWidth - disabled={isStatusFieldDisabled()} + disabled={disabled || isStatusFieldDisabled()} dataOptions={statusOptions} defaultValue={initialValues.active} format={(v) => (v ? v.toString() : 'false')} @@ -331,6 +338,7 @@ class EditUserInfo extends React.Component { name="expirationDate" id="adduser_expirationdate" parse={this.parseExpirationDate} + disabled={disabled} validate={validateMinDate('ui-users.errors.personal.dateOfBirth')} /> {checkShowRecalculateButton() && ( @@ -350,6 +358,7 @@ class EditUserInfo extends React.Component { component={TextField} validate={asyncValidateField('barcode', barcode, uniquenessValidator)} fullWidth + disabled={disabled} /> diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js index 61ed877de..30fa34f41 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js @@ -175,4 +175,11 @@ describe('Render Edit User Information component', () => { } expect(screen.getByRole('option', { name: `ui-users.information.userType.${type}` })).toHaveValue(type); }); + + it('should have disabled fields with disabled prop is true', () => { + renderEditUserInfo({ ...props, disabled: true }); + + expect(screen.getByRole('textbox', { name: /lastName/ })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /firstName/ })).toBeDisabled(); + }); }); diff --git a/src/views/UserDetail/UserDetail.js b/src/views/UserDetail/UserDetail.js index d28be72b5..dc521f84b 100644 --- a/src/views/UserDetail/UserDetail.js +++ b/src/views/UserDetail/UserDetail.js @@ -64,6 +64,9 @@ import { } from '../../components/util'; import RequestFeeFineBlockButtons from '../../components/RequestFeeFineBlockButtons'; import { departmentsShape } from '../../shapes'; +import IfConsortiumPermission from '../../components/IfConsortiumPermission'; +import { USER_TYPES } from '../../constants'; +import LostItemsLink from '../../components/LostItemsLink'; import OpenTransactionModal from './components/OpenTransactionModal'; import DeleteUserModal from './components/DeleteUserModal'; @@ -71,8 +74,6 @@ import ExportFeesFinesReportButton from './components'; import ErrorPane from '../../components/ErrorPane'; import ActionMenuEditButton from './components/ActionMenuEditButton'; import ActionMenuDeleteButton from './components/ActionMenuDeleteButton'; -import LostItemsLink from '../../components/LostItemsLink'; -import IfConsortiumPermission from '../../components/IfConsortiumPermission'; class UserDetail extends React.Component { static propTypes = { @@ -622,6 +623,9 @@ class UserDetail extends React.Component { .map(departmentId => departments.find(({ id }) => id === departmentId)?.name); const accounts = resources?.accounts; + const isShadowUser = user?.type === USER_TYPES.SHADOW; + const showPatronBlocksSection = hasPatronBlocksPermissions && !isShadowUser; + if (this.userNotFound()) { return ( - {hasPatronBlocksPermissions && + {showPatronBlocksSection && } noCustomFieldsFoundLabel={} /> - - - - - - - - - - - - { /* Check without version, so can support either of multiple versions. - Replace with specific check when facility for providing - multiple versions is available */ } - - - - - - - - - - - - - + { + !isShadowUser && ( + <> + + + + + + + + + + + + + { /* Check without version, so can support either of multiple versions. + Replace with specific check when facility for providing + multiple versions is available */ } + + + + + + + + + + + + + + + ) + } diff --git a/src/views/UserDetail/UserDetail.test.js b/src/views/UserDetail/UserDetail.test.js index d466beb13..e6b568c34 100644 --- a/src/views/UserDetail/UserDetail.test.js +++ b/src/views/UserDetail/UserDetail.test.js @@ -321,4 +321,15 @@ describe('UserDetail', () => { expect(screen.getByText('LoadingPane')).toBeDefined(); }); }); + + describe('when user type is shadow', () => { + it('should not render Fee-fines Requests Loans Proxy-sponsor Patron blocks', () => { + renderUserDetail(stripes, { resources: { ...resources, selUser: { records: [{ ...resources.selUser.records[0], type: 'shadow' }] } } }); + expect(screen.queryByText('ui-users.loans.title')).toBeNull(); + expect(screen.queryByText('ui-users.requests.title')).toBeNull(); + expect(screen.queryByText('ui-users.accounts.title')).toBeNull(); + expect(screen.queryByText('ui-users.proxySponsor')).toBeNull(); + expect(screen.queryByText('ui-users.patronBlocks')).toBeNull(); + }); + }); }); diff --git a/src/views/UserEdit/UserForm.js b/src/views/UserEdit/UserForm.js index 0a4c3f188..f96fb888f 100644 --- a/src/views/UserEdit/UserForm.js +++ b/src/views/UserEdit/UserForm.js @@ -38,6 +38,7 @@ import getProxySponsorWarning from '../../components/util/getProxySponsorWarning import TenantsPermissionsAccordion from './TenantsPermissionsAccordion'; import css from './UserForm.css'; +import { USER_TYPES } from '../../constants'; export function validate(values) { const errors = {}; @@ -296,6 +297,7 @@ class UserForm extends React.Component { const paneTitle = initialValues.id ? : ; + const isShadowUser = initialValues?.type === USER_TYPES.SHADOW; return ( @@ -354,6 +356,7 @@ class UserForm extends React.Component { form={form} selectedPatronGroup={selectedPatronGroup} uniquenessValidator={uniquenessValidator} + disabled={isShadowUser} /> {initialValues.id &&
- + { + !isShadowUser && ( + + ) + }