@openmrs/esm-form-engine-lib 2.1.0-pre.1362

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 (272) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +58 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +4 -0
  7. package/LICENSE.txt +401 -0
  8. package/README.md +136 -0
  9. package/__mocks__/concepts.mock.json +140 -0
  10. package/__mocks__/forms/afe-forms/component_art.json +38 -0
  11. package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
  12. package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
  13. package/__mocks__/forms/afe-forms/form-component.json +38 -0
  14. package/__mocks__/forms/afe-forms/mini-form.json +31 -0
  15. package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
  16. package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
  17. package/__mocks__/forms/afe-forms/test-orders.json +72 -0
  18. package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
  19. package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
  20. package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
  21. package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
  22. package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
  23. package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
  24. package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
  25. package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
  26. package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
  27. package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
  28. package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
  29. package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
  30. package/__mocks__/forms/rfe-forms/form-component.json +43 -0
  31. package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
  32. package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
  33. package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
  34. package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
  35. package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
  36. package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
  37. package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
  38. package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
  39. package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
  40. package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
  41. package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
  42. package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
  43. package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
  44. package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
  45. package/__mocks__/forms/rfe-forms/required-form.json +50 -0
  46. package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
  47. package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
  48. package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
  49. package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
  50. package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
  51. package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
  52. package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
  53. package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
  54. package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
  55. package/__mocks__/packages/test-forms-registry.ts +12 -0
  56. package/__mocks__/patient.mock.ts +173 -0
  57. package/__mocks__/react-i18next.js +49 -0
  58. package/__mocks__/react-markdown.tsx +5 -0
  59. package/__mocks__/session.mock.ts +117 -0
  60. package/__mocks__/single-spa-react.js +11 -0
  61. package/__mocks__/use-initial-values/encounter.mock.json +963 -0
  62. package/__mocks__/use-initial-values/patient.mock.json +73 -0
  63. package/__mocks__/visit.mock.ts +19 -0
  64. package/dist/openmrs-esm-form-engine-lib.js +1 -0
  65. package/jest.config.js +30 -0
  66. package/package.json +104 -0
  67. package/prettier.config.js +8 -0
  68. package/readme/form-engine.jpeg +0 -0
  69. package/src/adapters/control-adapter.ts +29 -0
  70. package/src/adapters/encounter-datetime-adapter.ts +38 -0
  71. package/src/adapters/encounter-location-adapter.ts +39 -0
  72. package/src/adapters/encounter-provider-adapter.ts +48 -0
  73. package/src/adapters/encounter-role-adapter.ts +54 -0
  74. package/src/adapters/inline-date-adapter.ts +58 -0
  75. package/src/adapters/obs-adapter.ts +280 -0
  76. package/src/adapters/obs-comment-adapter.ts +60 -0
  77. package/src/adapters/orders-adapter.ts +75 -0
  78. package/src/adapters/patient-identifier-adapter.ts +40 -0
  79. package/src/adapters/program-state-adapter.ts +52 -0
  80. package/src/api/index.ts +178 -0
  81. package/src/components/error/error-modal.component.tsx +37 -0
  82. package/src/components/error/error.scss +4 -0
  83. package/src/components/extension/extension-parcel.component.tsx +32 -0
  84. package/src/components/field-label/field-label.component.tsx +32 -0
  85. package/src/components/field-label/field-label.scss +11 -0
  86. package/src/components/group/obs-group.component.tsx +29 -0
  87. package/src/components/group/obs-group.scss +12 -0
  88. package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
  89. package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
  90. package/src/components/inputs/date/date.component.tsx +149 -0
  91. package/src/components/inputs/date/date.scss +36 -0
  92. package/src/components/inputs/file/camera/camera.component.tsx +34 -0
  93. package/src/components/inputs/file/camera/camera.scss +3 -0
  94. package/src/components/inputs/file/file.component.tsx +159 -0
  95. package/src/components/inputs/file/file.scss +101 -0
  96. package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
  97. package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
  98. package/src/components/inputs/markdown/markdown.component.tsx +8 -0
  99. package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
  100. package/src/components/inputs/multi-select/multi-select.scss +25 -0
  101. package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
  102. package/src/components/inputs/number/number.component.tsx +69 -0
  103. package/src/components/inputs/number/number.scss +15 -0
  104. package/src/components/inputs/radio/radio.component.tsx +79 -0
  105. package/src/components/inputs/radio/radio.scss +36 -0
  106. package/src/components/inputs/select/dropdown.component.tsx +73 -0
  107. package/src/components/inputs/select/dropdown.scss +11 -0
  108. package/src/components/inputs/select/dropdown.test.tsx +120 -0
  109. package/src/components/inputs/text/text.component.tsx +65 -0
  110. package/src/components/inputs/text/text.scss +15 -0
  111. package/src/components/inputs/text/text.test.tsx +104 -0
  112. package/src/components/inputs/text-area/text-area.component.tsx +63 -0
  113. package/src/components/inputs/text-area/text-area.scss +11 -0
  114. package/src/components/inputs/toggle/toggle.component.tsx +66 -0
  115. package/src/components/inputs/toggle/toggle.scss +12 -0
  116. package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
  117. package/src/components/inputs/tooltip/tooltip.scss +8 -0
  118. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
  119. package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
  120. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
  121. package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
  122. package/src/components/inputs/unspecified/unspecified.scss +7 -0
  123. package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
  124. package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
  125. package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
  126. package/src/components/label/label.component.tsx +20 -0
  127. package/src/components/label/label.scss +11 -0
  128. package/src/components/loaders/loader.component.tsx +16 -0
  129. package/src/components/loaders/loader.scss +20 -0
  130. package/src/components/patient-banner/patient-banner.component.tsx +20 -0
  131. package/src/components/patient-banner/patient-banner.scss +12 -0
  132. package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
  133. package/src/components/previous-value-review/previous-value-review.scss +36 -0
  134. package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
  135. package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
  136. package/src/components/renderer/field/fieldLogic.ts +214 -0
  137. package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
  138. package/src/components/renderer/field/form-field-renderer.scss +5 -0
  139. package/src/components/renderer/form/form-renderer.component.tsx +89 -0
  140. package/src/components/renderer/form/state.ts +54 -0
  141. package/src/components/renderer/page/page.renderer.component.tsx +50 -0
  142. package/src/components/renderer/page/page.renderer.scss +36 -0
  143. package/src/components/renderer/section/section-renderer.component.tsx +21 -0
  144. package/src/components/renderer/section/section-renderer.scss +19 -0
  145. package/src/components/repeat/helpers.test.ts +29 -0
  146. package/src/components/repeat/helpers.ts +68 -0
  147. package/src/components/repeat/repeat-controls.component.tsx +38 -0
  148. package/src/components/repeat/repeat-controls.scss +7 -0
  149. package/src/components/repeat/repeat.component.tsx +201 -0
  150. package/src/components/repeat/repeat.scss +30 -0
  151. package/src/components/repeat/repeat.test.ts +29 -0
  152. package/src/components/sidebar/sidebar.component.tsx +134 -0
  153. package/src/components/sidebar/sidebar.scss +121 -0
  154. package/src/components/value/value.component.tsx +27 -0
  155. package/src/components/value/value.scss +17 -0
  156. package/src/components/value/view/field-value-view.component.tsx +33 -0
  157. package/src/components/value/view/field-value-view.scss +31 -0
  158. package/src/constants.ts +12 -0
  159. package/src/datasources/concept-data-source.ts +42 -0
  160. package/src/datasources/data-source.ts +23 -0
  161. package/src/datasources/encounter-role-datasource.ts +15 -0
  162. package/src/datasources/historical-data-source.ts +11 -0
  163. package/src/datasources/location-data-source.ts +27 -0
  164. package/src/datasources/provider-datasource.ts +15 -0
  165. package/src/datasources/select-concept-answers-datasource.ts +15 -0
  166. package/src/declarations.d.ts +4 -0
  167. package/src/external-function-context.tsx +8 -0
  168. package/src/form-context.tsx +42 -0
  169. package/src/form-engine.component.tsx +178 -0
  170. package/src/form-engine.scss +140 -0
  171. package/src/form-engine.test.tsx +817 -0
  172. package/src/globals.ts +1 -0
  173. package/src/hooks/useClobData.tsx +21 -0
  174. package/src/hooks/useConcepts.tsx +55 -0
  175. package/src/hooks/useDatasourceDependentValue.ts +16 -0
  176. package/src/hooks/useEncounter.tsx +32 -0
  177. package/src/hooks/useEncounterRole.tsx +15 -0
  178. package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
  179. package/src/hooks/useFieldValidationResults.ts +18 -0
  180. package/src/hooks/useFormCollapse.tsx +36 -0
  181. package/src/hooks/useFormFieldValidators.ts +22 -0
  182. package/src/hooks/useFormFieldValueAdapters.ts +24 -0
  183. package/src/hooks/useFormFields.ts +37 -0
  184. package/src/hooks/useFormFieldsMeta.ts +48 -0
  185. package/src/hooks/useFormJson.test.tsx +173 -0
  186. package/src/hooks/useFormJson.tsx +237 -0
  187. package/src/hooks/useFormStateHelpers.ts +50 -0
  188. package/src/hooks/useFormsConfig.tsx +27 -0
  189. package/src/hooks/useInitialValues.ts +38 -0
  190. package/src/hooks/usePatientData.tsx +32 -0
  191. package/src/hooks/usePatientPrograms.ts +32 -0
  192. package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
  193. package/src/hooks/usePostSubmissionActions.ts +31 -0
  194. package/src/hooks/useProcessorDependencies.ts +30 -0
  195. package/src/hooks/useRestMaxResultsCount.ts +5 -0
  196. package/src/hooks/useSystemSetting.ts +36 -0
  197. package/src/hooks/useWorkspaceLayout.ts +29 -0
  198. package/src/index.ts +12 -0
  199. package/src/lifecycle.ts +33 -0
  200. package/src/post-submission-actions/program-enrollment-action.ts +138 -0
  201. package/src/processors/encounter/encounter-form-processor.ts +337 -0
  202. package/src/processors/encounter/encounter-processor-helper.ts +320 -0
  203. package/src/processors/form-processor.ts +41 -0
  204. package/src/provider/form-factory-helper.ts +100 -0
  205. package/src/provider/form-factory-provider.tsx +169 -0
  206. package/src/provider/form-provider.tsx +37 -0
  207. package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
  208. package/src/registry/inbuilt-components/control-templates.ts +57 -0
  209. package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
  210. package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
  211. package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
  212. package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
  213. package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
  214. package/src/registry/inbuilt-components/template-component-map.ts +28 -0
  215. package/src/registry/registry.test.ts +20 -0
  216. package/src/registry/registry.ts +261 -0
  217. package/src/routes.json +1 -0
  218. package/src/setupI18n.ts +16 -0
  219. package/src/setupTests.ts +5 -0
  220. package/src/transformers/default-schema-transformer.test.ts +155 -0
  221. package/src/transformers/default-schema-transformer.ts +239 -0
  222. package/src/types/domain.ts +129 -0
  223. package/src/types/index.ts +130 -0
  224. package/src/types/schema.ts +238 -0
  225. package/src/typings.d.ts +9 -0
  226. package/src/utils/boolean-utils.ts +25 -0
  227. package/src/utils/common-expression-helpers.ts +503 -0
  228. package/src/utils/common-utils.test.ts +136 -0
  229. package/src/utils/common-utils.ts +55 -0
  230. package/src/utils/error-utils.ts +37 -0
  231. package/src/utils/expression-parser.test.ts +308 -0
  232. package/src/utils/expression-parser.ts +158 -0
  233. package/src/utils/expression-runner.test.ts +387 -0
  234. package/src/utils/expression-runner.ts +219 -0
  235. package/src/utils/form-helper.test.ts +482 -0
  236. package/src/utils/form-helper.ts +210 -0
  237. package/src/utils/form-page-utils.ts +13 -0
  238. package/src/utils/forms-loader.test.ts +323 -0
  239. package/src/utils/forms-loader.ts +306 -0
  240. package/src/utils/post-submission-action-helper.ts +71 -0
  241. package/src/utils/test-utils.ts +54 -0
  242. package/src/utils/zscore-service.ts +59 -0
  243. package/src/validators/conditional-answered-validator.test.ts +61 -0
  244. package/src/validators/conditional-answered-validator.ts +17 -0
  245. package/src/validators/date-validator.test.ts +46 -0
  246. package/src/validators/date-validator.ts +19 -0
  247. package/src/validators/default-value-validator.test.ts +90 -0
  248. package/src/validators/default-value-validator.ts +36 -0
  249. package/src/validators/form-validator.test.ts +188 -0
  250. package/src/validators/form-validator.ts +95 -0
  251. package/src/validators/js-expression-validator.test.ts +118 -0
  252. package/src/validators/js-expression-validator.ts +44 -0
  253. package/src/validators/schema.ts +34 -0
  254. package/src/zscore/bfa_boys_5_above.json +2522 -0
  255. package/src/zscore/bfa_girls_5_above.json +2522 -0
  256. package/src/zscore/hfa_boys_5_above.json +2186 -0
  257. package/src/zscore/hfa_boys_below5.json +22286 -0
  258. package/src/zscore/hfa_girls_5_above.json +2186 -0
  259. package/src/zscore/hfa_girls_below5.json +22286 -0
  260. package/src/zscore/wfl_boys_below5.json +7814 -0
  261. package/src/zscore/wfl_girls_below5.json +7814 -0
  262. package/src/zscore-tests/bmi-age.test.tsx +88 -0
  263. package/src/zscore-tests/height-age.test.tsx +96 -0
  264. package/src/zscore-tests/weight-height.test.tsx +87 -0
  265. package/tools/i18next-parser.config.js +93 -0
  266. package/translations/en.json +47 -0
  267. package/translations/es.json +38 -0
  268. package/translations/fr.json +38 -0
  269. package/translations/km.json +38 -0
  270. package/tsconfig.json +19 -0
  271. package/turbo.json +15 -0
  272. package/webpack.config.js +1 -0
