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) O3-1831: Registration: Support person attribute of type Location #1032

Merged
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
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;
usamaidrsk marked this conversation as resolved.
Show resolved Hide resolved
// 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);
Comment on lines +56 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

I would feel more confident about this if you'd posted a video showing you testing a few cases—starting to type a valid location name, typing nonsense, typing a location name exactly, selecting a location, changing the input text after having selected a location. I think this code makes sense but I don't have 100% confidence in it.

}
},
[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}
brandones marked this conversation as resolved.
Show resolved Hide resolved
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
Loading