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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,294 @@
1
+ import React from 'react';
2
+ import { Form, Formik } from 'formik';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { useConfig } from '@openmrs/esm-framework';
5
+ import { Field } from './field.component';
6
+ import type { AddressTemplate, FormValues } from '../patient-registration.types';
7
+ import { type Resources, ResourcesContext } from '../../offline.resources';
8
+ import { PatientRegistrationContext } from '../patient-registration-context';
9
+
10
+ jest.mock('@openmrs/esm-framework', () => ({
11
+ ...jest.requireActual('@openmrs/esm-framework'),
12
+ useConfig: jest.fn(),
13
+ }));
14
+
15
+ const predefinedAddressTemplate = {
16
+ uuid: 'test-address-template-uuid',
17
+ property: 'layout.address.format',
18
+ description: 'Test Address Template',
19
+ display:
20
+ 'Layout - Address Format = <org.openmrs.layout.address.AddressTemplate>\n <nameMappings class="properties">\n <property name="postalCode" value="Location.postalCode"/>\n <property name="address2" value="Location.address2"/>\n <property name="address1" value="Location.address1"/>\n <property name="country" value="Location.country"/>\n <property name="stateProvince" value="Location.stateProvince"/>\n <property name="cityVillage" value="Location.cityVillage"/>\n </nameMappings>\n <sizeMappings class="properties">\n <property name="postalCode" value="10"/>\n <property name="address2" value="40"/>\n <property name="address1" value="40"/>\n <property name="country" value="10"/>\n <property name="stateProvince" value="10"/>\n <property name="cityVillage" value="10"/>\n </sizeMappings>\n <lineByLineFormat>\n <string>address1</string>\n <string>address2</string>\n <string>cityVillage stateProvince country postalCode</string>\n </lineByLineFormat>\n </org.openmrs.layout.address.AddressTemplate>',
21
+ value:
22
+ '<org.openmrs.layout.address.AddressTemplate>\r\n <nameMappings class="properties">\r\n <property name="postalCode" value="Location.postalCode"/>\r\n <property name="address2" value="Location.address2"/>\r\n <property name="address1" value="Location.address1"/>\r\n <property name="country" value="Location.country"/>\r\n <property name="stateProvince" value="Location.stateProvince"/>\r\n <property name="cityVillage" value="Location.cityVillage"/>\r\n </nameMappings>\r\n <sizeMappings class="properties">\r\n <property name="postalCode" value="4"/>\r\n <property name="address1" value="40"/>\r\n <property name="address2" value="40"/>\r\n <property name="country" value="10"/>\r\n <property name="stateProvince" value="10"/>\r\n <property name="cityVillage" value="10"/>\r\n <asset name="cityVillage" value="10"/>\r\n </sizeMappings>\r\n <lineByLineFormat>\r\n <string>address1 address2</string>\r\n <string>cityVillage stateProvince postalCode</string>\r\n <string>country</string>\r\n </lineByLineFormat>\r\n <elementDefaults class="properties">\r\n <property name="country" value=""/>\r\n </elementDefaults>\r\n <elementRegex class="properties">\r\n <property name="address1" value="[a-zA-Z]+$"/>\r\n </elementRegex>\r\n <elementRegexFormats class="properties">\r\n <property name="address1" value="Countries can only be letters"/>\r\n </elementRegexFormats>\r\n </org.openmrs.layout.address.AddressTemplate>',
23
+ };
24
+
25
+ const mockedIdentifierTypes = [
26
+ {
27
+ fieldName: 'openMrsId',
28
+ format: '',
29
+ identifierSources: [
30
+ {
31
+ uuid: '8549f706-7e85-4c1d-9424-217d50a2988b',
32
+ name: 'Generator for OpenMRS ID',
33
+ description: 'Generator for OpenMRS ID',
34
+ baseCharacterSet: '0123456789ACDEFGHJKLMNPRTUVWXY',
35
+ prefix: '',
36
+ },
37
+ ],
38
+ isPrimary: true,
39
+ name: 'OpenMRS ID',
40
+ required: true,
41
+ uniquenessBehavior: 'UNIQUE' as const,
42
+ uuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
43
+ },
44
+ {
45
+ fieldName: 'idCard',
46
+ format: '',
47
+ identifierSources: [],
48
+ isPrimary: false,
49
+ name: 'ID Card',
50
+ required: false,
51
+ uniquenessBehavior: 'UNIQUE' as const,
52
+ uuid: 'b4143563-16cd-4439-b288-f83d61670fc8',
53
+ },
54
+ {
55
+ fieldName: 'legacyId',
56
+ format: '',
57
+ identifierSources: [],
58
+ isPrimary: false,
59
+ name: 'Legacy ID',
60
+ required: false,
61
+ uniquenessBehavior: null,
62
+ uuid: '22348099-3873-459e-a32e-d93b17eda533',
63
+ },
64
+ {
65
+ fieldName: 'oldIdentificationNumber',
66
+ format: '',
67
+ identifierSources: [],
68
+ isPrimary: false,
69
+ name: 'Old Identification Number',
70
+ required: false,
71
+ uniquenessBehavior: null,
72
+ uuid: '8d79403a-c2cc-11de-8d13-0010c6dffd0f',
73
+ },
74
+ {
75
+ fieldName: 'openMrsIdentificationNumber',
76
+ format: '',
77
+ identifierSources: [],
78
+ isPrimary: false,
79
+ name: 'OpenMRS Identification Number',
80
+ required: false,
81
+ uniquenessBehavior: null,
82
+ uuid: '8d793bee-c2cc-11de-8d13-0010c6dffd0f',
83
+ },
84
+ ];
85
+
86
+ const mockResourcesContextValue: Resources = {
87
+ addressTemplate: predefinedAddressTemplate as unknown as AddressTemplate,
88
+ currentSession: {
89
+ authenticated: true,
90
+ sessionId: 'JSESSION',
91
+ currentProvider: { uuid: 'provider-uuid', identifier: 'PRO-123' },
92
+ },
93
+ relationshipTypes: [],
94
+ identifierTypes: [...mockedIdentifierTypes],
95
+ };
96
+
97
+ const initialContextValues = {
98
+ currentPhoto: 'data:image/png;base64,1234567890',
99
+ identifierTypes: [],
100
+ inEditMode: false,
101
+ initialFormValues: {} as FormValues,
102
+ isOffline: false,
103
+ setCapturePhotoProps: jest.fn(),
104
+ setFieldValue: jest.fn(),
105
+ setInitialFormValues: jest.fn(),
106
+ validationSchema: null,
107
+ values: {} as FormValues,
108
+ };
109
+
110
+ describe('Field', () => {
111
+ let ContextWrapper;
112
+
113
+ beforeEach(() => {
114
+ ContextWrapper = ({ children }) => (
115
+ <ResourcesContext.Provider value={mockResourcesContextValue}>
116
+ <Formik initialValues={{}} onSubmit={jest.fn()}>
117
+ <Form>
118
+ <PatientRegistrationContext.Provider value={initialContextValues}>
119
+ {children}
120
+ </PatientRegistrationContext.Provider>
121
+ </Form>
122
+ </Formik>
123
+ </ResourcesContext.Provider>
124
+ );
125
+ });
126
+
127
+ afterEach(() => {
128
+ jest.clearAllMocks();
129
+ });
130
+
131
+ it('should render NameField component when name prop is "name"', () => {
132
+ (useConfig as jest.Mock).mockImplementation(() => ({
133
+ fieldConfigurations: {
134
+ name: {
135
+ displayMiddleName: true,
136
+ unidentifiedPatient: true,
137
+ defaultUnknownGivenName: 'UNKNOWN',
138
+ defaultUnknownFamilyName: 'UNKNOWN',
139
+ },
140
+ },
141
+ }));
142
+
143
+ render(<Field name="name" />, { wrapper: ContextWrapper });
144
+
145
+ expect(screen.getByText('Full Name')).toBeInTheDocument();
146
+ });
147
+
148
+ it('should render GenderField component when name prop is "gender"', () => {
149
+ (useConfig as jest.Mock).mockImplementation(() => ({
150
+ fieldConfigurations: {
151
+ gender: [
152
+ {
153
+ value: 'Male',
154
+ label: 'Male',
155
+ id: 'male',
156
+ },
157
+ ],
158
+ },
159
+ }));
160
+
161
+ render(<Field name="gender" />, { wrapper: ContextWrapper });
162
+
163
+ expect(screen.getByLabelText('Male')).toBeInTheDocument();
164
+ });
165
+
166
+ it('should render DobField component when name prop is "dob"', () => {
167
+ (useConfig as jest.Mock).mockImplementation(() => ({
168
+ fieldConfigurations: {
169
+ dob: {
170
+ minAgeLimit: 0,
171
+ maxAgeLimit: 120,
172
+ },
173
+ },
174
+ }));
175
+ render(<Field name="dob" />, { wrapper: ContextWrapper });
176
+ expect(screen.getByText('Birth')).toBeInTheDocument();
177
+ });
178
+
179
+ it('should render AddressComponent component when name prop is "address"', () => {
180
+ jest.mock('./address/address-hierarchy.resource', () => ({
181
+ ...(jest.requireActual('../address-hierarchy.resource') as jest.Mock),
182
+ useOrderedAddressHierarchyLevels: jest.fn(),
183
+ }));
184
+ (useConfig as jest.Mock).mockImplementation(() => ({
185
+ fieldConfigurations: {
186
+ address: {
187
+ useAddressHierarchy: {
188
+ enabled: false,
189
+ useQuickSearch: false,
190
+ searchAddressByLevel: false,
191
+ },
192
+ },
193
+ },
194
+ }));
195
+
196
+ render(<Field name="address" />, { wrapper: ContextWrapper });
197
+
198
+ expect(screen.getByText('Address')).toBeInTheDocument();
199
+ });
200
+
201
+ it('should render Identifiers component when name prop is "id"', () => {
202
+ (useConfig as jest.Mock).mockImplementation(() => ({
203
+ defaultPatientIdentifierTypes: ['OpenMRS ID'],
204
+ }));
205
+
206
+ const openmrsID = {
207
+ name: 'OpenMRS ID',
208
+ fieldName: 'openMrsId',
209
+ required: true,
210
+ uuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
211
+ format: null,
212
+ isPrimary: true,
213
+ identifierSources: [
214
+ {
215
+ uuid: '691eed12-c0f1-11e2-94be-8c13b969e334',
216
+ name: 'Generator 1 for OpenMRS ID',
217
+ autoGenerationOption: {
218
+ manualEntryEnabled: false,
219
+ automaticGenerationEnabled: true,
220
+ },
221
+ },
222
+ {
223
+ uuid: '01af8526-cea4-4175-aa90-340acb411771',
224
+ name: 'Generator 2 for OpenMRS ID',
225
+ autoGenerationOption: {
226
+ manualEntryEnabled: true,
227
+ automaticGenerationEnabled: true,
228
+ },
229
+ },
230
+ ],
231
+ identifierUuid: 'openmrs-identifier-uuid',
232
+ identifierTypeUuid: 'openmrs-id-identifier-type-uuid',
233
+ initialValue: '12345',
234
+ identifierValue: '12345',
235
+ identifierName: 'OpenMRS ID',
236
+ preferred: true,
237
+ selectedSource: {
238
+ uuid: 'openmrs-id-selected-source-uuid',
239
+ name: 'Generator 1 for OpenMRS ID',
240
+ autoGenerationOption: {
241
+ manualEntryEnabled: false,
242
+ automaticGenerationEnabled: true,
243
+ },
244
+ },
245
+ autoGenerationSource: null,
246
+ };
247
+
248
+ const updatedContextValues = {
249
+ currentPhoto: 'data:image/png;base64,1234567890',
250
+ identifierTypes: [],
251
+ inEditMode: false,
252
+ initialFormValues: { identifiers: { openmrsID } } as unknown as FormValues,
253
+ isOffline: false,
254
+ setCapturePhotoProps: jest.fn(),
255
+ setFieldValue: jest.fn(),
256
+ setInitialFormValues: jest.fn(),
257
+ validationSchema: null,
258
+ values: { identifiers: { openmrsID } } as unknown as FormValues,
259
+ };
260
+
261
+ render(
262
+ <ResourcesContext.Provider value={mockResourcesContextValue}>
263
+ <Formik initialValues={{}} onSubmit={jest.fn()}>
264
+ <Form>
265
+ <PatientRegistrationContext.Provider value={updatedContextValues}>
266
+ <Field name="id" />
267
+ </PatientRegistrationContext.Provider>
268
+ </Form>
269
+ </Formik>
270
+ </ResourcesContext.Provider>,
271
+ );
272
+ expect(screen.getByText('Identifiers')).toBeInTheDocument();
273
+ });
274
+
275
+ it('should return null and report an error for an invalid field name', () => {
276
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
277
+
278
+ (useConfig as jest.Mock).mockImplementation(() => ({
279
+ fieldDefinitions: [{ id: 'weight' }],
280
+ }));
281
+ let error = null;
282
+
283
+ try {
284
+ render(<Field name="invalidField" />);
285
+ } catch (err) {
286
+ error = err;
287
+ }
288
+
289
+ expect(error).toMatch(/Invalid field name 'invalidField'. Valid options are /);
290
+ expect(screen.queryByTestId('invalid-field')).not.toBeInTheDocument();
291
+
292
+ consoleError.mockRestore();
293
+ });
294
+ });
@@ -0,0 +1,49 @@
1
+ import React, { useContext } from 'react';
2
+ import { RadioButton, RadioButtonGroup } from '@carbon/react';
3
+ import styles from '../field.scss';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { PatientRegistrationContext } from '../../patient-registration-context';
6
+ import { useField } from 'formik';
7
+ import { type RegistrationConfig } from '../../../config-schema';
8
+ import { useConfig } from '@openmrs/esm-framework';
9
+
10
+ export const GenderField: React.FC = () => {
11
+ const { fieldConfigurations } = useConfig() as RegistrationConfig;
12
+ const { t } = useTranslation();
13
+ const [field, meta] = useField('gender');
14
+ const { setFieldValue } = useContext(PatientRegistrationContext);
15
+ const fieldConfigs = fieldConfigurations?.gender;
16
+
17
+ const setGender = (gender: string) => {
18
+ setFieldValue('gender', gender);
19
+ };
20
+ /**
21
+ * DO NOT REMOVE THIS COMMENT HERE, ADDS TRANSLATION FOR SEX OPTIONS
22
+ * t('male', 'Male')
23
+ * t('female', 'Female')
24
+ * t('other', 'Other')
25
+ * t('unknown', 'Unknown')
26
+ */
27
+
28
+ return (
29
+ <div className={styles.halfWidthInDesktopView}>
30
+ <h4 className={styles.productiveHeading02Light}>{t('sexFieldLabelText', 'Sex')}</h4>
31
+ <div className={styles.sexField}>
32
+ <p className="cds--label">{t('genderLabelText', 'Sex')}</p>
33
+ <RadioButtonGroup name="gender" orientation="vertical" onChange={setGender} valueSelected={field.value}>
34
+ {fieldConfigs.map((option) => (
35
+ <RadioButton
36
+ key={option.label ?? option.value}
37
+ id={`gender-option-${option.value}`}
38
+ value={option.value}
39
+ labelText={t(option.label ?? option.value, option.label ?? option.value)}
40
+ />
41
+ ))}
42
+ </RadioButtonGroup>
43
+ {meta.touched && meta.error && (
44
+ <div className={styles.radioFieldError}>{t(meta.error, 'Gender is required')}</div>
45
+ )}
46
+ </div>
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Formik, Form } from 'formik';
4
+ import { render } from '@testing-library/react';
5
+ import { GenderField } from './gender-field.component';
6
+
7
+ jest.mock('@openmrs/esm-framework', () => ({
8
+ ...(jest.requireActual('@openmrs/esm-framework') as any),
9
+ useConfig: jest.fn(() => ({
10
+ fieldConfigurations: {
11
+ gender: [
12
+ {
13
+ value: 'male',
14
+ label: 'Male',
15
+ },
16
+ ],
17
+ name: {
18
+ displayMiddleName: false,
19
+ unidentifiedPatient: false,
20
+ defaultUnknownGivenName: '',
21
+ defaultUnknownFamilyName: '',
22
+ },
23
+ },
24
+ })),
25
+ }));
26
+
27
+ jest.mock('react', () => ({
28
+ ...(jest.requireActual('react') as any),
29
+ useContext: jest.fn(() => ({
30
+ setFieldValue: jest.fn(),
31
+ })),
32
+ }));
33
+
34
+ jest.mock('formik', () => ({
35
+ ...(jest.requireActual('formik') as any),
36
+ useField: jest.fn(() => [{}, {}]),
37
+ }));
38
+
39
+ describe('GenderField', () => {
40
+ const renderComponent = () => {
41
+ return render(
42
+ <Formik initialValues={{}} onSubmit={null}>
43
+ <Form>
44
+ <GenderField />
45
+ </Form>
46
+ </Formik>,
47
+ );
48
+ };
49
+
50
+ it('has a label', () => {
51
+ expect(renderComponent().getAllByText('Sex')).toBeTruthy();
52
+ });
53
+
54
+ it('checks an option', async () => {
55
+ const user = userEvent.setup();
56
+ const component = renderComponent();
57
+ expect(component.getByLabelText('Male').getAttribute('value')).toBe('male');
58
+ });
59
+ });
@@ -0,0 +1,144 @@
1
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, SkeletonText } from '@carbon/react';
4
+ import { ArrowRight } from '@carbon/react/icons';
5
+ import { useLayoutType, useConfig, isDesktop, UserHasAccess } from '@openmrs/esm-framework';
6
+ import IdentifierSelectionOverlay from './identifier-selection-overlay.component';
7
+ import { IdentifierInput } from '../../input/custom-input/identifier/identifier-input.component';
8
+ import { PatientRegistrationContext } from '../../patient-registration-context';
9
+ import {
10
+ type FormValues,
11
+ type IdentifierSource,
12
+ type PatientIdentifierType,
13
+ type PatientIdentifierValue,
14
+ } from '../../patient-registration.types';
15
+ import { ResourcesContext } from '../../../offline.resources';
16
+ import styles from '../field.scss';
17
+
18
+ export function setIdentifierSource(
19
+ identifierSource: IdentifierSource,
20
+ identifierValue: string,
21
+ initialValue: string,
22
+ ): {
23
+ identifierValue: string;
24
+ autoGeneration: boolean;
25
+ selectedSource: IdentifierSource;
26
+ } {
27
+ const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
28
+ return {
29
+ selectedSource: identifierSource,
30
+ autoGeneration,
31
+ identifierValue: autoGeneration
32
+ ? 'auto-generated'
33
+ : identifierValue !== 'auto-generated'
34
+ ? identifierValue
35
+ : initialValue,
36
+ };
37
+ }
38
+
39
+ export function initializeIdentifier(identifierType: PatientIdentifierType, identifierProps): PatientIdentifierValue {
40
+ return {
41
+ identifierTypeUuid: identifierType.uuid,
42
+ identifierName: identifierType.name,
43
+ preferred: identifierType.isPrimary,
44
+ initialValue: '',
45
+ required: identifierType.isPrimary || identifierType.required,
46
+ ...identifierProps,
47
+ ...setIdentifierSource(
48
+ identifierProps?.selectedSource ?? identifierType.identifierSources?.[0],
49
+ identifierProps?.identifierValue,
50
+ identifierProps?.initialValue ?? '',
51
+ ),
52
+ };
53
+ }
54
+
55
+ export function deleteIdentifierType(identifiers: FormValues['identifiers'], identifierFieldName) {
56
+ return Object.fromEntries(Object.entries(identifiers).filter(([fieldName]) => fieldName !== identifierFieldName));
57
+ }
58
+
59
+ export const Identifiers: React.FC = () => {
60
+ const { identifierTypes } = useContext(ResourcesContext);
61
+ const isLoading = !identifierTypes;
62
+ const { values, setFieldValue, initialFormValues, isOffline } = useContext(PatientRegistrationContext);
63
+ const { t } = useTranslation();
64
+ const layout = useLayoutType();
65
+ const [showIdentifierOverlay, setShowIdentifierOverlay] = useState(false);
66
+ const config = useConfig();
67
+ const { defaultPatientIdentifierTypes } = config;
68
+
69
+ useEffect(() => {
70
+ // Initialization
71
+ if (identifierTypes) {
72
+ const identifiers = {};
73
+ identifierTypes
74
+ .filter(
75
+ (type) =>
76
+ type.isPrimary ||
77
+ type.required ||
78
+ !!defaultPatientIdentifierTypes?.find(
79
+ (defaultIdentifierTypeUuid) => defaultIdentifierTypeUuid === type.uuid,
80
+ ),
81
+ )
82
+ .filter((type) => !values.identifiers[type.fieldName])
83
+ .forEach((type) => {
84
+ identifiers[type.fieldName] = initializeIdentifier(
85
+ type,
86
+ values.identifiers[type.uuid] ?? initialFormValues.identifiers[type.uuid] ?? {},
87
+ );
88
+ });
89
+ /*
90
+ Identifier value should only be updated if there is any update in the
91
+ identifier values, otherwise, if the below 'if' clause is removed, it will
92
+ fall into an infinite run.
93
+ */
94
+ if (Object.keys(identifiers).length) {
95
+ setFieldValue('identifiers', {
96
+ ...values.identifiers,
97
+ ...identifiers,
98
+ });
99
+ }
100
+ }
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, [identifierTypes, setFieldValue, defaultPatientIdentifierTypes, values.identifiers, initializeIdentifier]);
103
+
104
+ const closeIdentifierSelectionOverlay = useCallback(
105
+ () => setShowIdentifierOverlay(false),
106
+ [setShowIdentifierOverlay],
107
+ );
108
+
109
+ if (isLoading && !isOffline) {
110
+ return (
111
+ <div data-testid="loading-skeleton" className={styles.halfWidthInDesktopView}>
112
+ <div className={styles.identifierLabelText}>
113
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
114
+ </div>
115
+ <SkeletonText />
116
+ </div>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <div className={styles.halfWidthInDesktopView}>
122
+ <UserHasAccess privilege={['Get Identifier Types', 'Add Patient Identifiers']}>
123
+ <div className={styles.identifierLabelText}>
124
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
125
+ <Button
126
+ kind="ghost"
127
+ className={styles.setIDNumberButton}
128
+ onClick={() => setShowIdentifierOverlay(true)}
129
+ size={isDesktop(layout) ? 'sm' : 'md'}>
130
+ {t('configure', 'Configure')} <ArrowRight size={16} />
131
+ </Button>
132
+ </div>
133
+ </UserHasAccess>
134
+ <div>
135
+ {Object.entries(values.identifiers).map(([fieldName, identifier]) => (
136
+ <IdentifierInput key={fieldName} fieldName={fieldName} patientIdentifier={identifier} />
137
+ ))}
138
+ {showIdentifierOverlay && (
139
+ <IdentifierSelectionOverlay setFieldValue={setFieldValue} closeOverlay={closeIdentifierSelectionOverlay} />
140
+ )}
141
+ </div>
142
+ </div>
143
+ );
144
+ };
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { Identifiers } from './id-field.component';
5
+ import { type Resources, ResourcesContext } from '../../../offline.resources';
6
+ import { Form, Formik } from 'formik';
7
+ import { PatientRegistrationContext } from '../../patient-registration-context';
8
+ import { openmrsID, mockedIdentifierTypes } from '__mocks__';
9
+
10
+ jest.mock('@openmrs/esm-framework', () => ({
11
+ ...jest.requireActual('@openmrs/esm-framework'),
12
+ useConfig: jest.fn().mockImplementation(() => ({
13
+ defaultPatientIdentifierTypes: ['OpenMRS ID'],
14
+ })),
15
+ }));
16
+
17
+ describe('Identifiers', () => {
18
+ const mockResourcesContextValue = {
19
+ addressTemplate: {},
20
+ currentSession: {
21
+ authenticated: true,
22
+ sessionId: 'JSESSION',
23
+ currentProvider: { uuid: 'provider-uuid', identifier: 'PRO-123' },
24
+ },
25
+ relationshipTypes: [],
26
+ identifierTypes: [...mockedIdentifierTypes],
27
+ } as Resources;
28
+
29
+ it('should render loading skeleton when identifier types are loading', () => {
30
+ render(
31
+ <ResourcesContext.Provider value={[]}>
32
+ <Formik initialValues={{}} onSubmit={null}>
33
+ <Form>
34
+ <PatientRegistrationContext.Provider
35
+ value={{
36
+ setFieldValue: jest.fn(),
37
+ initialFormValues: { identifiers: { ...mockedIdentifierTypes[0] } },
38
+ setInitialFormValues: jest.fn(),
39
+ values: {
40
+ identifiers: { openmrsID },
41
+ },
42
+ }}>
43
+ <Identifiers />
44
+ </PatientRegistrationContext.Provider>
45
+ </Form>
46
+ </Formik>
47
+ </ResourcesContext.Provider>,
48
+ );
49
+ expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
50
+ });
51
+
52
+ it('should render identifier inputs when identifier types are loaded', () => {
53
+ render(
54
+ <ResourcesContext.Provider value={mockResourcesContextValue}>
55
+ <Formik initialValues={{}} onSubmit={null}>
56
+ <Form>
57
+ <PatientRegistrationContext.Provider
58
+ value={{
59
+ setFieldValue: jest.fn(),
60
+ initialFormValues: { identifiers: { ...mockedIdentifierTypes[0] } },
61
+ setInitialFormValues: jest.fn(),
62
+ values: {
63
+ identifiers: { openmrsID },
64
+ },
65
+ }}>
66
+ <Identifiers />
67
+ </PatientRegistrationContext.Provider>
68
+ </Form>
69
+ </Formik>
70
+ </ResourcesContext.Provider>,
71
+ );
72
+
73
+ expect(screen.getByText('Identifiers')).toBeInTheDocument();
74
+ const configureButton = screen.getByRole('button', { name: 'Configure' });
75
+ expect(configureButton).toBeInTheDocument();
76
+ expect(configureButton).toBeEnabled();
77
+ });
78
+
79
+ it('should open identifier selection overlay when "Configure" button is clicked', async () => {
80
+ const user = userEvent.setup();
81
+
82
+ render(
83
+ <ResourcesContext.Provider value={mockResourcesContextValue}>
84
+ <Formik initialValues={{}} onSubmit={null}>
85
+ <Form>
86
+ <PatientRegistrationContext.Provider
87
+ value={{
88
+ setFieldValue: jest.fn(),
89
+ initialFormValues: { identifiers: { ...mockedIdentifierTypes[0] } },
90
+ setInitialFormValues: jest.fn(),
91
+ values: {
92
+ identifiers: { openmrsID },
93
+ },
94
+ }}>
95
+ <Identifiers />
96
+ </PatientRegistrationContext.Provider>
97
+ </Form>
98
+ </Formik>
99
+ </ResourcesContext.Provider>,
100
+ );
101
+
102
+ const configureButton = screen.getByRole('button', { name: 'Configure' });
103
+ await user.click(configureButton);
104
+
105
+ expect(screen.getByRole('button', { name: 'Close overlay' })).toBeInTheDocument();
106
+ });
107
+ });