@ampath/esm-patient-registration-app 6.0.1-pre.98 → 9.2.0-next.12

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 (288) hide show
  1. package/dist/1119.js +1 -0
  2. package/dist/1197.js +1 -0
  3. package/dist/21.js +1 -0
  4. package/dist/21.js.map +1 -0
  5. package/dist/2146.js +1 -0
  6. package/dist/2372.js +1 -0
  7. package/dist/2372.js.map +1 -0
  8. package/dist/2470.js +1 -0
  9. package/dist/2470.js.map +1 -0
  10. package/dist/2690.js +1 -0
  11. package/dist/2913.js +2 -0
  12. package/dist/{913.js.LICENSE.txt → 2913.js.LICENSE.txt} +3 -23
  13. package/dist/2913.js.map +1 -0
  14. package/dist/3093.js +1 -0
  15. package/dist/3093.js.map +1 -0
  16. package/dist/3099.js +1 -0
  17. package/dist/3144.js +2 -0
  18. package/dist/3144.js.LICENSE.txt +19 -0
  19. package/dist/3144.js.map +1 -0
  20. package/dist/320.js +2 -0
  21. package/dist/{876.js.LICENSE.txt → 320.js.LICENSE.txt} +2 -3
  22. package/dist/320.js.map +1 -0
  23. package/dist/3464.js +1 -0
  24. package/dist/3464.js.map +1 -0
  25. package/dist/3474.js +2 -0
  26. package/dist/3474.js.LICENSE.txt +8 -0
  27. package/dist/3474.js.map +1 -0
  28. package/dist/3584.js +1 -0
  29. package/dist/4041.js +2 -0
  30. package/dist/4041.js.map +1 -0
  31. package/dist/4055.js +1 -0
  32. package/dist/4132.js +1 -0
  33. package/dist/4300.js +1 -0
  34. package/dist/4335.js +1 -0
  35. package/dist/4463.js +1 -0
  36. package/dist/4463.js.map +1 -0
  37. package/dist/4618.js +1 -0
  38. package/dist/4652.js +1 -0
  39. package/dist/4944.js +1 -0
  40. package/dist/5173.js +1 -0
  41. package/dist/5220.js +2 -0
  42. package/dist/5220.js.LICENSE.txt +29 -0
  43. package/dist/5220.js.map +1 -0
  44. package/dist/5241.js +1 -0
  45. package/dist/5442.js +1 -0
  46. package/dist/5661.js +1 -0
  47. package/dist/6022.js +1 -0
  48. package/dist/6078.js +2 -0
  49. package/dist/6078.js.LICENSE.txt +9 -0
  50. package/dist/6078.js.map +1 -0
  51. package/dist/627.js +1 -0
  52. package/dist/627.js.map +1 -0
  53. package/dist/6276.js +1 -0
  54. package/dist/6276.js.map +1 -0
  55. package/dist/6468.js +1 -0
  56. package/dist/6679.js +1 -0
  57. package/dist/6737.js +2 -0
  58. package/dist/6737.js.LICENSE.txt +9 -0
  59. package/dist/6737.js.map +1 -0
  60. package/dist/6840.js +1 -0
  61. package/dist/6859.js +1 -0
  62. package/dist/7092.js +1 -0
  63. package/dist/7092.js.map +1 -0
  64. package/dist/7097.js +1 -0
  65. package/dist/7159.js +1 -0
  66. package/dist/723.js +1 -0
  67. package/dist/7495.js +2 -0
  68. package/dist/7495.js.LICENSE.txt +9 -0
  69. package/dist/7495.js.map +1 -0
  70. package/dist/7617.js +1 -0
  71. package/dist/795.js +1 -0
  72. package/dist/8163.js +1 -0
  73. package/dist/8349.js +1 -0
  74. package/dist/8404.js +2 -0
  75. package/dist/{629.js.LICENSE.txt → 8404.js.LICENSE.txt} +9 -3
  76. package/dist/8404.js.map +1 -0
  77. package/dist/8434.js +1 -0
  78. package/dist/8434.js.map +1 -0
  79. package/dist/8618.js +1 -0
  80. package/dist/89.js +2 -0
  81. package/dist/89.js.LICENSE.txt +9 -0
  82. package/dist/89.js.map +1 -0
  83. package/dist/890.js +1 -0
  84. package/dist/9214.js +1 -0
  85. package/dist/9538.js +1 -0
  86. package/dist/9569.js +1 -0
  87. package/dist/986.js +1 -0
  88. package/dist/9876.js +1 -0
  89. package/dist/9876.js.map +1 -0
  90. package/dist/9879.js +1 -0
  91. package/dist/9895.js +1 -0
  92. package/dist/9900.js +1 -0
  93. package/dist/9913.js +1 -0
  94. package/dist/main.js +1 -1
  95. package/dist/main.js.LICENSE.txt +36 -1
  96. package/dist/main.js.map +1 -1
  97. package/dist/openmrs-esm-patient-registration-app.js +1 -0
  98. package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +1576 -0
  99. package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
  100. package/dist/routes.json +1 -1
  101. package/package.json +16 -15
  102. package/src/{add-patient-link.tsx → add-patient-link.extension.tsx} +4 -2
  103. package/src/add-patient-link.test.tsx +6 -10
  104. package/src/config-schema.ts +109 -55
  105. package/src/constants.ts +1 -1
  106. package/src/declarations.d.ts +5 -4
  107. package/src/index.ts +10 -29
  108. package/src/nav-link.test.tsx +3 -3
  109. package/src/offline.resources.ts +26 -18
  110. package/src/patient-photo.extension.tsx +3 -1
  111. package/src/patient-registration/field/address/address-field.component.tsx +58 -37
  112. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +16 -18
  113. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +3 -3
  114. package/src/patient-registration/field/address/address-hierarchy.test.tsx +290 -0
  115. package/src/patient-registration/field/address/address-search.component.tsx +7 -5
  116. package/src/patient-registration/field/address/address-search.scss +5 -5
  117. package/src/patient-registration/field/address/address-search.test.tsx +140 -0
  118. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  119. package/src/patient-registration/field/custom-field.component.tsx +3 -9
  120. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
  121. package/src/patient-registration/field/dob/dob.component.tsx +55 -50
  122. package/src/patient-registration/field/dob/dob.test.tsx +90 -0
  123. package/src/patient-registration/field/field.component.tsx +12 -6
  124. package/src/patient-registration/field/field.resource.ts +11 -4
  125. package/src/patient-registration/field/field.scss +69 -25
  126. package/src/patient-registration/field/field.test.tsx +329 -0
  127. package/src/patient-registration/field/gender/gender-field.component.tsx +14 -9
  128. package/src/patient-registration/field/gender/gender-field.test.tsx +73 -33
  129. package/src/patient-registration/field/id/id-field.component.tsx +24 -23
  130. package/src/patient-registration/field/id/id-field.test.tsx +147 -0
  131. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +12 -10
  132. package/src/patient-registration/field/id/identifier-selection.scss +12 -8
  133. package/src/patient-registration/field/name/name-field.component.tsx +10 -5
  134. package/src/patient-registration/field/obs/obs-field.component.tsx +59 -2
  135. package/src/patient-registration/field/obs/obs-field.test.tsx +133 -39
  136. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +3 -3
  137. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
  138. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  139. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  140. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +19 -22
  141. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
  142. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
  143. package/src/patient-registration/form-manager.test.ts +91 -0
  144. package/src/patient-registration/form-manager.ts +49 -23
  145. package/src/patient-registration/input/basic-input/input/input.component.tsx +6 -2
  146. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  147. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +5 -5
  148. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
  149. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +73 -36
  150. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
  151. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
  152. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +2 -11
  153. package/src/patient-registration/input/input.scss +17 -13
  154. package/src/patient-registration/patient-registration-context.ts +22 -11
  155. package/src/patient-registration/patient-registration-hooks.ts +158 -193
  156. package/src/patient-registration/patient-registration-utils.test.ts +33 -0
  157. package/src/patient-registration/patient-registration-utils.ts +11 -13
  158. package/src/patient-registration/patient-registration.component.tsx +87 -103
  159. package/src/patient-registration/{patient-registration.resource.testt.tsx → patient-registration.resource.test.tsx} +0 -4
  160. package/src/patient-registration/patient-registration.resource.ts +27 -3
  161. package/src/patient-registration/patient-registration.scss +27 -38
  162. package/src/patient-registration/patient-registration.test.tsx +579 -0
  163. package/src/patient-registration/patient-registration.types.ts +23 -25
  164. package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
  165. package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
  166. package/src/patient-registration/section/demographics/demographics-section.component.tsx +5 -5
  167. package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
  168. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +8 -7
  169. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
  170. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +28 -28
  171. package/src/patient-registration/section/patient-relationships/relationships.scss +4 -4
  172. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  173. package/src/patient-registration/section/section.component.tsx +1 -1
  174. package/src/patient-registration/section/section.scss +21 -1
  175. package/src/patient-registration/ui-components/overlay/overlay.scss +8 -8
  176. package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +71 -23
  177. package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
  178. package/src/resources-context.ts +14 -0
  179. package/src/root.component.tsx +3 -3
  180. package/src/routes.json +10 -24
  181. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  182. package/src/widgets/cancel-patient-edit.test.tsx +22 -0
  183. package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
  184. package/src/widgets/{delete-identifier-confirmation-modal.testt.tsx → delete-identifier-confirmation.test.tsx} +5 -7
  185. package/src/widgets/edit-patient-details-button.component.tsx +0 -1
  186. package/src/widgets/edit-patient-details-button.test.tsx +35 -0
  187. package/translations/am.json +43 -35
  188. package/translations/ar.json +41 -33
  189. package/translations/ar_SY.json +119 -0
  190. package/translations/bn.json +119 -0
  191. package/translations/de.json +119 -0
  192. package/translations/en.json +44 -42
  193. package/translations/en_US.json +119 -0
  194. package/translations/es.json +69 -57
  195. package/translations/es_MX.json +119 -0
  196. package/translations/fr.json +74 -58
  197. package/translations/he.json +44 -40
  198. package/translations/hi.json +119 -0
  199. package/translations/hi_IN.json +119 -0
  200. package/translations/id.json +119 -0
  201. package/translations/it.json +119 -0
  202. package/translations/ka.json +119 -0
  203. package/translations/km.json +44 -40
  204. package/translations/ku.json +119 -0
  205. package/translations/ky.json +119 -0
  206. package/translations/lg.json +119 -0
  207. package/translations/ne.json +119 -0
  208. package/translations/pl.json +119 -0
  209. package/translations/pt.json +119 -0
  210. package/translations/pt_BR.json +119 -0
  211. package/translations/qu.json +119 -0
  212. package/translations/ro_RO.json +119 -0
  213. package/translations/ru_RU.json +119 -0
  214. package/translations/si.json +119 -0
  215. package/translations/sw.json +119 -0
  216. package/translations/sw_KE.json +119 -0
  217. package/translations/tr.json +119 -0
  218. package/translations/tr_TR.json +119 -0
  219. package/translations/uk.json +119 -0
  220. package/translations/uz.json +119 -0
  221. package/translations/uz@Latn.json +119 -0
  222. package/translations/uz_UZ.json +119 -0
  223. package/translations/vi.json +119 -0
  224. package/translations/zh.json +45 -23
  225. package/translations/zh_CN.json +39 -17
  226. package/.turbo/turbo-build.log +0 -40
  227. package/dist/132.js +0 -1
  228. package/dist/197.js +0 -1
  229. package/dist/236.js +0 -1
  230. package/dist/236.js.map +0 -1
  231. package/dist/300.js +0 -1
  232. package/dist/335.js +0 -1
  233. package/dist/372.js +0 -1
  234. package/dist/372.js.map +0 -1
  235. package/dist/41.js +0 -2
  236. package/dist/41.js.map +0 -1
  237. package/dist/449.js +0 -1
  238. package/dist/449.js.map +0 -1
  239. package/dist/464.js +0 -1
  240. package/dist/464.js.map +0 -1
  241. package/dist/495.js +0 -1
  242. package/dist/495.js.map +0 -1
  243. package/dist/55.js +0 -1
  244. package/dist/56.js +0 -1
  245. package/dist/56.js.map +0 -1
  246. package/dist/621.js +0 -1
  247. package/dist/621.js.map +0 -1
  248. package/dist/629.js +0 -2
  249. package/dist/629.js.map +0 -1
  250. package/dist/652.js +0 -1
  251. package/dist/661.js +0 -1
  252. package/dist/757.js +0 -1
  253. package/dist/757.js.map +0 -1
  254. package/dist/828.js +0 -1
  255. package/dist/828.js.map +0 -1
  256. package/dist/830.js +0 -1
  257. package/dist/830.js.map +0 -1
  258. package/dist/831.js +0 -2
  259. package/dist/831.js.LICENSE.txt +0 -3
  260. package/dist/831.js.map +0 -1
  261. package/dist/876.js +0 -2
  262. package/dist/876.js.map +0 -1
  263. package/dist/879.js +0 -1
  264. package/dist/913.js +0 -2
  265. package/dist/913.js.map +0 -1
  266. package/dist/927.js +0 -1
  267. package/dist/927.js.map +0 -1
  268. package/dist/99.js +0 -1
  269. package/dist/ampath-esm-patient-registration-app.js +0 -1
  270. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +0 -694
  271. package/dist/ampath-esm-patient-registration-app.js.map +0 -1
  272. package/src/patient-registration/date-util.ts +0 -52
  273. package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +0 -56
  274. package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
  275. package/src/patient-verification/assets/counties.json +0 -236
  276. package/src/patient-verification/assets/verification-assets.ts +0 -11
  277. package/src/patient-verification/patient-verification-hook.tsx +0 -176
  278. package/src/patient-verification/patient-verification-utils.ts +0 -179
  279. package/src/patient-verification/patient-verification.component.tsx +0 -124
  280. package/src/patient-verification/patient-verification.scss +0 -25
  281. package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +0 -72
  282. package/src/patient-verification/verification-modal/empty-prompt.component.tsx +0 -35
  283. package/src/patient-verification/verification-types.ts +0 -50
  284. package/src/widgets/cancel-patient-edit.component.tsx +0 -37
  285. package/src/widgets/delete-identifier-confirmation-modal.tsx +0 -41
  286. package/src/widgets/delete-identifier-modal.scss +0 -34
  287. /package/dist/{41.js.LICENSE.txt → 4041.js.LICENSE.txt} +0 -0
  288. /package/src/patient-registration/input/custom-input/identifier/{utils.testt.ts → utils.test.ts} +0 -0
