@olaboot/esm-patient-registration-app 9.2.1 → 10.0.2

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 (342) hide show
  1. package/dist/1339.js +1 -0
  2. package/dist/1339.js.map +1 -0
  3. package/dist/1480.js +1 -0
  4. package/dist/1480.js.map +1 -0
  5. package/dist/1646.js +1 -0
  6. package/dist/1646.js.map +1 -0
  7. package/dist/1789.js +1 -0
  8. package/dist/1789.js.map +1 -0
  9. package/dist/1869.js +1 -0
  10. package/dist/1869.js.map +1 -0
  11. package/dist/1877.js +1 -0
  12. package/dist/1877.js.map +1 -0
  13. package/dist/2317.js +1 -0
  14. package/dist/2317.js.map +1 -0
  15. package/dist/2416.js +1 -0
  16. package/dist/2416.js.map +1 -0
  17. package/dist/2747.js +1 -0
  18. package/dist/2747.js.map +1 -0
  19. package/dist/282.js +1 -0
  20. package/dist/282.js.map +1 -0
  21. package/dist/2881.js +1 -0
  22. package/dist/2881.js.map +1 -0
  23. package/dist/3378.js +1 -0
  24. package/dist/3378.js.map +1 -0
  25. package/dist/3720.js +1 -0
  26. package/dist/3720.js.map +1 -0
  27. package/dist/3906.js +1 -0
  28. package/dist/3906.js.map +1 -0
  29. package/dist/3963.js +1 -0
  30. package/dist/3963.js.map +1 -0
  31. package/dist/3989.js +1 -0
  32. package/dist/3989.js.map +1 -0
  33. package/dist/4106.js +1 -0
  34. package/dist/4106.js.map +1 -0
  35. package/dist/4111.js +1 -0
  36. package/dist/4111.js.map +1 -0
  37. package/dist/434.js +1 -0
  38. package/dist/434.js.map +1 -0
  39. package/dist/4348.js +1 -0
  40. package/dist/4348.js.map +1 -0
  41. package/dist/4383.js +1 -0
  42. package/dist/4383.js.map +1 -0
  43. package/dist/4658.js +1 -0
  44. package/dist/4658.js.map +1 -0
  45. package/dist/466.js +1 -0
  46. package/dist/466.js.map +1 -0
  47. package/dist/4928.js +1 -0
  48. package/dist/4928.js.map +1 -0
  49. package/dist/5117.js +1 -0
  50. package/dist/5117.js.map +1 -0
  51. package/dist/5132.js +1 -0
  52. package/dist/5132.js.map +1 -0
  53. package/dist/5145.js +1 -0
  54. package/dist/5145.js.map +1 -0
  55. package/dist/5208.js +43 -0
  56. package/dist/5208.js.map +1 -0
  57. package/dist/527.js +1 -0
  58. package/dist/527.js.map +1 -0
  59. package/dist/5280.js +1 -0
  60. package/dist/5280.js.map +1 -0
  61. package/dist/5338.js +6 -0
  62. package/dist/5338.js.map +1 -0
  63. package/dist/5503.js +1 -0
  64. package/dist/5503.js.map +1 -0
  65. package/dist/555.js +1 -0
  66. package/dist/555.js.map +1 -0
  67. package/dist/556.js +1 -0
  68. package/dist/556.js.map +1 -0
  69. package/dist/5644.js +1 -0
  70. package/dist/5644.js.map +1 -0
  71. package/dist/5697.js +1 -0
  72. package/dist/{4024.js.map → 5697.js.map} +1 -1
  73. package/dist/5940.js +1 -0
  74. package/dist/5940.js.map +1 -0
  75. package/dist/6047.js +1 -0
  76. package/dist/6047.js.map +1 -0
  77. package/dist/6371.js +1 -0
  78. package/dist/6371.js.map +1 -0
  79. package/dist/6377.js +1 -0
  80. package/dist/6377.js.map +1 -0
  81. package/dist/6388.js +1 -0
  82. package/dist/6388.js.map +1 -0
  83. package/dist/6444.js +1 -0
  84. package/dist/6444.js.map +1 -0
  85. package/dist/6508.js +1 -0
  86. package/dist/6508.js.map +1 -0
  87. package/dist/6724.js +1 -0
  88. package/dist/6724.js.map +1 -0
  89. package/dist/689.js +1 -0
  90. package/dist/689.js.map +1 -0
  91. package/dist/6904.js +1 -0
  92. package/dist/6904.js.map +1 -0
  93. package/dist/7045.js +1 -0
  94. package/dist/7045.js.map +1 -0
  95. package/dist/7175.js +1 -0
  96. package/dist/7175.js.map +1 -0
  97. package/dist/7182.js +1 -0
  98. package/dist/7182.js.map +1 -0
  99. package/dist/7649.js +1 -0
  100. package/dist/7649.js.map +1 -0
  101. package/dist/7742.js +1 -0
  102. package/dist/7742.js.map +1 -0
  103. package/dist/7912.js +1 -0
  104. package/dist/7912.js.map +1 -0
  105. package/dist/8358.js +1 -0
  106. package/dist/8358.js.map +1 -0
  107. package/dist/8359.js +1 -0
  108. package/dist/8359.js.map +1 -0
  109. package/dist/8695.js +1 -0
  110. package/dist/8695.js.map +1 -0
  111. package/dist/903.js +1 -0
  112. package/dist/903.js.map +1 -0
  113. package/dist/9061.js +1 -0
  114. package/dist/9061.js.map +1 -0
  115. package/dist/9072.js +1 -0
  116. package/dist/9072.js.map +1 -0
  117. package/dist/9397.js +1 -0
  118. package/dist/9397.js.map +1 -0
  119. package/dist/9712.js +1 -0
  120. package/dist/9712.js.map +1 -0
  121. package/dist/9771.js +1 -0
  122. package/dist/9771.js.map +1 -0
  123. package/dist/9806.js +1 -0
  124. package/dist/9806.js.map +1 -0
  125. package/dist/9816.js +1 -0
  126. package/dist/9816.js.map +1 -0
  127. package/dist/main.js +7 -6
  128. package/dist/main.js.map +1 -1
  129. package/dist/openmrs-esm-patient-registration-app.js +6 -0
  130. package/dist/{olaboot-esm-patient-registration-app.js.buildmanifest.json → openmrs-esm-patient-registration-app.js.buildmanifest.json} +540 -455
  131. package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
  132. package/dist/routes.json +1 -1
  133. package/package.json +8 -9
  134. package/src/add-patient-link.extension.tsx +3 -2
  135. package/src/add-patient-link.test.tsx +2 -1
  136. package/src/config-schema.ts +507 -507
  137. package/src/constants.ts +1 -1
  138. package/src/index.ts +2 -24
  139. package/src/nav-link.test.tsx +1 -0
  140. package/src/offline.resources.ts +97 -31
  141. package/src/patient-registration/before-save-prompt.test.tsx +199 -0
  142. package/src/patient-registration/field/__mocks__/field.resource.ts +8 -7
  143. package/src/patient-registration/field/address/address-field.component.tsx +183 -186
  144. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +6 -1
  145. package/src/patient-registration/field/address/address-hierarchy.test.tsx +289 -296
  146. package/src/patient-registration/field/address/address-search.component.tsx +20 -8
  147. package/src/patient-registration/field/address/address-search.scss +19 -2
  148. package/src/patient-registration/field/address/address-search.test.tsx +333 -141
  149. package/src/patient-registration/field/address/custom-address-field.component.tsx +1 -1
  150. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +1 -1
  151. package/src/patient-registration/field/cause-of-death/cause-of-death.test.tsx +251 -0
  152. package/src/patient-registration/field/custom-field.component.tsx +1 -1
  153. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +1 -1
  154. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.test.tsx +144 -0
  155. package/src/patient-registration/field/dob/dob.component.tsx +2 -2
  156. package/src/patient-registration/field/dob/dob.test.tsx +370 -54
  157. package/src/patient-registration/field/field.component.tsx +1 -1
  158. package/src/patient-registration/field/field.resource.ts +2 -2
  159. package/src/patient-registration/field/field.test.tsx +333 -330
  160. package/src/patient-registration/field/gender/gender-field.test.tsx +240 -54
  161. package/src/patient-registration/field/id/id-field.component.tsx +146 -144
  162. package/src/patient-registration/field/id/id-field.test.tsx +177 -121
  163. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -200
  164. package/src/patient-registration/field/id/identifier-selection-overlay.test.tsx +346 -0
  165. package/src/patient-registration/field/name/name-field.component.tsx +2 -2
  166. package/src/patient-registration/field/name/name-field.test.tsx +282 -0
  167. package/src/patient-registration/field/obs/obs-field.test.tsx +294 -118
  168. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +172 -108
  169. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +3 -3
  170. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +2 -5
  171. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +2 -2
  172. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +249 -131
  173. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +1 -1
  174. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +98 -70
  175. package/src/patient-registration/field/phone/phone-field.test.tsx +100 -0
  176. package/src/patient-registration/form-manager.test.ts +6 -5
  177. package/src/patient-registration/form-manager.ts +5 -2
  178. package/src/patient-registration/input/basic-input/input/input.component.tsx +3 -121
  179. package/src/patient-registration/input/basic-input/input/input.test.tsx +151 -51
  180. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +113 -33
  181. package/src/patient-registration/input/combo-input/combo-input.component.tsx +60 -24
  182. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +10 -101
  183. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +144 -108
  184. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +241 -177
  185. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +47 -8
  186. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +12 -12
  187. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +52 -20
  188. package/src/patient-registration/input/input.scss +1 -2
  189. package/src/patient-registration/patient-registration-context.ts +5 -3
  190. package/src/patient-registration/patient-registration-hooks.ts +4 -12
  191. package/src/patient-registration/patient-registration-utils.test.ts +2 -1
  192. package/src/patient-registration/patient-registration-utils.ts +2 -98
  193. package/src/patient-registration/patient-registration.component.tsx +50 -46
  194. package/src/patient-registration/patient-registration.resource.test.tsx +4 -7
  195. package/src/patient-registration/patient-registration.resource.ts +1 -4
  196. package/src/patient-registration/patient-registration.scss +16 -3
  197. package/src/patient-registration/patient-registration.test.tsx +614 -580
  198. package/src/patient-registration/patient-registration.types.ts +17 -28
  199. package/src/patient-registration/section/death-info/death-info-section.test.tsx +130 -34
  200. package/src/patient-registration/section/demographics/demographics-section.test.tsx +122 -68
  201. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +15 -15
  202. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +278 -84
  203. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  204. package/src/patient-registration/ui-components/overlay/overlay.test.tsx +104 -0
  205. package/src/patient-registration/validation/patient-registration-validation.test.ts +2 -1
  206. package/src/patient-registration/validation/patient-registration-validation.ts +9 -3
  207. package/src/root.component.tsx +2 -5
  208. package/src/widgets/cancel-patient-edit.test.tsx +48 -11
  209. package/src/widgets/delete-identifier-confirmation.test.tsx +77 -24
  210. package/src/widgets/edit-patient-details-button.component.tsx +14 -18
  211. package/src/widgets/edit-patient-details-button.scss +2 -2
  212. package/src/widgets/edit-patient-details-button.test.tsx +11 -13
  213. package/translations/am.json +9 -4
  214. package/translations/ar.json +9 -4
  215. package/translations/ar_SY.json +9 -4
  216. package/translations/bn.json +9 -4
  217. package/translations/cs.json +9 -4
  218. package/translations/de.json +120 -115
  219. package/translations/en.json +10 -4
  220. package/translations/en_US.json +9 -4
  221. package/translations/es.json +9 -4
  222. package/translations/es_MX.json +9 -4
  223. package/translations/fr.json +9 -4
  224. package/translations/he.json +9 -4
  225. package/translations/hi.json +9 -4
  226. package/translations/hi_IN.json +9 -4
  227. package/translations/id.json +9 -4
  228. package/translations/it.json +9 -4
  229. package/translations/ka.json +9 -4
  230. package/translations/km.json +9 -4
  231. package/translations/ku.json +9 -4
  232. package/translations/ky.json +9 -4
  233. package/translations/lg.json +9 -4
  234. package/translations/ne.json +9 -4
  235. package/translations/pl.json +9 -4
  236. package/translations/pt.json +9 -4
  237. package/translations/pt_BR.json +10 -5
  238. package/translations/qu.json +9 -4
  239. package/translations/ro_RO.json +9 -4
  240. package/translations/ru_RU.json +9 -4
  241. package/translations/si.json +9 -4
  242. package/translations/sq.json +9 -4
  243. package/translations/sw.json +9 -4
  244. package/translations/sw_KE.json +9 -4
  245. package/translations/tr.json +9 -4
  246. package/translations/tr_TR.json +9 -4
  247. package/translations/uk.json +9 -4
  248. package/translations/uz.json +9 -4
  249. package/translations/uz@Latn.json +9 -4
  250. package/translations/uz_UZ.json +9 -4
  251. package/translations/vi.json +9 -4
  252. package/translations/zh.json +50 -45
  253. package/translations/zh_CN.json +9 -4
  254. package/translations/zh_TW.json +9 -4
  255. package/vitest.config.ts +4 -0
  256. package/dist/126.js +0 -1
  257. package/dist/15.js +0 -1
  258. package/dist/1564.js +0 -1
  259. package/dist/1567.js +0 -1
  260. package/dist/1845.js +0 -1
  261. package/dist/1953.js +0 -1
  262. package/dist/200.js +0 -1
  263. package/dist/200.js.map +0 -1
  264. package/dist/215.js +0 -1
  265. package/dist/2178.js +0 -1
  266. package/dist/250.js +0 -1
  267. package/dist/250.js.map +0 -1
  268. package/dist/2523.js +0 -1
  269. package/dist/2523.js.map +0 -1
  270. package/dist/2566.js +0 -1
  271. package/dist/2586.js +0 -1
  272. package/dist/2586.js.map +0 -1
  273. package/dist/2716.js +0 -1
  274. package/dist/2716.js.map +0 -1
  275. package/dist/2759.js +0 -1
  276. package/dist/2821.js +0 -6
  277. package/dist/2821.js.map +0 -1
  278. package/dist/3089.js +0 -1
  279. package/dist/3089.js.map +0 -1
  280. package/dist/3230.js +0 -1
  281. package/dist/3441.js +0 -1
  282. package/dist/3565.js +0 -1
  283. package/dist/3571.js +0 -1
  284. package/dist/3571.js.map +0 -1
  285. package/dist/3746.js +0 -1
  286. package/dist/3925.js +0 -1
  287. package/dist/3946.js +0 -1
  288. package/dist/4024.js +0 -1
  289. package/dist/4744.js +0 -1
  290. package/dist/4744.js.map +0 -1
  291. package/dist/4809.js +0 -1
  292. package/dist/4894.js +0 -1
  293. package/dist/4970.js +0 -1
  294. package/dist/4970.js.map +0 -1
  295. package/dist/5130.js +0 -1
  296. package/dist/5187.js +0 -1
  297. package/dist/5491.js +0 -1
  298. package/dist/5491.js.map +0 -1
  299. package/dist/5595.js +0 -1
  300. package/dist/5961.js +0 -1
  301. package/dist/6133.js +0 -1
  302. package/dist/634.js +0 -1
  303. package/dist/634.js.map +0 -1
  304. package/dist/6456.js +0 -1
  305. package/dist/6466.js +0 -1
  306. package/dist/6613.js +0 -1
  307. package/dist/6783.js +0 -1
  308. package/dist/7073.js +0 -38
  309. package/dist/7073.js.map +0 -1
  310. package/dist/7154.js +0 -1
  311. package/dist/7154.js.map +0 -1
  312. package/dist/7348.js +0 -1
  313. package/dist/7439.js +0 -1
  314. package/dist/7439.js.map +0 -1
  315. package/dist/7543.js +0 -1
  316. package/dist/7607.js +0 -1
  317. package/dist/772.js +0 -1
  318. package/dist/7984.js +0 -1
  319. package/dist/7984.js.map +0 -1
  320. package/dist/8538.js +0 -1
  321. package/dist/8538.js.map +0 -1
  322. package/dist/8599.js +0 -1
  323. package/dist/8727.js +0 -1
  324. package/dist/8847.js +0 -1
  325. package/dist/9015.js +0 -1
  326. package/dist/906.js +0 -1
  327. package/dist/9065.js +0 -1
  328. package/dist/9182.js +0 -1
  329. package/dist/9339.js +0 -1
  330. package/dist/9453.js +0 -1
  331. package/dist/9833.js +0 -1
  332. package/dist/9833.js.map +0 -1
  333. package/dist/9856.js +0 -1
  334. package/dist/9856.js.map +0 -1
  335. package/dist/9920.js +0 -1
  336. package/dist/9938.js +0 -1
  337. package/dist/9943.js +0 -1
  338. package/dist/9943.js.map +0 -1
  339. package/dist/olaboot-esm-patient-registration-app.js +0 -5
  340. package/dist/olaboot-esm-patient-registration-app.js.map +0 -1
  341. package/jest.config.js +0 -3
  342. package/src/resource.ts +0 -12
