Skip to content

Commit

Permalink
(refactor) O3-3326 Patient Search - migrate to use workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
chibongho committed Oct 25, 2024
1 parent 2d17ea0 commit d92625f
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({

const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const debouncedSearchTerm = useDebounce(searchTerm);
const hasSearchTerm = Boolean(debouncedSearchTerm.trim());
const hasSearchTerm = Boolean(debouncedSearchTerm?.trim());

const config = useConfig<PatientSearchConfig>();
const { showRecentlySearchedPatients } = config.search;
Expand Down Expand Up @@ -138,7 +138,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({

const handleSubmit = useCallback(
(debouncedSearchTerm) => {
if (shouldNavigateToPatientSearchPage && debouncedSearchTerm.trim()) {
if (shouldNavigateToPatientSearchPage && hasSearchTerm) {
if (!isSearchPage) {
window.sessionStorage.setItem('searchReturnUrl', window.location.pathname);
}
Expand All @@ -147,7 +147,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
});
}
},
[isSearchPage, shouldNavigateToPatientSearchPage],
[isSearchPage, shouldNavigateToPatientSearchPage, hasSearchTerm],
);

const handleClear = useCallback(() => {
Expand Down
6 changes: 6 additions & 0 deletions packages/esm-patient-search-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
defineConfigSchema,
fetchCurrentPatient,
fhirBaseUrl,
getAsyncLifecycle,
getSyncLifecycle,
makeUrl,
messageOmrsServiceWorker,
Expand Down Expand Up @@ -32,6 +33,11 @@ export const patientSearchButton = getSyncLifecycle(patientSearchButtonComponent
// This extension is not compatible with the tablet view.
export const patientSearchBar = getSyncLifecycle(patientSearchBarComponent, options);

export const patientSearchWorkspace = getAsyncLifecycle(
() => import('./patient-search-workspace/patient-search.workspace'),
options,
);

export function startupApp() {
defineConfigSchema(moduleName, configSchema);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@carbon/react';
import { Search } from '@carbon/react/icons';
import PatientSearchOverlay from '../patient-search-overlay/patient-search-overlay.component';
import { PatientSearchContext } from '../patient-search-context';
import { launchWorkspace } from '@openmrs/esm-framework';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { type PatientSearchWorkspaceProps } from '../patient-search-workspace/patient-search.workspace';

interface PatientSearchButtonProps {
buttonText?: string;
Expand All @@ -15,6 +15,17 @@ interface PatientSearchButtonProps {
searchQuery?: string;
}

/**
*
* This patient search button is an extension that other apps can include
* to add patient search functionality. It opens the search UI in a workspace.
*
* As it is possible to launch the patient search workspace directly with
* `launchWorkspace('patient-search-workspace', props)`, this button only exists
* for compatibility and should not be used otherwise.
*
* @returns
*/
const PatientSearchButton: React.FC<PatientSearchButtonProps> = ({
buttonText,
overlayHeader,
Expand All @@ -25,48 +36,37 @@ const PatientSearchButton: React.FC<PatientSearchButtonProps> = ({
searchQuery = '',
}) => {
const { t } = useTranslation();
const [showSearchOverlay, setShowSearchOverlay] = useState<boolean>(isOpen);

const launchPatientSearchWorkspace = () => {
const workspaceProps: PatientSearchWorkspaceProps = {
handleSearchTermUpdated: searchQueryUpdatedAction,
initialQuery: searchQuery,
nonNavigationSelectPatientAction: selectPatientAction,
};
launchWorkspace('patient-search-workspace', {
...workspaceProps,
workspaceTitle: overlayHeader,
});
};

useEffect(() => {
setShowSearchOverlay(isOpen);
if (isOpen) {
launchPatientSearchWorkspace();
}
}, [isOpen]);

const hidePanel = useCallback(() => {
setShowSearchOverlay(false);
}, [setShowSearchOverlay]);

return (
<>
{showSearchOverlay && (
<PatientSearchContext.Provider
value={{
nonNavigationSelectPatientAction: selectPatientAction,
patientClickSideEffect: hidePanel,
}}>
<PatientSearchOverlay
onClose={() => {
hidePanel();
searchQueryUpdatedAction && searchQueryUpdatedAction('');
}}
handleSearchTermUpdated={searchQueryUpdatedAction}
header={overlayHeader}
query={searchQuery}
/>
</PatientSearchContext.Provider>
)}

<Button
onClick={() => {
setShowSearchOverlay(true);
searchQueryUpdatedAction && searchQueryUpdatedAction('');
}}
aria-label="Search Patient Button"
aria-labelledby="Search Patient Button"
renderIcon={(props) => <Search size={20} {...props} />}
{...buttonProps}>
{buttonText ? buttonText : t('searchPatient', 'Search Patient')}
</Button>
</>
<Button
onClick={() => {
launchPatientSearchWorkspace();
searchQueryUpdatedAction && searchQueryUpdatedAction('');
}}
aria-label="Search Patient Button"
aria-labelledby="Search Patient Button"
renderIcon={(props) => <Search size={20} {...props} />}
{...buttonProps}>
{buttonText ? buttonText : t('searchPatient', 'Search patient')}
</Button>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
import { getDefaultsFromConfigSchema, launchWorkspace, useConfig } from '@openmrs/esm-framework';
import { type PatientSearchConfig, configSchema } from '../config-schema';
import PatientSearchButton from './patient-search-button.component';

const mockUseConfig = jest.mocked(useConfig<PatientSearchConfig>);
const mockedLaunchWorkspace = jest.mocked(launchWorkspace);

describe('PatientSearchButton', () => {
beforeEach(() => {
Expand Down Expand Up @@ -34,7 +35,7 @@ describe('PatientSearchButton', () => {
expect(customButton).toBeInTheDocument();
});

it('displays overlay when button is clicked', async () => {
it('displays workspace when patient search button is clicked', async () => {
const user = userEvent.setup();

render(<PatientSearchButton />);
Expand All @@ -43,8 +44,6 @@ describe('PatientSearchButton', () => {

await user.click(searchButton);

const overlayHeader = screen.getByText('Search results');

expect(overlayHeader).toBeInTheDocument();
expect(mockedLaunchWorkspace).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext } from 'react';

interface PatientSearchContextProps {
export interface PatientSearchContextProps {
/**
* A function to execute instead of navigating the user to the patient
* dashboard. If null/undefined, patient results will be links to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ const PatientSearchLaunch: React.FC<PatientSearchLaunchProps> = () => {
onPatientSelect={resetToInitialState}
/>
) : (
<PatientSearchContext.Provider value={{ patientClickSideEffect: closePatientSearch }}>
<PatientSearchOverlay onClose={closePatientSearch} query={initialSearchTerm} />
</PatientSearchContext.Provider>
<PatientSearchOverlay
onClose={closePatientSearch}
query={initialSearchTerm}
patientClickSideEffect={closePatientSearch}
/>
)}
<div className={styles.closeButton}>
<HeaderGlobalAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,44 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfig, useDebounce } from '@openmrs/esm-framework';
import { type PatientSearchConfig } from '../config-schema';
import AdvancedPatientSearchComponent from '../patient-search-page/advanced-patient-search.component';
import React from 'react';
import PatientSearchWorkspace from '../patient-search-workspace/patient-search.workspace';
import Overlay from '../ui-components/overlay';
import PatientSearchBar from '../patient-search-bar/patient-search-bar.component';
import { type PatientSearchContextProps } from '../patient-search-context';
import { useTranslation } from 'react-i18next';

interface PatientSearchOverlayProps {
interface PatientSearchOverlayProps extends PatientSearchContextProps {
onClose: () => void;
handleSearchTermUpdated?: (value: string) => void;
query?: string;
header?: string;
}

/**
* The PatientSearchOverlay is *only* used in tablet mode, in:
* - openmrs/spa/search (in desktop mode, PatientSearchPageComponent renders
* its own search component in the main page instead of in an overlay)
* - in the top nav, when the user clicks on the magnifying glass icon
* (in desktop mode, the inline CompactPatientSearchComponent is used instead)
*
* Although similar looking, this overlay behaves somewhat differently from a regular
* workspace, and has its own overlay logic.
*/
const PatientSearchOverlay: React.FC<PatientSearchOverlayProps> = ({
onClose,
query = '',
header,
handleSearchTermUpdated,
nonNavigationSelectPatientAction,
patientClickSideEffect,
}) => {
const { t } = useTranslation();
const {
search: { disableTabletSearchOnKeyUp },
} = useConfig<PatientSearchConfig>();
const [searchTerm, setSearchTerm] = useState(query);
const showSearchResults = Boolean(searchTerm?.trim());
const debouncedSearchTerm = useDebounce(searchTerm);

const handleClearSearchTerm = useCallback(() => setSearchTerm(''), [setSearchTerm]);

const onSearchTermChange = useCallback((value: string) => {
setSearchTerm(value);
handleSearchTermUpdated && handleSearchTermUpdated(value);
}, []);

return (
<Overlay header={header ?? t('searchResults', 'Search results')} close={onClose}>
<PatientSearchBar
initialSearchTerm={query}
onChange={(value) => !disableTabletSearchOnKeyUp && onSearchTermChange(value)}
onClear={handleClearSearchTerm}
onSubmit={onSearchTermChange}
<PatientSearchWorkspace
initialQuery={query}
handleSearchTermUpdated={handleSearchTermUpdated}
nonNavigationSelectPatientAction={nonNavigationSelectPatientAction}
patientClickSideEffect={patientClickSideEffect}
/>
{showSearchResults && <AdvancedPatientSearchComponent query={debouncedSearchTerm} inTabletOrOverlay />}
</Overlay>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isDesktop, navigate, useLayoutType } from '@openmrs/esm-framework';
import React, { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { isDesktop, navigate, useLayoutType } from '@openmrs/esm-framework';
import AdvancedPatientSearchComponent from './advanced-patient-search.component';
import { PatientSearchContext } from '../patient-search-context';
import PatientSearchOverlay from '../patient-search-overlay/patient-search-overlay.component';
import AdvancedPatientSearchComponent from './advanced-patient-search.component';
import styles from './patient-search-page.scss';
import { PatientSearchContext } from '../patient-search-context';

interface PatientSearchPageComponentProps {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useConfig, useDebounce } from '@openmrs/esm-framework';
import React, { useCallback, useState } from 'react';
import { type PatientSearchConfig } from '../config-schema';
import PatientSearchBar from '../patient-search-bar/patient-search-bar.component';
import { PatientSearchContext, type PatientSearchContextProps } from '../patient-search-context';
import AdvancedPatientSearchComponent from '../patient-search-page/advanced-patient-search.component';

export interface PatientSearchWorkspaceProps extends PatientSearchContextProps {
initialQuery?: string;
handleSearchTermUpdated?: (value: string) => void;
}

/**
* The workspace allows other apps to include patient search functionality.
*/
const PatientSearchWorkspace: React.FC<PatientSearchWorkspaceProps> = ({
initialQuery = '',
handleSearchTermUpdated,
nonNavigationSelectPatientAction,
patientClickSideEffect,
}) => {
const {
search: { disableTabletSearchOnKeyUp },
} = useConfig<PatientSearchConfig>();
const [searchTerm, setSearchTerm] = useState(initialQuery);
const showSearchResults = Boolean(searchTerm?.trim());
const debouncedSearchTerm = useDebounce(searchTerm);

const handleClearSearchTerm = useCallback(() => setSearchTerm(''), [setSearchTerm]);

const onSearchTermChange = useCallback((value: string) => {
setSearchTerm(value);
handleSearchTermUpdated && handleSearchTermUpdated(value);
}, []);

return (
<PatientSearchContext.Provider value={{ nonNavigationSelectPatientAction, patientClickSideEffect }}>
<PatientSearchBar
initialSearchTerm={initialQuery}
onChange={(value) => !disableTabletSearchOnKeyUp && onSearchTermChange(value)}
onClear={handleClearSearchTerm}
onSubmit={onSearchTermChange}
/>
{showSearchResults && <AdvancedPatientSearchComponent query={debouncedSearchTerm} inTabletOrOverlay />}
</PatientSearchContext.Provider>
);
};

export default PatientSearchWorkspace;
7 changes: 7 additions & 0 deletions packages/esm-patient-search-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@
"slot": "patient-search-bar-slot",
"offline": true
}
],
"workspaces": [
{
"name": "patient-search-workspace",
"component": "patientSearchWorkspace",
"title": "searchPatient"
}
]
}
4 changes: 2 additions & 2 deletions packages/esm-patient-search-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"search": "Search",
"searchForPatient": "Search for a patient by name or identifier number",
"searchingText": "Searching...",
"searchPatient": "Search Patient",
"searchResults": "Search Results",
"searchPatient": "Search patient",
"searchResults": "Search results",
"searchResultsCount_one": "{{count}} search result",
"searchResultsCount_other": "{{count}} search results",
"sex": "Sex",
Expand Down

0 comments on commit d92625f

Please sign in to comment.