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

(refactor) O3-2831 Move patient banner into styleguide #1645

Merged
merged 9 commits into from
Mar 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import { mockPatient } from 'tools';
import { mockDeceasedPatient } from '__mocks__';
import DeceasedPatientBannerTag from './deceased-patient-tag.component';
import DeceasedPatientBannerTag from './deceased-patient-tag.extension';

describe('DeceasedPatientTag', () => {
it('does not render Deceased tag for patients who are still alive', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { formatDatetime } from '@openmrs/esm-framework';
import { useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
import { mockCurrentVisit } from '__mocks__';
import { mockPatient } from 'tools';
import VisitTag from './visit-tag.component';
import VisitTag from './visit-tag.extension';

const mockUseVisitOrOfflineVisit = useVisitOrOfflineVisit as jest.Mock;

Expand Down
182 changes: 34 additions & 148 deletions packages/esm-patient-banner-app/src/banner/patient-banner.component.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
import React, { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button, Tag } from '@carbon/react';
import { ChevronDown, ChevronUp, OverflowMenuVertical } from '@carbon/react/icons';
import { ExtensionSlot, age, formatDate, parseDate, useConfig, useConnectedExtensions } from '@openmrs/esm-framework';
import ContactDetails from '../contact-details/contact-details.component';
import CustomOverflowMenuComponent from '../ui-components/overflow-menu.component';
import {
PatientBannerActionsMenu,
PatientBannerContactDetails,
PatientBannerPatientInfo,
PatientBannerToggleContactDetailsButton,
PatientPhoto,
} from '@openmrs/esm-framework';
import styles from './patient-banner.scss';

interface PatientBannerProps {
patient: fhir.Patient;
patientUuid: string;
onClick?: (patientUuid: string) => void;
onTransition?: () => void;
hideActionsOverflow?: boolean;
}

const PatientBanner: React.FC<PatientBannerProps> = ({
patient,
patientUuid,
onClick,
onTransition,
hideActionsOverflow,
Comment on lines -22 to -23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onClick and onTransition extension state props are not used in any use of patient-header-slot on GitHub. I think they are cruft from before we refactored out the patient banner in esm-search.

}) => {
const { t } = useTranslation();
const overflowMenuRef = useRef(null);
const PatientBanner: React.FC<PatientBannerProps> = ({ patient, patientUuid, hideActionsOverflow }) => {
const patientBannerRef = useRef(null);
const [isTabletViewport, setIsTabletViewport] = useState(false);
const { excludePatientIdentifierCodeTypes } = useConfig();
const patientActions = useConnectedExtensions('patient-actions-slot');

useEffect(() => {
const currentRef = patientBannerRef.current;
Expand All @@ -45,13 +34,7 @@ const PatientBanner: React.FC<PatientBannerProps> = ({
};
}, [patientBannerRef, setIsTabletViewport]);

const patientActionsSlotState = useMemo(
() => ({ patientUuid, onClick, onTransition }),
[patientUuid, onClick, onTransition],
);

const patientName = `${patient?.name?.[0]?.given?.join(' ')} ${patient?.name?.[0].family}`;
const patientPhotoSlotState = useMemo(() => ({ patientUuid, patientName }), [patientUuid, patientName]);

const [showContactDetails, setShowContactDetails] = useState(false);
const toggleContactDetails = useCallback(() => {
Expand All @@ -60,51 +43,6 @@ const PatientBanner: React.FC<PatientBannerProps> = ({

const isDeceased = Boolean(patient?.deceasedDateTime);

const patientAvatar = (
<div className={styles.patientAvatar} role="img">
<ExtensionSlot name="patient-photo-slot" state={patientPhotoSlotState} />
</div>
);

const handleNavigateToPatientChart = useCallback(
(event: MouseEvent) => {
if (onClick) {
!(overflowMenuRef?.current && overflowMenuRef?.current.contains(event.target)) && onClick(patientUuid);
}
},
[onClick, patientUuid],
);

const [showDropdown, setShowDropdown] = useState(false);
const closeDropdownMenu = useCallback(() => {
setShowDropdown((value) => !value);
}, []);

const showActionsMenu = useMemo(
() => !hideActionsOverflow && patientActions.length > 0,
[patientActions.length, hideActionsOverflow],
);

const getGender = (gender: string): string => {
switch (gender) {
case 'male':
return t('male', 'Male');
case 'female':
return t('female', 'Female');
case 'other':
return t('other', 'Other');
case 'unknown':
return t('unknown', 'Unknown');
default:
return gender;
}
};

const identifiers =
patient?.identifier?.filter(
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type.coding[0].code),
) ?? [];

return (
<header
className={classNames(
Expand All @@ -113,86 +51,34 @@ const PatientBanner: React.FC<PatientBannerProps> = ({
)}
ref={patientBannerRef}
>
<div
className={classNames(styles.patientBanner, { [styles.patientAvatarButton]: onClick })}
onClick={handleNavigateToPatientChart}
role="button"
tabIndex={0}
>
{patientAvatar}
<div className={styles.patientInfo}>
<div className={classNames(styles.row, styles.patientNameRow)}>
<div className={styles.flexRow}>
<span className={styles.patientName}>{patientName}</span>
<ExtensionSlot
name="patient-banner-tags-slot"
state={{ patientUuid, patient }}
className={styles.flexRow}
/>
</div>
{showActionsMenu && (
<div className={styles.overflowMenuContainer} ref={overflowMenuRef}>
<CustomOverflowMenuComponent
deceased={isDeceased}
menuTitle={
<>
<span className={styles.actionsButtonText}>{t('actions', 'Actions')}</span>{' '}
<OverflowMenuVertical size={16} style={{ marginLeft: '0.5rem', fill: '#78A9FF' }} />
</>
}
dropDownMenu={showDropdown}
>
<ExtensionSlot
onClick={closeDropdownMenu}
name="patient-actions-slot"
key="patient-actions-slot"
className={styles.overflowMenuItemList}
state={patientActionsSlotState}
/>
</CustomOverflowMenuComponent>
</div>
)}
</div>
<div className={styles.demographics}>
<span>{getGender(patient?.gender)}</span> &middot; <span>{age(patient?.birthDate)}</span> &middot;{' '}
<span>{formatDate(parseDate(patient?.birthDate), { mode: 'wide', time: false })}</span>
</div>
<div className={styles.row}>
<div className={styles.identifiers}>
{identifiers?.length
? identifiers.map(({ value, type }) => (
<span key={value} className={styles.identifierTag}>
<Tag className={styles.tag} type="gray" title={type.text}>
{type.text}
</Tag>
{value}
</span>
))
: ''}
</div>
<Button
className={styles.toggleContactDetailsButton}
kind="ghost"
renderIcon={(props) =>
showContactDetails ? <ChevronUp size={16} {...props} /> : <ChevronDown size={16} {...props} />
}
iconDescription="Toggle contact details"
onClick={toggleContactDetails}
style={{ marginTop: '-0.25rem' }}
>
{showContactDetails ? t('hideDetails', 'Hide details') : t('showDetails', 'Show details')}
</Button>
</div>
<div className={styles.patientBanner}>
<div className={styles.patientAvatar} role="img">
<PatientPhoto patientUuid={patientUuid} patientName={patientName} />
</div>
<PatientBannerPatientInfo patient={patient} />
<div className={styles.buttonCol}>
{!hideActionsOverflow ? (
<PatientBannerActionsMenu
patientUuid={patientUuid}
actionsSlotName={'patient-actions-slot'}
isDeceased={patient.deceasedBoolean}
/>
) : null}
<PatientBannerToggleContactDetailsButton
className={styles.toggleContactDetailsButton}
toggleContactDetails={toggleContactDetails}
showContactDetails={showContactDetails}
/>
</div>
</div>
{showContactDetails && (
<ContactDetails
isTabletViewport={isTabletViewport}
address={patient?.address ?? []}
telecom={patient?.telecom ?? []}
patientId={patient?.id}
deceased={isDeceased}
/>
<div
className={`${styles.contactDetails} ${styles[patient.deceasedBoolean && 'deceasedContactDetails']} ${
styles[isTabletViewport && 'tabletContactDetails']
}`}
>
<PatientBannerContactDetails patientId={patient?.id} deceased={isDeceased} />
</div>
)}
</header>
);
Expand Down
114 changes: 30 additions & 84 deletions packages/esm-patient-banner-app/src/banner/patient-banner.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
background-color: $ui-01;
}

.buttonCol {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
}

.deceasedPatientContainer {
background-color: colors.$gray-80;

.patientName {
color: $ui-01;
}

.demographics, .row, .identifierTag, .identifier, .contactDetails .heading {
color: $ui-02;
}
Expand All @@ -32,19 +35,10 @@
}
}

.overflowMenuContainer {
margin: -0.75rem 0;
}

.patientBanner {
display: flex;
}

.patientName {
@include type.type-style("heading-03");
margin-right: 0.25rem;
}

.patientAvatar {
width: 5rem;
height: 5rem;
Expand All @@ -59,88 +53,40 @@
background: none;
}

.patientInfo {
width: 100%;
}

.demographics {
@include type.type-style("body-compact-02");
.contactDetails {
color: $text-02;
margin-top: 0.375rem;
}

.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
}

.patientNameRow {
margin-top: 0.875rem;
}

.flexRow {
display: flex;
flex-flow: row wrap;
align-items: center;
}

.identifiers {
@include type.type-style("body-compact-02");
color: $ui-04;
display: flex;
flex-wrap: wrap;
}

.identifierTag {
width: 100%;
border-top: 1px solid $ui-03;
display: flex;
align-items: center;
margin-right: 0.75rem;

a {
@include type.type-style("body-compact-01");
text-decoration: none;
}
}

.tag {
margin: 0.25rem 0.25rem 0.25rem 0rem;
}

.tooltipPadding {
padding: 0.25rem;
}
.deceasedContactDetails {
.contactDetails, .heading, .row, .row > .col {
color: $ui-02;
}

.tooltipSmallText {
font-size: 80%;
a {
color: $inverse-link
}
}

.actionsButtonText {
@include type.type-style("body-compact-01");
color: $interactive-01;
}
.tabletContactDetails {
display: block;

// Overriding styles for RTL support
html[dir='rtl'] {
.overflowMenuContainer {
& > div {
margin-left: unset;
margin-right: -1.5rem;
& > div {
left: unset;
right: -6.025rem;
}
.row {
&:first-child {
border-bottom: 1px solid $ui-03;
}
}

.demographics {
display: flex;
gap: 0.25rem;
}

.tag {
margin: 0.25rem 0rem 0.25rem 0.25rem;
}

.patientName {
margin-right: unset;
margin-left: 0.25rem;
.row > .col {
&:nth-of-type(2n + 1) {
border-right: 1px solid $ui-03;
}
}
}
Loading
Loading