Skip to content

Commit

Permalink
(feat) O3-1831: Registration: Support person attribute of type Locati…
Browse files Browse the repository at this point in the history
…on (#1032)

* ft-create-location-attribute component

* feat: create person location attribute

* (fix) remove useSWR in useCallback

* (fix) update locationTag key description

* (fix) selectedItem logic

* (fix) fix interface naming

* add commonets on the onInputChange func

* (fix) add required param

* (fix) update location-tag description

* (fix) use handleInputChange function naming

* feat: add loading status for searching location

* feat: append loading on the location attribute combobox

* fix: increase number of locations loaded
  • Loading branch information
usamaidrsk authored Oct 22, 2024
1 parent 2d17ea0 commit fe14d60
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 1 deletion.
7 changes: 7 additions & 0 deletions packages/esm-patient-registration-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface FieldDefinition {
required: boolean;
matches?: string;
};
locationTag?: string;
answerConceptSetUuid?: string;
customConceptAnswers?: Array<CustomConceptAnswer>;
}
Expand Down Expand Up @@ -183,6 +184,12 @@ export const esmPatientRegistrationSchema = {
_description: 'Optional RegEx for testing the validity of the input.',
},
},
locationTag: {
_type: Type.String,
_default: null,
_description:
'Only for fields with "person attribute" type `org.openmrs.Location`. This filters the list of location options in the dropdown based on their location tag. By default, all locations are shown.',
},
answerConceptSetUuid: {
_type: Type.ConceptUuid,
_default: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
margin-bottom: layout.$spacing-05;
}

.locationAttributeFieldContainer {
position: relative;

.loadingContainer {
background-color: colors.$white;
position: absolute;
right: layout.$spacing-07;
bottom: layout.$spacing-02;
}
}

:global(.omrs-breakpoint-lt-desktop) {
.grid {
grid-template-columns: 1fr;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { Field, useField } from 'formik';
import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
import styles from './../field.scss';
import { useLocations } from './location-person-attribute-field.resource';
import { ComboBox, InlineLoading, Layer } from '@carbon/react';
import { useTranslation } from 'react-i18next';

export interface LocationPersonAttributeFieldProps {
id: string;
personAttributeType: PersonAttributeTypeResponse;
label?: string;
locationTag: string;
required?: boolean;
}

export function LocationPersonAttributeField({
personAttributeType,
id,
label,
locationTag,
required,
}: LocationPersonAttributeFieldProps) {
const { t } = useTranslation();
const fieldName = `attributes.${personAttributeType.uuid}`;
const [field, meta, { setValue }] = useField(`attributes.${personAttributeType.uuid}`);
const [searchQuery, setSearchQuery] = useState<string>('');
const { locations, isLoading, loadingNewData } = useLocations(locationTag || null, searchQuery);
const prevLocationOptions = useRef([]);

const locationOptions = useMemo(() => {
if (!(isLoading && loadingNewData)) {
const newOptions = locations.map(({ resource: { id, name } }) => ({ value: id, label: name }));
prevLocationOptions.current = newOptions;
return newOptions;
}
return prevLocationOptions.current;
}, [locations, isLoading, loadingNewData]);

const selectedItem = useMemo(() => {
if (typeof meta.value === 'string') {
return locationOptions.find(({ value }) => value === meta.value) || null;
}
if (typeof meta.value === 'object' && meta.value) {
return locationOptions.find(({ value }) => value === meta.value.uuid) || null;
}
return null;
}, [locationOptions, meta.value]);

// Callback for when updating the combobox input
const handleInputChange = useCallback(
(value: string | null) => {
if (value) {
// If the value exists in the locationOptions (i.e. a label matches the input), exit the function
if (locationOptions.find(({ label }) => label === value)) return;
// If the input is a new value, set the search query
setSearchQuery(value);
// Clear the current selected value since the input doesn't match any existing options
setValue(null);
}
},
[locationOptions, setValue],
);
const handleSelect = useCallback(
({ selectedItem }) => {
if (selectedItem) {
setValue(selectedItem.value);
}
},
[setValue],
);

return (
<div
className={classNames(styles.customField, styles.halfWidthInDesktopView, styles.locationAttributeFieldContainer)}>
<Layer>
<Field name={fieldName}>
{({ field, form: { touched, errors } }) => {
return (
<ComboBox
id={id}
name={`person-attribute-${personAttributeType.uuid}`}
titleText={label}
items={locationOptions}
placeholder={t('searchLocationPersonAttribute', 'Search location')}
onInputChange={handleInputChange}
required={required}
onChange={handleSelect}
selectedItem={selectedItem}
invalid={errors[fieldName] && touched[fieldName]}
typeahead
/>
);
}}
</Field>
</Layer>
{loadingNewData && (
<div className={styles.loadingContainer}>
<InlineLoading />
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMemo } from 'react';
import { type FetchResponse, fhirBaseUrl, openmrsFetch, useDebounce } from '@openmrs/esm-framework';
import { type LocationEntry, type LocationResponse } from '@openmrs/esm-service-queues-app/src/types';
import useSWR from 'swr';

interface UseLocationsResult {
locations: Array<LocationEntry>;
isLoading: boolean;
loadingNewData: boolean;
}

export function useLocations(locationTag: string | null, searchQuery: string = ''): UseLocationsResult {
const debouncedSearchQuery = useDebounce(searchQuery);

const constructUrl = useMemo(() => {
let url = `${fhirBaseUrl}/Location?`;
let urlSearchParameters = new URLSearchParams();
urlSearchParameters.append('_summary', 'data');

if (!debouncedSearchQuery) {
urlSearchParameters.append('_count', '10');
}

if (locationTag) {
urlSearchParameters.append('_tag', locationTag);
}

if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') {
urlSearchParameters.append('name:contains', debouncedSearchQuery);
}

return url + urlSearchParameters.toString();
}, [locationTag, debouncedSearchQuery]);

const { data, error, isLoading, isValidating } = useSWR<FetchResponse<LocationResponse>, Error>(
constructUrl,
openmrsFetch,
);

return useMemo(
() => ({
locations: data?.data?.entry || [],
isLoading,
loadingNewData: isValidating,
}),
[data, isLoading, isValidating],
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useMemo } from 'react';
import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
import { InlineNotification, TextInputSkeleton } from '@carbon/react';
import { type FieldDefinition } from '../../../config-schema';
import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
import { usePersonAttributeType } from './person-attributes.resource';
import { TextPersonAttributeField } from './text-person-attribute-field.component';
import { useTranslation } from 'react-i18next';
import styles from '../field.scss';
import { LocationPersonAttributeField } from './location-person-attribute-field.component';

export interface PersonAttributeFieldProps {
fieldDefinition: FieldDefinition;
Expand Down Expand Up @@ -41,6 +42,16 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
required={fieldDefinition.validation?.required ?? false}
/>
);
case 'org.openmrs.Location':
return (
<LocationPersonAttributeField
personAttributeType={personAttributeType}
locationTag={fieldDefinition.locationTag}
label={fieldDefinition.label}
id={fieldDefinition?.id}
required={fieldDefinition.validation?.required ?? false}
/>
);
default:
return (
<InlineNotification kind="error" title="Error">
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-registration-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"restoreRelationshipActionButton": "Undo",
"searchAddress": "Search address",
"searchIdentifierPlaceholder": "Search identifier",
"searchLocationPersonAttribute": "Search location",
"selectAnOption": "Select an option",
"sexFieldLabelText": "Sex",
"source": "Source",
Expand Down

0 comments on commit fe14d60

Please sign in to comment.