package/src/constants.ts CHANGED
@@ -6,7 +6,7 @@ export const personRelationshipRepresentation =
6
6
  'personB:(age,display,birthdate,uuid),' +
7
7
  'relationshipType:(uuid,display,description,aIsToB,bIsToA))';
8
8
 
9
- export const moduleName = '@olaboot/esm-patient-registration-app';
9
+ export const moduleName = '@openmrs/esm-patient-registration-app';
10
10
  export const patientRegistration = 'patient-registration';
11
11
 
12
12
  export const cacheForOfflineHeaders: OmrsOfflineHttpHeaders = {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
2
2
  import { esmPatientRegistrationSchema } from './config-schema';
3
- import { moduleName, patientRegistration } from './constants';
3
+ import { moduleName } from './constants';
4
4
  import { setupOffline } from './offline';
5
5
  import addPatientLinkComponent from './add-patient-link.extension';
6
6
 
@@ -13,28 +13,6 @@ const options = {
13
13
 
14
14
  export function startupApp() {
15
15
  defineConfigSchema(moduleName, esmPatientRegistrationSchema);
16
-
17
- registerBreadcrumbs([
18
- {
19
- path: `${window.spaBase}/${patientRegistration}`,
20
- // t('patientRegistrationBreadcrumb', 'Patient Registration')
21
- title: () =>
22
- Promise.resolve(
23
- window.i18next.t('patientRegistrationBreadcrumb', { defaultValue: 'Patient Registration', ns: moduleName }),
24
- ),
25
- parent: `${window.spaBase}/home`,
26
- },
27
- {
28
- path: `${window.spaBase}/patient/:patientUuid/edit`,
29
- // t('editPatientDetailsBreadcrumb', 'Edit patient details')
30
- title: () =>
31
- Promise.resolve(
32
- window.i18next.t('editPatientDetailsBreadcrumb', { defaultValue: 'Edit patient details', ns: moduleName }),
33
- ),
34
- parent: `${window.spaBase}/patient/:patientUuid/chart`,
35
- },
36
- ]);
37
-
38
16
  setupOffline();
39
17
  }
40
18
 
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { vi, describe, it, expect } from 'vitest';
2
3
  import { render, screen } from '@testing-library/react';
3
4
  import Root from './nav-link';
4
5
 
@@ -1,16 +1,28 @@
1
1
  import { camelCase, escapeRegExp, find } from 'lodash-es';
2
- import { getConfig, messageOmrsServiceWorker, openmrsFetch, restBaseUrl, type Session } from '@openmrs/esm-framework';
2
+ import {
3
+ getConfig,
4
+ messageOmrsServiceWorker,
5
+ openmrsFetch,
6
+ restBaseUrl,
7
+ type FetchResponse,
8
+ type Session,
9
+ } from '@openmrs/esm-framework';
3
10
  import type {
4
- PatientIdentifierType,
5
- FetchedPatientIdentifierType,
6
11
  AddressTemplate,
12
+ ConceptResponse,
13
+ FetchedPatientIdentifierType,
14
+ IdentifierSourceAutoGenerationOption,
15
+ PatientIdentifierType,
16
+ PersonAttributeTypeResponse,
17
+ RelationshipTypesResponse,
7
18
  } from './patient-registration/patient-registration.types';
8
19
  import { cacheForOfflineHeaders, moduleName } from './constants';
20
+ import type { FieldDefinition } from './config-schema';
9
21
 
10
22
  export interface Resources {
11
23
  addressTemplate: AddressTemplate;
12
24
  currentSession: Session;
13
- relationshipTypes: any;
25
+ relationshipTypes: RelationshipTypesResponse;
14
26
  identifierTypes: Array<PatientIdentifierType>;
15
27
  }
16
28
 
@@ -24,8 +36,8 @@ export async function fetchAddressTemplate() {
24
36
  return data;
25
37
  }
26
38
 
27
- export async function fetchAllRelationshipTypes() {
28
- const { data } = await cacheAndFetch(`${restBaseUrl}/relationshiptype?v=default`);
39
+ export async function fetchAllRelationshipTypes(): Promise<RelationshipTypesResponse> {
40
+ const { data } = await cacheAndFetch<RelationshipTypesResponse>(`${restBaseUrl}/relationshiptype?v=default`);
29
41
  return data;
30
42
  }
31
43
 
@@ -50,39 +62,61 @@ export async function fetchAllFieldDefinitionTypes() {
50
62
  return mergedData;
51
63
  }
52
64
 
53
- async function fetchFieldDefinitionType(fieldDefinition) {
65
+ async function fetchFieldDefinitionType(
66
+ fieldDefinition: FieldDefinition,
67
+ ): Promise<PersonAttributeTypeResponse | undefined> {
54
68
  let apiUrl = '';
55
69
 
56
70
  if (fieldDefinition.type === 'person attribute') {
57
71
  apiUrl = `${restBaseUrl}/personattributetype/${fieldDefinition.uuid}`;
58
72
  }
59
73
 
74
+ // Pre-fetch the concept for offline caching if answerConceptSetUuid is provided.
75
+ // This ensures the concept answers are available offline even though we don't use the result here.
76
+ // The actual concept data is fetched later via useConceptAnswers hook when rendering the field.
60
77
  if (fieldDefinition.answerConceptSetUuid) {
61
- await cacheAndFetch(`${restBaseUrl}/concept/${fieldDefinition.answerConceptSetUuid}`);
78
+ await cacheAndFetch<ConceptResponse>(`${restBaseUrl}/concept/${fieldDefinition.answerConceptSetUuid}`);
79
+ }
80
+
81
+ if (!apiUrl) {
82
+ return undefined;
62
83
  }
63
- const { data } = await cacheAndFetch(apiUrl);
84
+
85
+ const { data } = await cacheAndFetch<PersonAttributeTypeResponse>(apiUrl);
64
86
  return data;
65
87
  }
66
88
 
89
+ function isValidFetchedIdentifierType(
90
+ type: FetchedPatientIdentifierType | null | undefined,
91
+ ): type is FetchedPatientIdentifierType {
92
+ return type !== null && type !== undefined;
93
+ }
94
+
67
95
  export async function fetchPatientIdentifierTypesWithSources(): Promise<Array<PatientIdentifierType>> {
68
96
  const patientIdentifierTypes = await fetchPatientIdentifierTypes();
69
97
 
70
- // @ts-ignore Reason: The required props of the type are generated below.
71
- const identifierTypes: Array<PatientIdentifierType> = patientIdentifierTypes.filter(Boolean);
98
+ const validIdentifierTypes = patientIdentifierTypes.filter(isValidFetchedIdentifierType);
99
+ // Convert FetchedPatientIdentifierType to PatientIdentifierType
100
+ const identifierTypes: Array<PatientIdentifierType> = validIdentifierTypes.map((type) => ({
101
+ ...type,
102
+ identifierSources: [],
103
+ }));
72
104
 
73
105
  const [autoGenOptions, identifierSourcesResponse] = await Promise.all([
74
106
  fetchAutoGenerationOptions(),
75
107
  fetchIdentifierSources(),
76
108
  ]);
77
109
 
78
- const allIdentifierSources = identifierSourcesResponse.data.results;
110
+ const allIdentifierSources = identifierSourcesResponse.data?.results || [];
79
111
 
80
- for (let i = 0; i < identifierTypes?.length; i++) {
112
+ for (let i = 0; i < identifierTypes.length; i++) {
81
113
  identifierTypes[i].identifierSources = allIdentifierSources
82
114
  .filter((source) => source.identifierType.uuid === identifierTypes[i].uuid)
83
115
  .map((source) => {
84
- const option = find(autoGenOptions.data.results, { source: { uuid: source.uuid } });
85
- source.autoGenerationOption = option;
116
+ const option = find(autoGenOptions.data?.results || [], { source: { uuid: source.uuid } });
117
+ if (option && 'manualEntryEnabled' in option && 'automaticGenerationEnabled' in option) {
118
+ source.autoGenerationOption = option as IdentifierSourceAutoGenerationOption;
119
+ }
86
120
  return source;
87
121
  });
88
122
  }
@@ -90,28 +124,40 @@ export async function fetchPatientIdentifierTypesWithSources(): Promise<Array<Pa
90
124
  return identifierTypes;
91
125
  }
92
126
 
93
- async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdentifierType>> {
127
+ interface ApiPatientIdentifierType {
128
+ display: string;
129
+ uuid: string;
130
+ name: string;
131
+ format: string;
132
+ formatDescription?: string;
133
+ required: boolean;
134
+ uniquenessBehavior: undefined | null | 'UNIQUE' | 'NON_UNIQUE' | 'LOCATION';
135
+ }
136
+
137
+ async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdentifierType | null>> {
94
138
  const [patientIdentifierTypesResponse, primaryIdentifierTypeResponse] = await Promise.all([
95
- cacheAndFetch(
139
+ cacheAndFetch<{ results: Array<ApiPatientIdentifierType> }>(
96
140
  `${restBaseUrl}/patientidentifiertype?v=custom:(display,uuid,name,format,formatDescription,required,uniquenessBehavior)`,
97
141
  ),
98
- cacheAndFetch(`${restBaseUrl}/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType`),
142
+ cacheAndFetch<{ results: Array<{ metadataUuid: string }> }>(
143
+ `${restBaseUrl}/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType`,
144
+ ),
99
145
  ]);
100
146
 
101
147
  if (patientIdentifierTypesResponse.ok) {
102
148
  // Primary identifier type is to be kept at the top of the list.
103
- const patientIdentifierTypes = patientIdentifierTypesResponse?.data?.results;
149
+ const patientIdentifierTypes = patientIdentifierTypesResponse?.data?.results || [];
104
150
 
105
151
  const primaryIdentifierTypeUuid = primaryIdentifierTypeResponse?.data?.results?.[0]?.metadataUuid;
106
152
 
107
- let identifierTypes = primaryIdentifierTypeResponse?.ok
108
- ? [
109
- mapPatientIdentifierType(
110
- patientIdentifierTypes?.find((type) => type.uuid === primaryIdentifierTypeUuid),
111
- true,
112
- ),
113
- ]
114
- : [];
153
+ const identifierTypes: Array<FetchedPatientIdentifierType | null> = [];
154
+
155
+ if (primaryIdentifierTypeResponse?.ok && primaryIdentifierTypeUuid) {
156
+ const primaryType = patientIdentifierTypes.find((type) => type.uuid === primaryIdentifierTypeUuid);
157
+ if (primaryType) {
158
+ identifierTypes.push(mapPatientIdentifierType(primaryType, true));
159
+ }
160
+ }
115
161
 
116
162
  patientIdentifierTypes.forEach((type) => {
117
163
  if (type.uuid !== primaryIdentifierTypeUuid) {
@@ -124,15 +170,32 @@ async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdenti
124
170
  return [];
125
171
  }
126
172
 
173
+ interface IdentifierSourceResponse {
174
+ results: Array<{
175
+ uuid: string;
176
+ name: string;
177
+ identifierType: { uuid: string };
178
+ autoGenerationOption?: IdentifierSourceAutoGenerationOption;
179
+ }>;
180
+ }
181
+
127
182
  async function fetchIdentifierSources() {
128
- return await cacheAndFetch(`${restBaseUrl}/idgen/identifiersource?v=default`);
183
+ return await cacheAndFetch<IdentifierSourceResponse>(`${restBaseUrl}/idgen/identifiersource?v=default`);
184
+ }
185
+
186
+ interface AutoGenerationOptionResponse {
187
+ results: Array<IdentifierSourceAutoGenerationOption & { source: { uuid: string } }>;
129
188
  }
130
189
 
131
190
  async function fetchAutoGenerationOptions(abortController?: AbortController) {
132
- return await cacheAndFetch(`${restBaseUrl}/idgen/autogenerationoption?v=full`);
191
+ return await cacheAndFetch<AutoGenerationOptionResponse>(`${restBaseUrl}/idgen/autogenerationoption?v=full`);
133
192
  }
134
193
 
135
- async function cacheAndFetch<T = any>(url?: string) {
194
+ async function cacheAndFetch<T = unknown>(url?: string): Promise<FetchResponse<T>> {
195
+ if (!url) {
196
+ throw new Error('URL is required for cacheAndFetch');
197
+ }
198
+
136
199
  const abortController = new AbortController();
137
200
 
138
201
  await messageOmrsServiceWorker({
@@ -143,7 +206,10 @@ async function cacheAndFetch<T = any>(url?: string) {
143
206
  return await openmrsFetch<T>(url, { headers: cacheForOfflineHeaders, signal: abortController?.signal });
144
207
  }
145
208
 
146
- function mapPatientIdentifierType(patientIdentifierType, isPrimary) {
209
+ function mapPatientIdentifierType(
210
+ patientIdentifierType: ApiPatientIdentifierType,
211
+ isPrimary: boolean,
212
+ ): FetchedPatientIdentifierType {
147
213
  return {
148
214
  name: patientIdentifierType.display,
149
215
  fieldName: camelCase(patientIdentifierType.name),
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach, afterEach, type Mock, type MockInstance } from 'vitest';
3
+ import { render, waitFor } from '@testing-library/react';
4
+ import { showModal, navigate } from '@openmrs/esm-framework';
5
+ import BeforeSavePrompt from './before-save-prompt.component';
6
+
7
+ const mockShowModal = vi.mocked(showModal);
8
+ const mockNavigate = vi.mocked(navigate);
9
+
10
+ const mockGetOpenmrsSpaBase = vi.fn(() => '/openmrs/spa');
11
+ window['getOpenmrsSpaBase'] = mockGetOpenmrsSpaBase;
12
+
13
+ /**
14
+ * Helper to render BeforeSavePrompt component.
15
+ */
16
+ function renderBeforeSavePrompt(when: boolean, redirect?: string) {
17
+ return render(<BeforeSavePrompt when={when} redirect={redirect} />);
18
+ }
19
+
20
+ describe('BeforeSavePrompt component', () => {
21
+ let addEventListenerSpy: MockInstance;
22
+ let removeEventListenerSpy: MockInstance;
23
+ let disposeMock: Mock;
24
+
25
+ beforeEach(() => {
26
+ disposeMock = vi.fn();
27
+ mockShowModal.mockReturnValue(disposeMock);
28
+ mockNavigate.mockClear();
29
+
30
+ // Spy on window.addEventListener and removeEventListener
31
+ addEventListenerSpy = vi.spyOn(window, 'addEventListener');
32
+ removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
33
+ });
34
+
35
+ afterEach(() => {
36
+ addEventListenerSpy.mockRestore();
37
+ removeEventListenerSpy.mockRestore();
38
+ });
39
+
40
+ describe('Event listener setup', () => {
41
+ it('does not set up event listeners when when is false', () => {
42
+ renderBeforeSavePrompt(false);
43
+
44
+ expect(addEventListenerSpy).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('sets up beforeunload and navigation event listeners when when is true', () => {
48
+ renderBeforeSavePrompt(true);
49
+
50
+ expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
51
+ expect(addEventListenerSpy).toHaveBeenCalledWith('single-spa:before-routing-event', expect.any(Function));
52
+ });
53
+
54
+ it('removes event listeners on unmount', () => {
55
+ const { unmount } = renderBeforeSavePrompt(true);
56
+
57
+ unmount();
58
+
59
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
60
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('single-spa:before-routing-event', expect.any(Function));
61
+ });
62
+
63
+ it('does not set up listeners when redirect is provided', () => {
64
+ renderBeforeSavePrompt(true, '/some/path');
65
+
66
+ expect(addEventListenerSpy).not.toHaveBeenCalled();
67
+ });
68
+ });
69
+
70
+ describe('Beforeunload event handling', () => {
71
+ it('prevents default and sets returnValue on beforeunload event', () => {
72
+ renderBeforeSavePrompt(true);
73
+
74
+ const beforeunloadHandler = addEventListenerSpy.mock.calls.find((call) => call[0] === 'beforeunload')?.[1] as (
75
+ e: BeforeUnloadEvent,
76
+ ) => string;
77
+
78
+ const mockEvent = {
79
+ preventDefault: vi.fn(),
80
+ returnValue: '',
81
+ } as unknown as BeforeUnloadEvent;
82
+
83
+ const result = beforeunloadHandler(mockEvent);
84
+
85
+ expect(mockEvent.preventDefault).toHaveBeenCalled();
86
+ expect(mockEvent.returnValue).toBeTruthy();
87
+ expect(result).toBeTruthy();
88
+ });
89
+ });
90
+
91
+ describe('Navigation event handling', () => {
92
+ it('shows modal and cancels navigation on single-spa routing event', () => {
93
+ renderBeforeSavePrompt(true);
94
+
95
+ const navigationHandler = addEventListenerSpy.mock.calls.find(
96
+ (call) => call[0] === 'single-spa:before-routing-event',
97
+ )?.[1] as (e: CustomEvent) => void;
98
+
99
+ const mockCancelNavigation = vi.fn();
100
+ const mockEvent = {
101
+ detail: {
102
+ newUrl: '/openmrs/spa/new-page',
103
+ navigationIsCanceled: false,
104
+ cancelNavigation: mockCancelNavigation,
105
+ },
106
+ } as unknown as CustomEvent;
107
+
108
+ navigationHandler(mockEvent);
109
+
110
+ expect(mockCancelNavigation).toHaveBeenCalled();
111
+ expect(mockShowModal).toHaveBeenCalledWith(
112
+ 'cancel-patient-edit-modal',
113
+ expect.objectContaining({
114
+ onConfirm: expect.any(Function),
115
+ }),
116
+ expect.any(Function),
117
+ );
118
+ });
119
+
120
+ it('does not show modal if navigation is already canceled', () => {
121
+ renderBeforeSavePrompt(true);
122
+
123
+ const navigationHandler = addEventListenerSpy.mock.calls.find(
124
+ (call) => call[0] === 'single-spa:before-routing-event',
125
+ )?.[1] as (e: CustomEvent) => void;
126
+
127
+ const mockEvent = {
128
+ detail: {
129
+ newUrl: '/openmrs/spa/new-page',
130
+ navigationIsCanceled: true,
131
+ cancelNavigation: vi.fn(),
132
+ },
133
+ } as unknown as CustomEvent;
134
+
135
+ navigationHandler(mockEvent);
136
+
137
+ expect(mockShowModal).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('navigates to target URL when modal is confirmed', async () => {
141
+ renderBeforeSavePrompt(true);
142
+
143
+ const navigationHandler = addEventListenerSpy.mock.calls.find(
144
+ (call) => call[0] === 'single-spa:before-routing-event',
145
+ )?.[1] as (e: CustomEvent) => void;
146
+
147
+ const mockEvent = {
148
+ detail: {
149
+ newUrl: '/openmrs/spa/new-page',
150
+ navigationIsCanceled: false,
151
+ cancelNavigation: vi.fn(),
152
+ },
153
+ } as unknown as CustomEvent;
154
+
155
+ // Call navigationHandler directly - it triggers showModal synchronously
156
+ navigationHandler(mockEvent);
157
+
158
+ // Get the onConfirm callback from showModal call
159
+ // showModal is called with: (modalName, props, onClose)
160
+ const modalProps = mockShowModal.mock.calls[0]?.[1] as { onConfirm?: () => void } | undefined;
161
+ const onConfirm = modalProps?.onConfirm;
162
+
163
+ expect(onConfirm).toBeDefined();
164
+
165
+ // Call onConfirm directly - waitFor will handle async updates
166
+ onConfirm?.();
167
+
168
+ await waitFor(() => {
169
+ expect(mockNavigate).toHaveBeenCalledWith({
170
+ to: expect.stringContaining('/new-page'),
171
+ });
172
+ });
173
+ expect(disposeMock).toHaveBeenCalled();
174
+ });
175
+ });
176
+
177
+ describe('Redirect handling', () => {
178
+ it('navigates to redirect URL when redirect prop is provided', async () => {
179
+ renderBeforeSavePrompt(true, '/openmrs/spa/target-page');
180
+
181
+ await waitFor(() => {
182
+ expect(mockNavigate).toHaveBeenCalledWith({
183
+ to: expect.stringContaining('/target-page'),
184
+ });
185
+ });
186
+ });
187
+
188
+ it('handles redirect with full URL', async () => {
189
+ mockGetOpenmrsSpaBase.mockReturnValue('/openmrs/spa');
190
+ renderBeforeSavePrompt(true, '/openmrs/spa/target-page');
191
+
192
+ await waitFor(() => {
193
+ expect(mockNavigate).toHaveBeenCalledWith({
194
+ to: expect.stringContaining('/target-page'),
195
+ });
196
+ });
197
+ });
198
+ });
199
+ });
@@ -1,11 +1,12 @@
1
+ import { vi } from 'vitest';
1
2
  import { type ConceptResponse } from '../../patient-registration.types';
2
3
 
3
- export const useConcept = jest.fn(function mockUseConceptImplementation(uuid: string): {
4
+ export const useConcept = vi.fn(function mockUseConceptImplementation(uuid: string): {
4
5
  data: ConceptResponse;
5
6
  isLoading: boolean;
6
7
  } {
7
8
  let data;
8
- if (uuid == 'weight-uuid') {
9
+ if (uuid === 'weight-uuid') {
9
10
  data = {
10
11
  uuid: 'weight-uuid',
11
12
  display: 'Weight (kg)',
@@ -13,7 +14,7 @@ export const useConcept = jest.fn(function mockUseConceptImplementation(uuid: st
13
14
  answers: [],
14
15
  setMembers: [],
15
16
  };
16
- } else if (uuid == 'chief-complaint-uuid') {
17
+ } else if (uuid === 'chief-complaint-uuid') {
17
18
  data = {
18
19
  uuid: 'chief-complaint-uuid',
19
20
  display: 'Chief Complaint',
@@ -21,7 +22,7 @@ export const useConcept = jest.fn(function mockUseConceptImplementation(uuid: st
21
22
  answers: [],
22
23
  setMembers: [],
23
24
  };
24
- } else if (uuid == 'nationality-uuid') {
25
+ } else if (uuid === 'nationality-uuid') {
25
26
  data = {
26
27
  uuid: 'nationality-uuid',
27
28
  display: 'Nationality',
@@ -39,8 +40,8 @@ export const useConcept = jest.fn(function mockUseConceptImplementation(uuid: st
39
40
  };
40
41
  });
41
42
 
42
- export const useConceptAnswers = jest.fn((uuid: string) => {
43
- if (uuid == 'nationality-uuid') {
43
+ export const useConceptAnswers = vi.fn((uuid: string) => {
44
+ if (uuid === 'nationality-uuid') {
44
45
  return {
45
46
  data: [
46
47
  { display: 'USA', uuid: 'usa' },
@@ -48,7 +49,7 @@ export const useConceptAnswers = jest.fn((uuid: string) => {
48
49
  ],
49
50
  isLoading: false,
50
51
  };
51
- } else if (uuid == 'other-countries-uuid') {
52
+ } else if (uuid === 'other-countries-uuid') {
52
53
  return {
53
54
  data: [
54
55
  { display: 'Kenya', uuid: 'ke' },