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

(feat) Restore recently searched patients functionality in compact search #1345

Merged
merged 3 commits into from
Oct 17, 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
@@ -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;
Expand All @@ -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]);
Comment on lines -35 to -41
Copy link
Member Author

Choose a reason for hiding this comment

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

handleReset and handleClear do the same thing.

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, {
Copy link
Member Author

Choose a reason for hiding this comment

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

interpolateString returns a string, so there's no need for the additional interpolation.

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);
Expand All @@ -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}
Expand All @@ -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} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useConfig,
} from '@openmrs/esm-framework';
import type { FHIRIdentifier, FHIRPatientType, Identifier, SearchedPatient } from '../types';
import { type PatientSearchConfig } from '../config-schema';
import { PatientSearchContext } from '../patient-search-context';
import styles from './compact-patient-banner.scss';

Expand All @@ -30,7 +31,7 @@ interface IdentifierTagProps {
}

const CompactPatientBanner = forwardRef<HTMLDivElement, CompactPatientBannerProps>(({ patients }, ref) => {
const config = useConfig();
const config = useConfig<PatientSearchConfig>();
const { t } = useTranslation();

const getGender = (gender: string) => {
Expand Down Expand Up @@ -135,7 +136,7 @@ const CompactPatientBanner = forwardRef<HTMLDivElement, CompactPatientBannerProp

const ClickablePatientContainer = ({ patient, children }: ClickablePatientContainerProps) => {
const { nonNavigationSelectPatientAction, patientClickSideEffect } = useContext(PatientSearchContext);
const config = useConfig();
const config = useConfig<PatientSearchConfig>();
const isDeceased = Boolean(patient?.person?.deathDate);

if (nonNavigationSelectPatientAction) {
Expand All @@ -160,9 +161,9 @@ const ClickablePatientContainer = ({ patient, children }: ClickablePatientContai
})}
key={patient.uuid}
onBeforeNavigate={() => patientClickSideEffect?.(patient.uuid)}
to={`${interpolateString(config.search.patientResultUrl, {
to={interpolateString(config.search.patientChartUrl, {
patientUuid: patient.uuid,
})}`}>
})}>
{children}
</ConfigurableLink>
);
Expand Down
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';
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -95,6 +118,24 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
}
}, [focusedResult, bannerContainerRef, handleFocusToInput]);

useEffect(() => {
if (fetchError) {
Copy link
Member Author

Choose a reason for hiding this comment

The 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 .tsx to .ts.

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()) {
Expand Down Expand Up @@ -122,7 +163,7 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
}}>
<div className={styles.patientSearchBar}>
<PatientSearchBar
small
isCompact
initialSearchTerm={initialSearchTerm ?? ''}
onChange={handleSearchTermChange}
onSubmit={handleSubmit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ describe('CompactPatientSearchComponent', () => {
search: {
showRecentlySearchedPatients: true,
disableTabletSearchOnKeyUp: true,
patientResultUrl: configSchema.search.patientResultUrl._default,
},
} as PatientSearchConfig['search'],
});
render(<CompactPatientSearchComponent isSearchPage={false} initialSearchTerm="" />);
const searchResultsContainer = screen.getByTestId('floatingSearchResultsContainer');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ interface PatientSearchProps extends PatientSearchResponse {
}

const PatientSearch = React.forwardRef<HTMLDivElement, PatientSearchProps>(
({ isLoading, data: searchResults, fetchError, loadingNewData, setPage, hasMore, totalResults }, ref) => {
({ data: searchResults, fetchError, hasMore, isLoading, isValidating, setPage, totalResults }, ref) => {
const { t } = useTranslation();
const observer = useRef(null);
const loadingIconRef = useCallback(
(node) => {
if (loadingNewData) {
if (isValidating) {
return;
}
if (observer.current) {
Expand All @@ -37,7 +37,7 @@ const PatientSearch = React.forwardRef<HTMLDivElement, PatientSearchProps>(
observer.current.observe(node);
}
},
[loadingNewData, hasMore, setPage],
[isValidating, hasMore, setPage],
);

if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@
color: $text-02;
line-height: layout.$spacing-05;
margin: layout.$spacing-03 layout.$spacing-05;
display: flex;
align-items: center;
}

.resultsTextCount {
flex: 1;
}

.validationIcon {
flex: 1;
justify-content: center;
align-items: center;
}

.spinner {
&:global(.cds--inline-loading) {
min-height: layout.$spacing-05 !important;
}
}

.helperText {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const defaultProps = {
fetchError: null,
hasMore: false,
isLoading: false,
loadingNewData: false,
isValidating: false,
setPage: jest.fn(),
totalResults: 1,
query: 'John',
Expand Down
Loading
Loading