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

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 (285) hide show
  1. package/dist/1119.js +1 -0
  2. package/dist/1197.js +1 -0
  3. package/dist/1909.js +2 -0
  4. package/dist/{629.js.LICENSE.txt → 1909.js.LICENSE.txt} +9 -3
  5. package/dist/1909.js.map +1 -0
  6. package/dist/2146.js +1 -0
  7. package/dist/2372.js +1 -0
  8. package/dist/2372.js.map +1 -0
  9. package/dist/2470.js +1 -0
  10. package/dist/2470.js.map +1 -0
  11. package/dist/2690.js +1 -0
  12. package/dist/2913.js +2 -0
  13. package/dist/{913.js.LICENSE.txt → 2913.js.LICENSE.txt} +3 -23
  14. package/dist/2913.js.map +1 -0
  15. package/dist/3093.js +1 -0
  16. package/dist/3093.js.map +1 -0
  17. package/dist/3099.js +1 -0
  18. package/dist/3144.js +2 -0
  19. package/dist/3144.js.LICENSE.txt +19 -0
  20. package/dist/3144.js.map +1 -0
  21. package/dist/320.js +2 -0
  22. package/dist/{876.js.LICENSE.txt → 320.js.LICENSE.txt} +2 -3
  23. package/dist/320.js.map +1 -0
  24. package/dist/3464.js +1 -0
  25. package/dist/3464.js.map +1 -0
  26. package/dist/3474.js +2 -0
  27. package/dist/3474.js.LICENSE.txt +8 -0
  28. package/dist/3474.js.map +1 -0
  29. package/dist/3584.js +1 -0
  30. package/dist/4041.js +2 -0
  31. package/dist/4041.js.map +1 -0
  32. package/dist/4055.js +1 -0
  33. package/dist/4132.js +1 -0
  34. package/dist/4300.js +1 -0
  35. package/dist/4335.js +1 -0
  36. package/dist/4463.js +1 -0
  37. package/dist/4463.js.map +1 -0
  38. package/dist/4618.js +1 -0
  39. package/dist/4652.js +1 -0
  40. package/dist/4944.js +1 -0
  41. package/dist/5173.js +1 -0
  42. package/dist/5239.js +2 -0
  43. package/dist/5239.js.LICENSE.txt +29 -0
  44. package/dist/5239.js.map +1 -0
  45. package/dist/5241.js +1 -0
  46. package/dist/5442.js +1 -0
  47. package/dist/5661.js +1 -0
  48. package/dist/6022.js +1 -0
  49. package/dist/627.js +1 -0
  50. package/dist/627.js.map +1 -0
  51. package/dist/6276.js +1 -0
  52. package/dist/6276.js.map +1 -0
  53. package/dist/6336.js +1 -0
  54. package/dist/6336.js.map +1 -0
  55. package/dist/6468.js +1 -0
  56. package/dist/6679.js +1 -0
  57. package/dist/6840.js +1 -0
  58. package/dist/6859.js +1 -0
  59. package/dist/7071.js +1 -0
  60. package/dist/7071.js.map +1 -0
  61. package/dist/7097.js +1 -0
  62. package/dist/7159.js +1 -0
  63. package/dist/723.js +1 -0
  64. package/dist/729.js +2 -0
  65. package/dist/729.js.LICENSE.txt +9 -0
  66. package/dist/729.js.map +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/8434.js +1 -0
  75. package/dist/8434.js.map +1 -0
  76. package/dist/8618.js +1 -0
  77. package/dist/89.js +2 -0
  78. package/dist/89.js.LICENSE.txt +9 -0
  79. package/dist/89.js.map +1 -0
  80. package/dist/890.js +1 -0
  81. package/dist/9214.js +1 -0
  82. package/dist/9538.js +1 -0
  83. package/dist/9569.js +1 -0
  84. package/dist/986.js +1 -0
  85. package/dist/9876.js +1 -0
  86. package/dist/9876.js.map +1 -0
  87. package/dist/9879.js +1 -0
  88. package/dist/9895.js +1 -0
  89. package/dist/9900.js +1 -0
  90. package/dist/9913.js +1 -0
  91. package/dist/main.js +1 -1
  92. package/dist/main.js.LICENSE.txt +36 -1
  93. package/dist/main.js.map +1 -1
  94. package/dist/openmrs-esm-patient-registration-app.js +1 -0
  95. package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +1549 -0
  96. package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
  97. package/dist/routes.json +1 -1
  98. package/package.json +15 -13
  99. package/src/{add-patient-link.tsx → add-patient-link.extension.tsx} +4 -2
  100. package/src/add-patient-link.test.tsx +6 -10
  101. package/src/config-schema.ts +109 -55
  102. package/src/constants.ts +1 -1
  103. package/src/declarations.d.ts +5 -4
  104. package/src/index.ts +10 -29
  105. package/src/nav-link.test.tsx +3 -3
  106. package/src/offline.resources.ts +26 -18
  107. package/src/patient-photo.extension.tsx +3 -1
  108. package/src/patient-registration/field/address/address-field.component.tsx +58 -37
  109. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +16 -18
  110. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +3 -3
  111. package/src/patient-registration/field/address/address-hierarchy.test.tsx +290 -0
  112. package/src/patient-registration/field/address/address-search.component.tsx +7 -5
  113. package/src/patient-registration/field/address/address-search.scss +5 -5
  114. package/src/patient-registration/field/address/address-search.test.tsx +140 -0
  115. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  116. package/src/patient-registration/field/custom-field.component.tsx +3 -9
  117. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
  118. package/src/patient-registration/field/dob/dob.component.tsx +55 -50
  119. package/src/patient-registration/field/dob/dob.test.tsx +90 -0
  120. package/src/patient-registration/field/field.component.tsx +12 -6
  121. package/src/patient-registration/field/field.resource.ts +11 -4
  122. package/src/patient-registration/field/field.scss +69 -25
  123. package/src/patient-registration/field/field.test.tsx +329 -0
  124. package/src/patient-registration/field/gender/gender-field.component.tsx +14 -9
  125. package/src/patient-registration/field/gender/gender-field.test.tsx +73 -33
  126. package/src/patient-registration/field/id/id-field.component.tsx +24 -23
  127. package/src/patient-registration/field/id/id-field.test.tsx +147 -0
  128. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +12 -10
  129. package/src/patient-registration/field/id/identifier-selection.scss +12 -8
  130. package/src/patient-registration/field/name/name-field.component.tsx +10 -5
  131. package/src/patient-registration/field/obs/obs-field.component.tsx +59 -2
  132. package/src/patient-registration/field/obs/obs-field.test.tsx +133 -39
  133. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +3 -3
  134. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
  135. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  136. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  137. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +19 -22
  138. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
  139. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
  140. package/src/patient-registration/form-manager.test.ts +91 -0
  141. package/src/patient-registration/form-manager.ts +49 -23
  142. package/src/patient-registration/input/basic-input/input/input.component.tsx +6 -2
  143. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  144. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +5 -5
  145. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
  146. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +73 -36
  147. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
  148. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
  149. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +2 -11
  150. package/src/patient-registration/input/input.scss +17 -13
  151. package/src/patient-registration/patient-registration-context.ts +22 -11
  152. package/src/patient-registration/patient-registration-hooks.ts +158 -193
  153. package/src/patient-registration/patient-registration-utils.test.ts +33 -0
  154. package/src/patient-registration/patient-registration-utils.ts +11 -13
  155. package/src/patient-registration/patient-registration.component.tsx +87 -103
  156. package/src/patient-registration/{patient-registration.resource.testt.tsx → patient-registration.resource.test.tsx} +0 -4
  157. package/src/patient-registration/patient-registration.resource.ts +27 -3
  158. package/src/patient-registration/patient-registration.scss +27 -38
  159. package/src/patient-registration/patient-registration.test.tsx +579 -0
  160. package/src/patient-registration/patient-registration.types.ts +23 -25
  161. package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
  162. package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
  163. package/src/patient-registration/section/demographics/demographics-section.component.tsx +5 -5
  164. package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
  165. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +8 -7
  166. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
  167. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +28 -28
  168. package/src/patient-registration/section/patient-relationships/relationships.scss +4 -4
  169. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  170. package/src/patient-registration/section/section.component.tsx +1 -1
  171. package/src/patient-registration/section/section.scss +21 -1
  172. package/src/patient-registration/ui-components/overlay/overlay.scss +8 -8
  173. package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +71 -23
  174. package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
  175. package/src/resources-context.ts +14 -0
  176. package/src/root.component.tsx +3 -3
  177. package/src/routes.json +10 -24
  178. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  179. package/src/widgets/cancel-patient-edit.test.tsx +22 -0
  180. package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
  181. package/src/widgets/{delete-identifier-confirmation-modal.testt.tsx → delete-identifier-confirmation.test.tsx} +5 -7
  182. package/src/widgets/edit-patient-details-button.component.tsx +0 -1
  183. package/src/widgets/edit-patient-details-button.test.tsx +35 -0
  184. package/translations/am.json +43 -35
  185. package/translations/ar.json +41 -33
  186. package/translations/ar_SY.json +119 -0
  187. package/translations/bn.json +119 -0
  188. package/translations/de.json +119 -0
  189. package/translations/en.json +44 -42
  190. package/translations/en_US.json +119 -0
  191. package/translations/es.json +69 -57
  192. package/translations/es_MX.json +119 -0
  193. package/translations/fr.json +74 -58
  194. package/translations/he.json +44 -40
  195. package/translations/hi.json +119 -0
  196. package/translations/hi_IN.json +119 -0
  197. package/translations/id.json +119 -0
  198. package/translations/it.json +119 -0
  199. package/translations/ka.json +119 -0
  200. package/translations/km.json +44 -40
  201. package/translations/ku.json +119 -0
  202. package/translations/ky.json +119 -0
  203. package/translations/lg.json +119 -0
  204. package/translations/ne.json +119 -0
  205. package/translations/pl.json +119 -0
  206. package/translations/pt.json +119 -0
  207. package/translations/pt_BR.json +119 -0
  208. package/translations/qu.json +119 -0
  209. package/translations/ro_RO.json +119 -0
  210. package/translations/ru_RU.json +119 -0
  211. package/translations/si.json +119 -0
  212. package/translations/sw.json +119 -0
  213. package/translations/sw_KE.json +119 -0
  214. package/translations/tr.json +119 -0
  215. package/translations/tr_TR.json +119 -0
  216. package/translations/uk.json +119 -0
  217. package/translations/uz.json +119 -0
  218. package/translations/uz@Latn.json +119 -0
  219. package/translations/uz_UZ.json +119 -0
  220. package/translations/vi.json +119 -0
  221. package/translations/zh.json +45 -23
  222. package/translations/zh_CN.json +39 -17
  223. package/.turbo/turbo-build.log +0 -40
  224. package/dist/132.js +0 -1
  225. package/dist/197.js +0 -1
  226. package/dist/236.js +0 -1
  227. package/dist/236.js.map +0 -1
  228. package/dist/300.js +0 -1
  229. package/dist/335.js +0 -1
  230. package/dist/372.js +0 -1
  231. package/dist/372.js.map +0 -1
  232. package/dist/41.js +0 -2
  233. package/dist/41.js.map +0 -1
  234. package/dist/449.js +0 -1
  235. package/dist/449.js.map +0 -1
  236. package/dist/464.js +0 -1
  237. package/dist/464.js.map +0 -1
  238. package/dist/495.js +0 -1
  239. package/dist/495.js.map +0 -1
  240. package/dist/55.js +0 -1
  241. package/dist/56.js +0 -1
  242. package/dist/56.js.map +0 -1
  243. package/dist/621.js +0 -1
  244. package/dist/621.js.map +0 -1
  245. package/dist/629.js +0 -2
  246. package/dist/629.js.map +0 -1
  247. package/dist/652.js +0 -1
  248. package/dist/661.js +0 -1
  249. package/dist/757.js +0 -1
  250. package/dist/757.js.map +0 -1
  251. package/dist/828.js +0 -1
  252. package/dist/828.js.map +0 -1
  253. package/dist/830.js +0 -1
  254. package/dist/830.js.map +0 -1
  255. package/dist/831.js +0 -2
  256. package/dist/831.js.LICENSE.txt +0 -3
  257. package/dist/831.js.map +0 -1
  258. package/dist/876.js +0 -2
  259. package/dist/876.js.map +0 -1
  260. package/dist/879.js +0 -1
  261. package/dist/913.js +0 -2
  262. package/dist/913.js.map +0 -1
  263. package/dist/927.js +0 -1
  264. package/dist/927.js.map +0 -1
  265. package/dist/99.js +0 -1
  266. package/dist/ampath-esm-patient-registration-app.js +0 -1
  267. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +0 -694
  268. package/dist/ampath-esm-patient-registration-app.js.map +0 -1
  269. package/src/patient-registration/date-util.ts +0 -52
  270. package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +0 -56
  271. package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
  272. package/src/patient-verification/assets/counties.json +0 -236
  273. package/src/patient-verification/assets/verification-assets.ts +0 -11
  274. package/src/patient-verification/patient-verification-hook.tsx +0 -176
  275. package/src/patient-verification/patient-verification-utils.ts +0 -179
  276. package/src/patient-verification/patient-verification.component.tsx +0 -124
  277. package/src/patient-verification/patient-verification.scss +0 -25
  278. package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +0 -72
  279. package/src/patient-verification/verification-modal/empty-prompt.component.tsx +0 -35
  280. package/src/patient-verification/verification-types.ts +0 -50
  281. package/src/widgets/cancel-patient-edit.component.tsx +0 -37
  282. package/src/widgets/delete-identifier-confirmation-modal.tsx +0 -41
  283. package/src/widgets/delete-identifier-modal.scss +0 -34
  284. /package/dist/{41.js.LICENSE.txt → 4041.js.LICENSE.txt} +0 -0
  285. /package/src/patient-registration/input/custom-input/identifier/{utils.testt.ts → utils.test.ts} +0 -0
