@ampath/esm-patient-registration-app 6.0.1-pre.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,157 @@
1
+ import { useCallback, useContext, useEffect, useMemo } from 'react';
2
+ import { useField } from 'formik';
3
+ import useSWRImmutable from 'swr/immutable';
4
+ import { type FetchResponse, openmrsFetch } from '@openmrs/esm-framework';
5
+ import { PatientRegistrationContext } from '../../patient-registration-context';
6
+
7
+ interface AddressFields {
8
+ addressField: string;
9
+ }
10
+
11
+ export function useOrderedAddressHierarchyLevels() {
12
+ const url = '/module/addresshierarchy/ajax/getOrderedAddressHierarchyLevels.form';
13
+ const { data, isLoading, error } = useSWRImmutable<FetchResponse<Array<AddressFields>>, Error>(url, openmrsFetch);
14
+
15
+ const results = useMemo(
16
+ () => ({
17
+ orderedFields: data?.data?.map((field) => field.addressField),
18
+ isLoadingFieldOrder: isLoading,
19
+ errorFetchingFieldOrder: error,
20
+ }),
21
+ [data, isLoading, error],
22
+ );
23
+
24
+ return results;
25
+ }
26
+
27
+ export function useAddressEntries(fetchResults, searchString) {
28
+ const encodedSearchString = encodeURIComponent(searchString);
29
+ const { data, isLoading, error } = useSWRImmutable<FetchResponse<Array<{ name: string }>>>(
30
+ fetchResults
31
+ ? `module/addresshierarchy/ajax/getChildAddressHierarchyEntries.form?searchString=${encodedSearchString}`
32
+ : null,
33
+ openmrsFetch,
34
+ );
35
+
36
+ useEffect(() => {
37
+ if (error) {
38
+ console.error(error);
39
+ }
40
+ }, [error]);
41
+
42
+ const results = useMemo(
43
+ () => ({
44
+ entries: data?.data?.map((item) => item.name),
45
+ isLoadingAddressEntries: isLoading,
46
+ errorFetchingAddressEntries: error,
47
+ }),
48
+ [data, isLoading, error],
49
+ );
50
+ return results;
51
+ }
52
+
53
+ /**
54
+ * This hook is being used to fetch ordered address fields as configured in the address hierarchy
55
+ * This hook returns the valid search term for valid fields to get suitable entries for the field
56
+ * This also returns the function to reset the lower ordered fields if the value of a field is changed.
57
+ */
58
+ export function useAddressEntryFetchConfig(addressField: string) {
59
+ const { orderedFields, isLoadingFieldOrder } = useOrderedAddressHierarchyLevels();
60
+ const { setFieldValue } = useContext(PatientRegistrationContext);
61
+ const [, { value: addressValues }] = useField('address');
62
+
63
+ const index = useMemo(
64
+ () => (!isLoadingFieldOrder ? orderedFields.findIndex((field) => field === addressField) : -1),
65
+ [orderedFields, addressField, isLoadingFieldOrder],
66
+ );
67
+
68
+ const addressFieldSearchConfig = useMemo(() => {
69
+ let fetchEntriesForField = true;
70
+ const previousSelectedFields = orderedFields?.slice(0, index) ?? [];
71
+ let previousSelectedValues = [];
72
+ for (const fieldName of previousSelectedFields) {
73
+ if (!addressValues[fieldName]) {
74
+ fetchEntriesForField = false;
75
+ break;
76
+ }
77
+ previousSelectedValues.push(addressValues[fieldName]);
78
+ }
79
+ return {
80
+ fetchEntriesForField,
81
+ searchString: previousSelectedValues.join('|'),
82
+ };
83
+ }, [orderedFields, index, addressValues]);
84
+
85
+ const updateChildElements = useCallback(() => {
86
+ if (isLoadingFieldOrder) {
87
+ return;
88
+ }
89
+ orderedFields.slice(index + 1).map((fieldName) => {
90
+ setFieldValue(`address.${fieldName}`, '');
91
+ });
92
+ }, [index, isLoadingFieldOrder, orderedFields, setFieldValue]);
93
+
94
+ const results = useMemo(
95
+ () => ({
96
+ ...addressFieldSearchConfig,
97
+ updateChildElements,
98
+ }),
99
+ [addressFieldSearchConfig, updateChildElements],
100
+ );
101
+
102
+ return results;
103
+ }
104
+
105
+ export function useAddressHierarchy(searchString: string, separator: string) {
106
+ const { data, error, isLoading } = useSWRImmutable<
107
+ FetchResponse<
108
+ Array<{
109
+ address: string;
110
+ }>
111
+ >,
112
+ Error
113
+ >(
114
+ searchString
115
+ ? `/module/addresshierarchy/ajax/getPossibleFullAddresses.form?separator=${separator}&searchString=${searchString}`
116
+ : null,
117
+ openmrsFetch,
118
+ );
119
+
120
+ const results = useMemo(
121
+ () => ({
122
+ addresses: data?.data?.map((address) => address.address) ?? [],
123
+ error,
124
+ isLoading,
125
+ }),
126
+ [data?.data, error, isLoading],
127
+ );
128
+ return results;
129
+ }
130
+
131
+ export function useAddressHierarchyWithParentSearch(addressField: string, parentid: string, query: string) {
132
+ const { data, error, isLoading } = useSWRImmutable<
133
+ FetchResponse<
134
+ Array<{
135
+ uuid: string;
136
+ name: string;
137
+ }>
138
+ >,
139
+ Error
140
+ >(
141
+ query
142
+ ? `/module/addresshierarchy/ajax/getPossibleAddressHierarchyEntriesWithParents.form?addressField=${addressField}&limit=20&searchString=${query}&parentUuid=${parentid}`
143
+ : null,
144
+ openmrsFetch,
145
+ );
146
+
147
+ const results = useMemo(
148
+ () => ({
149
+ error: error,
150
+ isLoading,
151
+ addresses: data?.data ?? [],
152
+ }),
153
+ [data?.data, error, isLoading],
154
+ );
155
+
156
+ return results;
157
+ }
@@ -0,0 +1,85 @@
1
+ import React, { useState, useRef, useEffect, useMemo } from 'react';
2
+ import { useAddressHierarchy } from './address-hierarchy.resource';
3
+ import { Search } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useFormikContext } from 'formik';
6
+ import styles from './address-search.scss';
7
+
8
+ interface AddressSearchComponentProps {
9
+ addressLayout: Array<any>;
10
+ }
11
+
12
+ const AddressSearchComponent: React.FC<AddressSearchComponentProps> = ({ addressLayout }) => {
13
+ const { t } = useTranslation();
14
+ const separator = ' > ';
15
+ const searchBox = useRef(null);
16
+ const wrapper = useRef(null);
17
+ const [searchString, setSearchString] = useState<string>('');
18
+ const { addresses, isLoading, error } = useAddressHierarchy(searchString, separator);
19
+ const addressOptions: Array<string> = useMemo(() => {
20
+ const options: Set<string> = new Set();
21
+ addresses.forEach((address) => {
22
+ const values = address.split(separator);
23
+ values.forEach((val, index) => {
24
+ if (val.toLowerCase().includes(searchString.toLowerCase())) {
25
+ options.add(values.slice(0, index + 1).join(separator));
26
+ }
27
+ });
28
+ });
29
+ return [...options];
30
+ }, [addresses, searchString]);
31
+
32
+ const { setFieldValue } = useFormikContext();
33
+
34
+ const handleInputChange = (e) => {
35
+ setSearchString(e.target.value);
36
+ };
37
+
38
+ const handleChange = (address) => {
39
+ if (address) {
40
+ const values = address.split(separator);
41
+ addressLayout.map(({ name }, index) => {
42
+ setFieldValue(`address.${name}`, values?.[index] ?? '');
43
+ });
44
+ setSearchString('');
45
+ }
46
+ };
47
+
48
+ const handleClickOutsideComponent = (e) => {
49
+ if (wrapper.current && !wrapper.current.contains(e.target)) {
50
+ setSearchString('');
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ document.addEventListener('mousedown', handleClickOutsideComponent);
56
+
57
+ return () => {
58
+ document.removeEventListener('mousedown', handleClickOutsideComponent);
59
+ };
60
+ }, [wrapper]);
61
+
62
+ return (
63
+ <div className={styles.autocomplete} ref={wrapper} style={{ marginBottom: '1rem' }}>
64
+ <Search
65
+ onChange={handleInputChange}
66
+ labelText={t('searchAddress', 'Search address')}
67
+ placeholder={t('searchAddress', 'Search address')}
68
+ ref={searchBox}
69
+ value={searchString}
70
+ />
71
+ {addressOptions.length > 0 && (
72
+ /* Since the input has a marginBottom of 1rem */
73
+ <ul className={styles.suggestions}>
74
+ {addressOptions.map((address, index) => (
75
+ <li key={index} onClick={(e) => handleChange(address)}>
76
+ {address}
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ )}
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default AddressSearchComponent;
@@ -0,0 +1,53 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .label01 {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .suggestions {
10
+ border-top-width: 0;
11
+ list-style: none;
12
+ margin-top: 0;
13
+ max-height: 20rem;
14
+ overflow-y: auto;
15
+ padding-left: 0;
16
+ width: 100%;
17
+ position: absolute;
18
+ left: 0;
19
+ background-color: #fff;
20
+ margin-bottom: 20px;
21
+ z-index: 99;
22
+ border: 1px solid $ui-03;
23
+ }
24
+
25
+ .suggestions li {
26
+ padding: spacing.$spacing-05;
27
+ line-height: 1.29;
28
+ color: #525252;
29
+ border-bottom: 1px solid #8d8d8d;
30
+ }
31
+
32
+ .suggestions li:hover {
33
+ background-color: #e5e5e5;
34
+ color: #161616;
35
+ cursor: pointer;
36
+ }
37
+
38
+ .suggestions li:not(:last-of-type) {
39
+ border-bottom: 1px solid #999;
40
+ }
41
+
42
+ .autocomplete {
43
+ position: relative;
44
+ }
45
+
46
+ .autocompleteSearch {
47
+ width: 100%;
48
+ }
49
+
50
+ .suggestions a {
51
+ color: inherit;
52
+ text-decoration: none;
53
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import { Field } from 'formik';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Input } from '../../input/basic-input/input/input.component';
6
+ import styles from '../field.scss';
7
+ import { type FieldDefinition } from '../../../config-schema';
8
+
9
+ export interface AddressFieldProps {
10
+ fieldDefinition: FieldDefinition;
11
+ }
12
+
13
+ export const AddressField: React.FC<AddressFieldProps> = ({ fieldDefinition }) => {
14
+ const { t } = useTranslation();
15
+
16
+ return (
17
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
18
+ <Field name={fieldDefinition.id}>
19
+ {({ field, form: { touched, errors }, meta }) => {
20
+ return (
21
+ <Input
22
+ id={fieldDefinition.id}
23
+ labelText={t(`${fieldDefinition.label}`, `${fieldDefinition.label}`)}
24
+ {...field}
25
+ />
26
+ );
27
+ }}
28
+ </Field>
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1,214 @@
1
+ import React from 'react';
2
+ import { cleanup, render, screen } from '@testing-library/react';
3
+ import { AddressComponent } from '../address-field.component';
4
+ import { Formik, Form } from 'formik';
5
+ import { type Resources, ResourcesContext } from '../../../../offline.resources';
6
+ import { PatientRegistrationContext } from '../../../patient-registration-context';
7
+ import { useConfig } from '@openmrs/esm-framework';
8
+ import { useOrderedAddressHierarchyLevels } from '../address-hierarchy.resource';
9
+ import { mockedAddressTemplate, mockedOrderedFields } from '__mocks__';
10
+
11
+ // Mocking the AddressSearchComponent
12
+ jest.mock('../address-search.component', () => jest.fn(() => <div data-testid="address-search-bar" />));
13
+ // Mocking the AddressHierarchyLevels
14
+ jest.mock('../address-hierarchy-levels.component', () => jest.fn(() => <div data-testid="address-hierarchy-levels" />));
15
+ // Mocking the SkeletonText
16
+ jest.mock('@carbon/react', () => ({
17
+ ...jest.requireActual('@carbon/react'),
18
+ SkeletonText: jest.fn(() => <div data-testid="skeleton-text" />),
19
+ InlineNotification: jest.fn(() => <div data-testid="inline-notification" />),
20
+ }));
21
+
22
+ jest.mock('@openmrs/esm-framework', () => ({
23
+ ...jest.requireActual('@openmrs/esm-framework'),
24
+ useConfig: jest.fn(),
25
+ }));
26
+
27
+ jest.mock('../address-hierarchy.resource', () => ({
28
+ ...(jest.requireActual('../address-hierarchy.resource') as jest.Mock),
29
+ useOrderedAddressHierarchyLevels: jest.fn(),
30
+ }));
31
+
32
+ async function renderAddressHierarchy(addressTemplate = mockedAddressTemplate) {
33
+ await render(
34
+ <ResourcesContext.Provider value={{ addressTemplate } as unknown as Resources}>
35
+ <Formik initialValues={{}} onSubmit={null}>
36
+ <Form>
37
+ <PatientRegistrationContext.Provider value={{ setFieldValue: jest.fn() } as any}>
38
+ <AddressComponent />
39
+ </PatientRegistrationContext.Provider>
40
+ </Form>
41
+ </Formik>
42
+ </ResourcesContext.Provider>,
43
+ );
44
+ }
45
+
46
+ describe('Testing address hierarchy', () => {
47
+ beforeEach(cleanup);
48
+
49
+ it('should render skeleton when address template is loading', () => {
50
+ (useConfig as jest.Mock).mockImplementation(() => ({
51
+ fieldConfigurations: {
52
+ address: {
53
+ useAddressHierarchy: {
54
+ enabled: false,
55
+ useQuickSearch: false,
56
+ searchAddressByLevel: false,
57
+ },
58
+ },
59
+ },
60
+ }));
61
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
62
+ orderedFields: [],
63
+ isLoadingFieldOrder: false,
64
+ errorFetchingFieldOrder: null,
65
+ }));
66
+ // @ts-ignore
67
+ renderAddressHierarchy(null);
68
+ const skeletonText = screen.getByTestId('skeleton-text');
69
+ expect(skeletonText).toBeInTheDocument();
70
+ });
71
+
72
+ it('should render skeleton when address hierarchy is enabled and addresshierarchy order is loading', () => {
73
+ (useConfig as jest.Mock).mockImplementation(() => ({
74
+ fieldConfigurations: {
75
+ address: {
76
+ useAddressHierarchy: {
77
+ enabled: true,
78
+ useQuickSearch: false,
79
+ searchAddressByLevel: false,
80
+ },
81
+ },
82
+ },
83
+ }));
84
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
85
+ orderedFields: [],
86
+ isLoadingFieldOrder: true,
87
+ errorFetchingFieldOrder: null,
88
+ }));
89
+ renderAddressHierarchy();
90
+ const skeletonText = screen.getByTestId('skeleton-text');
91
+ expect(skeletonText).toBeInTheDocument();
92
+ });
93
+
94
+ it('should render skeleton when address hierarchy is enabled and addresshierarchy order is loading', () => {
95
+ (useConfig as jest.Mock).mockImplementation(() => ({
96
+ fieldConfigurations: {
97
+ address: {
98
+ useAddressHierarchy: {
99
+ enabled: true,
100
+ useQuickSearch: false,
101
+ searchAddressByLevel: false,
102
+ },
103
+ },
104
+ },
105
+ }));
106
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
107
+ orderedFields: [],
108
+ isLoadingFieldOrder: false,
109
+ errorFetchingFieldOrder: true,
110
+ }));
111
+ renderAddressHierarchy();
112
+ const inlineNotification = screen.getByTestId('inline-notification');
113
+ expect(inlineNotification).toBeInTheDocument();
114
+ });
115
+
116
+ it('should render the address component with address hierarchy disabled', () => {
117
+ (useConfig as jest.Mock).mockImplementation(() => ({
118
+ fieldConfigurations: {
119
+ address: {
120
+ useAddressHierarchy: {
121
+ enabled: false,
122
+ useQuickSearch: false,
123
+ searchAddressByLevel: false,
124
+ },
125
+ },
126
+ },
127
+ }));
128
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
129
+ orderedFields: [],
130
+ isLoadingFieldOrder: false,
131
+ errorFetchingFieldOrder: null,
132
+ }));
133
+ renderAddressHierarchy();
134
+ const allFields = mockedAddressTemplate.lines.flat().filter(({ isToken }) => isToken === 'IS_ADDR_TOKEN');
135
+ allFields.forEach((field) => {
136
+ const textFieldInput = screen.getByLabelText(`${field.displayText} (optional)`);
137
+ expect(textFieldInput).toBeInTheDocument();
138
+ });
139
+ });
140
+
141
+ it('should render the fields in order if the address hierarcy is enabled', () => {
142
+ (useConfig as jest.Mock).mockImplementation(() => ({
143
+ fieldConfigurations: {
144
+ address: {
145
+ useAddressHierarchy: {
146
+ enabled: true,
147
+ useQuickSearch: false,
148
+ searchAddressByLevel: false,
149
+ },
150
+ },
151
+ },
152
+ }));
153
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
154
+ orderedFields: [],
155
+ isLoadingFieldOrder: false,
156
+ errorFetchingFieldOrder: null,
157
+ }));
158
+ renderAddressHierarchy();
159
+ const allFields = mockedAddressTemplate.lines.flat().filter(({ isToken }) => isToken === 'IS_ADDR_TOKEN');
160
+ const orderMap = Object.fromEntries(mockedOrderedFields.map((field, indx) => [field, indx]));
161
+ allFields.sort(
162
+ (existingField1, existingField2) =>
163
+ orderMap[existingField1.codeName ?? 0] - orderMap[existingField2.codeName ?? 0],
164
+ );
165
+ allFields.forEach((field) => {
166
+ const textFieldInput = screen.getByLabelText(`${field.displayText} (optional)`);
167
+ expect(textFieldInput).toBeInTheDocument();
168
+ });
169
+ });
170
+
171
+ it('should render quick search bar on above the fields when address hierarchy is enabled and quicksearch is set to true', () => {
172
+ (useConfig as jest.Mock).mockImplementation(() => ({
173
+ fieldConfigurations: {
174
+ address: {
175
+ useAddressHierarchy: {
176
+ enabled: true,
177
+ useQuickSearch: true,
178
+ searchAddressByLevel: false,
179
+ },
180
+ },
181
+ },
182
+ }));
183
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
184
+ orderedFields: [],
185
+ isLoadingFieldOrder: false,
186
+ errorFetchingFieldOrder: null,
187
+ }));
188
+ renderAddressHierarchy();
189
+ const addressSearchBar = screen.getByTestId('address-search-bar');
190
+ expect(addressSearchBar).toBeInTheDocument();
191
+ });
192
+
193
+ it('should render combo boxes fields when address hierarchy is enabled and searchAddressByLevel is set to true', () => {
194
+ (useConfig as jest.Mock).mockImplementation(() => ({
195
+ fieldConfigurations: {
196
+ address: {
197
+ useAddressHierarchy: {
198
+ enabled: true,
199
+ useQuickSearch: false,
200
+ searchAddressByLevel: true,
201
+ },
202
+ },
203
+ },
204
+ }));
205
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
206
+ orderedFields: [],
207
+ isLoadingFieldOrder: false,
208
+ errorFetchingFieldOrder: null,
209
+ }));
210
+ renderAddressHierarchy();
211
+ const addressHierarchyLevels = screen.getByTestId('address-hierarchy-levels');
212
+ expect(addressHierarchyLevels).toBeInTheDocument();
213
+ });
214
+ });
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { Formik, Form, useFormikContext } from 'formik';
5
+ import { type Resources, ResourcesContext } from '../../../../offline.resources';
6
+ import { PatientRegistrationContext } from '../../../patient-registration-context';
7
+ import { useConfig } from '@openmrs/esm-framework';
8
+ import { useAddressHierarchy, useOrderedAddressHierarchyLevels } from '../address-hierarchy.resource';
9
+ import { mockedAddressTemplate, mockedAddressOptions, mockedOrderedFields } from '__mocks__';
10
+ import AddressSearchComponent from '../address-search.component';
11
+
12
+ useAddressHierarchy;
13
+ jest.mock('@openmrs/esm-framework', () => ({
14
+ ...jest.requireActual('@openmrs/esm-framework'),
15
+ useConfig: jest.fn(),
16
+ }));
17
+
18
+ jest.mock('../address-hierarchy.resource', () => ({
19
+ ...(jest.requireActual('../address-hierarchy.resource') as jest.Mock),
20
+ useOrderedAddressHierarchyLevels: jest.fn(),
21
+ useAddressHierarchy: jest.fn(),
22
+ }));
23
+
24
+ jest.mock('../../../patient-registration.resource', () => ({
25
+ ...(jest.requireActual('../../../../patient-registration.resource') as jest.Mock),
26
+ useAddressHierarchy: jest.fn(),
27
+ }));
28
+
29
+ jest.mock('formik', () => ({
30
+ ...(jest.requireActual('formik') as jest.Mock),
31
+ useFormikContext: jest.fn(() => ({})),
32
+ }));
33
+
34
+ const allFields = mockedAddressTemplate.lines
35
+ .flat()
36
+ .filter((field) => field.isToken === 'IS_ADDR_TOKEN')
37
+ .map(({ codeName, displayText }) => ({
38
+ id: codeName,
39
+ name: codeName,
40
+ label: displayText,
41
+ }));
42
+ const orderMap = Object.fromEntries(mockedOrderedFields.map((field, indx) => [field, indx]));
43
+ allFields.sort((existingField1, existingField2) => orderMap[existingField1.name] - orderMap[existingField2.name]);
44
+
45
+ async function renderAddressHierarchy(addressTemplate = mockedAddressTemplate) {
46
+ await render(
47
+ <ResourcesContext.Provider value={{ addressTemplate } as Resources}>
48
+ <Formik initialValues={{}} onSubmit={null}>
49
+ <Form>
50
+ <PatientRegistrationContext.Provider value={{ setFieldValue: jest.fn() } as any}>
51
+ <AddressSearchComponent addressLayout={allFields} />
52
+ </PatientRegistrationContext.Provider>
53
+ </Form>
54
+ </Formik>
55
+ </ResourcesContext.Provider>,
56
+ );
57
+ }
58
+
59
+ const setFieldValue = jest.fn();
60
+
61
+ describe('Testing address search bar', () => {
62
+ beforeEach(() => {
63
+ (useConfig as jest.Mock).mockImplementation(() => ({
64
+ fieldConfigurations: {
65
+ address: {
66
+ useAddressHierarchy: {
67
+ enabled: true,
68
+ useQuickSearch: true,
69
+ searchAddressByLevel: false,
70
+ },
71
+ },
72
+ },
73
+ }));
74
+ (useOrderedAddressHierarchyLevels as jest.Mock).mockImplementation(() => ({
75
+ orderedFields: mockedOrderedFields,
76
+ isLoadingFieldOrder: false,
77
+ errorFetchingFieldOrder: null,
78
+ }));
79
+ (useFormikContext as jest.Mock).mockImplementation(() => ({
80
+ setFieldValue,
81
+ }));
82
+ });
83
+
84
+ it('should render the search bar', () => {
85
+ (useAddressHierarchy as jest.Mock).mockImplementation(() => ({
86
+ addresses: [],
87
+ error: null,
88
+ isLoading: false,
89
+ }));
90
+
91
+ renderAddressHierarchy();
92
+
93
+ const searchbox = screen.getByRole('searchbox');
94
+ expect(searchbox).toBeInTheDocument();
95
+
96
+ const ul = screen.queryByRole('list');
97
+ expect(ul).not.toBeInTheDocument();
98
+ });
99
+
100
+ it("should render only the results for the search term matched address' parents", async () => {
101
+ const user = userEvent.setup();
102
+
103
+ (useAddressHierarchy as jest.Mock).mockImplementation(() => ({
104
+ addresses: mockedAddressOptions,
105
+ error: null,
106
+ isLoading: false,
107
+ }));
108
+
109
+ renderAddressHierarchy();
110
+
111
+ const searchString = 'nea';
112
+ const separator = ' > ';
113
+ const options: Set<string> = new Set();
114
+
115
+ mockedAddressOptions.forEach((address) => {
116
+ const values = address.split(separator);
117
+ values.forEach((val, index) => {
118
+ if (val.toLowerCase().includes(searchString.toLowerCase())) {
119
+ options.add(values.slice(0, index + 1).join(separator));
120
+ }
121
+ });
122
+ });
123
+
124
+ const addressOptions = [...options];
125
+ addressOptions.forEach(async (address) => {
126
+ const optionElement = screen.getByText(address);
127
+ expect(optionElement).toBeInTheDocument();
128
+ await user.click(optionElement);
129
+ const values = address.split(separator);
130
+ allFields.map(({ name }, index) => {
131
+ expect(setFieldValue).toHaveBeenCalledWith(`address.${name}`, values?.[index]);
132
+ });
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,25 @@
1
+ import { useConfig } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { type RegistrationConfig } from '../../config-schema';
4
+ import { AddressField } from './address/custom-address-field.component';
5
+ import { ObsField } from './obs/obs-field.component';
6
+ import { PersonAttributeField } from './person-attributes/person-attribute-field.component';
7
+
8
+ export interface CustomFieldProps {
9
+ name: string;
10
+ }
11
+
12
+ export function CustomField({ name }: CustomFieldProps) {
13
+ const config = useConfig() as RegistrationConfig;
14
+ const fieldDefinition = config.fieldDefinitions.filter((def) => def.id == name)[0];
15
+
16
+ if (fieldDefinition.type === 'person attribute') {
17
+ return <PersonAttributeField fieldDefinition={fieldDefinition} />;
18
+ } else if (fieldDefinition.type === 'obs') {
19
+ return <ObsField fieldDefinition={fieldDefinition} />;
20
+ } else if (fieldDefinition.type === 'address') {
21
+ return <AddressField fieldDefinition={fieldDefinition} />;
22
+ } else {
23
+ return <div>Error: Unknown field type {fieldDefinition.type}</div>;
24
+ }
25
+ }