From d92625fc6454a8d7e4527fae98d66798e0e35a39 Mon Sep 17 00:00:00 2001 From: Chi Bong Ho Date: Fri, 25 Oct 2024 10:48:01 -0400 Subject: [PATCH] (refactor) O3-3326 Patient Search - migrate to use workspace --- .../compact-patient-search.component.tsx | 6 +- packages/esm-patient-search-app/src/index.ts | 6 ++ .../patient-search-button.component.tsx | 82 +++++++++---------- .../patient-search-button.test.tsx | 9 +- .../src/patient-search-context.ts | 2 +- .../patient-search-icon.component.tsx | 8 +- .../patient-search-overlay.component.tsx | 48 +++++------ .../patient-search-page.component.tsx | 6 +- .../patient-search.workspace.tsx | 49 +++++++++++ .../esm-patient-search-app/src/routes.json | 7 ++ .../translations/en.json | 4 +- 11 files changed, 143 insertions(+), 84 deletions(-) create mode 100644 packages/esm-patient-search-app/src/patient-search-workspace/patient-search.workspace.tsx diff --git a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx index 968dd33e8..7973a5bc9 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx @@ -31,7 +31,7 @@ const CompactPatientSearchComponent: React.FC = ({ const [searchTerm, setSearchTerm] = useState(initialSearchTerm); const debouncedSearchTerm = useDebounce(searchTerm); - const hasSearchTerm = Boolean(debouncedSearchTerm.trim()); + const hasSearchTerm = Boolean(debouncedSearchTerm?.trim()); const config = useConfig(); const { showRecentlySearchedPatients } = config.search; @@ -138,7 +138,7 @@ const CompactPatientSearchComponent: React.FC = ({ const handleSubmit = useCallback( (debouncedSearchTerm) => { - if (shouldNavigateToPatientSearchPage && debouncedSearchTerm.trim()) { + if (shouldNavigateToPatientSearchPage && hasSearchTerm) { if (!isSearchPage) { window.sessionStorage.setItem('searchReturnUrl', window.location.pathname); } @@ -147,7 +147,7 @@ const CompactPatientSearchComponent: React.FC = ({ }); } }, - [isSearchPage, shouldNavigateToPatientSearchPage], + [isSearchPage, shouldNavigateToPatientSearchPage, hasSearchTerm], ); const handleClear = useCallback(() => { diff --git a/packages/esm-patient-search-app/src/index.ts b/packages/esm-patient-search-app/src/index.ts index efeb91047..33c6575e3 100644 --- a/packages/esm-patient-search-app/src/index.ts +++ b/packages/esm-patient-search-app/src/index.ts @@ -2,6 +2,7 @@ import { defineConfigSchema, fetchCurrentPatient, fhirBaseUrl, + getAsyncLifecycle, getSyncLifecycle, makeUrl, messageOmrsServiceWorker, @@ -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); diff --git a/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.component.tsx b/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.component.tsx index 0cf110cc8..5382abad5 100644 --- a/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.component.tsx +++ b/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.component.tsx @@ -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; @@ -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 = ({ buttonText, overlayHeader, @@ -25,48 +36,37 @@ const PatientSearchButton: React.FC = ({ searchQuery = '', }) => { const { t } = useTranslation(); - const [showSearchOverlay, setShowSearchOverlay] = useState(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 && ( - - { - hidePanel(); - searchQueryUpdatedAction && searchQueryUpdatedAction(''); - }} - handleSearchTermUpdated={searchQueryUpdatedAction} - header={overlayHeader} - query={searchQuery} - /> - - )} - - - + ); }; diff --git a/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.test.tsx b/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.test.tsx index 34c312b2e..99a9c9102 100644 --- a/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.test.tsx +++ b/packages/esm-patient-search-app/src/patient-search-button/patient-search-button.test.tsx @@ -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); +const mockedLaunchWorkspace = jest.mocked(launchWorkspace); describe('PatientSearchButton', () => { beforeEach(() => { @@ -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(); @@ -43,8 +44,6 @@ describe('PatientSearchButton', () => { await user.click(searchButton); - const overlayHeader = screen.getByText('Search results'); - - expect(overlayHeader).toBeInTheDocument(); + expect(mockedLaunchWorkspace).toHaveBeenCalled(); }); }); diff --git a/packages/esm-patient-search-app/src/patient-search-context.ts b/packages/esm-patient-search-app/src/patient-search-context.ts index 8df4e5be5..56ef4deaf 100644 --- a/packages/esm-patient-search-app/src/patient-search-context.ts +++ b/packages/esm-patient-search-app/src/patient-search-context.ts @@ -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 diff --git a/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.component.tsx b/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.component.tsx index b40017a7c..9ad5dcefc 100644 --- a/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.component.tsx +++ b/packages/esm-patient-search-app/src/patient-search-icon/patient-search-icon.component.tsx @@ -92,9 +92,11 @@ const PatientSearchLaunch: React.FC = () => { onPatientSelect={resetToInitialState} /> ) : ( - - - + )}
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 = ({ onClose, query = '', header, handleSearchTermUpdated, + nonNavigationSelectPatientAction, + patientClickSideEffect, }) => { const { t } = useTranslation(); - const { - search: { disableTabletSearchOnKeyUp }, - } = useConfig(); - 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 ( - !disableTabletSearchOnKeyUp && onSearchTermChange(value)} - onClear={handleClearSearchTerm} - onSubmit={onSearchTermChange} + - {showSearchResults && } ); }; diff --git a/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.component.tsx b/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.component.tsx index 4e4972444..fb3478127 100644 --- a/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.component.tsx +++ b/packages/esm-patient-search-app/src/patient-search-page/patient-search-page.component.tsx @@ -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 {} diff --git a/packages/esm-patient-search-app/src/patient-search-workspace/patient-search.workspace.tsx b/packages/esm-patient-search-app/src/patient-search-workspace/patient-search.workspace.tsx new file mode 100644 index 000000000..c8419cfa2 --- /dev/null +++ b/packages/esm-patient-search-app/src/patient-search-workspace/patient-search.workspace.tsx @@ -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 = ({ + initialQuery = '', + handleSearchTermUpdated, + nonNavigationSelectPatientAction, + patientClickSideEffect, +}) => { + const { + search: { disableTabletSearchOnKeyUp }, + } = useConfig(); + 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 ( + + !disableTabletSearchOnKeyUp && onSearchTermChange(value)} + onClear={handleClearSearchTerm} + onSubmit={onSearchTermChange} + /> + {showSearchResults && } + + ); +}; + +export default PatientSearchWorkspace; diff --git a/packages/esm-patient-search-app/src/routes.json b/packages/esm-patient-search-app/src/routes.json index 851460d2e..062ee7b8a 100644 --- a/packages/esm-patient-search-app/src/routes.json +++ b/packages/esm-patient-search-app/src/routes.json @@ -28,5 +28,12 @@ "slot": "patient-search-bar-slot", "offline": true } + ], + "workspaces": [ + { + "name": "patient-search-workspace", + "component": "patientSearchWorkspace", + "title": "searchPatient" + } ] } diff --git a/packages/esm-patient-search-app/translations/en.json b/packages/esm-patient-search-app/translations/en.json index 100468ede..6ab1b0c7b 100644 --- a/packages/esm-patient-search-app/translations/en.json +++ b/packages/esm-patient-search-app/translations/en.json @@ -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",