@olaboot/esm-patient-registration-app 9.2.0

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 (250) hide show
  1. package/ADDRESS_CONFIGURATION.md +152 -0
  2. package/IDENTIFIER_CONFIGURATION.md +142 -0
  3. package/IMPLEMENTATION_SUMMARY.md +111 -0
  4. package/QUICK_START.md +95 -0
  5. package/README.md +7 -0
  6. package/address-required-fields-config.json +26 -0
  7. package/dist/126.js +1 -0
  8. package/dist/15.js +1 -0
  9. package/dist/1564.js +1 -0
  10. package/dist/1567.js +1 -0
  11. package/dist/1845.js +1 -0
  12. package/dist/1953.js +1 -0
  13. package/dist/200.js +1 -0
  14. package/dist/200.js.map +1 -0
  15. package/dist/215.js +1 -0
  16. package/dist/2178.js +1 -0
  17. package/dist/250.js +1 -0
  18. package/dist/250.js.map +1 -0
  19. package/dist/2523.js +1 -0
  20. package/dist/2523.js.map +1 -0
  21. package/dist/2566.js +1 -0
  22. package/dist/2586.js +1 -0
  23. package/dist/2586.js.map +1 -0
  24. package/dist/2716.js +1 -0
  25. package/dist/2716.js.map +1 -0
  26. package/dist/2759.js +1 -0
  27. package/dist/2821.js +6 -0
  28. package/dist/2821.js.map +1 -0
  29. package/dist/3089.js +1 -0
  30. package/dist/3089.js.map +1 -0
  31. package/dist/3230.js +1 -0
  32. package/dist/3441.js +1 -0
  33. package/dist/3565.js +1 -0
  34. package/dist/3571.js +1 -0
  35. package/dist/3571.js.map +1 -0
  36. package/dist/3746.js +1 -0
  37. package/dist/3925.js +1 -0
  38. package/dist/3946.js +1 -0
  39. package/dist/4024.js +1 -0
  40. package/dist/4024.js.map +1 -0
  41. package/dist/4744.js +1 -0
  42. package/dist/4744.js.map +1 -0
  43. package/dist/4809.js +1 -0
  44. package/dist/4894.js +1 -0
  45. package/dist/4970.js +1 -0
  46. package/dist/4970.js.map +1 -0
  47. package/dist/5130.js +1 -0
  48. package/dist/5187.js +1 -0
  49. package/dist/5491.js +1 -0
  50. package/dist/5491.js.map +1 -0
  51. package/dist/5595.js +1 -0
  52. package/dist/5961.js +1 -0
  53. package/dist/6133.js +1 -0
  54. package/dist/634.js +1 -0
  55. package/dist/634.js.map +1 -0
  56. package/dist/6456.js +1 -0
  57. package/dist/6466.js +1 -0
  58. package/dist/6613.js +1 -0
  59. package/dist/6783.js +1 -0
  60. package/dist/7073.js +38 -0
  61. package/dist/7073.js.map +1 -0
  62. package/dist/7154.js +1 -0
  63. package/dist/7154.js.map +1 -0
  64. package/dist/7348.js +1 -0
  65. package/dist/7439.js +1 -0
  66. package/dist/7439.js.map +1 -0
  67. package/dist/7543.js +1 -0
  68. package/dist/7607.js +1 -0
  69. package/dist/772.js +1 -0
  70. package/dist/7984.js +1 -0
  71. package/dist/7984.js.map +1 -0
  72. package/dist/8538.js +1 -0
  73. package/dist/8538.js.map +1 -0
  74. package/dist/8599.js +1 -0
  75. package/dist/8727.js +1 -0
  76. package/dist/8847.js +1 -0
  77. package/dist/9015.js +1 -0
  78. package/dist/906.js +1 -0
  79. package/dist/9065.js +1 -0
  80. package/dist/9182.js +1 -0
  81. package/dist/9339.js +1 -0
  82. package/dist/9453.js +1 -0
  83. package/dist/9833.js +1 -0
  84. package/dist/9833.js.map +1 -0
  85. package/dist/9856.js +1 -0
  86. package/dist/9856.js.map +1 -0
  87. package/dist/9920.js +1 -0
  88. package/dist/9938.js +1 -0
  89. package/dist/9943.js +1 -0
  90. package/dist/9943.js.map +1 -0
  91. package/dist/main.js +10 -0
  92. package/dist/main.js.map +1 -0
  93. package/dist/olaboot-esm-patient-registration-app.js +5 -0
  94. package/dist/olaboot-esm-patient-registration-app.js.buildmanifest.json +1627 -0
  95. package/dist/olaboot-esm-patient-registration-app.js.map +1 -0
  96. package/dist/routes.json +1 -0
  97. package/docs/images/patient-registration-hierarchy.png +0 -0
  98. package/example-config.json +14 -0
  99. package/jest.config.js +3 -0
  100. package/package.json +60 -0
  101. package/rspack.config.js +1 -0
  102. package/src/add-patient-link.extension.tsx +21 -0
  103. package/src/add-patient-link.scss +3 -0
  104. package/src/add-patient-link.test.tsx +16 -0
  105. package/src/config-schema.ts +507 -0
  106. package/src/constants.ts +14 -0
  107. package/src/declarations.d.ts +4 -0
  108. package/src/index.ts +59 -0
  109. package/src/nav-link.test.tsx +13 -0
  110. package/src/nav-link.tsx +10 -0
  111. package/src/offline.resources.ts +157 -0
  112. package/src/offline.ts +93 -0
  113. package/src/patient-photo.extension.tsx +11 -0
  114. package/src/patient-registration/before-save-prompt.component.tsx +72 -0
  115. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  116. package/src/patient-registration/field/address/address-field.component.tsx +186 -0
  117. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +71 -0
  118. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  119. package/src/patient-registration/field/address/address-hierarchy.test.tsx +296 -0
  120. package/src/patient-registration/field/address/address-search.component.tsx +87 -0
  121. package/src/patient-registration/field/address/address-search.scss +53 -0
  122. package/src/patient-registration/field/address/address-search.test.tsx +141 -0
  123. package/src/patient-registration/field/address/custom-address-field.component.tsx +32 -0
  124. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  125. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  126. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +79 -0
  127. package/src/patient-registration/field/dob/dob.component.tsx +167 -0
  128. package/src/patient-registration/field/dob/dob.test.tsx +90 -0
  129. package/src/patient-registration/field/field.component.tsx +53 -0
  130. package/src/patient-registration/field/field.resource.ts +42 -0
  131. package/src/patient-registration/field/field.scss +171 -0
  132. package/src/patient-registration/field/field.test.tsx +330 -0
  133. package/src/patient-registration/field/gender/gender-field.component.tsx +54 -0
  134. package/src/patient-registration/field/gender/gender-field.test.tsx +99 -0
  135. package/src/patient-registration/field/id/id-field.component.tsx +136 -0
  136. package/src/patient-registration/field/id/id-field.test.tsx +121 -0
  137. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -0
  138. package/src/patient-registration/field/id/identifier-selection.scss +41 -0
  139. package/src/patient-registration/field/name/name-field.component.tsx +148 -0
  140. package/src/patient-registration/field/obs/obs-field.component.tsx +261 -0
  141. package/src/patient-registration/field/obs/obs-field.test.tsx +299 -0
  142. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +120 -0
  143. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
  144. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  145. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  146. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +100 -0
  147. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
  148. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  149. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  150. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
  151. package/src/patient-registration/field/phone/phone-field.component.tsx +17 -0
  152. package/src/patient-registration/form-manager.test.ts +91 -0
  153. package/src/patient-registration/form-manager.ts +443 -0
  154. package/src/patient-registration/input/basic-input/input/input.component.tsx +183 -0
  155. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  156. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  157. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  158. package/src/patient-registration/input/combo-input/combo-input.component.tsx +130 -0
  159. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  160. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  161. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  162. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
  163. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +193 -0
  164. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
  165. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  166. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  167. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +56 -0
  168. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +34 -0
  169. package/src/patient-registration/input/input.scss +122 -0
  170. package/src/patient-registration/patient-registration-context.ts +35 -0
  171. package/src/patient-registration/patient-registration-hooks.ts +376 -0
  172. package/src/patient-registration/patient-registration-utils.test.ts +33 -0
  173. package/src/patient-registration/patient-registration-utils.ts +214 -0
  174. package/src/patient-registration/patient-registration.component.tsx +266 -0
  175. package/src/patient-registration/patient-registration.resource.test.tsx +22 -0
  176. package/src/patient-registration/patient-registration.resource.ts +198 -0
  177. package/src/patient-registration/patient-registration.scss +103 -0
  178. package/src/patient-registration/patient-registration.test.tsx +580 -0
  179. package/src/patient-registration/patient-registration.types.ts +322 -0
  180. package/src/patient-registration/section/death-info/death-info-section.component.tsx +36 -0
  181. package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
  182. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  183. package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
  184. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  185. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +234 -0
  186. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
  187. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  188. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  189. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  190. package/src/patient-registration/section/section.component.tsx +23 -0
  191. package/src/patient-registration/section/section.scss +21 -0
  192. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  193. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  194. package/src/patient-registration/validation/patient-registration-validation.test.ts +205 -0
  195. package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
  196. package/src/resource.ts +12 -0
  197. package/src/resources-context.ts +14 -0
  198. package/src/root.component.tsx +63 -0
  199. package/src/root.scss +7 -0
  200. package/src/routes.json +61 -0
  201. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  202. package/src/widgets/cancel-patient-edit.test.tsx +22 -0
  203. package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
  204. package/src/widgets/delete-identifier-confirmation.test.tsx +32 -0
  205. package/src/widgets/edit-patient-details-button.component.tsx +33 -0
  206. package/src/widgets/edit-patient-details-button.scss +3 -0
  207. package/src/widgets/edit-patient-details-button.test.tsx +35 -0
  208. package/translations/am.json +120 -0
  209. package/translations/ar.json +120 -0
  210. package/translations/ar_SY.json +120 -0
  211. package/translations/bn.json +120 -0
  212. package/translations/cs.json +120 -0
  213. package/translations/de.json +120 -0
  214. package/translations/en.json +120 -0
  215. package/translations/en_US.json +120 -0
  216. package/translations/es.json +120 -0
  217. package/translations/es_MX.json +120 -0
  218. package/translations/fr.json +120 -0
  219. package/translations/he.json +120 -0
  220. package/translations/hi.json +120 -0
  221. package/translations/hi_IN.json +120 -0
  222. package/translations/id.json +120 -0
  223. package/translations/it.json +120 -0
  224. package/translations/ka.json +120 -0
  225. package/translations/km.json +120 -0
  226. package/translations/ku.json +120 -0
  227. package/translations/ky.json +120 -0
  228. package/translations/lg.json +120 -0
  229. package/translations/ne.json +120 -0
  230. package/translations/pl.json +120 -0
  231. package/translations/pt.json +120 -0
  232. package/translations/pt_BR.json +120 -0
  233. package/translations/qu.json +120 -0
  234. package/translations/ro_RO.json +120 -0
  235. package/translations/ru_RU.json +120 -0
  236. package/translations/si.json +120 -0
  237. package/translations/sq.json +120 -0
  238. package/translations/sw.json +120 -0
  239. package/translations/sw_KE.json +120 -0
  240. package/translations/tr.json +120 -0
  241. package/translations/tr_TR.json +120 -0
  242. package/translations/uk.json +120 -0
  243. package/translations/uz.json +120 -0
  244. package/translations/uz@Latn.json +120 -0
  245. package/translations/uz_UZ.json +120 -0
  246. package/translations/vi.json +120 -0
  247. package/translations/zh.json +120 -0
  248. package/translations/zh_CN.json +120 -0
  249. package/translations/zh_TW.json +120 -0
  250. package/tsconfig.json +5 -0