@@ -0,0 +1,164 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { renderWithRouter } from 'tools';
5
+ import { Autosuggest } from './autosuggest.component';
6
+
7
+ const mockPersons = [
8
+ {
9
+ uuid: 'randomuuid1',
10
+ display: 'John Doe',
11
+ },
12
+ {
13
+ uuid: 'randomuuid2',
14
+ display: 'John Smith',
15
+ },
16
+ {
17
+ uuid: 'randomuuid3',
18
+ display: 'James Smith',
19
+ },
20
+ {
21
+ uuid: 'randomuuid4',
22
+ display: 'Spider Man',
23
+ },
24
+ ];
25
+
26
+ const mockGetSearchResults = async (query: string) => {
27
+ return mockPersons.filter((person) => {
28
+ return person.display.toUpperCase().includes(query.toUpperCase());
29
+ });
30
+ };
31
+
32
+ const mockHandleSuggestionSelected = jest.fn((field, value) => [field, value]);
33
+
34
+ describe('Autosuggest', () => {
35
+ it('renders a search box', () => {
36
+ renderWithRouter(
37
+ <Autosuggest
38
+ getSearchResults={mockGetSearchResults}
39
+ getDisplayValue={(item) => item.display}
40
+ getFieldValue={(item) => item.uuid}
41
+ id="person"
42
+ labelText=""
43
+ onSuggestionSelected={mockHandleSuggestionSelected}
44
+ placeholder="Find Person"
45
+ />,
46
+ );
47
+
48
+ expect(screen.getByRole('searchbox')).toBeInTheDocument();
49
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
50
+ });
51
+
52
+ it('renders matching search results in a list when the user types a query', async () => {
53
+ const user = userEvent.setup();
54
+
55
+ renderWithRouter(
56
+ <Autosuggest
57
+ getSearchResults={mockGetSearchResults}
58
+ getDisplayValue={(item) => item.display}
59
+ getFieldValue={(item) => item.uuid}
60
+ id="person"
61
+ labelText=""
62
+ onSuggestionSelected={mockHandleSuggestionSelected}
63
+ placeholder="Find Person"
64
+ />,
65
+ );
66
+
67
+ const searchbox = screen.getByRole('searchbox');
68
+ await user.type(searchbox, 'john');
69
+
70
+ const list = screen.getByRole('list');
71
+
72
+ expect(list).toBeInTheDocument();
73
+ expect(screen.getAllByRole('listitem').length).toEqual(2);
74
+ expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('John Doe');
75
+ expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('John Smith');
76
+ });
77
+
78
+ it('clears the list of suggestions when a suggestion is selected', async () => {
79
+ const user = userEvent.setup();
80
+
81
+ renderWithRouter(
82
+ <Autosuggest
83
+ getSearchResults={mockGetSearchResults}
84
+ getDisplayValue={(item) => item.display}
85
+ getFieldValue={(item) => item.uuid}
86
+ id="person"
87
+ labelText=""
88
+ onSuggestionSelected={mockHandleSuggestionSelected}
89
+ placeholder="Find Person"
90
+ />,
91
+ );
92
+
93
+ let list = screen.queryByRole('list');
94
+ expect(list).not.toBeInTheDocument();
95
+
96
+ const searchbox = screen.getByRole('searchbox');
97
+ await user.type(searchbox, 'john');
98
+
99
+ list = screen.getByRole('list');
100
+ expect(list).toBeInTheDocument();
101
+
102
+ const listitems = screen.getAllByRole('listitem');
103
+ await user.click(listitems[0]);
104
+
105
+ expect(mockHandleSuggestionSelected).toHaveBeenLastCalledWith('person', 'randomuuid1');
106
+
107
+ list = screen.queryByRole('list');
108
+ expect(list).not.toBeInTheDocument();
109
+ });
110
+
111
+ it('changes suggestions when a search input is changed', async () => {
112
+ const user = userEvent.setup();
113
+
114
+ renderWithRouter(
115
+ <Autosuggest
116
+ getSearchResults={mockGetSearchResults}
117
+ getDisplayValue={(item) => item.display}
118
+ getFieldValue={(item) => item.uuid}
119
+ id="person"
120
+ labelText=""
121
+ onSuggestionSelected={mockHandleSuggestionSelected}
122
+ placeholder="Find Person"
123
+ />,
124
+ );
125
+
126
+ let list = screen.queryByRole('list');
127
+ expect(list).not.toBeInTheDocument();
128
+
129
+ const searchbox = screen.getByRole('searchbox');
130
+ await user.type(searchbox, 'john');
131
+
132
+ const suggestion = await screen.findByText('John Doe');
133
+ expect(suggestion).toBeInTheDocument();
134
+
135
+ await user.clear(searchbox);
136
+
137
+ list = screen.queryByRole('list');
138
+ expect(list).not.toBeInTheDocument();
139
+ });
140
+
141
+ it('hides the list of suggestions when the user clicks outside of the component', async () => {
142
+ const user = userEvent.setup();
143
+
144
+ renderWithRouter(
145
+ <Autosuggest
146
+ getSearchResults={mockGetSearchResults}
147
+ getDisplayValue={(item) => item.display}
148
+ getFieldValue={(item) => item.uuid}
149
+ id="person"
150
+ labelText=""
151
+ onSuggestionSelected={mockHandleSuggestionSelected}
152
+ placeholder="Find Person"
153
+ />,
154
+ );
155
+
156
+ const input = screen.getByRole('searchbox');
157
+
158
+ await user.type(input, 'john');
159
+ await screen.findByText('John Doe');
160
+ await user.click(document.body);
161
+
162
+ expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
163
+ });
164
+ });
@@ -1,15 +1,16 @@
1
- import React, { useState, useCallback, useContext, useMemo } from 'react';
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { useField } from 'formik';
3
+ import { useField, Field } from 'formik';
4
4
  import { Button } from '@carbon/react';
