Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIU-3011 - UserInformation in UserDetails to display profile picture. #2618

Merged
merged 13 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Add patron notice print jobs to action menu. Refs UIU-3029.
* 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.

## [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)
Expand Down
Binary file added icons/profilePicThumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"user-settings.custom-fields.item.get",
"user-settings.custom-fields.item.stats.get",
"departments.collection.get",
"departments.item.get"
"departments.item.get",
"users.configurations.item.get"
],
"visible": true
},
Expand Down Expand Up @@ -987,8 +988,7 @@
"description": "Also includes basic permissions to view users",
"subPermissions": [
"ui-users.view",
"users.profile-picture.item.get",
"users.configurations.item.get"
"users.profile-picture.item.get"
],
"visible": true
},
Expand Down
1 change: 1 addition & 0 deletions src/components/UserDetailSections/UserInfo/UserInfo.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
.profilePlaceholder {
width: 100px;
height: 100px;
object-fit: scale-down;
}
261 changes: 143 additions & 118 deletions src/components/UserDetailSections/UserInfo/UserInfo.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,137 +13,162 @@ 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';
import { isAValidUUID } from '../../util/util';

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);
}
const UserInfo = (props) => {
const {
user,
patronGroup,
settings,
expanded,
accordionId,
onToggle
} = props;
const profilePictureLink = user?.personal?.profilePictureLink;
const stripes = useStripes();
const intl = useIntl();
const userStatus = (user?.active ?
<FormattedMessage id="ui-users.active" /> :
<FormattedMessage id="ui-users.inactive" />);
const hasProfilePicture = Boolean(profilePictureLink);
/**
* Profile Picture Link can be
* 1. an id(uuid) of profile picture stored in database or
* 2. a link to an image - a url
*/
const isProfilePictureLinkAURL = !isAValidUUID(profilePictureLink);
const profilePicturesEnabled = Boolean(settings.length) && settings[0].enabled;
const hasViewProfilePicturePerm = stripes.hasPerm('ui-users.profile-pictures.view');
const { isFetching, profilePictureData } = useProfilePicture({ profilePictureId: profilePictureLink });

render() {
const {
user,
patronGroup,
settings,
expanded,
accordionId,
onToggle,
} = this.props;
const userStatus = (user?.active ?
<FormattedMessage id="ui-users.active" /> :
<FormattedMessage id="ui-users.inactive" />);
const hasProfilePicture = (settings.length && settings[0].value === 'true');
const renderProfilePic = () => {
const profilePictureSrc = isProfilePictureLinkAURL ? profilePictureLink : 'data:;base64,' + profilePictureData;
const imgSrc = isFetching || !hasProfilePicture ? profilePicThumbnail : profilePictureSrc;

return (
<Accordion
open={expanded}
id={accordionId}
onToggle={onToggle}
label={(
<Headline
size="large"
tag="h3"
>
<FormattedMessage id="ui-users.information.userInformation" />
</Headline>)}
>
<Row>
<Col xs={12}>
<this.cViewMetaData metadata={user.metadata} />
</Col>
</Row>
<Row>
<Col xs={hasProfilePicture ? 9 : 12}>
<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.lastName" />}
value={get(user, ['personal', 'lastName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.firstName" />}
value={get(user, ['personal', 'firstName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.middleName" />}
value={get(user, ['personal', 'middleName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.preferredName" />}
value={get(user, ['personal', 'preferredFirstName']) || <NoValue />}
/>
</Col>
</Row>
<img
className={css.profilePlaceholder}
alt={intl.formatMessage({ id: 'ui-users.information.profilePicture' })}
src={imgSrc}
/>
);
};

<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.patronGroup" />}
value={patronGroup.group}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.status" />}
value={userStatus}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.expirationDate" />}
value={user.expirationDate ? <FormattedDate value={user.expirationDate} /> : '-'}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.barcode" />}
value={get(user, ['barcode'], '')}
/>
</Col>
</Row>
<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.userType" />}
value={get(user, [USER_TYPE_FIELD], '')}
/>
</Col>
</Row>
</Col>
return (
<Accordion
open={expanded}
id={accordionId}
onToggle={onToggle}
label={(
<Headline
size="large"
tag="h3"
>
<FormattedMessage id="ui-users.information.userInformation" />
</Headline>)}
>
<Row>
<Col xs={12}>
<ViewMetaData metadata={user?.metadata} />
</Col>
</Row>
<Row>
<Col xs={profilePicturesEnabled && hasViewProfilePicturePerm ? 9 : 12}>
<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.lastName" />}
value={get(user, ['personal', 'lastName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.firstName" />}
value={get(user, ['personal', 'firstName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.middleName" />}
value={get(user, ['personal', 'middleName'], '')}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.preferredName" />}
value={get(user, ['personal', 'preferredFirstName']) || <NoValue />}
/>
</Col>
</Row>

{hasProfilePicture === true &&
<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.patronGroup" />}
value={patronGroup.group}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.status" />}
value={userStatus}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.expirationDate" />}
value={user.expirationDate ? <FormattedDate value={user.expirationDate} /> : '-'}
/>
</Col>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.barcode" />}
value={get(user, ['barcode'], '')}
/>
</Col>
</Row>
<Row>
<Col xs={3}>
<KeyValue
label={<FormattedMessage id="ui-users.information.userType" />}
value={get(user, [USER_TYPE_FIELD], '')}
/>
</Col>
</Row>
</Col>

{
profilePicturesEnabled &&
hasViewProfilePicturePerm &&
<Col xs={3}>
<Row>
<Col xs={12}>
<img className={`floatEnd ${css.profilePlaceholder}`} src={appIcon} alt="presentation" />
<KeyValue
label={<FormattedMessage id="ui-users.information.profilePicture" />}
value={renderProfilePic()}
/>
</Col>
</Row>
</Col>
}
</Row>
</Accordion>
);
}
}
}
</Row>
</Accordion>
);
};

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;
22 changes: 16 additions & 6 deletions src/components/UserDetailSections/UserInfo/UserInfo.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { screen } from '@folio/jest-config-stripes/testing-library/react';
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(<UserInfo {...props} />);

const props = {
expanded: true,
onToggle: toggleMock,
accordionId: 'userInformationSection',
stripes: {
connect: (Component) => Component,
},
patronGroup: {
desc: 'Staff Member',
expirationOffsetInDays: 730,
Expand All @@ -28,16 +31,19 @@ const props = {
departments: [],
id: 'ec6d380d-bcdd-4ef6-bb65-15677ab7cb84',
patronGroup: '3684a786-6671-4268-8ed0-9db82ebca60b',
personal: { lastName: 'Admin', firstName: 'acq-admin', addresses: [] },
personal: { lastName: 'Admin', firstName: 'acq-admin', addresses: [], profilePictureLink: 'profilePictureLink' },
proxyFor: [],
type: 'patron',
updatedDate: '2022-05-10T02:00:49.576+00:00',
username: 'acq-admin'
},
settings: [{ value: true }]
settings: [{ enabled: true }]
};

describe('Render userInfo component', () => {
beforeEach(() => {
useProfilePicture.mockClear().mockReturnValue(profilePicData.profile_picture_blob);
});
describe('Check if user data are shown', () => {
it('Active Users', () => {
renderUserInfo(props);
Expand All @@ -50,5 +56,9 @@ describe('Render userInfo component', () => {
expect(screen.getByText('acq-admin')).toBeInTheDocument();
expect(screen.getByText('1652148049552566548')).toBeInTheDocument();
});
it('should display profile picture', () => {
renderUserInfo(props);
expect(screen.getByText('ui-users.information.profilePicture')).toBeInTheDocument();
});
});
});
1 change: 1 addition & 0 deletions src/components/UserDetailSections/UserInfo/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useProfilePicture } from './useProfilePicture';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useProfilePicture';
Loading
Loading