@@ -0,0 +1,323 @@
1
+ import { type FormJsonFile, getForm, getFormByVersion, getLatestFormVersion, applyFormIntent } from './forms-loader';
2
+ import formsRegistry from '__mocks__/packages/test-forms-registry';
3
+ import {
4
+ htsHivtestResultingSchemaV2,
5
+ htsRetrospectiveResultingSchemaV2,
6
+ htsWildcardResultingSchemaV2,
7
+ testSchemaV2,
8
+ } from '__mocks__/forms/rfe-forms/forms-loader.test.schema';
9
+
10
+ const htsTestForms: FormJsonFile[] = [
11
+ {
12
+ version: '1.0',
13
+ semanticVersion: '1.0.0',
14
+ json: {
15
+ name: 'Test HTS POC',
16
+ pages: [],
17
+ processor: 'EncounterFormProcessor',
18
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
19
+ referencedForms: [],
20
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
21
+ },
22
+ },
23
+ {
24
+ version: '1.1',
25
+ semanticVersion: '1.1.0',
26
+ json: {
27
+ name: 'Test HTS POC',
28
+ pages: [],
29
+ processor: null,
30
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
31
+ referencedForms: [],
32
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
33
+ },
34
+ },
35
+ {
36
+ version: '2.0',
37
+ semanticVersion: '2.0.0',
38
+ json: {
39
+ name: 'Test HTS POC',
40
+ pages: [],
41
+ processor: null,
42
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
43
+ referencedForms: ['f23de883-ea70-45e5-bff7-eff334b28c4b'],
44
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
45
+ },
46
+ },
47
+ ];
48
+
49
+ describe('Forms loader - getForm', () => {
50
+ it('should get latest form if no version was specified', () => {
51
+ // replay
52
+ const latestHTSForm = getForm('hiv', 'hts_poc', null, false, formsRegistry);
53
+ // verify
54
+ expect(latestHTSForm).toEqual({
55
+ name: 'Test HTS POC',
56
+ pages: [
57
+ {
58
+ label: 'Screening',
59
+ sections: [
60
+ {
61
+ label: 'Testing history',
62
+ isExpanded: 'true',
63
+ questions: [
64
+ {
65
+ label: 'When was the HIV test conducted?',
66
+ type: 'obs',
67
+ questionOptions: {
68
+ rendering: 'date',
69
+ concept: '164400AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
70
+ weeksList: '',
71
+ },
72
+ required: 'true',
73
+ unspecified: 'true',
74
+ hide: {
75
+ hideWhenExpression: 'false',
76
+ },
77
+ validators: [
78
+ {
79
+ type: 'date',
80
+ allowFutureDates: 'false',
81
+ },
82
+ {
83
+ type: 'js_expression',
84
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
85
+ },
86
+ ],
87
+ behaviours: [
88
+ {
89
+ intent: 'HTS_RETROSPECTIVE',
90
+ required: 'true',
91
+ unspecified: 'true',
92
+ hide: {
93
+ hideWhenExpression: 'false',
94
+ },
95
+ validators: [
96
+ {
97
+ type: 'date',
98
+ allowFutureDates: 'false',
99
+ },
100
+ {
101
+ type: 'js_expression',
102
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
103
+ },
104
+ ],
105
+ },
106
+ {
107
+ intent: 'HTS_HIVTEST',
108
+ required: 'true',
109
+ },
110
+ {
111
+ intent: '',
112
+ required: 'false',
113
+ hide: {
114
+ hideWhenExpression: "hivTestConducted !== 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3'",
115
+ },
116
+ validators: [
117
+ {
118
+ type: 'date',
119
+ allowFutureDates: 'false',
120
+ },
121
+ {
122
+ type: 'js_expression',
123
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ id: 'dateTestPerformed',
129
+ },
130
+ ],
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ availableIntents: ['HTS_RETROSPECTIVE', 'HTS_HIVTEST', '*'],
136
+ processor: 'EncounterFormProcessor',
137
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
138
+ referencedForms: [],
139
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
140
+ });
141
+ });
142
+
143
+ it('should get form with specified version', () => {
144
+ // replay
145
+ const htsFormV1_0 = getForm('hiv', 'hts_poc', '1.0', false, formsRegistry);
146
+ // verify
147
+ expect(htsFormV1_0).toEqual({
148
+ name: 'Test HTS POC',
149
+ pages: [],
150
+ processor: 'EncounterFormProcessor',
151
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
152
+ referencedForms: [],
153
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
154
+ });
155
+ });
156
+
157
+ it('should throw an error if specified version was not found while in strict mode', () => {
158
+ // replay
159
+ try {
160
+ getForm('hiv', 'hts_poc', '9.1', true, formsRegistry);
161
+ // fail test if this point is hit
162
+ fail('An error was expected to be called');
163
+ } catch (error) {
164
+ // verify
165
+ expect(error.message).toBe("Couldn't find form with version: 9.1");
166
+ }
167
+ });
168
+
169
+ it('should get lastet if required version was not found while in none strict mode', () => {
170
+ // replay
171
+ const latestForm = getForm('hiv', 'hts_poc', '9.1', false, formsRegistry);
172
+ // verify
173
+ expect(latestForm).toEqual({
174
+ name: 'Test HTS POC',
175
+ pages: [
176
+ {
177
+ label: 'Screening',
178
+ sections: [
179
+ {
180
+ label: 'Testing history',
181
+ isExpanded: 'true',
182
+ questions: [
183
+ {
184
+ label: 'When was the HIV test conducted?',
185
+ type: 'obs',
186
+ questionOptions: {
187
+ rendering: 'date',
188
+ concept: '164400AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
189
+ weeksList: '',
190
+ },
191
+ required: 'true',
192
+ unspecified: 'true',
193
+ hide: {
194
+ hideWhenExpression: 'false',
195
+ },
196
+ validators: [
197
+ {
198
+ type: 'date',
199
+ allowFutureDates: 'false',
200
+ },
201
+ {
202
+ type: 'js_expression',
203
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
204
+ },
205
+ ],
206
+ behaviours: [
207
+ {
208
+ intent: 'HTS_RETROSPECTIVE',
209
+ required: 'true',
210
+ unspecified: 'true',
211
+ hide: {
212
+ hideWhenExpression: 'false',
213
+ },
214
+ validators: [
215
+ {
216
+ type: 'date',
217
+ allowFutureDates: 'false',
218
+ },
219
+ {
220
+ type: 'js_expression',
221
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
222
+ },
223
+ ],
224
+ },
225
+ {
226
+ intent: 'HTS_HIVTEST',
227
+ required: 'true',
228
+ },
229
+ {
230
+ intent: '',
231
+ required: 'false',
232
+ hide: {
233
+ hideWhenExpression: "hivTestConducted !== 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3'",
234
+ },
235
+ validators: [
236
+ {
237
+ type: 'date',
238
+ allowFutureDates: 'false',
239
+ },
240
+ {
241
+ type: 'js_expression',
242
+ failsWhenExpression: "myValue < '1/1/1980' || myValue > today()",
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ id: 'dateTestPerformed',
248
+ },
249
+ ],
250
+ },
251
+ ],
252
+ },
253
+ ],
254
+ availableIntents: ['HTS_RETROSPECTIVE', 'HTS_HIVTEST', '*'],
255
+ processor: 'EncounterFormProcessor',
256
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
257
+ referencedForms: [],
258
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('Forms loader - getLatestFormVersion', () => {
264
+ it('should get latest form', () => {
265
+ // replay
266
+ const latest = getLatestFormVersion(htsTestForms);
267
+
268
+ // verify
269
+ expect(latest).toEqual({
270
+ version: '2.0',
271
+ semanticVersion: '2.0.0',
272
+ json: {
273
+ name: 'Test HTS POC',
274
+ pages: [],
275
+ processor: null,
276
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
277
+ referencedForms: ['f23de883-ea70-45e5-bff7-eff334b28c4b'],
278
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
279
+ },
280
+ });
281
+ });
282
+ });
283
+
284
+ describe('Forms loader - getFormByVersion', () => {
285
+ it('should get required form version', () => {
286
+ // replay
287
+ const htsFormV1_1 = getFormByVersion(htsTestForms, '1.1');
288
+
289
+ // verify
290
+ expect(htsFormV1_1).toEqual({
291
+ version: '1.1',
292
+ semanticVersion: '1.1.0',
293
+ json: {
294
+ name: 'Test HTS POC',
295
+ pages: [],
296
+ processor: null,
297
+ uuid: 'da24c540-cc83-43bc-978f-c1ef180a497f',
298
+ referencedForms: [],
299
+ encounterType: '79c1f50f-f77d-42e2-ad2a-d29304dde2fe',
300
+ },
301
+ });
302
+ });
303
+ });
304
+
305
+ xdescribe('Forms loader - applyFormIntent', () => {
306
+ it('should return correct fields for HTS_RETROSPECTIVE intent', () => {
307
+ let resultingSchema = applyFormIntent('HTS_RETROSPECTIVE', testSchemaV2);
308
+
309
+ expect(resultingSchema).toEqual(htsRetrospectiveResultingSchemaV2);
310
+ });
311
+
312
+ it('should return correct fields for HTS_HIVTEST intent', () => {
313
+ let resultingSchema = applyFormIntent('HTS_HIVTEST', testSchemaV2);
314
+
315
+ expect(resultingSchema).toEqual(htsHivtestResultingSchemaV2);
316
+ });
317
+
318
+ it('should return correct fields for * intent', () => {
319
+ let resultingSchema = applyFormIntent('*', testSchemaV2);
320
+
321
+ expect(resultingSchema).toEqual(htsWildcardResultingSchemaV2);
322
+ });
323
+ });
@@ -0,0 +1,306 @@
1
+ import * as semver from 'semver';
2
+ import { type FormField } from '../types';
3
+
4
+ let baseRegistry = {};
5
+ export interface FormJsonFile {
6
+ version: string;
7
+ semanticVersion?: string;
8
+ json: any;
9
+ }
10
+
11
+ /**
12
+ * This is a form behaviour property applied on `page` or `section` or `question`
13
+ */
14
+ interface BehaviourProperty {
15
+ name: string;
16
+ type: 'field' | 'section' | 'page' | 'all';
17
+ value: string;
18
+ }
19
+
20
+ /**
21
+ * Convinience function for loading form(s) associated to a given package or form version.
22
+ *
23
+ * @param packageName The package associated with the form
24
+ * @param formNamespace The form namespace
25
+ * @param version The form version
26
+ * @param isStrict If `true`, throws error if specified form version wasn't found
27
+ * @param formsRegistry Form registry. (This was added for testing purposes)
28
+ * @returns The form json
29
+ */
30
+ export function getForm(
31
+ packageName: string,
32
+ formNamespace: string,
33
+ version?: string,
34
+ isStrict?: boolean,
35
+ formsRegistry?: any,
36
+ ) {
37
+ const forms = lookupForms(packageName, formNamespace, formsRegistry);
38
+ let form = null;
39
+ if (version) {
40
+ form = getFormByVersion(forms, version, isStrict);
41
+ }
42
+ if (!form) {
43
+ form = getLatestFormVersion(forms);
44
+ }
45
+ return loadSubforms(form.json);
46
+ }
47
+
48
+ export function loadSubforms(parentForm) {
49
+ parentForm.pages = parentForm.pages || [];
50
+ parentForm.pages.forEach((page) => {
51
+ if (page.isSubform && page.subform?.name && page.subform.package) {
52
+ try {
53
+ const subform = getForm(page.subform.package, page.subform.name);
54
+ if (!subform) {
55
+ console.error(`Form with name "${page.subform.package}/${page.subform.name}" was not found in registry.`);
56
+ }
57
+ page.subform.form = subform;
58
+ } catch (error) {
59
+ console.error(error);
60
+ }
61
+ }
62
+ });
63
+ return parentForm;
64
+ }
65
+
66
+ export function getLatestFormVersion(forms: FormJsonFile[]) {
67
+ if (forms.length == 1) {
68
+ return forms[0];
69
+ }
70
+ const candidates = forms.map((f) => f.semanticVersion);
71
+ const latest = candidates.sort(formsVersionComparator)[candidates.length - 1];
72
+ return forms.find((f) => f.semanticVersion == latest);
73
+ }
74
+
75
+ export function getFormByVersion(forms: FormJsonFile[], requiredVersion: string, isStrict?: boolean) {
76
+ for (let form of forms) {
77
+ if (semver.satisfies(form.semanticVersion, requiredVersion)) {
78
+ return form;
79
+ }
80
+ }
81
+ if (isStrict) {
82
+ throw new Error(`Couldn't find form with version: ${requiredVersion}`);
83
+ } else {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export function lookupForms(packageName, formNamespace, formsRegistry) {
89
+ const pkg = formsRegistry ? formsRegistry[packageName] : baseRegistry[packageName];
90
+ if (!pkg) {
91
+ throw Error(`Package with name ${packageName} was not found in registry`);
92
+ }
93
+ if (!pkg[formNamespace]) {
94
+ throw new Error(`Form namespace '${formNamespace}' was not found in forms registry`);
95
+ }
96
+ return Object.keys(pkg[formNamespace]).map((formVersion) => {
97
+ return {
98
+ version: formVersion,
99
+ semanticVersion: semver.coerce(formVersion).version,
100
+ json: pkg[formNamespace][formVersion],
101
+ };
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Function parses JSON form input and filters validation behaviours according to a given intent
107
+ *
108
+ * @param {string} intent The specified intent
109
+ * @param {object} originalJson The original JSON form schema object
110
+ * @param parentOverrides An array of behaviour overrides from parent form to be applied to a subform
111
+ * @returns {object} The form json
112
+ */
113
+ export function applyFormIntent(intent, originalJson, parentOverrides?: Array<BehaviourProperty>) {
114
+ if (!intent) {
115
+ return originalJson;
116
+ }
117
+ const jsonBuffer = JSON.parse(JSON.stringify(originalJson));
118
+ // Set the default page based on the current intent
119
+ jsonBuffer.defaultPage = jsonBuffer.availableIntents?.find(
120
+ (candidate) => candidate.intent === (intent?.intent || intent),
121
+ )?.defaultPage;
122
+
123
+ // filter form-level markdown behaviour
124
+ if (jsonBuffer.markdown) {
125
+ updateMarkdownRequiredBehaviour(jsonBuffer.markdown, intent);
126
+ }
127
+
128
+ // Before starting traversal, ensure nodes exist, at least as empty-arrays
129
+ jsonBuffer.pages = jsonBuffer.pages || [];
130
+
131
+ // Traverse the property tree with items of interest for validation
132
+ jsonBuffer.pages.forEach((page) => {
133
+ if (page.isSubform && page.subform?.form) {
134
+ const behaviourOverrides = [];
135
+ const targetBehaviour = page.subform.behaviours?.find(
136
+ (behaviour) => behaviour.intent == intent?.intent || intent,
137
+ );
138
+ if (targetBehaviour?.readonly !== undefined || targetBehaviour?.readonly != null) {
139
+ behaviourOverrides.push({ name: 'readonly', type: 'field', value: targetBehaviour?.readonly });
140
+ }
141
+
142
+ page.subform.form = applyFormIntent(
143
+ targetBehaviour?.subform_intent || '*',
144
+ page.subform?.form,
145
+ behaviourOverrides,
146
+ );
147
+ }
148
+ // TODO: Apply parentOverrides to pages if applicable
149
+ const pageBehaviour = page.behaviours?.find((behaviour) => behaviour.intent === (intent?.intent || intent));
150
+ if (pageBehaviour) {
151
+ page.hide = pageBehaviour?.hide;
152
+ page.readonly = pageBehaviour?.readonly;
153
+ } else {
154
+ const fallBackBehaviour = page.behaviours?.find((behaviour) => behaviour.intent === '*');
155
+ page.hide = fallBackBehaviour?.hide;
156
+ page.readonly = fallBackBehaviour?.readonly;
157
+ }
158
+
159
+ // filter page-level markdown behaviour
160
+ if (page.markdown) {
161
+ updateMarkdownRequiredBehaviour(page.markdown, intent);
162
+ }
163
+ // Before starting traversal, ensure nodes exist, at least as empty-arrays
164
+ page.sections = page.sections || [];
165
+
166
+ page.sections.forEach((section) => {
167
+ // TODO: Apply parentOverrides to sections if applicable
168
+ const secBehaviour = section.behaviours?.find((behaviour) => behaviour.intent === intent?.intent || intent);
169
+ if (secBehaviour) {
170
+ section.hide = secBehaviour?.hide;
171
+ } else {
172
+ const fallBackBehaviour = section.behaviours?.find((behaviour) => behaviour.intent === '*');
173
+ section.hide = fallBackBehaviour?.hide ?? section.hide;
174
+ }
175
+
176
+ // filter section-level markdown behaviour
177
+ if (section.markdown) {
178
+ updateMarkdownRequiredBehaviour(section.markdown, intent);
179
+ }
180
+
181
+ // Before starting traversal, ensure nodes exist, at least as empty-arrays
182
+ section.questions = section.questions || [];
183
+
184
+ section.questions.forEach((question: FormField) => {
185
+ if (question['behaviours']) {
186
+ updateQuestionRequiredBehaviour(question, intent?.intent || intent);
187
+ parentOverrides
188
+ ?.filter((override) => override.type == 'all' || override.type == 'field')
189
+ ?.forEach((override) => {
190
+ question[override.name] = override.value;
191
+ });
192
+ }
193
+
194
+ if (question.questions && question.questions.length) {
195
+ question.questions.forEach((childQuestion) => {
196
+ updateQuestionRequiredBehaviour(childQuestion, intent?.intent || intent);
197
+
198
+ parentOverrides
199
+ ?.filter((override) => override.type == 'all' || override.type == 'field')
200
+ ?.forEach((override) => {
201
+ childQuestion[override.name] = override.value;
202
+ });
203
+ });
204
+ }
205
+ });
206
+ });
207
+ });
208
+ return jsonBuffer;
209
+ }
210
+
211
+ // Helpers
212
+
213
+ function updateQuestionRequiredBehaviour(question, intent: string) {
214
+ const requiredIntentBehaviour = question.behaviours?.find((behaviour) => behaviour.intent === intent);
215
+
216
+ const defaultIntentBehaviour = question.behaviours?.find((bevahiour) => bevahiour.intent === '*');
217
+ // If both required and default intents exist, combine them and update to question
218
+ if (requiredIntentBehaviour || defaultIntentBehaviour) {
219
+ // Remove the intent name props from each object
220
+ delete requiredIntentBehaviour?.intent;
221
+ delete defaultIntentBehaviour?.intent;
222
+
223
+ // Combine required and default intents following the rules:
224
+ // 1. The default intent is applied to all other intents
225
+ // 2. Intent-specific behaviour overrides default behaviour
226
+ const combinedBehaviours = Object.assign(defaultIntentBehaviour || {}, requiredIntentBehaviour || {});
227
+ const defaultValue = combinedBehaviours.defaultValue;
228
+ if (defaultValue != undefined) {
229
+ // add the default value under the question options
230
+ question.questionOptions.defaultValue = defaultValue;
231
+ // delete it so that it's not added at the root level of the question
232
+ delete combinedBehaviours.defaultValue;
233
+ }
234
+ // Add the combinedBehaviours data to initial question
235
+ question = Object.assign(question, combinedBehaviours);
236
+ // Remove behaviours list
237
+ delete question.behaviours;
238
+ }
239
+ }
240
+
241
+ function updateMarkdownRequiredBehaviour(markdown, intent) {
242
+ const requiredIntentBehaviour = markdown.behaviours?.find((behaviour) => behaviour.intent === intent);
243
+ const defaultIntentBehaviour = markdown.behaviours?.find((behaviour) => behaviour.intent === '*');
244
+
245
+ if (requiredIntentBehaviour && defaultIntentBehaviour) {
246
+ delete requiredIntentBehaviour.intent;
247
+ delete defaultIntentBehaviour.intent;
248
+ const combinedBehaviours = Object.assign(defaultIntentBehaviour, requiredIntentBehaviour);
249
+
250
+ markdown = Object.assign(markdown, combinedBehaviours);
251
+ delete markdown.behaviours;
252
+ } else if (!requiredIntentBehaviour && defaultIntentBehaviour) {
253
+ delete defaultIntentBehaviour.intent;
254
+
255
+ markdown = Object.assign(markdown, defaultIntentBehaviour);
256
+ delete markdown.behaviours;
257
+ }
258
+ }
259
+
260
+ export function updateExcludeIntentBehaviour(excludedIntents: Array<string>, originalJson) {
261
+ originalJson.availableIntents = originalJson.availableIntents.filter(
262
+ (intent) => !excludedIntents.includes(intent?.intent || intent),
263
+ );
264
+ return originalJson;
265
+ }
266
+
267
+ export function addToBaseFormsRegistry(customRegistry: Record<string, any>) {
268
+ baseRegistry = { ...baseRegistry, ...customRegistry };
269
+ }
270
+
271
+ function isPositiveInteger(x) {
272
+ return /^\d+$/.test(x);
273
+ }
274
+
275
+ function formsVersionComparator(v1, v2) {
276
+ let v1parts = v1.split('.');
277
+ let v2parts = v2.split('.');
278
+ // First, validate both numbers are true version numbers
279
+ function validateParts(parts) {
280
+ for (let i = 0; i < parts.length; ++i) {
281
+ if (!isPositiveInteger(parts[i])) {
282
+ return false;
283
+ }
284
+ }
285
+ return true;
286
+ }
287
+ if (!validateParts(v1parts) || !validateParts(v2parts)) {
288
+ return NaN;
289
+ }
290
+ for (let i = 0; i < v1parts.length; ++i) {
291
+ if (v2parts.length === i) {
292
+ return 1;
293
+ }
294
+ if (v1parts[i] === v2parts[i]) {
295
+ continue;
296
+ }
297
+ if (v1parts[i] > v2parts[i]) {
298
+ return 1;
299
+ }
300
+ return -1;
301
+ }
302
+ if (v1parts.length != v2parts.length) {
303
+ return -1;
304
+ }
305
+ return 0;
306
+ }
@@ -0,0 +1,71 @@
1
+ export function evaluatePostSubmissionExpression(expression: string, encounters: any[]): boolean {
2
+ const encounter = encounters[0];
3
+ const regx = /(?:\w+|'(?:\\'|[^'\n])*')/g;
4
+ let match;
5
+ const fieldIds = new Set<string>();
6
+ try {
7
+ while ((match = regx.exec(expression))) {
8
+ const value = match[0].replace(/\\'/g, "'"); // Replace escaped single quotes
9
+
10
+ const isBoolean = /^(true|false)$/i.test(value);
11
+ const isNumber = /^-?\d+$/.test(value);
12
+ const isFloat = /^-?\d+\.\d+$/.test(value);
13
+
14
+ if (
15
+ !(value.startsWith("'") && value.endsWith("'")) &&
16
+ typeof value === 'string' &&
17
+ !isBoolean &&
18
+ !isNumber &&
19
+ !isFloat
20
+ ) {
21
+ fieldIds.add(value);
22
+ }
23
+ }
24
+
25
+ let fieldToValueMap = {};
26
+ let replacedExpression;
27
+ if (fieldIds.size) {
28
+ fieldToValueMap = getFieldValues(fieldIds, encounter);
29
+ }
30
+
31
+ if (Object.keys(fieldToValueMap).length) {
32
+ replacedExpression = expression.replace(/(\w+)/g, (match) => {
33
+ return Object.prototype.hasOwnProperty.call(fieldToValueMap, match) ? fieldToValueMap[match] : match;
34
+ });
35
+ } else {
36
+ replacedExpression = expression;
37
+ }
38
+
39
+ return eval(replacedExpression);
40
+ } catch (error) {
41
+ throw new Error('Error evaluating expression');
42
+ }
43
+ }
44
+
45
+ function getFieldValues(fieldIds: Set<string>, encounter: any): Record<string, any> {
46
+ const result: Record<string, any> = {};
47
+ fieldIds.forEach((fieldId) => {
48
+ let value = encounter.obs?.find((item) => item.formFieldPath.includes(fieldId))?.value;
49
+ if (typeof value === 'object') {
50
+ value = value.uuid;
51
+ }
52
+ if (value) {
53
+ value = formatValue(value);
54
+ }
55
+ result[fieldId] = value;
56
+ });
57
+
58
+ return result;
59
+ }
60
+
61
+ //This function wraps string values in single quotes which Javascript will evaluate
62
+ function formatValue(value: any): any {
63
+ if (typeof value === 'string') {
64
+ if (value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'") {
65
+ return value;
66
+ } else {
67
+ return `'${value}'`;
68
+ }
69
+ }
70
+ return value;
71
+ }