5
5
  import { TrashCan, Edit, Reset } from '@carbon/react/icons';
6
- import { ResourcesContext } from '../../../../offline.resources';
6
+ import { type RegistrationConfig } from '../../../../config-schema';
7
7
  import { showModal, useConfig, UserHasAccess } from '@openmrs/esm-framework';
8
- import { shouldBlockPatientIdentifierInOfflineMode } from './utils';
9
8
  import { deleteIdentifierType, setIdentifierSource } from '../../../field/id/id-field.component';
10
- import { type PatientIdentifierValue } from '../../../patient-registration.types';
11
- import { PatientRegistrationContext } from '../../../patient-registration-context';
12
9
  import { Input } from '../../basic-input/input/input.component';
10
+ import { usePatientRegistrationContext } from '../../../patient-registration-context';
11
+ import { useResourcesContext } from '../../../../resources-context';
12
+ import { shouldBlockPatientIdentifierInOfflineMode } from './utils';
13
+ import { type PatientIdentifierValue } from '../../../patient-registration.types';
13
14
  import styles from '../../input.scss';
14
15
 
15
16
  interface IdentifierInputProps {
@@ -17,16 +18,17 @@ interface IdentifierInputProps {
17
18
  fieldName: string;
18
19
  }
19
20
 
20
- export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fieldName }) => {
21
+ const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentifier, fieldName }) => {
21
22
  const { t } = useTranslation();
22
- const { defaultPatientIdentifierTypes } = useConfig();
23
- const { identifierTypes } = useContext(ResourcesContext);
24
- const { isOffline, values, setFieldValue } = useContext(PatientRegistrationContext);
23
+ const { defaultPatientIdentifierTypes } = useConfig<RegistrationConfig>();
24
+ const { identifierTypes } = useResourcesContext();
25
+ const { isOffline, values, setFieldValue } = usePatientRegistrationContext();
25
26
  const identifierType = useMemo(
26
27
  () => identifierTypes.find((identifierType) => identifierType.uuid === patientIdentifier.identifierTypeUuid),
27
28
  [patientIdentifier, identifierTypes],
28
29
  );
29
- const { autoGeneration, initialValue, identifierValue, identifierName, required } = patientIdentifier;
30
+ const { autoGeneration, initialValue, identifierValue, identifierName, required, selectedSource } = patientIdentifier;
31
+ const manualEntryEnabled = selectedSource?.autoGenerationOption?.manualEntryEnabled;
30
32
  const [hideInputField, setHideInputField] = useState(autoGeneration || initialValue === identifierValue);
31
33
  const name = `identifiers.${fieldName}.identifierValue`;
32
34
  const [identifierField, identifierFieldMeta] = useField(name);
@@ -41,13 +43,35 @@ export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentif
41
43
  return map;
42
44
  }, [defaultPatientIdentifierTypes]);
