Skip to content

Commit

Permalink
(feat) O3-3241 : User onboarding for patient registration (#9)
Browse files Browse the repository at this point in the history
* feat : added tutorials for patient registration

* Navigate all tutorials to home page

* handle steps for multi pages

* Add Register Patient step

* Modify the unit test for modal according to navigation

* Add custom tooltip and updatesd the styles

* Add patient tutorial

* Remove unwanted steps
  • Loading branch information
Vijaykv5 authored Jul 27, 2024
1 parent 4fb4152 commit 14696d7
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 44 deletions.
106 changes: 81 additions & 25 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Type} from '@openmrs/esm-framework';
import { Type } from '@openmrs/esm-framework';

export const configSchema = {
showTutorial: {
Expand All @@ -12,30 +12,33 @@ export const configSchema = {
_default: [
{
title: 'Basic Tutorial',
description: 'Learn how to efficiently search for patients, register new patients, access user settings, and view ongoing visits and appointments.',
steps: [{
target: '[aria-label="OpenMRS"]',
content: 'Welcome to OpenMRS! This is the main dashboard where you can navigate to various features of the system.'
},
description:
'Learn how to efficiently search for patients, register new patients, access user settings, and view ongoing visits and appointments.',
steps: [
{
target: '[aria-label="OpenMRS"]',
content:
'Welcome to OpenMRS! This is the main dashboard where you can navigate to various features of the system.',
},
{
target: '[name="SearchPatientIcon"]',
content: 'This is the search icon. Use it to find patients in the system quickly.'
content: 'This is the search icon. Use it to find patients in the system quickly.',
},
{
target: '[name="AddPatientIcon"]',
content: 'This is the add patient icon. Click here to register a new patient into the system.'
content: 'This is the add patient icon. Click here to register a new patient into the system.',
},
{
target: '[name="User"]',
content: 'The user icon. Click here to change your user preferences and settings.'
content: 'The user icon. Click here to change your user preferences and settings.',
},
{
target: '[data-extension-id="active-visits-widget"]',
content: 'This table displays active visits. Here you can see all the ongoing patient visits.'
content: 'This table displays active visits. Here you can see all the ongoing patient visits.',
},
{
target: '[data-extension-id="home-appointments"]',
content: 'This table shows appointments. View and manage patient appointments from this section.'
content: 'This table shows appointments. View and manage patient appointments from this section.',
},
],
},
Expand All @@ -50,14 +53,15 @@ export const configSchema = {
},
{
target: '[data-extension-id="clinical-appointments-dashboard-link"]',
title: 'Click on this link. This step is configured to be automatic and will take you to the next step. Once the given query selector resolves an element on the page, it will proceed automatically.',
title:
'Click on this link. This step is configured to be automatic and will take you to the next step. Once the given query selector resolves an element on the page, it will proceed automatically.',
hideCloseButton: true,
hideNextButton: true,
disableOverlayClose: true,
spotlightClicks: true,
data: {
autoNextOn: '[data-extension-id="clinical-appointments-dashboard"]',
}
},
},
{
target: '[data-extension-id="clinical-appointments-dashboard"]',
Expand All @@ -66,39 +70,90 @@ export const configSchema = {
},
{
target: '[aria-label="OpenMRS"]',
title: 'Now, let’s see how this behaves when elements take a bit longer to load. Set your network throttling to "Slow 3G" and hit "Next".',
title:
'Now, let’s see how this behaves when elements take a bit longer to load. Set your network throttling to "Slow 3G" and hit "Next".',
disableOverlayClose: true,
hideBackButton: true,
},
{
target: '[data-extension-id="laboratory-dashboard-link"]',
title: 'Let\'s navigate to the laboratory page. Our next target is the "Tests Ordered" table. I’ll disappear once you reach the laboratory page and reappear when the table is loaded. See you there!',
title:
'Let\'s navigate to the laboratory page. Our next target is the "Tests Ordered" table. I’ll disappear once you reach the laboratory page and reappear when the table is loaded. See you there!',
hideCloseButton: true,
hideNextButton: true,
disableOverlayClose: true,
spotlightClicks: true,
data: {
autoNextOn: '[data-extension-id="laboratory-dashboard"]',
}
},
},
{
target: '[data-extension-id="all-lab-requests-table"] table',
title: 'It\'s me again. By default, I\'ll wait for the element to appear, so you don\'t have to worry about slow components when writing a new tutorial.',
title:
"It's me again. By default, I'll wait for the element to appear, so you don't have to worry about slow components when writing a new tutorial.",
disableOverlayClose: true,
hideBackButton: true,
},
{
target: '[aria-label="OpenMRS"]',
title: 'Now let\'s do a fun exercise. Can you find out how to view a patient\'s allergies on your own? Feel free to turn off network throttling. ;) ',
title:
"Now let's do a fun exercise. Can you find out how to view a patient's allergies on your own? Feel free to turn off network throttling. ;) ",
},
{
target: '[data-extension-slot-name="patient-chart-allergies-dashboard-slot"]',
title: 'Great job! You found the allergies section! This is the end of the tutorial. Feel free to explore the system on your own or check out the other tutorials.',
}
]
}
]
}
title:
'Great job! You found the allergies section! This is the end of the tutorial. Feel free to explore the system on your own or check out the other tutorials.',
},
],
},
{
title: 'Patient Registration Tutorial',
description: 'Learn how to register a new patient into the system.',
steps: [
{
target: '[name="AddPatientIcon"]',
title: 'Add Patient',
content: 'Click here to add a patient to the system.',
disableBeacon: true,
disableOverlayClose: true,
spotlightClicks: true,
hideCloseButton: true,
hideNextButton: true,
hideFooter: true,
data: {
autoNextOn: '#demographics',
},
},
{
target: '#demographics',
title: 'Demographics',
content:
'This is the Demographics section. Here you can find various fields and information related to the patient.',
disableBeacon: true,
hideBackButton: true,
},
{
target: '#contact',
title: 'Contact Details',
content: "Here you can update the patient's contact information.",
disableBeacon: true,
},
{
target: '#relationships',
title: 'Relationships',
content: "In this section, you can manage the patient's relationships.",
disableBeacon: true,
},
{
target: 'button[type="submit"]',
title: 'Register Patient',
content: "Click this button to register the patient's information into the system.",
disableBeacon: true,
},
],
},
],
},
};