@@ -1,17 +1,21 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
- import { useConfig } from '@openmrs/esm-framework';
5
- import { type FieldDefinition } from '../../../config-schema';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
5
+ import { esmPatientRegistrationSchema, type FieldDefinition, type RegistrationConfig } from '../../../config-schema';
6
6
  import { useConcept, useConceptAnswers } from '../field.resource';
7
7
  import { ObsField } from './obs-field.component';
8
+ import {
9
+ PatientRegistrationContextProvider,
10
+ type PatientRegistrationContextProps,
11
+ } from '../../patient-registration-context';
12
+ import { mockOpenmrsId, mockPatient } from '__mocks__';
8
13
 
9
- const mockUseConfig = useConfig as jest.Mock;
10
-
11
- jest.mock('../field.resource'); // Mock the useConceptAnswers hook
14
+ const mockUseConcept = jest.mocked(useConcept);
15
+ const mockUseConceptAnswers = jest.mocked(useConceptAnswers);
16
+ const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
12
17
 
13
- const mockedUseConcept = useConcept as jest.Mock;
14
- const mockedUseConceptAnswers = useConceptAnswers as jest.Mock;
18
+ jest.mock('../field.resource');
15
19
 
16
20
  const useConceptMockImpl = (uuid: string) => {
17
21
  let data;
@@ -42,6 +46,14 @@ const useConceptMockImpl = (uuid: string) => {
42
46
  ],
43
47
  setMembers: [],
44
48
  };