43
45
 
46
+ const validateInput = (value: string) => {
47
+ if (!value || value === '') {
48
+ return;
49
+ }
50
+
51
+ if (!identifierType?.format) {
52
+ return;
53
+ }
54
+
55
+ try {
56
+ const regex = new RegExp(identifierType.format);
57
+ if (regex.test(value)) {
58
+ return;
59
+ }
60
+
61
+ return identifierType.formatDescription ?? `Expected format: ${identifierType.format}`;
62
+ } catch (e) {
63
+ console.error('Invalid regex pattern:', identifierType.format);
64
+ return;
65
+ }
66
+ };
67
+
44
68
  const handleReset = useCallback(() => {
45
69
  setHideInputField(true);
46
70
  setFieldValue(`identifiers.${fieldName}`, {
47
71
  ...patientIdentifier,
48
72
  identifierValue: initialValue,
49
- selectedSource: null,
50
- autoGeneration: false,
73
+ selectedSource,
74
+ autoGeneration,
51
75
  } as PatientIdentifierValue);
52
76
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
77
  }, [initialValue, setHideInputField]);
@@ -57,6 +81,7 @@ export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentif
57
81
  setFieldValue(`identifiers.${fieldName}`, {
58
82
  ...patientIdentifier,
59
83
  ...setIdentifierSource(identifierType?.identifierSources?.[0], initialValue, initialValue),
84
+ ...(autoGeneration && manualEntryEnabled && { identifierValue: initialValue ?? '' }),
60
85
  });
