From cba4384dac2d6f1885163d5c3086522881c3260b Mon Sep 17 00:00:00 2001 From: Priyanka Terala <104053200+Terala-Priyanka@users.noreply.github.com> Date: Wed, 7 Feb 2024 08:30:32 +0530 Subject: [PATCH] UIU-3005 - Display Profile picture on edit screen based on user permission. (#2620) * UIU-3005 - edit screen * UIU-3005 - fix linting configuration * cleanup * Revert "UIU-3005 - fix linting configuration" This reverts commit 322a45533dc0aeca9950430bd902f09ff3fb712b. * UIU-3005 - cleanup - userdetails - pic useProfilePicture hook from hooks folder * UIU-3005 - cleanup * UIU-3005 - refinement * UIU-3005 - add unit tests * UIU-3005 - cleanup * UIU-3005 - i18'ned Update button * UIU-3005-add more translations --- CHANGELOG.md | 1 + .../EditUserInfo/EditUserInfo.css | 5 + .../EditSections/EditUserInfo/EditUserInfo.js | 274 ++++++++++-------- .../EditUserInfo/EditUserInfo.test.js | 21 +- .../ProfilePicture/ProfilePicture.js | 92 ++++++ .../ProfilePicture/ProfilePicture.test.js | 58 ++++ .../components/ProfilePicture/index.js | 1 + .../EditUserInfo/components/index.js | 2 + .../UserDetailSections/UserInfo/UserInfo.js | 2 +- .../UserInfo/UserInfo.test.js | 4 +- .../UserInfo/hooks/index.js | 1 - src/hooks/index.js | 1 + .../hooks/useProfilePicture/index.js | 0 .../useProfilePicture/useProfilePicture.js | 6 +- .../useProfilePicture.test.js | 4 +- src/views/UserDetail/UserDetail.js | 12 +- src/views/UserDetail/UserDetail.test.js | 1 + src/views/UserEdit/UserEdit.js | 3 + src/views/UserEdit/UserForm.js | 3 + translations/ui-users/en.json | 4 + 20 files changed, 360 insertions(+), 135 deletions(-) create mode 100644 src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.js create mode 100644 src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.test.js create mode 100644 src/components/EditSections/EditUserInfo/components/ProfilePicture/index.js create mode 100644 src/components/EditSections/EditUserInfo/components/index.js delete mode 100644 src/components/UserDetailSections/UserInfo/hooks/index.js rename src/{components/UserDetailSections/UserInfo => }/hooks/useProfilePicture/index.js (100%) rename src/{components/UserDetailSections/UserInfo => }/hooks/useProfilePicture/useProfilePicture.js (83%) rename src/{components/UserDetailSections/UserInfo => }/hooks/useProfilePicture/useProfilePicture.test.js (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3514b4f24..b96ae37f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Update sub permissions of permission 'Users: Can view user profiles'. Refs UIU-3038. * Create new permission 'Users: Can view, edit, and delete profile pictures'. Refs UIU-3025. * UserInformation in UserDetails to display profile picture. Refs UIU-3011. +* User Information in User Edit to display profile picture and update button set. Refs UIU-3005. ## [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.css b/src/components/EditSections/EditUserInfo/EditUserInfo.css index a970323c2..87607a4c3 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.css +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.css @@ -33,3 +33,8 @@ top: -10px; font-size: var(--font-size-small); } + +.profilePlaceholder { + max-width: 100px; + max-height: 100px; +} diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.js b/src/components/EditSections/EditUserInfo/EditUserInfo.js index b81c5919d..2aebf9cf8 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.js @@ -26,6 +26,7 @@ import asyncValidateField from '../../validators/asyncValidateField'; import validateMinDate from '../../validators/validateMinDate'; import css from './EditUserInfo.css'; +import ProfilePicture from './components/ProfilePicture'; class EditUserInfo extends React.Component { static propTypes = { @@ -41,10 +42,12 @@ class EditUserInfo extends React.Component { dispatch: PropTypes.func.isRequired, getState: PropTypes.func, }), + hasPerm: PropTypes.func, }).isRequired, form: PropTypes.object, disabled: PropTypes.bool, uniquenessValidator: PropTypes.object, + areProfilePicturesEnabled: PropTypes.bool.isRequired, }; constructor(props) { @@ -117,6 +120,7 @@ class EditUserInfo extends React.Component { stripes, uniquenessValidator, disabled, + areProfilePicturesEnabled, } = this.props; const isConsortium = isConsortiumEnabled(stripes); @@ -223,6 +227,8 @@ class EditUserInfo extends React.Component { ); + const hasViewProfilePicturePerm = stripes.hasPerm('ui-users.profile-pictures.view'); + return ( <> } - - } - name="personal.lastName" - id="adduser_lastname" - component={TextField} - required - fullWidth - autoFocus - disabled={disabled} - /> - - - } - name="personal.firstName" - id="adduser_firstname" - component={TextField} - fullWidth - disabled={disabled} - /> - - - } - name="personal.middleName" - id="adduser_middlename" - component={TextField} - fullWidth - disabled={disabled} - /> - - - } - name="personal.preferredFirstName" - id="adduser_preferredname" - component={TextField} - fullWidth - disabled={disabled} - /> + + + + } + name="personal.lastName" + id="adduser_lastname" + component={TextField} + required + fullWidth + autoFocus + disabled={disabled} + /> + + + } + name="personal.firstName" + id="adduser_firstname" + component={TextField} + fullWidth + disabled={disabled} + /> + + + } + name="personal.middleName" + id="adduser_middlename" + component={TextField} + fullWidth + disabled={disabled} + /> + + + } + name="personal.preferredFirstName" + id="adduser_preferredname" + component={TextField} + fullWidth + disabled={disabled} + /> + + + + + + } + name="patronGroup" + id="adduser_group" + component={Select} + selectClass={css.patronGroup} + fullWidth + dataOptions={patronGroupOptions} + defaultValue={initialValues.patronGroup} + aria-required="true" + required={!disabled} + /> + + {(selectedPatronGroup) => { + this.setState({ selectedPatronGroup }, () => { + if (this.getPatronGroupOffset()) { + this.showModal(true); + } + }); + }} + + + + } + name="active" + id="useractive" + component={Select} + fullWidth + disabled={disabled || isStatusFieldDisabled()} + dataOptions={statusOptions} + defaultValue={initialValues.active} + format={(v) => (v ? v.toString() : 'false')} + aria-required="true" + required + /> + {isUserExpired() && ( + + + + )} + {isUserExpired() && willUserExtend() && ( +

+ +

+ )} + + + } + dateFormat="YYYY-MM-DD" + defaultValue={initialValues.expirationDate} + name="expirationDate" + id="adduser_expirationdate" + parse={this.parseExpirationDate} + disabled={disabled} + validate={validateMinDate('ui-users.errors.personal.dateOfBirth')} + /> + {checkShowRecalculateButton() && ( + + )} + + + } + name="barcode" + id="adduser_barcode" + component={TextField} + validate={asyncValidateField('barcode', barcode, uniquenessValidator)} + fullWidth + disabled={disabled} + /> + +
-
+ { + areProfilePicturesEnabled && hasViewProfilePicturePerm && + + + + } + id="profilePicture" + name="profilePicture" + profilePictureLink={initialValues?.personal?.profilePictureLink} + render={(props) => ()} + /> + + + + } + - - } - name="patronGroup" - id="adduser_group" - component={Select} - selectClass={css.patronGroup} - fullWidth - dataOptions={patronGroupOptions} - defaultValue={initialValues.patronGroup} - aria-required="true" - required={!disabled} - /> - - {(selectedPatronGroup) => { - this.setState({ selectedPatronGroup }, () => { - if (this.getPatronGroupOffset()) { - this.showModal(true); - } - }); - }} - - - - } - name="active" - id="useractive" - component={Select} - fullWidth - disabled={disabled || isStatusFieldDisabled()} - dataOptions={statusOptions} - defaultValue={initialValues.active} - format={(v) => (v ? v.toString() : 'false')} - aria-required="true" - required - /> - {isUserExpired() && ( - - - - )} - {isUserExpired() && willUserExtend() && ( -

- -

- )} - - - } - dateFormat="YYYY-MM-DD" - defaultValue={initialValues.expirationDate} - name="expirationDate" - id="adduser_expirationdate" - parse={this.parseExpirationDate} - disabled={disabled} - validate={validateMinDate('ui-users.errors.personal.dateOfBirth')} - /> - {checkShowRecalculateButton() && ( - - )} - - - } - name="barcode" - id="adduser_barcode" - component={TextField} - validate={asyncValidateField('barcode', barcode, uniquenessValidator)} - fullWidth - disabled={disabled} - /> - } @@ -374,6 +403,7 @@ class EditUserInfo extends React.Component { />
+
({ + useProfilePicture: jest.fn(), +})); jest.mock('@folio/stripes/components', () => ({ ...jest.requireActual('@folio/stripes/components'), Modal: jest.fn(({ children, label, footer, ...rest }) => { @@ -34,6 +37,8 @@ jest.mock('../../util', () => ({ isConsortiumEnabled: jest.fn(() => true), })); +jest.mock('./components/ProfilePicture', () => jest.fn(() => 'Profile Picture')); + const onSubmit = jest.fn(); const arrayMutators = { @@ -86,6 +91,7 @@ const props = { connect: (Component) => Component, timezone: 'USA/TestTimeZone', hasInterface: () => true, + hasPerm: () => true, }, patronGroups: [{ desc: 'Staff Member', @@ -121,7 +127,8 @@ const props = { PUT: jest.fn(), cancel: jest.fn(), reset: jest.fn() - } + }, + areProfilePicturesEnabled: true, }; describe('Render Edit User Information component', () => { @@ -182,4 +189,16 @@ describe('Render Edit User Information component', () => { expect(screen.getByRole('textbox', { name: /lastName/ })).toBeDisabled(); expect(screen.getByRole('textbox', { name: /firstName/ })).toBeDisabled(); }); + + it('should display profile picture', () => { + renderEditUserInfo(props); + expect(screen.getByText('Profile Picture')).toBeInTheDocument(); + }); + + describe('when profilePicture configuration is not enabled', () => { + it('should not render profile picture', () => { + renderEditUserInfo({ ...props, areProfilePicturesEnabled: false }); + expect(screen.queryByText('Profile Picture')).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.js b/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.js new file mode 100644 index 000000000..00ea90c80 --- /dev/null +++ b/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.js @@ -0,0 +1,92 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; + +import { + Button, + Dropdown, + DropdownMenu, + Icon, + Label, +} from '@folio/stripes/components'; +import { useStripes } from '@folio/stripes/core'; + +import { useProfilePicture } from '../../../../../hooks'; +import { isAValidUUID } from '../../../../util/util'; +import profilePicThumbnail from '../../../../../../icons/profilePicThumbnail.png'; +import css from '../../EditUserInfo.css'; + +const ProfilePicture = ({ label, profilePictureLink }) => { + const intl = useIntl(); + const stripes = useStripes(); + const isProfilePictureLinkAURL = !isAValidUUID(profilePictureLink); + const hasProfilePicture = Boolean(profilePictureLink); + const { isFetching, profilePictureData } = useProfilePicture({ profilePictureId: profilePictureLink }); + const hasAllProfilePicturePerms = stripes.hasPerm('ui-users.profile-pictures.all'); + + const renderProfilePic = () => { + const profilePictureSrc = isProfilePictureLinkAURL ? profilePictureLink : 'data:;base64,' + profilePictureData; + const imgSrc = isFetching || !hasProfilePicture ? profilePicThumbnail : profilePictureSrc; + + return ( + {intl.formatMessage({ + ); + }; + + const renderMenu = () => ( + + + + + + ); + + return ( + <> + + { renderProfilePic()} +
+ { + hasAllProfilePicturePerms && ( + } + placement="bottom-end" + renderMenu={renderMenu} + /> + ) + } + + ); +}; + +ProfilePicture.propTypes = { + label: PropTypes.node.isRequired, + profilePictureLink: PropTypes.string, +}; + +export default ProfilePicture; diff --git a/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.test.js b/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.test.js new file mode 100644 index 000000000..1e709c263 --- /dev/null +++ b/src/components/EditSections/EditUserInfo/components/ProfilePicture/ProfilePicture.test.js @@ -0,0 +1,58 @@ +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; +import profilePicData from 'fixtures/profilePicture'; + +import ProfilePicture from './ProfilePicture'; +import { useProfilePicture } from '../../../../../hooks'; + +jest.unmock('@folio/stripes/components'); + +jest.mock('../../../../../hooks', () => ({ + useProfilePicture: jest.fn(), +})); + +const props = { + label: 'Profile picture', + profilePictureLink: 'profilePictureLink' +}; + +describe('Profile Picture', () => { + beforeEach(() => { + useProfilePicture.mockClear().mockReturnValue(profilePicData.profile_picture_blob); + render(); + }); + + it('should display Profile picture', () => { + expect(screen.getByTestId('profile-picture')).toBeInTheDocument(); + }); + + it('Image to be displayed with correct src', () => { + const image = screen.getByTestId('profile-picture'); + expect(image.src).toContain('profilePictureLink'); + }); + + it('Update button to be displayed', () => { + expect(screen.getByTestId('updateProfilePictureDropdown')).toBeInTheDocument(); + }); + + it('Local file button to be displayed', async () => { + const updateButton = screen.getByTestId('updateProfilePictureDropdown'); + await userEvent.click(updateButton); + + expect(screen.getByText('Icon (profile)')).toBeInTheDocument(); + }); + + it('External link button to be displayed', async () => { + const updateButton = screen.getByTestId('updateProfilePictureDropdown'); + await userEvent.click(updateButton); + + expect(screen.getByText('Icon (external-link)')).toBeInTheDocument(); + }); + + it('Delete link button to be displayed', async () => { + const updateButton = screen.getByTestId('updateProfilePictureDropdown'); + await userEvent.click(updateButton); + + expect(screen.getByText('Icon (trash)')).toBeInTheDocument(); + }); +}); diff --git a/src/components/EditSections/EditUserInfo/components/ProfilePicture/index.js b/src/components/EditSections/EditUserInfo/components/ProfilePicture/index.js new file mode 100644 index 000000000..ffff0ba8c --- /dev/null +++ b/src/components/EditSections/EditUserInfo/components/ProfilePicture/index.js @@ -0,0 +1 @@ +export { default } from './ProfilePicture'; diff --git a/src/components/EditSections/EditUserInfo/components/index.js b/src/components/EditSections/EditUserInfo/components/index.js new file mode 100644 index 000000000..2497a0f13 --- /dev/null +++ b/src/components/EditSections/EditUserInfo/components/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as ProfilePicture } from './ProfilePicture'; diff --git a/src/components/UserDetailSections/UserInfo/UserInfo.js b/src/components/UserDetailSections/UserInfo/UserInfo.js index f0d77a594..1ef82fbe6 100644 --- a/src/components/UserDetailSections/UserInfo/UserInfo.js +++ b/src/components/UserDetailSections/UserInfo/UserInfo.js @@ -19,7 +19,7 @@ import { USER_TYPE_FIELD } from '../../../constants'; import profilePicThumbnail from '../../../../icons/profilePicThumbnail.png'; import { isAValidUUID } from '../../util/util'; -import { useProfilePicture } from './hooks'; +import { useProfilePicture } from '../../../hooks'; const UserInfo = (props) => { const { diff --git a/src/components/UserDetailSections/UserInfo/UserInfo.test.js b/src/components/UserDetailSections/UserInfo/UserInfo.test.js index ce992f91c..befd59af2 100644 --- a/src/components/UserDetailSections/UserInfo/UserInfo.test.js +++ b/src/components/UserDetailSections/UserInfo/UserInfo.test.js @@ -2,13 +2,13 @@ import { screen } from '@folio/jest-config-stripes/testing-library/react'; import renderWithRouter from 'helpers/renderWithRouter'; import UserInfo from './UserInfo'; -import { useProfilePicture } from './hooks'; +import { useProfilePicture } from '../../../hooks'; import profilePicData from '../../../../test/jest/fixtures/profilePicture'; const toggleMock = jest.fn(); -jest.mock('./hooks', () => ({ +jest.mock('../../../hooks', () => ({ useProfilePicture: jest.fn(), })); diff --git a/src/components/UserDetailSections/UserInfo/hooks/index.js b/src/components/UserDetailSections/UserInfo/hooks/index.js deleted file mode 100644 index e2cdaa9b8..000000000 --- a/src/components/UserDetailSections/UserInfo/hooks/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as useProfilePicture } from './useProfilePicture'; diff --git a/src/hooks/index.js b/src/hooks/index.js index 317157751..a2348027d 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -5,3 +5,4 @@ export { default as useToggle } from './useToggle'; export { default as useUserAffiliations } from './useUserAffiliations'; export { default as useUserAffiliationsMutation } from './useUserAffiliationsMutation'; export { default as useUserTenantPermissions } from './useUserTenantPermissions'; +export { default as useProfilePicture } from './useProfilePicture'; diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/index.js b/src/hooks/useProfilePicture/index.js similarity index 100% rename from src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/index.js rename to src/hooks/useProfilePicture/index.js diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js b/src/hooks/useProfilePicture/useProfilePicture.js similarity index 83% rename from src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js rename to src/hooks/useProfilePicture/useProfilePicture.js index 588b01d94..5cb7a0b23 100644 --- a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js +++ b/src/hooks/useProfilePicture/useProfilePicture.js @@ -5,14 +5,15 @@ import { useOkapiKy, } from '@folio/stripes/core'; -import { PROFILE_PIC_API } from '../../../../../constants'; -import { isAValidUUID } from '../../../../util/util'; +import { PROFILE_PIC_API } from '../../constants'; +import { isAValidUUID } from '../../components/util/util'; const useProfilePicture = ({ profilePictureId }, options = {}) => { const ky = useOkapiKy(); const [namespace] = useNamespace({ key: 'get-profile-picture-of-a-user' }); const { isFetching, + isLoading, data = {}, } = useQuery( [namespace, profilePictureId], @@ -26,6 +27,7 @@ const useProfilePicture = ({ profilePictureId }, options = {}) => { ); return ({ + isLoading, isFetching, profilePictureData: data?.profile_picture_blob, }); diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js b/src/hooks/useProfilePicture/useProfilePicture.test.js similarity index 93% rename from src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js rename to src/hooks/useProfilePicture/useProfilePicture.test.js index ac29960dc..22309ca9f 100644 --- a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js +++ b/src/hooks/useProfilePicture/useProfilePicture.test.js @@ -29,10 +29,11 @@ describe('useProfilePicture', () => { profilePictureId: undefined, }), { wrapper }); - await waitFor(() => !result.current.isFetching); + await waitFor(() => !result.current.isLoading); expect(result.current).toEqual(expect.objectContaining({ 'isFetching': false, + 'isLoading': false, 'profilePictureData': undefined, })); }); @@ -46,6 +47,7 @@ describe('useProfilePicture', () => { expect(result.current).toEqual(expect.objectContaining({ 'isFetching': false, + 'isLoading': false, 'profilePictureData': profilePicture.profile_picture_blob })); }); diff --git a/src/views/UserDetail/UserDetail.js b/src/views/UserDetail/UserDetail.js index 86885bcbf..259fa9cd3 100644 --- a/src/views/UserDetail/UserDetail.js +++ b/src/views/UserDetail/UserDetail.js @@ -424,6 +424,7 @@ class UserDetail extends React.Component { }, }, resources, + stripes, } = this.props; const user = this.getUser(); const patronGroup = this.getPatronGroup(user); @@ -442,11 +443,12 @@ class UserDetail extends React.Component { loans, }; - const showActionMenu = this.props.stripes.hasPerm('ui-users.edit') - || this.props.stripes.hasPerm('ui-users.patron_blocks') - || this.props.stripes.hasPerm('ui-users.feesfines.actions.all') - || this.props.stripes.hasPerm('ui-requests.all') - || this.props.stripes.hasPerm('ui-users.delete,ui-users.opentransactions'); + const showActionMenu = stripes.hasPerm('ui-users.edit') + || stripes.hasPerm('ui-users.patron_blocks') + || stripes.hasPerm('ui-users.feesfines.actions.all') + || stripes.hasPerm('ui-requests.all') + || stripes.hasPerm('ui-users.delete,ui-users.opentransactions') + || stripes.hasPerm('ui-users.profile-pictures.all'); if (showActionMenu && !isVirtualPatron) { return ( diff --git a/src/views/UserDetail/UserDetail.test.js b/src/views/UserDetail/UserDetail.test.js index 5a5d46c24..92de85592 100644 --- a/src/views/UserDetail/UserDetail.test.js +++ b/src/views/UserDetail/UserDetail.test.js @@ -198,6 +198,7 @@ describe('UserDetail', () => { beforeEach(() => { stripes = useStripes(); + stripes.hasPerm = () => true; mutator.hasManualPatronBlocks.GET.mockImplementation(() => Promise.resolve([])); mutator.hasAutomatedPatronBlocks.GET.mockImplementation(() => Promise.resolve([])); }); diff --git a/src/views/UserEdit/UserEdit.js b/src/views/UserEdit/UserEdit.js index 6698ff9c4..5db197074 100644 --- a/src/views/UserEdit/UserEdit.js +++ b/src/views/UserEdit/UserEdit.js @@ -377,6 +377,8 @@ class UserEdit extends React.Component { match: { params }, } = this.props; + const areProfilePicturesEnabled = get(resources, 'settings.records[0].enabled'); + if (!resourcesLoaded(resources, ['uniquenessValidator']) || (!this.getUser() && this.props.match.params.id)) { return ( ); } diff --git a/src/views/UserEdit/UserForm.js b/src/views/UserEdit/UserForm.js index eb16cf854..de9d1ed62 100644 --- a/src/views/UserEdit/UserForm.js +++ b/src/views/UserEdit/UserForm.js @@ -103,6 +103,7 @@ class UserForm extends React.Component { stripes: PropTypes.object, form: PropTypes.object, // provided by final-form intl: PropTypes.object, + areProfilePicturesEnabled: PropTypes.bool.isRequired, }; static defaultProps = { @@ -295,6 +296,7 @@ class UserForm extends React.Component { stripes, form, uniquenessValidator, + areProfilePicturesEnabled, } = this.props; const selectedPatronGroup = form.getFieldState('patronGroup')?.value; @@ -364,6 +366,7 @@ class UserForm extends React.Component { selectedPatronGroup={selectedPatronGroup} uniquenessValidator={uniquenessValidator} disabled={isShadowUser} + areProfilePicturesEnabled={areProfilePicturesEnabled} />