49
+ } else if (uuid == 'vaccination-date-uuid') {
50
+ data = {
51
+ uuid: 'vaccination-date-uuid',
52
+ display: 'Vaccination Date',
53
+ datatype: { display: 'Date', uuid: 'date' },
54
+ answers: [],
55
+ setMembers: [],
56
+ };
45
57
  } else {
46
58
  throw Error(`Programming error, you probably didn't mean to do this: unknown concept uuid '${uuid}'`);
47
59
  }
@@ -59,6 +71,7 @@ const useConceptAnswersMockImpl = (uuid: string) => {
59
71
  { display: 'Mexico', uuid: 'mex' },
60
72
  ],
61
73
  isLoading: false,
74
+ error: null,
62
75
  };
63
76
  } else if (uuid == 'other-countries-uuid') {
64
77
  return {
@@ -67,11 +80,13 @@ const useConceptAnswersMockImpl = (uuid: string) => {
67
80
  { display: 'Uganda', uuid: 'ug' },
68
81
  ],
69
82
  isLoading: false,
83
+ error: null,
70
84
  };
71
85
  } else if (uuid == '') {
72
86
  return {
73
87
  data: [],
74
88
  isLoading: false,
89
+ error: null,
75
90
  };
76
91
  } else {
77
92
  throw Error(`Programming error, you probably didn't mean to do this: unknown concept answer set uuid '${uuid}'`);
@@ -79,19 +94,21 @@ const useConceptAnswersMockImpl = (uuid: string) => {
79
94
  };
80
95
 
81
96
  type FieldProps = {
82
- children: ({ field, form: { touched, errors } }) => React.ReactNode;
97
+ children: ({ field, form: { touched, errors }, meta }) => React.ReactNode;
83
98
  };
84
99
 
85
100
  jest.mock('formik', () => ({
86
101
  ...(jest.requireActual('formik') as object),
87
- Field: jest.fn(({ children }: FieldProps) => <>{children({ field: {}, form: { touched: {}, errors: {} } })}</>),
102
+ Field: jest.fn(({ children }: FieldProps) => (
103
+ <>{children({ field: {}, form: { touched: {}, errors: {} }, meta: { error: undefined } })}</>
104
+ )),
88
105
  useField: jest.fn(() => [{ value: null }, {}]),
89
106
  }));
90
107
 
91
108
  const textFieldDef: FieldDefinition = {
92
109
  id: 'chief-complaint',
93
110
  type: 'obs',
94
- label: '',
111
+ label: 'Chief complaint',
95
112
  placeholder: '',
96
113
  showHeading: false,
97
114
  uuid: 'chief-complaint-uuid',
@@ -106,7 +123,7 @@ const textFieldDef: FieldDefinition = {
106
123
  const numberFieldDef: FieldDefinition = {
107
124
  id: 'weight',
108
125
  type: 'obs',
109
- label: '',
126
+ label: 'Weight',
110
127
  placeholder: '',
111
128
  showHeading: false,
112
129
  uuid: 'weight-uuid',
@@ -118,10 +135,25 @@ const numberFieldDef: FieldDefinition = {
118
135
  customConceptAnswers: [],
119
136
  };
120
137
 
138
+ const dateFieldDef: FieldDefinition = {
139
+ id: 'vac_date',
140
+ type: 'obs',
141
+ label: 'Vaccination date',
142
+ placeholder: '',
143
+ showHeading: false,
144
+ uuid: 'vaccination-date-uuid',
145
+ validation: {
146
+ required: false,
147
+ matches: null,
148
+ },
149
+ answerConceptSetUuid: null,
150
+ customConceptAnswers: [],
151
+ };
152
+
121
153
  const codedFieldDef: FieldDefinition = {
122
154
  id: 'nationality',
123
155
  type: 'obs',
124
- label: '',
156
+ label: 'Nationality',
125
157
  placeholder: '',
126
158
  showHeading: false,
127
159
  uuid: 'nationality-uuid',
@@ -133,57 +165,119 @@ const codedFieldDef: FieldDefinition = {
133
165
  customConceptAnswers: [],
134
166
  };
135
167
 
168
+ const mockInitialFormValues = {
169
+ additionalFamilyName: '',
170
+ additionalGivenName: '',
171
+ additionalMiddleName: '',
172
+ addNameInLocalLanguage: false,
173
+ address: {},
174
+ birthdate: null,
175
+ birthdateEstimated: false,
176
+ deathCause: '',
177
+ deathDate: '',
178
+ familyName: 'Doe',
179
+ gender: 'male',
180
+ givenName: 'John',
181
+ identifiers: mockOpenmrsId,
182
+ isDead: false,
183
+ middleName: 'Test',
184
+ monthsEstimated: 0,
185
+ patientUuid: mockPatient.uuid,
186
+ relationships: [],
187
+ telephoneNumber: '',
188
+ yearsEstimated: 0,
189
+ deathTime: '',
190
+ deathTimeFormat: 'AM' as const,
191
+ nonCodedCauseOfDeath: '',
192
+ };
193
+
194
+ const initialContextValues: PatientRegistrationContextProps = {
195
+ currentPhoto: null,
196
+ inEditMode: false,
197
+ identifierTypes: [],
198
+ initialFormValues: mockInitialFormValues,
199
+ isOffline: false,
200
+ setCapturePhotoProps: jest.fn(),
201
+ setFieldValue: jest.fn(),
202
+ setInitialFormValues: jest.fn(),
203
+ validationSchema: null,
204
+ values: mockInitialFormValues,
205
+ setFieldTouched: jest.fn(),
206
+ };
207
+
136
208
  describe('ObsField', () => {
137
209
  beforeEach(() => {
138
- mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' } });
139
- mockedUseConcept.mockImplementation(useConceptMockImpl);
140
- mockedUseConceptAnswers.mockImplementation(useConceptAnswersMockImpl);
210
+ mockUseConfig.mockReturnValue({
211
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
212
+ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' },
213
+ } as RegistrationConfig);
214
+ mockUseConcept.mockImplementation(useConceptMockImpl);
215
+ mockUseConceptAnswers.mockImplementation(useConceptAnswersMockImpl);
141
216
  });
142
217
 
143
218
  it("logs an error and doesn't render if no registration encounter type is provided", () => {
144
- mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: null } });
219
+ mockUseConfig.mockReturnValue({
220
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
221
+ registrationObs: { encounterTypeUuid: null },
222
+ } as RegistrationConfig);
223
+
145
224
  console.error = jest.fn();
146
225
  render(<ObsField fieldDefinition={textFieldDef} />);
147
226
  expect(console.error).toHaveBeenCalledWith(
148
227
  expect.stringMatching(/no registration encounter type has been configured/i),
149
228
  );
150
- expect(screen.queryByRole('textbox')).toBeNull();
229
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
151
230
  });
152
231
 
153
232
  it('renders a text box for text concept', () => {
154
233
  render(<ObsField fieldDefinition={textFieldDef} />);
155
- // I don't know why the labels aren't in the DOM, but they aren't
156
- // expect(screen.getByLabelText("Chief Complaint")).toBeInTheDocument();
157
- expect(screen.getByRole('textbox')).toBeInTheDocument();
234
+
235
+ expect(screen.getByRole('textbox', { name: 'Chief complaint (optional)' })).toBeInTheDocument();
158
236
  });
159
237
 
160
238
  it('renders a number box for number concept', () => {
161
239
  render(<ObsField fieldDefinition={numberFieldDef} />);
162
- // expect(screen.getByLabelText("Weight (kg)")).toBeInTheDocument();
163
- expect(screen.getByRole('spinbutton')).toBeInTheDocument();
240
+
241
+ expect(screen.getByRole('spinbutton', { name: 'Weight (optional)' })).toBeInTheDocument();
242
+ });
243
+
244
+ it('renders a datepicker for date concept', async () => {
245
+ const user = userEvent.setup();
246
+ render(
247
+ <PatientRegistrationContextProvider value={initialContextValues}>
248
+ <ObsField fieldDefinition={dateFieldDef} />
249
+ </PatientRegistrationContextProvider>,
250
+ );
251
+
252
+ expect(screen.getByText(/vaccination date/i)).toBeInTheDocument();
253
+
254
+ const dateInput = screen.getByLabelText(/vaccination date/i);
255
+ expect(dateInput).toBeInTheDocument();
256
+ await user.clear(dateInput);
257
+ await user.type(dateInput, '28/05/2024');
258
+ // FIXME: Make the date input work
259
+ // expect(dateInput).toHaveValue('28/05/2024');
164
260
  });
165
261
 
166
262
  it('renders a select for a coded concept', () => {
167
263
  render(<ObsField fieldDefinition={codedFieldDef} />);
168
- // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
169
- const select = screen.getByRole('combobox');
170
- expect(select).toBeInTheDocument();
171
- expect(select).toHaveDisplayValue('Select an option');
264
+
265
+ expect(screen.getByRole('combobox', { name: 'Nationality' })).toBeInTheDocument();
266
+ expect(screen.getByRole('option', { name: 'USA' })).toBeInTheDocument();
267
+ expect(screen.getByRole('option', { name: 'Mexico' })).toBeInTheDocument();
172
268
  });
173
269
 
174
270
  it('select uses answerConcept for answers when it is provided', async () => {
175
- const user = userEvent.setup();
176
-
177
271
  render(<ObsField fieldDefinition={{ ...codedFieldDef, answerConceptSetUuid: 'other-countries-uuid' }} />);
178
- // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
179
- const select = screen.getByRole('combobox');
180
- expect(select).toBeInTheDocument();
181
- await user.selectOptions(select, 'Kenya');
272
+
273
+ expect(screen.getByRole('combobox', { name: 'Nationality' })).toBeInTheDocument();
274
+ expect(screen.getByRole('option', { name: 'Kenya' })).toBeInTheDocument();
275
+ expect(screen.getByRole('option', { name: 'Uganda' })).toBeInTheDocument();
276
+ expect(screen.queryByRole('option', { name: 'USA' })).not.toBeInTheDocument();
277
+ expect(screen.queryByRole('option', { name: 'Mexico' })).not.toBeInTheDocument();
182
278
  });
183
279
 
184
280
  it('select uses customConceptAnswers for answers when provided', async () => {
185
- const user = userEvent.setup();
186
-
187
281
  render(
188
282
  <ObsField
189
283
  fieldDefinition={{
@@ -197,9 +291,9 @@ describe('ObsField', () => {
197
291
  }}
198
292
  />,
199
293
  );
200
- // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
201
- const select = screen.getByRole('combobox');
202
- expect(select).toBeInTheDocument();
203
- await user.selectOptions(select, 'Mozambique');
294
+
295
+ expect(screen.getByRole('combobox', { name: 'Nationality' })).toBeInTheDocument();
296
+ expect(screen.getByRole('option', { name: 'Mozambique' })).toBeInTheDocument();
297
+ expect(screen.queryByRole('option', { name: 'Uganda' })).not.toBeInTheDocument();
204
298
  });
205
299
  });
@@ -3,10 +3,10 @@ import classNames from 'classnames';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Field } from 'formik';
5
5
  import { Layer, Select, SelectItem } from '@carbon/react';
6
+ import { reportError } from '@openmrs/esm-framework';
6
7
  import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
7
8
  import { useConceptAnswers } from '../field.resource';
8
9
  import styles from './../field.scss';
9
- import { reportError } from '@openmrs/esm-framework';
10
10
 
11
11
  export interface CodedPersonAttributeFieldProps {
12
12
  id: string;
@@ -44,7 +44,7 @@ export function CodedPersonAttributeField({
44
44
  );
45
45
  setError(true);
46
46
  }
47
- }, [answerConceptSetUuid, customConceptAnswers]);
47
+ }, [answerConceptSetUuid, customConceptAnswers, id, t]);
48
48
 