61
86
  };
62
87
 
@@ -68,52 +93,62 @@ export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentif
68
93
  */
69
94
 
70
95
  if (initialValue) {
71
- const confirmDeleteIdentifierModal = showModal('delete-identifier-confirmation-modal', {
72
- deleteIdentifier: (deleteIdentifier) => {
73
- if (deleteIdentifier) {
96
+ const dispose = showModal('delete-identifier-confirmation-modal', {
97
+ closeModal: () => dispose(),
98
+ deleteIdentifier: (isConfirmed) => {
99
+ if (isConfirmed) {
74
100
  setFieldValue('identifiers', deleteIdentifierType(values.identifiers, fieldName));
75
101
  }
76
- confirmDeleteIdentifierModal();
102
+ dispose();
77
103
  },
78
104
  identifierName,
79
- initialValue,
105
+ identifierValue: initialValue,
80
106
  });
81
107
  } else {
82
108
  setFieldValue('identifiers', deleteIdentifierType(values.identifiers, fieldName));
83
109
  }
84
110
  };
85
111
 
112
+ const showEditButton = !required && hideInputField && (!!initialValue || manualEntryEnabled);
113
+ const showResetButton =
114
+ (!!initialValue && initialValue !== identifierValue) || (!hideInputField && manualEntryEnabled);
86
115
  return (
87
116
  <div className={styles.IDInput}>
88
- {!autoGeneration && !hideInputField ? (
89
- <Input
90
- id={name}
91
- labelText={identifierName}
92
- name={name}
93
- disabled={disabled}
94
- required={required}
95
- invalid={!!(identifierFieldMeta.touched && identifierFieldMeta.error)}
96
- invalidText={identifierFieldMeta.error && t(identifierFieldMeta.error)}
97
- // t('identifierValueRequired', 'Identifier value is required')
98
- {...identifierField}
99
- />
117
+ {!hideInputField ? (
118
+ <Field name={name} validate={validateInput}>
119
+ {({ field, form: { touched, errors } }) => (
120
+ <Input
121
+ id={name}
122
+ labelText={identifierName}
123
+ name={name}
124
+ disabled={disabled}
125
+ required={required}
126
+ invalid={errors[name] && touched[name]}
127
+ invalidText={errors[name] && t(errors[name])}
128
+ {...field}
129
+ />
130
+ )}
131
+ </Field>
100
132
  ) : (
101
133
  <div className={styles.textID}>
102
- <p className={styles.label}>{identifierName}</p>
103
- <p className={styles.bodyShort02}>
134
+ <p data-testid="identifier-label" className={styles.label}>
135
+ {required ? identifierName : `${t('optionalIdentifierLabel', { identifierName })}`}
136
+ </p>
137
+ <p data-testid="identifier-placeholder" className={styles.bodyShort02}>
104
138
  {autoGeneration ? t('autoGeneratedPlaceholderText', 'Auto-generated') : identifierValue}
105
139
  </p>
106
- <input type="hidden" {...identifierField} disabled />
140
+ <input data-testid="identifier-input" type="hidden" {...identifierField} disabled />
107
141
  {/* This is added for any error descriptions */}
108
142
  {!!(identifierFieldMeta.touched && identifierFieldMeta.error) && (
109
143
  <span className={styles.dangerLabel01}>{identifierFieldMeta.error && t(identifierFieldMeta.error)}</span>
110
144
  )}
111
145
  </div>
112
146
  )}
113
- <div style={{ marginBottom: '1rem' }}>
114
- {!patientIdentifier.required && patientIdentifier.initialValue && hideInputField && (
147
+ <div className={styles.actionButtonContainer}>
148
+ {showEditButton && (
115
149
  <UserHasAccess privilege="Edit Patient Identifiers">
116
150
  <Button
151
+ data-testid="edit-button"
117
152
  size="md"
118
153
  kind="ghost"
119
154
  onClick={handleEdit}
@@ -124,7 +159,7 @@ export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentif
124
159
  </Button>
125
160
  </UserHasAccess>
126
161
  )}
127
- {initialValue && initialValue !== identifierValue && (
162
+ {showResetButton && (
128
163
  <UserHasAccess privilege="Edit Patient Identifiers">
129
164
  <Button
130
165
  size="md"
@@ -154,3 +189,5 @@ export const IdentifierInput: React.FC<IdentifierInputProps> = ({ patientIdentif
154
189
  </div>
155
190
  );
156
191
  };
192
+
193
+ export default IdentifierInput;
@@ -0,0 +1,335 @@
1
+ /* eslint-disable testing-library/no-node-access */
2
+ import React from 'react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { screen } from '@testing-library/react';
5
+ import { Form, Formik } from 'formik';
6
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
7
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../../../../config-schema';
8
+ import { renderWithContext } from 'tools';
9
+ import { ResourcesContextProvider } from '../../../../resources-context';
10
+ import { type Resources } from '../../../../offline.resources';
11
+ import {
12
+ PatientRegistrationContextProvider,
13
+ type PatientRegistrationContextProps,
14
+ } from '../../../patient-registration-context';
15
+ import type {
16
+ AddressTemplate,
17
+ FormValues,
18
+ IdentifierSource,
19
+ PatientIdentifierValue,
20
+ } from '../../../patient-registration.types';
21
+ import IdentifierInput from './identifier-input.component';
22
+
23
+ const mockIdentifierTypes = [
24
+ {
25
+ fieldName: 'openMrsId',
26
+ format: '',
27
+ identifierSources: [
28
+ {
29
+ uuid: '01af8526-cea4-4175-aa90-340acb411771',
30
+ name: 'Generator 2 for OpenMRS ID',
31
+ autoGenerationOption: {
32
+ manualEntryEnabled: true,
33
+ automaticGenerationEnabled: true,
34
+ },
35
+ },
36
+ ],
37
+ isPrimary: true,
38
+ name: 'OpenMRS ID',
39
+ required: true,
40
+ uniquenessBehavior: 'UNIQUE' as const,
41
+ uuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
42
+ },
43
+ {
44
+ fieldName: 'ssn',
45
+ format: '^[A-Z]{1}-[0-9]{7}$',
46
+ formatDescription: 'Identifier should be one letter, followed by a dash, and then 7 digits e.g. A-1234567',
47
+ identifierSources: [
48
+ {
49
+ uuid: '01af8526-cea4-4175-aa90-340acb411771',
50
+ name: 'Generator 2 for SSN',
51
+ autoGenerationOption: {
52
+ manualEntryEnabled: true,
53
+ automaticGenerationEnabled: false,
54
+ },
55
+ },
56
+ ],
57
+ isPrimary: false,
58
+ name: 'SSN',
59
+ required: true,
60
+ uniquenessBehavior: 'UNIQUE' as const,
61
+ uuid: 'a71403f3-8584-4289-ab41-2b4e5570bd45',
62
+ },
63
+ ];
64
+
65
+ const mockResourcesContextValue: Resources = {
66
+ addressTemplate: {} as AddressTemplate,
67
+ currentSession: {
68
+ authenticated: true,
69
+ sessionId: 'JSESSION',
70
+ currentProvider: { uuid: 'provider-uuid', identifier: 'PRO-123' },
71
+ },
72
+ relationshipTypes: [],
73
+ identifierTypes: [...mockIdentifierTypes],
74
+ };
75
+
76
+ const mockInitialFormValues = {
77
+ additionalFamilyName: '',
78
+ additionalGivenName: '',
79
+ additionalMiddleName: '',
80
+ addNameInLocalLanguage: false,
81
+ address: {},
82
+ attributes: {},
83
+ birthdate: null,
84
+ deathDate: null,
85
+ familyName: '',
86
+ gender: '',
87
+ givenName: '',
88
+ identifiers: {},
89
+ middleName: '',
90
+ relationships: [],
91
+ } as FormValues;
92
+
93
+ const mockContextValues: PatientRegistrationContextProps = {
94
+ currentPhoto: '',
95
+ inEditMode: false,
96
+ identifierTypes: [],
97
+ initialFormValues: mockInitialFormValues,
98
+ isOffline: false,
99
+ setCapturePhotoProps: jest.fn(),
100
+ setFieldValue: jest.fn(),
101
+ setInitialFormValues: jest.fn(),
102
+ setFieldTouched: jest.fn(),
103
+ validationSchema: null,
104
+ values: mockInitialFormValues,
105
+ };
106
+
107
+ const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
108
+
109
+ describe('identifier input', () => {
110
+ mockUseConfig.mockReturnValue({
111
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
112
+ });
113
+
114
+ const fieldName = 'openMrsId';
115
+ const openmrsID = {
116
+ identifierTypeUuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
117
+ initialValue: '',
118
+ identifierName: 'OpenMRS ID',
119
+ selectedSource: {
120
+ uuid: '01af8526-cea4-4175-aa90-340acb411771',
121
+ name: 'Generator 2 for OpenMRS ID',
122
+ autoGenerationOption: {
123
+ manualEntryEnabled: false,
124
+ automaticGenerationEnabled: true,
125
+ },
126
+ } as IdentifierSource,
127
+ autoGeneration: false,
128
+ preferred: true,
129
+ required: true,
130
+ } as PatientIdentifierValue;
131
+
132
+ const setupIdentifierInput = (patientIdentifier: PatientIdentifierValue, initialValues = {}) => {
133
+ renderWithContext(
134
+ <Formik initialValues={initialValues} onSubmit={jest.fn()}>
135
+ <Form>
136
+ <PatientRegistrationContextProvider value={mockContextValues}>
137
+ <IdentifierInput patientIdentifier={patientIdentifier} fieldName={fieldName} />
138
+ </PatientRegistrationContextProvider>
139
+ </Form>
140
+ </Formik>,
141
+ ResourcesContextProvider,
142
+ mockResourcesContextValue,
143
+ );
144
+ };
145
+
146
+ it('shows the identifier input', () => {
147
+ openmrsID.autoGeneration = false;
148
+ setupIdentifierInput(openmrsID as PatientIdentifierValue);
149
+ expect(screen.getByLabelText(openmrsID.identifierName)).toBeInTheDocument();
150
+ });
151
+
152
+ it('displays an edit button when there is an initial value', async () => {
153
+ // setup
154
+ openmrsID.autoGeneration = false;
155
+ openmrsID.required = false;
156
+ openmrsID.initialValue = '1002UU9';
157
+ openmrsID.identifierValue = '1002UU9';
158
+ // replay
159
+ setupIdentifierInput(openmrsID as PatientIdentifierValue);
160
+ expect(screen.getByText('Edit')).toBeInTheDocument();
161
+ });
162
+
163
+ it('hides the edit button when the identifier is required', async () => {
164
+ // setup
165
+ openmrsID.autoGeneration = false;
166
+ openmrsID.required = true;
167
+ openmrsID.initialValue = '1002UU9';
168
+ openmrsID.identifierValue = '1002UU9';
169
+ // replay
170
+ setupIdentifierInput(openmrsID);
171
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
172
+ });
173
+
174
+ it('displays a delete button when the identifier is not a default type', () => {
175
+ // setup
176
+ openmrsID.required = false;
177
+ // replay
178
+ setupIdentifierInput(openmrsID);
179
+ expect(screen.getByText('Delete')).toBeInTheDocument();
180
+ });
181
+
182
+ describe('auto-generated identifier', () => {
183
+ it('hides the input when the identifier is auto-generated', () => {
184
+ openmrsID.autoGeneration = true;
185
+ setupIdentifierInput(openmrsID);
186
+ expect(screen.getByTestId('identifier-input')).toHaveAttribute('type', 'hidden');
187
+ });
188
+
189
+ it("displays 'Auto-Generated' when the indentifier has auto generation", () => {
190
+ openmrsID.autoGeneration = true;
191
+ setupIdentifierInput(openmrsID);
192
+ expect(screen.getByTestId('identifier-placeholder').innerHTML).toBe('Auto-generated');
193
+ expect(screen.getByTestId('identifier-input')).toBeDisabled();
194
+ });
195
+
196
+ describe('manual entry allowed', () => {
197
+ openmrsID.selectedSource = {
198
+ autoGenerationOption: {
199
+ manualEntryEnabled: true,
200
+ },
201
+ } as IdentifierSource;
202
+
203
+ it('shows the edit button', () => {
204
+ openmrsID.autoGeneration = true;
205
+ setupIdentifierInput(openmrsID);
206
+ expect(screen.getByText('Edit')).toBeInTheDocument();
207
+ });
208
+
209
+ describe('edit button clicked', () => {
210
+ it('displays an empty input field', async () => {
211
+ const user = userEvent.setup();
212
+ openmrsID.autoGeneration = true;
213
+ openmrsID.required = false;
214
+ openmrsID.selectedSource = {
215
+ autoGenerationOption: {
216
+ manualEntryEnabled: true,
217
+ },
218
+ } as IdentifierSource;
219
+ setupIdentifierInput(openmrsID);
220
+ const editButton = screen.getByTestId('edit-button');
221
+ await user.click(editButton);
222
+ expect(screen.getByLabelText(new RegExp(`${openmrsID.identifierName}`))).toHaveValue('');
223
+ });
224
+
225
+ it('displays an input field with the identifier value if it exists', async () => {
226
+ const user = userEvent.setup();
227
+ openmrsID.autoGeneration = true;
228
+ openmrsID.required = false;
229
+ openmrsID.selectedSource = {
230
+ autoGenerationOption: {
231
+ manualEntryEnabled: true,
232
+ },
233
+ } as IdentifierSource;
234
+ setupIdentifierInput(openmrsID, { identifiers: { [fieldName]: { identifierValue: '10001V' } } });
235
+ const editButton = screen.getByTestId('edit-button');
236
+ await user.click(editButton);
237
+ expect(screen.getByLabelText(new RegExp(`${openmrsID.identifierName}`))).toHaveValue('10001V');
238
+ });
239
+ });
240
+ });
241
+ });
242
+
243
+ it('validates identifier format correctly for identifier types with regex formats', async () => {
244
+ const user = userEvent.setup();
245
+
246
+ const ssnIdentifier = {
247
+ ...openmrsID,
248
+ autoGeneration: false,
249
+ format: '^[A-Z]{1}-[0-9]{7}$',
250
+ identifierName: 'SSN',
251
+ identifierTypeUuid: 'a71403f3-8584-4289-ab41-2b4e5570bd45',
252
+ identifierValue: undefined,
253
+ initialValue: '',
254
+ required: true,
255
+ selectedSource: {
256
+ uuid: '01af8526-cea4-4175-aa90-340acb411771',
257
+ name: 'Generator 2 for SSN',
258
+ autoGenerationOption: {
259
+ manualEntryEnabled: true,
260
+ automaticGenerationEnabled: false,
261
+ },
262
+ } as IdentifierSource,
263
+ };
264
+
265
+ const mockSetFieldTouched = jest.fn();
266
+ const mockSetFieldValue = jest.fn();
267
+ const testContextValues = {
268
+ ...mockContextValues,
269
+ setFieldTouched: mockSetFieldTouched,
270
+ setFieldValue: mockSetFieldValue,
271
+ values: {
272
+ ...mockInitialFormValues,
273
+ identifiers: {
274
+ [fieldName]: ssnIdentifier,
275
+ },
276
+ },
277
+ };
278
+
279
+ const initialValues = {
280
+ identifiers: {
281
+ [fieldName]: {
282
+ identifierValue: '',
283
+ },
284
+ },
285
+ };
286
+
287
+ renderWithContext(
288
+ <Formik initialValues={initialValues} onSubmit={jest.fn()}>
289
+ <Form>
290
+ <PatientRegistrationContextProvider value={testContextValues}>
291
+ <IdentifierInput patientIdentifier={ssnIdentifier} fieldName={fieldName} />
292
+ </PatientRegistrationContextProvider>
293
+ </Form>
294
+ </Formik>,
295
+ ResourcesContextProvider,
296
+ mockResourcesContextValue,
297
+ );
298
+
299
+ const input = screen.getByRole('textbox', {
300
+ name: /ssn/i,
301
+ });
302
+
303
+ // Valid cases
304
+ await user.type(input, 'A-1234567');
305
+ await user.tab();
306
+ expect(input).toHaveValue('A-1234567');
307
+ expect(input).not.toHaveClass('cds--text-input--invalid');
308
+ expect(screen.queryByText(/identifier should be/i)).not.toBeInTheDocument();
309
+
310
+ // Invalid cases
311
+ await user.clear(input);
312
+ await user.type(input, 'A-0010902aaa'); // Extra characters
313
+ await user.tab();
314
+ expect(input).toHaveClass('cds--text-input--invalid');
315
+ expect(screen.getByText(/identifier should be/i)).toBeInTheDocument();
316
+
317
+ await user.clear(input);
318
+ await user.type(input, 'a-1234567'); // Lowercase letter
319
+ await user.tab();
320
+ expect(input).toHaveClass('cds--text-input--invalid');
321
+ expect(screen.getByText(/identifier should be/i)).toBeInTheDocument();
322
+
323
+ await user.clear(input);
324
+ await user.type(input, 'AB-1234567'); // Two letters
325
+ await user.tab();
326
+ expect(input).toHaveClass('cds--text-input--invalid');
327
+ expect(screen.getByText(/identifier should be/i)).toBeInTheDocument();
328
+
329
+ await user.clear(input);
330
+ await user.type(input, 'A-123456'); // Only 6 digits
331
+ await user.tab();
332
+ expect(input).toHaveClass('cds--text-input--invalid');
333
+ expect(screen.getByText(/identifier should be/i)).toBeInTheDocument();
334
+ });
335
+ });