From fe14d6097c10bc48af58c4f175ba19363687da7e Mon Sep 17 00:00:00 2001 From: Usama Idriss Kakumba <53287480+usamaidrsk@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:48:31 +0300 Subject: [PATCH] (feat) O3-1831: Registration: Support person attribute of type Location (#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 --- .../src/config-schema.ts | 7 ++ .../src/patient-registration/field/field.scss | 11 ++ ...ation-person-attribute-field.component.tsx | 105 ++++++++++++++++++ ...cation-person-attribute-field.resource.tsx | 48 ++++++++ .../person-attribute-field.component.tsx | 13 ++- .../translations/en.json | 1 + 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx create mode 100644 packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx diff --git a/packages/esm-patient-registration-app/src/config-schema.ts b/packages/esm-patient-registration-app/src/config-schema.ts index 1e2cadd78..7dc42f171 100644 --- a/packages/esm-patient-registration-app/src/config-schema.ts +++ b/packages/esm-patient-registration-app/src/config-schema.ts @@ -18,6 +18,7 @@ export interface FieldDefinition { required: boolean; matches?: string; }; + locationTag?: string; answerConceptSetUuid?: string; customConceptAnswers?: Array; } @@ -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, diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/field.scss b/packages/esm-patient-registration-app/src/patient-registration/field/field.scss index 5cca51fc0..887abea6b 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/field.scss +++ b/packages/esm-patient-registration-app/src/patient-registration/field/field.scss @@ -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; diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx new file mode 100644 index 000000000..ba1156a23 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx @@ -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(''); + 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 ( +
+ + + {({ field, form: { touched, errors } }) => { + return ( + + ); + }} + + + {loadingNewData && ( +
+ +
+ )} +
+ ); +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx new file mode 100644 index 000000000..5ce5d8837 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx @@ -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; + 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, Error>( + constructUrl, + openmrsFetch, + ); + + return useMemo( + () => ({ + locations: data?.data?.entry || [], + isLoading, + loadingNewData: isValidating, + }), + [data, isLoading, isValidating], + ); +} diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx index fe16305a8..8dd8f4347 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx @@ -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; @@ -41,6 +42,16 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr required={fieldDefinition.validation?.required ?? false} /> ); + case 'org.openmrs.Location': + return ( + + ); default: return ( diff --git a/packages/esm-patient-registration-app/translations/en.json b/packages/esm-patient-registration-app/translations/en.json index ebc875ad9..5c21d45fc 100644 --- a/packages/esm-patient-registration-app/translations/en.json +++ b/packages/esm-patient-registration-app/translations/en.json @@ -99,6 +99,7 @@ "restoreRelationshipActionButton": "Undo", "searchAddress": "Search address", "searchIdentifierPlaceholder": "Search identifier", + "searchLocationPersonAttribute": "Search location", "selectAnOption": "Select an option", "sexFieldLabelText": "Sex", "source": "Source",