49
49
  useEffect(() => {
50
50
  if (!isLoadingConceptAnswers && !customConceptAnswers.length) {
@@ -72,7 +72,7 @@ export function CodedPersonAttributeField({
72
72
  setError(true);
73
73
  }
74
74
  }
75
- }, [isLoadingConceptAnswers, conceptAnswers, customConceptAnswers]);
75
+ }, [isLoadingConceptAnswers, conceptAnswers, customConceptAnswers, t, id, answerConceptSetUuid]);
76
76
 
77
77
  const answers = useMemo(() => {
78
78
  if (customConceptAnswers.length) {
@@ -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
+ }
@@ -1,12 +1,12 @@
1
1
  import React, { useMemo } from 'react';
2
- import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
2
+ import { InlineNotification, TextInputSkeleton } from '@carbon/react';
3
3
  import { type FieldDefinition } from '../../../config-schema';
4
4
  import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
5
  import { usePersonAttributeType } from './person-attributes.resource';
6
6
  import { TextPersonAttributeField } from './text-person-attribute-field.component';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import styles from '../field.scss';
9
- import CustomPersonAttributeField from './custom-person-attribute-field.component';
9
+ import { LocationPersonAttributeField } from './location-person-attribute-field.component';
10
10
 
11
11
  export interface PersonAttributeFieldProps {
12
12
  fieldDefinition: FieldDefinition;
@@ -23,26 +23,13 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
23
23
  switch (personAttributeType.format) {
24
24
  case 'java.lang.String':
25
25
  return (
26
- <>
27
- {fieldDefinition.renderType === 'select' ? (
28
- <CustomPersonAttributeField
29
- personAttributeType={personAttributeType}
30
- answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
31
- label={fieldDefinition.label}
32
- id={fieldDefinition?.id}
33
- customConceptAnswers={fieldDefinition.customConceptAnswers ?? []}
34
- required={fieldDefinition.validation?.required ?? false}
35
- />
36
- ) : (
37
- <TextPersonAttributeField
38
- personAttributeType={personAttributeType}
39
- validationRegex={fieldDefinition.validation?.matches ?? ''}
40
- label={fieldDefinition.label}
41
- required={fieldDefinition.validation?.required ?? false}
42
- id={fieldDefinition?.id}
43
- />
44
- )}
45
- </>
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
+ />
46
33
  );
47
34
  case 'org.openmrs.Concept':
48
35
  return (
@@ -55,6 +42,16 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
55
42
  required={fieldDefinition.validation?.required ?? false}
56
43
  />
57
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
+ );
58
55
  default:
59
56
  return (
60
57
  <InlineNotification kind="error" title="Error">