diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2c2e7cf4..b015184a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
* Create new permission 'Users: Can view profile pictures'. Refs UIU-3018.
* Format currency values as currencies, not numbers. Refs UIU-2026.
* Show country name in user address instead of country id. Refs UIU-2976.
+* UserInformation in UserDetails to display profile picture. Refs UIU-3011.
## [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/icons/ProfilePicThumbnail.png b/icons/ProfilePicThumbnail.png
new file mode 100644
index 000000000..82cc3094b
Binary files /dev/null and b/icons/ProfilePicThumbnail.png differ
diff --git a/src/components/UserDetailSections/UserInfo/UserInfo.css b/src/components/UserDetailSections/UserInfo/UserInfo.css
index 569a1e98c..f87a4c591 100644
--- a/src/components/UserDetailSections/UserInfo/UserInfo.css
+++ b/src/components/UserDetailSections/UserInfo/UserInfo.css
@@ -27,4 +27,5 @@
.profilePlaceholder {
width: 100px;
height: 100px;
+ object-fit: scale-down;
}
diff --git a/src/components/UserDetailSections/UserInfo/UserInfo.js b/src/components/UserDetailSections/UserInfo/UserInfo.js
index ff64e8174..5216d7a8d 100644
--- a/src/components/UserDetailSections/UserInfo/UserInfo.js
+++ b/src/components/UserDetailSections/UserInfo/UserInfo.js
@@ -1,7 +1,7 @@
import { get } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import {
Row,
Col,
@@ -13,137 +13,152 @@ import {
} from '@folio/stripes/components';
import { ViewMetaData } from '@folio/stripes/smart-components';
+import { useStripes } from '@folio/stripes/core';
import css from './UserInfo.css';
-import appIcon from '../../../../icons/app.png';
import { USER_TYPE_FIELD } from '../../../constants';
+import ProfilePicThumbnail from '../../../../icons/ProfilePicThumbnail.png';
-class UserInfo extends React.Component {
- static propTypes = {
- expanded: PropTypes.bool,
- stripes: PropTypes.object.isRequired,
- onToggle: PropTypes.func,
- accordionId: PropTypes.string.isRequired,
- user: PropTypes.object.isRequired,
- patronGroup: PropTypes.object.isRequired,
- settings: PropTypes.arrayOf(PropTypes.object).isRequired,
- };
-
- constructor(props) {
- super(props);
+import { useProfilePicture } from './hooks';
- this.cViewMetaData = props.stripes.connect(ViewMetaData);
- }
-
- render() {
- const {
- user,
- patronGroup,
- settings,
- expanded,
- accordionId,
- onToggle,
- } = this.props;
- const userStatus = (user?.active ?
- :
- );
- const hasProfilePicture = (settings.length && settings[0].value === 'true');
+const UserInfo = (props) => {
+ const {
+ user,
+ patronGroup,
+ settings,
+ expanded,
+ accordionId,
+ onToggle
+ } = props;
+ const stripes = useStripes();
+ const intl = useIntl();
+ const userStatus = (user?.active ?
+ :
+ );
+ const hasProfilePicture = Boolean(user?.personal?.profilePictureLink);
+ const profilePicturesEnabled = Boolean(settings.length) && settings[0].enabled;
+ const hasViewProfilePicPerm = stripes.hasPerm('ui-users.profilepictures.view');
+ const { isFetching, isLoading, profilePictureData } = useProfilePicture({ profilePictureId: user?.personal?.profilePictureLink });
+ const renderProfilePic = () => {
+ const imgSrc = isLoading || isFetching || !hasProfilePicture ? ProfilePicThumbnail : 'data:;base64,' + profilePictureData;
return (
-
-
- )}
- >
-
-
-
-
-
-
-
-
-
- }
- value={get(user, ['personal', 'lastName'], '')}
- />
-
-
- }
- value={get(user, ['personal', 'firstName'], '')}
- />
-
-
- }
- value={get(user, ['personal', 'middleName'], '')}
- />
-
-
- }
- value={get(user, ['personal', 'preferredFirstName']) || }
- />
-
-
+
+ );
+ };
+
+ return (
+
+
+ )}
+ >
+
+
+
+
+
+
+
+
+
+ }
+ value={get(user, ['personal', 'lastName'], '')}
+ />
+
+
+ }
+ value={get(user, ['personal', 'firstName'], '')}
+ />
+
+
+ }
+ value={get(user, ['personal', 'middleName'], '')}
+ />
+
+
+ }
+ value={get(user, ['personal', 'preferredFirstName']) || }
+ />
+
+
-
-
- }
- value={patronGroup.group}
- />
-
-
- }
- value={userStatus}
- />
-
-
- }
- value={user.expirationDate ? : '-'}
- />
-
-
- }
- value={get(user, ['barcode'], '')}
- />
-
-
-
-
- }
- value={get(user, [USER_TYPE_FIELD], '')}
- />
-
-
-
+
+
+ }
+ value={patronGroup.group}
+ />
+
+
+ }
+ value={userStatus}
+ />
+
+
+ }
+ value={user.expirationDate ? : '-'}
+ />
+
+
+ }
+ value={get(user, ['barcode'], '')}
+ />
+
+
+
+
+ }
+ value={get(user, [USER_TYPE_FIELD], '')}
+ />
+
+
+
- {hasProfilePicture === true &&
+ {
+ profilePicturesEnabled &&
+ hasViewProfilePicPerm &&
-
+ }
+ value={renderProfilePic()}
+ />
- }
-
-
- );
- }
-}
+ }
+
+
+ );
+};
+
+UserInfo.propTypes = {
+ expanded: PropTypes.bool,
+ onToggle: PropTypes.func,
+ accordionId: PropTypes.string.isRequired,
+ user: PropTypes.object.isRequired,
+ patronGroup: PropTypes.object.isRequired,
+ settings: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
export default UserInfo;
diff --git a/src/components/UserDetailSections/UserInfo/UserInfo.test.js b/src/components/UserDetailSections/UserInfo/UserInfo.test.js
index 13a40430f..712a63fcc 100644
--- a/src/components/UserDetailSections/UserInfo/UserInfo.test.js
+++ b/src/components/UserDetailSections/UserInfo/UserInfo.test.js
@@ -3,9 +3,16 @@ import '__mock__/stripesComponents.mock';
import renderWithRouter from 'helpers/renderWithRouter';
import UserInfo from './UserInfo';
+import { useProfilePicture } from './hooks';
+
+import profilePicData from '../../../../test/jest/fixtures/profilePicture';
const toggleMock = jest.fn();
+jest.mock('./hooks', () => ({
+ useProfilePicture: jest.fn(),
+}));
+
const renderUserInfo = (props) => renderWithRouter();
const props = {
@@ -38,6 +45,9 @@ const props = {
};
describe('Render userInfo component', () => {
+ beforeEach(() => {
+ useProfilePicture.mockClear().mockReturnValue(profilePicData.profile_picture_blob);
+ });
describe('Check if user data are shown', () => {
it('Active Users', () => {
renderUserInfo(props);
diff --git a/src/components/UserDetailSections/UserInfo/hooks/index.js b/src/components/UserDetailSections/UserInfo/hooks/index.js
new file mode 100644
index 000000000..e2cdaa9b8
--- /dev/null
+++ b/src/components/UserDetailSections/UserInfo/hooks/index.js
@@ -0,0 +1 @@
+export { default as useProfilePicture } from './useProfilePicture';
diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/index.js b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/index.js
new file mode 100644
index 000000000..9e023fdae
--- /dev/null
+++ b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/index.js
@@ -0,0 +1 @@
+export { default } from './useProfilePicture';
diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js
new file mode 100644
index 000000000..26085782f
--- /dev/null
+++ b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.js
@@ -0,0 +1,35 @@
+import { useQuery } from 'react-query';
+
+import {
+ useNamespace,
+ useOkapiKy,
+} from '@folio/stripes/core';
+
+import { PROFILE_PIC_API } from '../../../../../constants';
+
+const useProfilePicture = ({ profilePictureId }, options = {}) => {
+ const ky = useOkapiKy();
+ const [namespace] = useNamespace({ key: 'get-profile-picture-of-a-user' });
+ const DEFAULT_DATA = {};
+ const {
+ isFetching,
+ isLoading,
+ data = DEFAULT_DATA,
+ } = useQuery(
+ [namespace, profilePictureId],
+ () => {
+ return ky.get(`${PROFILE_PIC_API}/${profilePictureId}`).json();
+ },
+ {
+ enabled: Boolean(profilePictureId),
+ ...options,
+ }
+ );
+
+ return ({
+ isLoading,
+ isFetching,
+ profilePictureData: data?.profile_picture_blob,
+ });
+};
+export default useProfilePicture;
diff --git a/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js
new file mode 100644
index 000000000..5c2c2945e
--- /dev/null
+++ b/src/components/UserDetailSections/UserInfo/hooks/useProfilePicture/useProfilePicture.test.js
@@ -0,0 +1,61 @@
+import profilePicture from 'fixtures/profilePicture';
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import useProfilePicture from './useProfilePicture';
+
+jest.mock('@folio/stripes/core', () => ({
+ ...jest.requireActual('@folio/stripes/core'),
+ useNamespace: jest.fn(() => ['test']),
+ useOkapiKy: jest.fn(),
+}));
+
+const queryClient = new QueryClient();
+
+// eslint-disable-next-line react/prop-types
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+describe('useProfilePicture', () => {
+ beforeEach(() => {
+ useOkapiKy.mockClear().mockReturnValue({
+ get: () => ({ json: () => Promise.resolve(profilePicture) }),
+ });
+ });
+
+ it('should return null', async () => {
+ const { result } = renderHook(() => useProfilePicture({
+ profilePictureId: undefined,
+ }), { wrapper });
+
+ await waitFor(() => !result.current.isLoading);
+
+ expect(result.current).toEqual(expect.objectContaining({
+ 'isFetching': false,
+ 'isLoading': false,
+ 'profilePictureData': undefined,
+ }));
+ });
+
+ it('should return profile picture data', async () => {
+ const { result } = renderHook(() => useProfilePicture({
+ profilePictureId: profilePicture.id,
+ }, {}), { wrapper });
+
+ await waitFor(() => expect(result.current.isFetching).toBeFalsy());
+
+ expect(result.current).toEqual(expect.objectContaining({
+ 'isFetching': false,
+ 'isLoading': false,
+ 'profilePictureData': profilePicture.profile_picture_blob
+ }));
+ });
+});
diff --git a/src/constants.js b/src/constants.js
index 314df3230..cd11a0b5a 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -335,6 +335,7 @@ export const CONFIGURATIONS_ENTRIES_API = `${CONFIGURATIONS_API}/entries`;
export const CONSORTIA_API = 'consortia';
export const CONSORTIA_TENANTS_API = 'tenants';
export const CONSORTIA_USER_TENANTS_API = 'user-tenants';
+export const PROFILE_PIC_API = 'users/profile-picture';
export const RECORD_SOURCE = {
CONSORTIUM: 'consortium',
diff --git a/src/routes/UserRecordContainer.js b/src/routes/UserRecordContainer.js
index bda59bec3..aaee34aef 100644
--- a/src/routes/UserRecordContainer.js
+++ b/src/routes/UserRecordContainer.js
@@ -158,8 +158,7 @@ class UserRecordContainer extends React.Component {
},
settings: {
type: 'okapi',
- records: 'configs',
- path: 'configurations/entries?query=(module==USERS and configName==profile_pictures)',
+ path: 'users/configurations/entry',
},
requestPreferences: {
type: 'okapi',
diff --git a/test/jest/fixtures/profilePicture.json b/test/jest/fixtures/profilePicture.json
new file mode 100644
index 000000000..d961130ed
--- /dev/null
+++ b/test/jest/fixtures/profilePicture.json
@@ -0,0 +1,4 @@
+{
+ "id": "1fe3a5f8-64d9-4913-a24a-ed169db923dd",
+ "profile_picture_blob": "base64-profile-pic-data"
+}
\ No newline at end of file
diff --git a/translations/ui-users/en.json b/translations/ui-users/en.json
index 303194116..80cc88b26 100644
--- a/translations/ui-users/en.json
+++ b/translations/ui-users/en.json
@@ -308,6 +308,7 @@
"information.lastName": "Last name",
"information.firstName": "First name",
"information.middleName": "Middle name",
+ "information.profilePicture": "Profile picture",
"information.barcode": "Barcode",
"information.selectUserType": "Select user type",
"information.userType": "User type",