-
Notifications
You must be signed in to change notification settings - Fork 229
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
(feat) Restore recently searched patients functionality in compact search #1345
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react'; | ||
import PatientSearch from '../compact-patient-search/patient-search.component'; | ||
import { Search, Button } from '@carbon/react'; | ||
import { Button, Search } from '@carbon/react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import styles from './compact-patient-search.scss'; | ||
import { useInfinitePatientSearch } from '../patient-search.resource'; | ||
import { useConfig, navigate, interpolateString } from '@openmrs/esm-framework'; | ||
import useArrowNavigation from '../hooks/useArrowNavigation'; | ||
import { type PatientSearchConfig } from '../config-schema'; | ||
import { useInfinitePatientSearch } from '../patient-search.resource'; | ||
import { PatientSearchContext } from '../patient-search-context'; | ||
import useArrowNavigation from '../hooks/useArrowNavigation'; | ||
import PatientSearch from '../compact-patient-search/patient-search.component'; | ||
import styles from './compact-patient-search.scss'; | ||
|
||
interface CompactPatientSearchProps { | ||
initialSearchTerm: string; | ||
|
@@ -21,48 +22,40 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
buttonProps, | ||
}) => { | ||
const { t } = useTranslation(); | ||
const config = useConfig<PatientSearchConfig>(); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const bannerContainerRef = useRef(null); | ||
const [searchTerm, setSearchTerm] = useState(initialSearchTerm); | ||
const handleChange = useCallback((val) => setSearchTerm(val), [setSearchTerm]); | ||
const showSearchResults = useMemo(() => !!searchTerm?.trim(), [searchTerm]); | ||
const config = useConfig(); | ||
const patientSearchResponse = useInfinitePatientSearch(searchTerm, config.includeDead, showSearchResults); | ||
const { data: patients } = patientSearchResponse; | ||
|
||
const handleSubmit = useCallback((evt) => { | ||
evt.preventDefault(); | ||
}, []); | ||
|
||
const handleClear = useCallback(() => { | ||
setSearchTerm(''); | ||
}, [setSearchTerm]); | ||
const handleChange = useCallback((val) => setSearchTerm(val), [setSearchTerm]); | ||
|
||
const handleReset = useCallback(() => { | ||
setSearchTerm(''); | ||
}, [setSearchTerm]); | ||
const handleClear = useCallback(() => setSearchTerm(''), [setSearchTerm]); | ||
|
||
// handlePatientSelection: Manually handles everything that needs to happen when a patient | ||
// from the result list is selected. This is used for the arrow navigation, but is not used | ||
// for click handling. | ||
/** | ||
* handlePatientSelection: Manually handles everything that needs to happen when a patient | ||
* from the result list is selected. This is used for the arrow navigation, but is not used | ||
* for click handling. | ||
*/ | ||
const handlePatientSelection = useCallback( | ||
(evt, index: number) => { | ||
evt.preventDefault(); | ||
(event, index: number) => { | ||
event.preventDefault(); | ||
if (selectPatientAction) { | ||
selectPatientAction(patients[index].uuid); | ||
} else { | ||
navigate({ | ||
to: `${interpolateString(config.search.patientResultUrl, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
to: interpolateString(config.search.patientChartUrl, { | ||
patientUuid: patients[index].uuid, | ||
})}`, | ||
}), | ||
}); | ||
} | ||
handleReset(); | ||
handleClear(); | ||
}, | ||
[config.search, selectPatientAction, patients, handleReset], | ||
[config.search, selectPatientAction, patients, handleClear], | ||
); | ||
|
||
const bannerContainerRef = useRef(null); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
|
||
const handleFocusToInput = useCallback(() => { | ||
let len = inputRef.current.value?.length ?? 0; | ||
inputRef.current.setSelectionRange(len, len); | ||
|
@@ -86,7 +79,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
|
||
return ( | ||
<div className={styles.patientSearchBar}> | ||
<form onSubmit={handleSubmit} className={styles.searchArea}> | ||
<form onSubmit={(event) => event.preventDefault()} className={styles.searchArea}> | ||
<Search | ||
autoFocus | ||
className={styles.patientSearchInput} | ||
|
@@ -95,19 +88,19 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
onChange={(event) => handleChange(event.target.value)} | ||
onClear={handleClear} | ||
placeholder={t('searchForPatient', 'Search for a patient by name or identifier number')} | ||
value={searchTerm} | ||
size="lg" | ||
ref={inputRef} | ||
size="lg" | ||
value={searchTerm} | ||
/> | ||
<Button type="submit" onClick={handleSubmit} {...buttonProps}> | ||
<Button type="submit" onClick={(event) => event.preventDefault()} {...buttonProps}> | ||
{t('search', 'Search')} | ||
</Button> | ||
</form> | ||
{showSearchResults && ( | ||
<PatientSearchContext.Provider | ||
value={{ | ||
nonNavigationSelectPatientAction: selectPatientAction, | ||
patientClickSideEffect: handleReset, | ||
patientClickSideEffect: handleClear, | ||
}}> | ||
<div className={styles.floatingSearchResultsContainer}> | ||
<PatientSearch query={searchTerm} ref={bannerContainerRef} {...patientSearchResponse} /> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import React, { useCallback, useRef, useState, useEffect } from 'react'; | ||
import { navigate, interpolateString, useConfig, useSession, useDebounce } from '@openmrs/esm-framework'; | ||
import useArrowNavigation from '../hooks/useArrowNavigation'; | ||
import type { SearchedPatient } from '../types'; | ||
import { useRecentlyViewedPatients, useInfinitePatientSearch, useRESTPatients } from '../patient-search.resource'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { navigate, interpolateString, useConfig, useSession, useDebounce, showSnackbar } from '@openmrs/esm-framework'; | ||
import { type PatientSearchConfig } from '../config-schema'; | ||
import { type SearchedPatient } from '../types'; | ||
import { useRecentlyViewedPatients, useInfinitePatientSearch, useRestPatients } from '../patient-search.resource'; | ||
import { PatientSearchContext } from '../patient-search-context'; | ||
import useArrowNavigation from '../hooks/useArrowNavigation'; | ||
import PatientSearch from './patient-search.component'; | ||
import PatientSearchBar from '../patient-search-bar/patient-search-bar.component'; | ||
import RecentlySearchedPatients from './recently-searched-patients.component'; | ||
|
@@ -22,43 +24,64 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
onPatientSelect, | ||
shouldNavigateToPatientSearchPage, | ||
}) => { | ||
const { t } = useTranslation(); | ||
|
||
const bannerContainerRef = useRef(null); | ||
const searchInputRef = useRef<HTMLInputElement>(null); | ||
|
||
const [searchTerm, setSearchTerm] = useState(initialSearchTerm); | ||
const debouncedSearchTerm = useDebounce(searchTerm); | ||
const hasSearchTerm = Boolean(debouncedSearchTerm.trim()); | ||
const bannerContainerRef = useRef(null); | ||
const searchInputRef = useRef<HTMLInputElement>(null); | ||
const config = useConfig(); | ||
|
||
const config = useConfig<PatientSearchConfig>(); | ||
const { showRecentlySearchedPatients } = config.search; | ||
const patientSearchResponse = useInfinitePatientSearch(debouncedSearchTerm, config.includeDead); | ||
const { data: searchedPatients } = patientSearchResponse; | ||
const { recentlyViewedPatients, addViewedPatient, mutateUserProperties } = | ||
useRecentlyViewedPatients(showRecentlySearchedPatients); | ||
const recentPatientSearchResponse = useRESTPatients(recentlyViewedPatients, !hasSearchTerm); | ||
const { data: recentPatients } = recentPatientSearchResponse; | ||
|
||
const { | ||
user, | ||
sessionLocation: { uuid: currentLocation }, | ||
} = useSession(); | ||
|
||
const patientSearchResponse = useInfinitePatientSearch(debouncedSearchTerm, config.includeDead); | ||
const { data: searchedPatients } = patientSearchResponse; | ||
|
||
const { | ||
error: errorFetchingUserProperties, | ||
mutateUserProperties, | ||
recentlyViewedPatientUuids, | ||
updateRecentlyViewedPatients, | ||
} = useRecentlyViewedPatients(showRecentlySearchedPatients); | ||
|
||
const recentPatientSearchResponse = useRestPatients(recentlyViewedPatientUuids, !hasSearchTerm); | ||
const { data: recentPatients, fetchError } = recentPatientSearchResponse; | ||
|
||
const handleFocusToInput = useCallback(() => { | ||
const len = searchInputRef.current.value?.length ?? 0; | ||
searchInputRef.current.setSelectionRange(len, len); | ||
searchInputRef.current.focus(); | ||
}, [searchInputRef]); | ||
if (searchInputRef.current) { | ||
const inputElement = searchInputRef.current; | ||
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length); | ||
inputElement.focus(); | ||
} | ||
}, []); | ||
|
||
const handleCloseSearchResults = useCallback(() => { | ||
setSearchTerm(''); | ||
onPatientSelect?.(); | ||
}, [onPatientSelect, setSearchTerm]); | ||
|
||
const addViewedPatientAndCloseSearchResults = useCallback( | ||
(patientUuid: string) => { | ||
addViewedPatient(patientUuid).then(() => { | ||
mutateUserProperties(); | ||
}); | ||
async (patientUuid: string) => { | ||
handleCloseSearchResults(); | ||
try { | ||
await updateRecentlyViewedPatients(patientUuid); | ||
await mutateUserProperties(); | ||
} catch (error) { | ||
showSnackbar({ | ||
kind: 'error', | ||
title: t('errorUpdatingRecentlyViewedPatients', 'Error updating recently viewed patients'), | ||
subtitle: error instanceof Error ? error.message : String(error), | ||
}); | ||
} | ||
}, | ||
[handleCloseSearchResults, mutateUserProperties, addViewedPatient], | ||
[handleCloseSearchResults, mutateUserProperties, updateRecentlyViewedPatients, t], | ||
); | ||
|
||
const handlePatientSelection = useCallback( | ||
|
@@ -67,13 +90,13 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
if (patients) { | ||
addViewedPatientAndCloseSearchResults(patients[index].uuid); | ||
navigate({ | ||
to: `${interpolateString(config.search.patientResultUrl, { | ||
to: interpolateString(config.search.patientChartUrl, { | ||
patientUuid: patients[index].uuid, | ||
})}`, | ||
}), | ||
}); | ||
} | ||
}, | ||
[config.search.patientResultUrl, user, currentLocation], | ||
[config.search.patientChartUrl, user, currentLocation], | ||
); | ||
const focusedResult = useArrowNavigation( | ||
!recentPatients ? searchedPatients?.length ?? 0 : recentPatients?.length ?? 0, | ||
|
@@ -95,6 +118,24 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
} | ||
}, [focusedResult, bannerContainerRef, handleFocusToInput]); | ||
|
||
useEffect(() => { | ||
if (fetchError) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've moved these here so that the resource file doesn't have to deal with any rendering concerns. A post-merge commit should change the resource file extension from |
||
showSnackbar({ | ||
kind: 'error', | ||
title: t('errorFetchingPatients', 'Error fetching patients'), | ||
subtitle: fetchError?.message, | ||
}); | ||
} | ||
|
||
if (errorFetchingUserProperties) { | ||
showSnackbar({ | ||
kind: 'error', | ||
title: t('errorFetchingUserProperties', 'Error fetching user properties'), | ||
subtitle: errorFetchingUserProperties?.message, | ||
}); | ||
} | ||
}, [fetchError, errorFetchingUserProperties]); | ||
|
||
const handleSubmit = useCallback( | ||
(debouncedSearchTerm) => { | ||
if (shouldNavigateToPatientSearchPage && debouncedSearchTerm.trim()) { | ||
|
@@ -122,7 +163,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({ | |
}}> | ||
<div className={styles.patientSearchBar}> | ||
<PatientSearchBar | ||
small | ||
isCompact | ||
initialSearchTerm={initialSearchTerm ?? ''} | ||
onChange={handleSearchTermChange} | ||
onSubmit={handleSubmit} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleReset
andhandleClear
do the same thing.