export type Config = {
Expand All @@ -108,11 +163,12 @@ export type Config = {
description: string;
steps: {
target: string;
title: string;
content: string;
disableBeacon: boolean;
data?: {
autoNextOn?: boolean;
}
};
}[];
}[];
};
2 changes: 2 additions & 0 deletions src/root.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import ReactJoyride, { ACTIONS, type CallBackProps, EVENTS, type Step } from 'react-joyride';
import { useDefineAppContext } from '@openmrs/esm-framework';
import { type TutorialContext } from './types';
import CustomTooltip from './tooltip/tooltip.component';

const RootComponent: React.FC = () => {

Expand Down Expand Up @@ -81,6 +82,7 @@ const RootComponent: React.FC = () => {
stepIndex={stepIndex}
run={showTutorial}
callback={handleJoyrideCallback}
tooltipComponent={(props) => <CustomTooltip {...props} step={steps[props.index]} totalSteps={steps.length} />}
styles={{
options: {
zIndex: 10000,
Expand Down
68 changes: 68 additions & 0 deletions src/tooltip/tooltip.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { Button } from '@carbon/react';
import { ArrowLeft, ArrowRight } from '@carbon/react/icons';
import { useTranslation } from 'react-i18next';
import styles from './tooltip.scss';

interface CustomTooltipProps {
continuous: boolean;
index: number;
step: any;
backProps: any;
closeProps: any;
primaryProps: any;
tooltipProps: any;
totalSteps: number;
}

const CustomTooltip: React.FC<CustomTooltipProps> = ({
continuous,
index,
step,
backProps,
primaryProps,
tooltipProps,
totalSteps,
}) => {
const { t } = useTranslation();
const isLastStep = index === totalSteps - 1;

return (
<div {...tooltipProps} className={styles.tooltipcontainer}>
<div className={styles.tooltipheader}>
<h4 className={styles.tooltiptitle}>{step.title}</h4>
<span className={styles.tooltipstep}>{`${index + 1}/${totalSteps}`}</span>
</div>
<div className={styles.tooltipcontent}>{step.content}</div>
<div className={styles.tooltipfooter}>
{!step.hideBackButton && index > 0 && (
<div {...backProps} size="sm" className={styles.buttonback}>
<ArrowLeft style={{ marginRight: '8px' }} />
{t('back', 'Back')}
</div>
)}
{continuous && !step.hideNextButton && (
<Button {...primaryProps} size="sm" className={styles.buttonnext}>
{isLastStep ? (
<>
{t('finish', 'Finish')}
<div className={styles.arrowContainer}>
<ArrowRight />
</div>
</>
) : (
<>
{t('next', 'Next')}
<div className={styles.arrowContainer}>
<ArrowRight />
</div>
</>
)}
</Button>
)}
</div>
</div>
);
};

export default CustomTooltip;
54 changes: 54 additions & 0 deletions src/tooltip/tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@import '~@openmrs/esm-styleguide/src/vars';

.tooltipcontainer {
background-color: $ui-02;
padding: spacing.$spacing-05;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
max-width: 360px;
border-radius: spacing.$spacing-02;
gap: spacing.$spacing-04;
background-color: $ui-02;

.tooltipheader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: spacing.$spacing-01;
.tooltiptitle {
@include type.type-style('heading-02');
}
.tooltipstep {
color: $color-gray-70;
@include type.type-style('body-compact-01');
}
}

.tooltipcontent {
color: $color-gray-70;
@include type.type-style('body-compact-01');
}

.tooltipfooter {
margin-top: spacing.$spacing-06;
display: flex;
justify-content: space-between;
align-items: center;
.buttonback {
color: $color-blue-60-2;
display: flex;
align-items: center;
}
.buttonnext {
margin-left: auto;
width:spacing.$spacing-12;
.arrowContainer {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: spacing.$spacing-05;
}
}
}
}
24 changes: 20 additions & 4 deletions src/tutorial/modal.component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import React from 'react';
import {render, screen, waitFor} from '@testing-library/react';
import TutorialModal from './modal.component';
import RootComponent from '../root.component';
import {useAppContext, useConfig} from "@openmrs/esm-framework";
import {useAppContext, useConfig, navigate} from "@openmrs/esm-framework";
import userEvent from '@testing-library/user-event'

jest.mock('@openmrs/esm-framework', () => ({
useConfig: jest.fn(),
useAppContext: jest.fn(),
navigate: jest.fn(),
}));

const setShowTutorial = jest.fn();
const setSteps = jest.fn();

const mockUseAppContext = jest.mocked(useAppContext);
const mockUseConfig = jest.mocked(useConfig)
const mockUseConfig = jest.mocked(useConfig);
const mockNavigate = jest.mocked(navigate);

const mockTutorialData = [
{
Expand All @@ -36,18 +38,32 @@ mockUseAppContext.mockReturnValue({
describe('TutorialModal', () => {
afterEach(() => {
jest.clearAllMocks();
delete window.location;
window.location = { pathname: '/patient-registration' } as any;
});

test('sends correct data to the root component when walkthrough button is clicked', async () => {
const user = userEvent.setup();

(window as any).getOpenmrsSpaBase = jest.fn(() => '/spa-base/');
Object.defineProperty(window, 'location', {
value: {
pathname: '/patient-registration',
},
});

render(<TutorialModal open={true} onClose={jest.fn()}/>);

const walkthroughButton = screen.getByText('Walkthrough');
await user.click(walkthroughButton);

expect(setSteps).toHaveBeenCalledWith(mockTutorialData[0].steps);
expect(setShowTutorial).toHaveBeenCalledWith(true);
expect(navigate).toHaveBeenCalledWith({ to: '/spa-base/home' });
Object.defineProperty(window.location, 'pathname', {
value: '/spa-base/home',
});

await waitFor(() => expect(setSteps).toHaveBeenCalledWith(mockTutorialData[0].steps));
await waitFor(() => expect(setShowTutorial).toHaveBeenCalledWith(true));
});

test.todo('renders tutorials properly');
Expand Down
Loading

0 comments on commit 14696d7

Please sign in to comment.