Skip to content

Commit

Permalink
(feat) Add configurability to Recently Searched Patients feature
Browse files Browse the repository at this point in the history
  • Loading branch information
denniskigen committed Jan 30, 2024
1 parent ef6236b commit 3c456ab
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
import { SkeletonIcon, SkeletonText, Tag } from '@carbon/react';
import { ExtensionSlot, useConfig, interpolateString, ConfigurableLink, age } from '@openmrs/esm-framework';
import type { FHIRIdentifier, FHIRPatientType, Identifier, SearchedPatient } from '../types';
import styles from './compact-patient-banner.scss';
import { PatientSearchContext } from '../patient-search-context';
import styles from './compact-patient-banner.scss';

interface PatientSearchResultsProps {
patients: Array<SearchedPatient>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import debounce from 'lodash-es/debounce';
import { navigate, interpolateString, useConfig, useSession } from '@openmrs/esm-framework';
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 { PatientSearchContext } from '../patient-search-context';
import PatientSearch from './patient-search.component';
import PatientSearchBar from '../patient-search-bar/patient-search-bar.component';
import RecentPatientSearch from './recent-patient-search.component';
import useArrowNavigation from '../hooks/useArrowNavigation';
import { useRecentlyViewedPatients, useInfinitePatientSearch, useRESTPatients } from '../patient-search.resource';
import styles from './compact-patient-search.scss';
import { PatientSearchContext } from '../patient-search-context';

interface CompactPatientSearchProps {
isSearchPage: boolean;
Expand All @@ -24,14 +23,17 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
shouldNavigateToPatientSearchPage,
}) => {
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const showSearchResults = useMemo(() => !!searchTerm.trim(), [searchTerm]);
const debouncedSearchTerm = useDebounce(searchTerm);
const hasSearchTerm = useMemo(() => Boolean(debouncedSearchTerm.trim()), [debouncedSearchTerm]);
const bannerContainerRef = useRef(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const config = useConfig();
const patientSearchResponse = useInfinitePatientSearch(searchTerm, config.includeDead, showSearchResults);
const { showRecentlySearchedPatients } = config.search;
const patientSearchResponse = useInfinitePatientSearch(debouncedSearchTerm, config.includeDead);
const { data: searchedPatients } = patientSearchResponse;
const { recentlyViewedPatients, addViewedPatient, mutateUserProperties } = useRecentlyViewedPatients();
const recentPatientSearchResponse = useRESTPatients(recentlyViewedPatients, !showSearchResults);
const { recentlyViewedPatients, addViewedPatient, mutateUserProperties } =
useRecentlyViewedPatients(showRecentlySearchedPatients);
const recentPatientSearchResponse = useRESTPatients(recentlyViewedPatients, !hasSearchTerm);
const { data: recentPatients } = recentPatientSearchResponse;
const {
user,
Expand Down Expand Up @@ -94,8 +96,8 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
}, [focusedResult, bannerContainerRef, handleFocusToInput]);

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

const handleSearchQueryChange = debounce((val) => setSearchTerm(val), 300);
const handleSearchTermChange = (searchTerm: string) => setSearchTerm(searchTerm ?? '');

return (
<PatientSearchContext.Provider
Expand All @@ -122,23 +124,23 @@ const CompactPatientSearchComponent: React.FC<CompactPatientSearchProps> = ({
<PatientSearchBar
small
initialSearchTerm={initialSearchTerm ?? ''}
onChange={handleSearchQueryChange}
onChange={handleSearchTermChange}
onSubmit={handleSubmit}
onClear={handleClear}
ref={searchInputRef}
/>
{!isSearchPage &&
(showSearchResults ? (
<div className={styles.floatingSearchResultsContainer} data-testid="floatingSearchResultsContainer">
<PatientSearch query={searchTerm} ref={bannerContainerRef} {...patientSearchResponse} />
</div>
) : (
<>
<div className={styles.floatingSearchResultsContainer} data-testid="floatingSearchResultsContainer">
<RecentPatientSearch ref={bannerContainerRef} {...recentPatientSearchResponse} />
</div>
</>
))}

{!isSearchPage && hasSearchTerm && (
<div className={styles.floatingSearchResultsContainer} data-testid="floatingSearchResultsContainer">
<PatientSearch query={debouncedSearchTerm} ref={bannerContainerRef} {...patientSearchResponse} />
</div>
)}

{!isSearchPage && !hasSearchTerm && showRecentlySearchedPatients && (
<div className={styles.floatingSearchResultsContainer} data-testid="floatingSearchResultsContainer">
<RecentPatientSearch ref={bannerContainerRef} {...recentPatientSearchResponse} />
</div>
)}
</div>
</PatientSearchContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,71 +1,93 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { useConfig } from '@openmrs/esm-framework';
import CompactPatientSearchComponent from './compact-patient-search.component';

const mockedUseConfig = jest.mocked(useConfig);

jest.mock('@openmrs/esm-framework', () => ({
...jest.requireActual('@openmrs/esm-framework'),
useSession: jest.fn(() => ({
...jest.requireActual('@openmrs/esm-framework').useSession(),
useSession: jest.fn().mockReturnValue({
sessionLocation: {
uuid: 'location-uuid',
},
})),
useConfig: jest.fn(() => ({
...jest.requireActual('@openmrs/esm-framework').useConfig(),
}),
useConfig: jest.fn().mockReturnValue({
search: {
patientResultUrl: '/patient/{{patientUuid}}/chart',
showRecentlySearchedPatients: false,
},
})),
}),
}));

describe('CompactPatientSearchComponent', () => {
it('renders without crashing', () => {
render(<CompactPatientSearchComponent isSearchPage={true} initialSearchTerm="" />);
expect(screen.getByRole('searchbox')).toBeInTheDocument();
beforeEach(() => mockedUseConfig.mockClear());

it('renders a compact search bar', () => {
render(<CompactPatientSearchComponent isSearchPage initialSearchTerm="" />);

expect(screen.getByPlaceholderText(/Search for a patient by name or identifier number/i)).toBeInTheDocument();
});

it('updates search term on input change', async () => {
it('typing into the searchbox updates the search term and triggers a search', async () => {
const user = userEvent.setup();
render(<CompactPatientSearchComponent isSearchPage={true} initialSearchTerm="" />);
const searchInput: HTMLInputElement = screen.getByRole('searchbox');

await user.type(searchInput, 'John');
render(<CompactPatientSearchComponent isSearchPage initialSearchTerm="" />);

const searchbox = screen.getByPlaceholderText(/Search for a patient by name or identifier number/i);

expect(searchInput.value).toBe('John');
await user.type(searchbox, 'John');

expect(screen.getByDisplayValue(/John/i)).toBeInTheDocument();
});

it('clears search term on clear button click', async () => {
const user = userEvent.setup();

render(<CompactPatientSearchComponent isSearchPage={true} initialSearchTerm="" />);
const searchInput: HTMLInputElement = screen.getByRole('searchbox');
const clearButton = screen.getByRole('button', { name: 'Clear' });

await user.type(searchInput, 'John');
const searchbox = screen.getByPlaceholderText(/Search for a patient by name or identifier number/i);

const clearButton = screen.getByRole('button', { name: /Clear/i });

await user.type(searchbox, 'John');
await user.click(clearButton);

expect(searchInput.value).toBe('');
expect(screen.queryByDisplayValue(/John/i)).not.toBeInTheDocument();
});

it('renders search results when search term is not empty', async () => {
const user = userEvent.setup();

render(<CompactPatientSearchComponent isSearchPage={false} initialSearchTerm="" />);
const searchInput = screen.getByRole('searchbox');

await user.type(searchInput, 'John');
const searchbox = screen.getByPlaceholderText(/Search for a patient by name or identifier number/i);

await user.type(searchbox, 'John');

const searchResultsContainer = screen.getByTestId('floatingSearchResultsContainer');

expect(searchResultsContainer).toBeInTheDocument();
});

it('renders a list of recently searched patients when a search term is not provided', async () => {
it('renders a list of recently searched patients when a search term is not provided and the showRecentlySearchedPatients config property is set', async () => {
const user = userEvent.setup();

mockedUseConfig.mockReturnValue({
search: {
showRecentlySearchedPatients: true,
},
});

render(<CompactPatientSearchComponent isSearchPage={false} initialSearchTerm="" />);
const searchInput = screen.getByRole('searchbox');

await user.clear(searchInput);
const searchbox = screen.getByRole('searchbox');

await user.clear(searchbox);

const searchResultsContainer = screen.getByTestId('floatingSearchResultsContainer');

expect(searchResultsContainer).toBeInTheDocument();
});
});
5 changes: 5 additions & 0 deletions packages/esm-patient-search-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export const configSchema = {
_description: 'Where clicking a patient result takes the user. Accepts template parameter ${patientUuid}',
_validators: [validators.isUrlWithTemplateParameters(['patientUuid'])],
},
showRecentlySearchedPatients: {
_type: Type.Boolean,
_default: false,
_description: 'Whether to show recently searched patients',
},
},
includeDead: {
_type: Type.Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite';
import { openmrsFetch, showNotification, useSession, type FetchResponse, LoggedInUser } from '@openmrs/esm-framework';
import { openmrsFetch, showNotification, useSession, type FetchResponse } from '@openmrs/esm-framework';
import type { PatientSearchResponse, SearchedPatient, User } from './types';

const v =
Expand Down Expand Up @@ -57,12 +57,12 @@ export function useInfinitePatientSearch(
return results;
}

export function useRecentlyViewedPatients() {
export function useRecentlyViewedPatients(showRecentlySearchedPatients: boolean = false) {
const { t } = useTranslation();
const { user } = useSession();
const userUuid = user?.uuid;
const { data, error, mutate } = useSWR<FetchResponse<User>, Error>(
userUuid ? `/ws/rest/v1/user/${userUuid}` : null,
showRecentlySearchedPatients && userUuid ? `/ws/rest/v1/user/${userUuid}` : null,
openmrsFetch,
);

Expand Down
Loading

0 comments on commit 3c456ab

Please sign in to comment.