@@ -0,0 +1,120 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Field } from 'formik';
5
+ import { Layer, Select, SelectItem } from '@carbon/react';
6
+ import { reportError } from '@openmrs/esm-framework';
7
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
8
+ import { useConceptAnswers } from '../field.resource';
9
+ import styles from './../field.scss';
10
+
11
+ export interface CodedPersonAttributeFieldProps {
12
+ id: string;
13
+ personAttributeType: PersonAttributeTypeResponse;
14
+ answerConceptSetUuid: string;
15
+ label?: string;
16
+ customConceptAnswers: Array<{ uuid: string; label?: string }>;
17
+ required: boolean;
18
+ }
19
+
20
+ export function CodedPersonAttributeField({
21
+ id,
22
+ personAttributeType,
23
+ answerConceptSetUuid,
24
+ label,
25
+ customConceptAnswers,
26
+ required,
27
+ }: CodedPersonAttributeFieldProps) {
28
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
29
+ customConceptAnswers.length ? '' : answerConceptSetUuid,
30
+ );
31
+
32
+ const { t } = useTranslation();
33
+ const fieldName = `attributes.${personAttributeType.uuid}`;
34
+ const [error, setError] = useState(false);
35
+
36
+ useEffect(() => {
37
+ if (!answerConceptSetUuid && !customConceptAnswers.length) {
38
+ reportError(
39
+ t(
40
+ 'codedPersonAttributeNoAnswerSet',
41
+ `The person attribute field '{{codedPersonAttributeFieldId}}' is of type 'coded' but has been defined without an answer concept set UUID. The 'answerConceptSetUuid' key is required.`,
42
+ { codedPersonAttributeFieldId: id },
43
+ ),
44
+ );
45
+ setError(true);
46
+ }
47
+ }, [answerConceptSetUuid, customConceptAnswers, id, t]);
48
+
49
+ useEffect(() => {
50
+ if (!isLoadingConceptAnswers && !customConceptAnswers.length) {
51
+ if (!conceptAnswers) {
52
+ reportError(
53
+ t(
54
+ 'codedPersonAttributeAnswerSetInvalid',
55
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an invalid answer concept set UUID '{{answerConceptSetUuid}}'.`,
56
+ { codedPersonAttributeFieldId: id, answerConceptSetUuid },
57
+ ),
58
+ );
59
+ setError(true);
60
+ }
61
+ if (conceptAnswers?.length == 0) {
62
+ reportError(
63
+ t(
64
+ 'codedPersonAttributeAnswerSetEmpty',
65
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an answer concept set UUID '{{answerConceptSetUuid}}' that does not have any concept answers.`,
66
+ {
67
+ codedPersonAttributeFieldId: id,
68
+ answerConceptSetUuid,
69
+ },
70
+ ),
71
+ );
72
+ setError(true);
73
+ }
74
+ }
75
+ }, [isLoadingConceptAnswers, conceptAnswers, customConceptAnswers, t, id, answerConceptSetUuid]);
76
+
77
+ const answers = useMemo(() => {
78
+ if (customConceptAnswers.length) {
79
+ return customConceptAnswers;
80
+ }
81
+ return isLoadingConceptAnswers || !conceptAnswers
82
+ ? []
83
+ : conceptAnswers
84
+ .map((answer) => ({ ...answer, label: answer.display }))
85
+ .sort((a, b) => a.label.localeCompare(b.label));
86
+ }, [customConceptAnswers, conceptAnswers, isLoadingConceptAnswers]);
87
+
88
+ if (error) {
89
+ return null;
90
+ }
91
+
92
+ return (
93
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
94
+ {!isLoadingConceptAnswers ? (
95
+ <Layer>
96
+ <Field name={fieldName}>
97
+ {({ field, form: { touched, errors }, meta }) => {
98
+ return (
99
+ <>
100
+ <Select
101
+ id={id}
102
+ name={`person-attribute-${personAttributeType.uuid}`}
103
+ labelText={label ?? personAttributeType?.display}
104
+ invalid={errors[fieldName] && touched[fieldName]}
105
+ required={required}
106
+ {...field}>
107
+ <SelectItem value={''} text={t('selectAnOption', 'Select an option')} />
108
+ {answers.map((answer) => (
109
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
110
+ ))}
111
+ </Select>
112
+ </>
113
+ );
114
+ }}
115
+ </Field>
116
+ </Layer>
117
+ ) : null}
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import { Form, Formik } from 'formik';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { useConceptAnswers } from '../field.resource';
5
+ import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
6
+
7
+ const mockUseConceptAnswers = jest.mocked(useConceptAnswers);
8
+
9
+ jest.mock('../field.resource', () => ({
10
+ ...jest.requireActual('../field.resource'),
11
+ useConceptAnswers: jest.fn(),
12
+ }));
13
+
14
+ describe('CodedPersonAttributeField', () => {
15
+ const conceptAnswers = [
16
+ { uuid: '1', display: 'Option 1' },
17
+ { uuid: '2', display: 'Option 2' },
18
+ ];
19
+
20
+ const personAttributeType = {
21
+ format: 'org.openmrs.Concept',
22
+ display: 'Referred by',
23
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
24
+ name: '',
25
+ description: '',
26
+ };
27
+
28
+ const answerConceptSetUuid = '6682d17f-0777-45e4-a39b-93f77eb3531c';
29
+ let consoleSpy: jest.SpyInstance;
30
+
31
+ beforeEach(() => {
32
+ mockUseConceptAnswers.mockReturnValue({
33
+ data: conceptAnswers,
34
+ isLoading: false,
35
+ error: null,
36
+ });
37
+
38
+ consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
39
+ });
40
+
41
+ afterEach(() => {
42
+ consoleSpy.mockRestore();
43
+ });
44
+
45
+ it('renders an error if there is no concept answer set provided', () => {
46
+ expect(() => {
47
+ render(
48
+ <Formik initialValues={{}} onSubmit={() => {}}>
49
+ <Form>
50
+ <CodedPersonAttributeField
51
+ answerConceptSetUuid={null}
52
+ customConceptAnswers={[]}
53
+ id="attributeId"
54
+ label={personAttributeType.display}
55
+ personAttributeType={personAttributeType}
56
+ required={false}
57
+ />
58
+ </Form>
59
+ </Formik>,
60
+ );
61
+ }).toThrow(expect.stringMatching(/has been defined without an answer concept set UUID/i));
62
+ });
63
+
64
+ it('renders an error if the concept answer set does not have any concept answers', () => {
65
+ mockUseConceptAnswers.mockReturnValue({
66
+ data: [],
67
+ isLoading: false,
68
+ error: null,
69
+ });
70
+
71
+ expect(() => {
72
+ render(
73
+ <Formik initialValues={{}} onSubmit={() => {}}>
74
+ <Form>
75
+ <CodedPersonAttributeField
76
+ id="attributeId"
77
+ personAttributeType={personAttributeType}
78
+ answerConceptSetUuid={answerConceptSetUuid}
79
+ label={personAttributeType.display}
80
+ customConceptAnswers={[]}
81
+ required={false}
82
+ />
83
+ </Form>
84
+ </Formik>,
85
+ );
86
+ }).toThrow(expect.stringMatching(/does not have any concept answers/i));
87
+ });
88
+
89
+ it('renders the conceptAnswers as select options', () => {
90
+ render(
91
+ <Formik initialValues={{}} onSubmit={() => {}}>
92
+ <Form>
93
+ <CodedPersonAttributeField
94
+ id="attributeId"
95
+ personAttributeType={personAttributeType}
96
+ answerConceptSetUuid={answerConceptSetUuid}
97
+ label={personAttributeType.display}
98
+ customConceptAnswers={[]}
99
+ required={false}
100
+ />
101
+ </Form>
102
+ </Formik>,
103
+ );
104
+
105
+ expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
106
+ expect(screen.getByText(/Option 1/i)).toBeInTheDocument();
107
+ expect(screen.getByText(/Option 2/i)).toBeInTheDocument();
108
+ });
109
+
110
+ it('renders customConceptAnswers as select options when they are provided', () => {
111
+ render(
112
+ <Formik initialValues={{}} onSubmit={() => {}}>
113
+ <Form>
114
+ <CodedPersonAttributeField
115
+ id="attributeId"
116
+ personAttributeType={personAttributeType}
117
+ answerConceptSetUuid={answerConceptSetUuid}
118
+ label={personAttributeType.display}
119
+ customConceptAnswers={[
120
+ {
121
+ uuid: 'A',
122
+ label: 'Special Option A',
123
+ },
124
+ {
125
+ uuid: 'B',
126
+ label: 'Special Option B',
127
+ },
128
+ ]}
129
+ required={false}
130
+ />
131
+ </Form>
132
+ </Formik>,
133
+ );
134
+
135
+ expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
136
+ expect(screen.getByText(/Special Option A/i)).toBeInTheDocument();
137
+ expect(screen.getByText(/Special Option B/i)).toBeInTheDocument();
138
+ expect(screen.queryByText(/Option 1/i)).not.toBeInTheDocument();
139
+ expect(screen.queryByText(/Option 2/i)).not.toBeInTheDocument();
140
+ });
141
+ });
@@ -0,0 +1,105 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Field, useField } from 'formik';
4
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
5
+ import styles from './../field.scss';
6
+ import { useLocations } from './location-person-attribute-field.resource';
7
+ import { ComboBox, InlineLoading, Layer } from '@carbon/react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ export interface LocationPersonAttributeFieldProps {
11
+ id: string;
12
+ personAttributeType: PersonAttributeTypeResponse;
13
+ label?: string;
14
+ locationTag: string;
15
+ required?: boolean;
16
+ }
17
+
18
+ export function LocationPersonAttributeField({
19
+ personAttributeType,
20
+ id,
21
+ label,
22
+ locationTag,
23
+ required,
24
+ }: LocationPersonAttributeFieldProps) {
25
+ const { t } = useTranslation();
26
+ const fieldName = `attributes.${personAttributeType.uuid}`;
27
+ const [field, meta, { setValue }] = useField(`attributes.${personAttributeType.uuid}`);
28
+ const [searchQuery, setSearchQuery] = useState('');
29
+ const { locations, isLoading, loadingNewData } = useLocations(locationTag || null, searchQuery);
30
+ const prevLocationOptions = useRef([]);
31
+
32
+ const locationOptions = useMemo(() => {
33
+ if (!(isLoading && loadingNewData)) {
34
+ const newOptions = locations.map(({ resource: { id, name } }) => ({ value: id, label: name }));
35
+ prevLocationOptions.current = newOptions;
36
+ return newOptions;
37
+ }
38
+ return prevLocationOptions.current;
39
+ }, [locations, isLoading, loadingNewData]);
40
+
41
+ const selectedItem = useMemo(() => {
42
+ if (typeof meta.value === 'string') {
43
+ return locationOptions.find(({ value }) => value === meta.value) || null;
44
+ }
45
+ if (typeof meta.value === 'object' && meta.value) {
46
+ return locationOptions.find(({ value }) => value === meta.value.uuid) || null;
47
+ }
48
+ return null;
49
+ }, [locationOptions, meta.value]);
50
+
51
+ // Callback for when updating the combobox input
52
+ const handleInputChange = useCallback(
53
+ (value: string | null) => {
54
+ if (value) {
55
+ // If the value exists in the locationOptions (i.e. a label matches the input), exit the function
56
+ if (locationOptions.find(({ label }) => label === value)) return;
57
+ // If the input is a new value, set the search query
58
+ setSearchQuery(value);
59
+ // Clear the current selected value since the input doesn't match any existing options
60
+ setValue(null);
61
+ }
62
+ },
63
+ [locationOptions, setValue],
64
+ );
65
+ const handleSelect = useCallback(
66
+ ({ selectedItem }) => {
67
+ if (selectedItem) {
68
+ setValue(selectedItem.value);
69
+ }
70
+ },
71
+ [setValue],
72
+ );
73
+
74
+ return (
75
+ <div
76
+ className={classNames(styles.customField, styles.halfWidthInDesktopView, styles.locationAttributeFieldContainer)}>
77
+ <Layer>
78
+ <Field name={fieldName}>
79
+ {({ field, form: { touched, errors } }) => {
80
+ return (
81
+ <ComboBox
82
+ id={id}
83
+ name={`person-attribute-${personAttributeType.uuid}`}
84
+ titleText={label}
85
+ items={locationOptions}
86
+ placeholder={t('searchLocationPersonAttribute', 'Search location')}
87
+ onInputChange={handleInputChange}
88
+ required={required}
89
+ onChange={handleSelect}
90
+ selectedItem={selectedItem}
91
+ invalid={errors[fieldName] && touched[fieldName]}
92
+ typeahead
93
+ />
94
+ );
95
+ }}
96
+ </Field>
97
+ </Layer>
98
+ {loadingNewData && (
99
+ <div className={styles.loadingContainer}>
100
+ <InlineLoading />
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,48 @@
1
+ import { useMemo } from 'react';
2
+ import { type FetchResponse, fhirBaseUrl, openmrsFetch, useDebounce } from '@openmrs/esm-framework';
3
+ import { type LocationEntry, type LocationResponse } from '@openmrs/esm-service-queues-app/src/types';
4
+ import useSWR from 'swr';
5
+
6
+ interface UseLocationsResult {
7
+ locations: Array<LocationEntry>;
8
+ isLoading: boolean;
9
+ loadingNewData: boolean;
10
+ }
11
+
12
+ export function useLocations(locationTag: string | null, searchQuery: string = ''): UseLocationsResult {
13
+ const debouncedSearchQuery = useDebounce(searchQuery);
14
+
15
+ const constructUrl = useMemo(() => {
16
+ let url = `${fhirBaseUrl}/Location?`;
17
+ let urlSearchParameters = new URLSearchParams();
18
+ urlSearchParameters.append('_summary', 'data');
19
+
20
+ if (!debouncedSearchQuery) {
21
+ urlSearchParameters.append('_count', '10');
22
+ }
23
+
24
+ if (locationTag) {
25
+ urlSearchParameters.append('_tag', locationTag);
26
+ }
27
+
28
+ if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') {
29
+ urlSearchParameters.append('name:contains', debouncedSearchQuery);
30
+ }
31
+
32
+ return url + urlSearchParameters.toString();
33
+ }, [locationTag, debouncedSearchQuery]);
34
+
35
+ const { data, error, isLoading, isValidating } = useSWR<FetchResponse<LocationResponse>, Error>(
36
+ constructUrl,
37
+ openmrsFetch,
38
+ );
39
+
40
+ return useMemo(
41
+ () => ({
42
+ locations: data?.data?.entry || [],
43
+ isLoading,
44
+ loadingNewData: isValidating,
45
+ }),
46
+ [data, isLoading, isValidating],
47
+ );
48
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineNotification, TextInputSkeleton } from '@carbon/react';
3
+ import { type FieldDefinition } from '../../../config-schema';
4
+ import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
+ import { usePersonAttributeType } from './person-attributes.resource';
6
+ import { TextPersonAttributeField } from './text-person-attribute-field.component';
7
+ import { useTranslation } from 'react-i18next';
8
+ import styles from '../field.scss';
9
+ import { LocationPersonAttributeField } from './location-person-attribute-field.component';
10
+
11
+ export interface PersonAttributeFieldProps {
12
+ fieldDefinition: FieldDefinition;
13
+ }
14
+
15
+ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldProps) {
16
+ const { data: personAttributeType, isLoading, error } = usePersonAttributeType(fieldDefinition.uuid);
17
+ const { t } = useTranslation();
18
+
19
+ const personAttributeField = useMemo(() => {
20
+ if (!personAttributeType) {
21
+ return null;
22
+ }
23
+ switch (personAttributeType.format) {
24
+ case 'java.lang.String':
25
+ return (
26
+ <TextPersonAttributeField
27
+ personAttributeType={personAttributeType}
28
+ validationRegex={fieldDefinition.validation?.matches ?? ''}
29
+ label={fieldDefinition.label}
30
+ required={fieldDefinition.validation?.required ?? false}
31
+ id={fieldDefinition?.id}
32
+ />
33
+ );
34
+ case 'org.openmrs.Concept':
35
+ return (
36
+ <CodedPersonAttributeField
37
+ personAttributeType={personAttributeType}
38
+ answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
39
+ label={fieldDefinition.label}
40
+ id={fieldDefinition?.id}
41
+ customConceptAnswers={fieldDefinition.customConceptAnswers ?? []}
42
+ required={fieldDefinition.validation?.required ?? false}
43
+ />
44
+ );
45
+ case 'org.openmrs.Location':
46
+ return (
47
+ <LocationPersonAttributeField
48
+ personAttributeType={personAttributeType}
49
+ locationTag={fieldDefinition.locationTag}
50
+ label={fieldDefinition.label}
51
+ id={fieldDefinition?.id}
52
+ required={fieldDefinition.validation?.required ?? false}
53
+ />
54
+ );
55
+ default:
56
+ return (
57
+ <InlineNotification kind="error" title="Error">
58
+ {t(
59
+ 'unknownPatientAttributeType',
60
+ 'Patient attribute type has unknown format {{personAttributeTypeFormat}}',
61
+ {
62
+ personAttributeTypeFormat: personAttributeType.format,
63
+ },
64
+ )}
65
+ </InlineNotification>
66
+ );
67
+ }
68
+ }, [personAttributeType, fieldDefinition, t]);
69
+
70
+ if (isLoading) {
71
+ return (
72
+ <div>
73
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
74
+ <TextInputSkeleton />
75
+ </div>
76
+ );
77
+ }
78
+
79
+ if (error) {
80
+ return (
81
+ <div>
82
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
83
+ <InlineNotification kind="error" title={t('error', 'Error')}>
84
+ {t('unableToFetch', 'Unable to fetch person attribute type - {{personattributetype}}', {
85
+ personattributetype: fieldDefinition?.label ?? fieldDefinition?.id,
86
+ })}
87
+ </InlineNotification>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <div>
94
+ {fieldDefinition.showHeading && (
95
+ <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label ?? personAttributeType?.display}</h4>
96
+ )}
97
+ {personAttributeField}
98
+ </div>
99
+ );
100
+ }