@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,482 @@
1
+ import {
2
+ findConceptByReference,
3
+ evaluateConditionalAnswered,
4
+ evaluateFieldReadonlyProp,
5
+ parseToLocalDateTime,
6
+ evaluateDisabled,
7
+ } from './form-helper';
8
+ import { DefaultValueValidator } from '../validators/default-value-validator';
9
+ import { type LayoutType } from '@openmrs/esm-framework';
10
+ import { ConceptTrue } from '../constants';
11
+ import { type FormField, type OpenmrsEncounter, type SessionMode } from '../types';
12
+ import { type EncounterContext } from '../form-context';
13
+
14
+ jest.mock('../validators/default-value-validator');
15
+
16
+ describe('Form Engine Helper', () => {
17
+ describe('findConceptByReference', () => {
18
+ const concepts = [
19
+ {
20
+ uuid: '3cd6f600-26fe-102b-80cb-0017a47871b2',
21
+ display: 'Yes',
22
+ conceptMappings: [
23
+ {
24
+ conceptReferenceTerm: {
25
+ conceptSource: {
26
+ name: 'SNOMED CT',
27
+ },
28
+ code: '373066001',
29
+ },
30
+ },
31
+ {
32
+ conceptReferenceTerm: {
33
+ conceptSource: {
34
+ name: 'PIH',
35
+ },
36
+ code: 'YES',
37
+ },
38
+ },
39
+ {
40
+ conceptReferenceTerm: {
41
+ conceptSource: {
42
+ name: 'CIEL',
43
+ },
44
+ code: '1065',
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ {
50
+ uuid: '3cd6f86c-26fe-102b-80cb-0017a47871b2',
51
+ display: 'No',
52
+ conceptMappings: [
53
+ {
54
+ conceptReferenceTerm: {
55
+ conceptSource: {
56
+ name: 'PIH',
57
+ },
58
+ code: 'NO',
59
+ },
60
+ },
61
+ {
62
+ conceptReferenceTerm: {
63
+ conceptSource: {
64
+ name: 'CIEL',
65
+ },
66
+ code: '1066',
67
+ },
68
+ },
69
+ {
70
+ conceptReferenceTerm: {
71
+ conceptSource: {
72
+ name: 'SNOMED CT',
73
+ },
74
+ code: '373067005',
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ ];
80
+
81
+ it('should find concept by mapping', () => {
82
+ expect(findConceptByReference('CIEL:1066', concepts).uuid).toBe('3cd6f86c-26fe-102b-80cb-0017a47871b2');
83
+ });
84
+ it('should find concept by uuid', () => {
85
+ expect(findConceptByReference('3cd6f86c-26fe-102b-80cb-0017a47871b2', concepts).uuid).toBe(
86
+ '3cd6f86c-26fe-102b-80cb-0017a47871b2',
87
+ );
88
+ });
89
+ it('should return undefined if no match', () => {
90
+ expect(findConceptByReference('CIEL:9999', concepts)).toBeUndefined();
91
+ });
92
+ it('should return undefined if null input', () => {
93
+ expect(findConceptByReference(null, concepts)).toBeUndefined();
94
+ });
95
+ });
96
+
97
+ // describe('inferInitialValueFromDefaultFieldValue', () => {
98
+ // const mockHandleFieldSubmission = jest.fn();
99
+ // const mockHandler = {
100
+ // handleFieldSubmission: mockHandleFieldSubmission,
101
+ // getInitialValue: function (
102
+ // encounter: OpenmrsEncounter,
103
+ // field: FormField,
104
+ // allFormFields?: FormField[],
105
+ // context?: EncounterContext,
106
+ // ): {} {
107
+ // throw new Error('Function not implemented.');
108
+ // },
109
+ // getDisplayValue: function (field: FormField, value: any) {
110
+ // throw new Error('Function not implemented.');
111
+ // },
112
+ // };
113
+
114
+ // const sampleContext: EncounterContext = {
115
+ // patient: {
116
+ // id: '833db896-c1f0-11eb-8529-0242ac130003',
117
+ // },
118
+ // encounter: {
119
+ // uuid: '773455da-3ec4-453c-b565-7c1fe35426be',
120
+ // encounterProviders: [],
121
+ // obs: [],
122
+ // },
123
+ // location: {},
124
+ // sessionMode: 'edit',
125
+ // encounterDate: new Date(),
126
+ // setEncounterDate: jest.fn(),
127
+ // encounterProvider: '',
128
+ // setEncounterProvider: jest.fn(),
129
+ // setEncounterLocation: jest.fn(),
130
+ // encounterRole: '',
131
+ // setEncounterRole: jest.fn(),
132
+ // };
133
+
134
+ // beforeEach(() => {
135
+ // jest.clearAllMocks();
136
+ // });
137
+
138
+ // it('should return true if rendering is toggle and default value is ConceptTrue', () => {
139
+ // const sampleField: FormField = {
140
+ // label: 'Sample Toggle Field',
141
+ // type: 'obs',
142
+ // questionOptions: { rendering: 'toggle', defaultValue: ConceptTrue },
143
+ // id: 'toggle-field',
144
+ // };
145
+
146
+ // const result = inferInitialValueFromDefaultFieldValue(sampleField, sampleContext, mockHandler);
147
+
148
+ // expect(result).toBe(true);
149
+ // });
150
+
151
+ // it('should validate default value and handle field submission if valid', () => {
152
+ // const sampleField: FormField = {
153
+ // label: 'Sample Field',
154
+ // type: 'obs',
155
+ // questionOptions: { rendering: 'text', defaultValue: 'valid-value' },
156
+ // id: 'text-field',
157
+ // };
158
+
159
+ // (DefaultValueValidator.validate as jest.Mock).mockReturnValue([]);
160
+
161
+ // const result = inferInitialValueFromDefaultFieldValue(sampleField, sampleContext, mockHandler);
162
+
163
+ // expect(DefaultValueValidator.validate).toHaveBeenCalledWith(sampleField, 'valid-value');
164
+ // expect(mockHandleFieldSubmission).toHaveBeenCalledWith(sampleField, 'valid-value', sampleContext);
165
+ // expect(result).toBe('valid-value');
166
+ // });
167
+
168
+ // it('should not handle field submission if default value is invalid', () => {
169
+ // const sampleField: FormField = {
170
+ // label: 'Sample Field',
171
+ // type: 'obs',
172
+ // questionOptions: { rendering: 'text', defaultValue: 'invalid-value' },
173
+ // id: 'text-field',
174
+ // };
175
+
176
+ // (DefaultValueValidator.validate as jest.Mock).mockReturnValue(['Error: Invalid value']);
177
+
178
+ // const result = inferInitialValueFromDefaultFieldValue(sampleField, sampleContext, mockHandler);
179
+
180
+ // expect(DefaultValueValidator.validate).toHaveBeenCalledWith(sampleField, 'invalid-value');
181
+ // expect(mockHandleFieldSubmission).not.toHaveBeenCalled();
182
+ // expect(result).toBeUndefined();
183
+ // });
184
+ // });
185
+
186
+ // describe('isInlineView', () => {
187
+ // it('should return true if sessionMode is embedded-view', () => {
188
+ // const result = isInlineView('single-line', 'desktop' as LayoutType, 'maximized', 'embedded-view' as SessionMode);
189
+ // expect(result).toBe(true);
190
+ // });
191
+
192
+ // it('should return true if renderingType is automatic, workspaceLayout is maximized, and layoutType ends with desktop', () => {
193
+ // const result = isInlineView('automatic', 'large-desktop' as LayoutType, 'maximized', 'edit' as SessionMode);
194
+ // expect(result).toBe(true);
195
+ // });
196
+
197
+ // it('should return false if renderingType is automatic, workspaceLayout is maximized, but layoutType does not end with desktop', () => {
198
+ // const result = isInlineView('automatic', 'tablet' as LayoutType, 'maximized', 'edit' as SessionMode);
199
+ // expect(result).toBe(false);
200
+ // });
201
+
202
+ // it('should return true if renderingType is single-line', () => {
203
+ // const result = isInlineView('single-line', 'desktop' as LayoutType, 'minimized', 'edit' as SessionMode);
204
+ // expect(result).toBe(true);
205
+ // });
206
+
207
+ // it('should return false if renderingType is multiline', () => {
208
+ // const result = isInlineView('multiline', 'desktop' as LayoutType, 'maximized', 'edit' as SessionMode);
209
+ // expect(result).toBe(false);
210
+ // });
211
+
212
+ // it('should return false if renderingType is automatic and workspaceLayout is minimized', () => {
213
+ // const result = isInlineView('automatic', 'large-desktop' as LayoutType, 'minimized', 'edit' as SessionMode);
214
+ // expect(result).toBe(false);
215
+ // });
216
+
217
+ // it('should return false if renderingType is automatic and layoutType does not end with desktop', () => {
218
+ // const result = isInlineView('automatic', 'mobile' as LayoutType, 'maximized', 'edit' as SessionMode);
219
+ // expect(result).toBe(false);
220
+ // });
221
+
222
+ // it('should return false if renderingType is multiline and sessionMode is not embedded-view', () => {
223
+ // const result = isInlineView('multiline', 'desktop' as LayoutType, 'maximized', 'edit' as SessionMode);
224
+ // expect(result).toBe(false);
225
+ // });
226
+ // });
227
+
228
+ // describe('evaluateConditionalAnswered', () => {
229
+ // it('should add field id to referencedField.fieldDependants when referenced field is found', () => {
230
+ // const field: FormField = {
231
+ // label: 'Field with Validator',
232
+ // type: 'obs',
233
+ // questionOptions: {
234
+ // rendering: 'number',
235
+ // },
236
+ // id: 'field-1',
237
+ // validators: [
238
+ // {
239
+ // type: 'conditionalAnswered',
240
+ // referenceQuestionId: 'field-2',
241
+ // },
242
+ // ],
243
+ // };
244
+
245
+ // const referencedField: FormField = {
246
+ // label: 'Referenced Field',
247
+ // type: 'obs',
248
+ // questionOptions: {
249
+ // rendering: 'number',
250
+ // },
251
+ // id: 'field-2',
252
+ // };
253
+
254
+ // const allFields: FormField[] = [field, referencedField];
255
+
256
+ // evaluateConditionalAnswered(field, allFields);
257
+
258
+ // expect(referencedField.fieldDependants).toEqual(new Set(['field-1']));
259
+ // });
260
+
261
+ // it('should not add field id to referencedField.fieldDependants when referenced field is not found', () => {
262
+ // const field: FormField = {
263
+ // label: 'Field with Validator',
264
+ // type: 'obs',
265
+ // questionOptions: {
266
+ // rendering: 'number',
267
+ // },
268
+ // id: 'field-1',
269
+ // validators: [
270
+ // {
271
+ // type: 'conditionalAnswered',
272
+ // referenceQuestionId: 'field-2',
273
+ // },
274
+ // ],
275
+ // };
276
+
277
+ // const allFields: FormField[] = [field];
278
+
279
+ // evaluateConditionalAnswered(field, allFields);
280
+
281
+ // // Since referenced field is not in allFields, nothing should be added
282
+ // allFields.forEach((field) => {
283
+ // expect(field.fieldDependants).toBeUndefined();
284
+ // });
285
+ // });
286
+
287
+ // it('should not overwrite existing fieldDependants', () => {
288
+ // const field: FormField = {
289
+ // label: 'Field with Validator',
290
+ // type: 'obs',
291
+ // questionOptions: {
292
+ // rendering: 'number',
293
+ // },
294
+ // id: 'field-1',
295
+ // validators: [
296
+ // {
297
+ // type: 'conditionalAnswered',
298
+ // referenceQuestionId: 'field-2',
299
+ // },
300
+ // ],
301
+ // };
302
+
303
+ // const referencedField: FormField = {
304
+ // label: 'Referenced Field',
305
+ // type: 'obs',
306
+ // questionOptions: {
307
+ // rendering: 'number',
308
+ // },
309
+ // id: 'field-2',
310
+ // fieldDependants: new Set(['field-3']),
311
+ // };
312
+
313
+ // const allFields: FormField[] = [field, referencedField];
314
+
315
+ // evaluateConditionalAnswered(field, allFields);
316
+
317
+ // expect(referencedField.fieldDependants).toEqual(new Set(['field-3', 'field-1']));
318
+ // });
319
+ // });
320
+
321
+ describe('evaluateFieldReadonlyProp', () => {
322
+ it('should not change field.readonly if it is not empty', () => {
323
+ const field: FormField = {
324
+ label: 'Test Field',
325
+ type: 'obs',
326
+ questionOptions: {
327
+ rendering: 'number',
328
+ },
329
+ id: 'field-1',
330
+ readonly: true,
331
+ };
332
+
333
+ evaluateFieldReadonlyProp(field, false, false, false);
334
+
335
+ expect(field.readonly).toBe(true);
336
+ });
337
+
338
+ it('should set field.readonly to true if sectionReadonly is not empty', () => {
339
+ const field: FormField = {
340
+ label: 'Test Field',
341
+ type: 'obs',
342
+ questionOptions: {
343
+ rendering: 'number',
344
+ },
345
+ id: 'field-1',
346
+ readonly: '',
347
+ };
348
+
349
+ evaluateFieldReadonlyProp(field, 'some value', false, false);
350
+
351
+ expect(field.readonly).toBe(true);
352
+ });
353
+
354
+ it('should set field.readonly to true if pageReadonly is not empty', () => {
355
+ const field: FormField = {
356
+ label: 'Test Field',
357
+ type: 'obs',
358
+ questionOptions: {
359
+ rendering: 'number',
360
+ },
361
+ id: 'field-1',
362
+ readonly: '',
363
+ };
364
+
365
+ evaluateFieldReadonlyProp(field, false, 'some value', false);
366
+
367
+ expect(field.readonly).toBe(true);
368
+ });
369
+
370
+ it('should set field.readonly to true if formReadonly is true', () => {
371
+ const field: FormField = {
372
+ label: 'Test Field',
373
+ type: 'obs',
374
+ questionOptions: {
375
+ rendering: 'number',
376
+ },
377
+ id: 'field-1',
378
+ readonly: '',
379
+ };
380
+
381
+ evaluateFieldReadonlyProp(field, false, false, true);
382
+
383
+ expect(field.readonly).toBe(true);
384
+ });
385
+ });
386
+
387
+ describe('parseToLocalDateTime', () => {
388
+ it('should parse valid date string with time correctly', () => {
389
+ const dateString = '2023-06-27T14:30:00';
390
+ const expectedDate = new Date(2023, 5, 27, 14, 30, 0);
391
+ const parsedDate = parseToLocalDateTime(dateString);
392
+
393
+ expect(parsedDate).toEqual(expectedDate);
394
+ });
395
+
396
+ it('should parse valid date string without time correctly', () => {
397
+ const dateString = '2023-06-27';
398
+ const expectedDate = new Date(2023, 5, 27);
399
+ const parsedDate = parseToLocalDateTime(dateString);
400
+
401
+ // Set hours, minutes, and seconds to 0 since the input doesn't contain time
402
+ expectedDate.setHours(0, 0, 0, 0);
403
+
404
+ expect(parsedDate).toEqual(expectedDate);
405
+ });
406
+
407
+ it('should handle invalid date string format gracefully', () => {
408
+ const dateString = 'invalid-date-string';
409
+ const parsedDate = parseToLocalDateTime(dateString);
410
+
411
+ // Check if the parsedDate is an Invalid Date
412
+ expect(isNaN(parsedDate.getTime())).toBe(true);
413
+ });
414
+ });
415
+
416
+ describe('evaluateDisabled', () => {
417
+ let mockExpressionRunnerFn;
418
+ let node;
419
+ let allFields;
420
+ let allValues;
421
+ let sessionMode;
422
+ let patient;
423
+
424
+ beforeEach(() => {
425
+ mockExpressionRunnerFn = jest.fn();
426
+ node = { value: { disabled: { disableWhenExpression: '' } } };
427
+ allFields = [{ id: 'field1', value: 'value1' }];
428
+ allValues = { field1: 'value1' };
429
+ sessionMode = 'edit';
430
+ patient = { id: 'patient1', name: 'John Doe' };
431
+ });
432
+
433
+ test('returns true when disableWhenExpression evaluates to true', () => {
434
+ mockExpressionRunnerFn.mockReturnValue(true);
435
+ const result = evaluateDisabled(node, allFields, allValues, sessionMode, patient, mockExpressionRunnerFn);
436
+ expect(result).toBe(true);
437
+ expect(mockExpressionRunnerFn).toHaveBeenCalledWith(
438
+ node.value.disabled.disableWhenExpression,
439
+ node,
440
+ allFields,
441
+ allValues,
442
+ { mode: sessionMode, patient },
443
+ );
444
+ });
445
+
446
+ test('returns false when disableWhenExpression evaluates to false', () => {
447
+ mockExpressionRunnerFn.mockReturnValue(false);
448
+ const result = evaluateDisabled(node, allFields, allValues, sessionMode, patient, mockExpressionRunnerFn);
449
+ expect(result).toBe(false);
450
+ expect(mockExpressionRunnerFn).toHaveBeenCalledWith(
451
+ node.value.disabled.disableWhenExpression,
452
+ node,
453
+ allFields,
454
+ allValues,
455
+ { mode: sessionMode, patient },
456
+ );
457
+ });
458
+
459
+ test('works with different sessionMode values', () => {
460
+ sessionMode = 'view';
461
+ mockExpressionRunnerFn.mockReturnValue(true);
462
+ const result = evaluateDisabled(node, allFields, allValues, sessionMode, patient, mockExpressionRunnerFn);
463
+ expect(result).toBe(true);
464
+ expect(mockExpressionRunnerFn).toHaveBeenCalledWith(
465
+ node.value.disabled.disableWhenExpression,
466
+ node,
467
+ allFields,
468
+ allValues,
469
+ { mode: sessionMode, patient },
470
+ );
471
+ });
472
+
473
+ test('throws if the expression causes an error', () => {
474
+ mockExpressionRunnerFn.mockImplementation(() => {
475
+ throw new Error('Invalid expression');
476
+ });
477
+ expect(() => evaluateDisabled(node, allFields, allValues, sessionMode, patient, mockExpressionRunnerFn)).toThrow(
478
+ 'Invalid expression',
479
+ );
480
+ });
481
+ });
482
+ });
@@ -0,0 +1,210 @@
1
+ import dayjs from 'dayjs';
2
+ import { type LayoutType } from '@openmrs/esm-framework';
3
+ import { type FormField, type FormPage, type FormSection, type SessionMode } from '../types';
4
+ import { isEmpty } from '../validators/form-validator';
5
+ import { getRegisteredControl } from '../registry/registry';
6
+
7
+ export function shouldUseInlineLayout(
8
+ renderingType: 'single-line' | 'multiline' | 'automatic',
9
+ layoutType: LayoutType,
10
+ workspaceLayout: 'minimized' | 'maximized',
11
+ sessionMode: SessionMode,
12
+ ): boolean {
13
+ if (sessionMode == 'embedded-view') {
14
+ return true;
15
+ }
16
+ if (renderingType == 'automatic') {
17
+ return workspaceLayout == 'maximized' && layoutType.endsWith('desktop');
18
+ }
19
+ return renderingType == 'single-line';
20
+ }
21
+
22
+ export function evaluateConditionalAnswered(field: FormField, allFields: FormField[]) {
23
+ const referencedFieldId = field.validators.find(
24
+ (validator) => validator.type === 'conditionalAnswered',
25
+ ).referenceQuestionId;
26
+ const referencedField = allFields.find((field) => field.id == referencedFieldId);
27
+ if (referencedField) {
28
+ (referencedField.fieldDependents || (referencedField.fieldDependents = new Set())).add(field.id);
29
+ }
30
+ }
31
+
32
+ export function evaluateFieldReadonlyProp(
33
+ field: FormField,
34
+ sectionReadonly: string | boolean,
35
+ pageReadonly: string | boolean,
36
+ formReadonly: string | boolean,
37
+ ) {
38
+ if (!isEmpty(field.readonly)) {
39
+ return;
40
+ }
41
+ field.readonly = !isEmpty(sectionReadonly) || !isEmpty(pageReadonly) || formReadonly;
42
+ }
43
+
44
+ export function findPagesWithErrors(pages: Set<FormPage>, errorFields: FormField[]): string[] {
45
+ let pagesWithErrors: string[] = [];
46
+ let allFormPages = [...pages];
47
+ if (errorFields?.length) {
48
+ //Find pages each of the errors belong to
49
+ errorFields.forEach((field) => {
50
+ allFormPages.forEach((page) => {
51
+ let errorPage = page.sections.find((section) => section.questions.find((question) => question === field));
52
+ if (errorPage && !pagesWithErrors.includes(page.label)) {
53
+ pagesWithErrors.push(page.label);
54
+ }
55
+ });
56
+ });
57
+ }
58
+ return pagesWithErrors;
59
+ }
60
+
61
+ export function parseToLocalDateTime(dateString: string): Date {
62
+ const dateObj = dayjs(dateString).toDate();
63
+ try {
64
+ const localTimeTokens = dateString.split('T')[1].split(':');
65
+ dateObj.setHours(parseInt(localTimeTokens[0]), parseInt(localTimeTokens[1]), 0);
66
+ } catch (e) {
67
+ console.error(e);
68
+ }
69
+ return dateObj;
70
+ }
71
+
72
+ export function evalConditionalRequired(field: FormField, allFields: FormField[], formValues: Record<string, any>) {
73
+ if (typeof field.required !== 'object') {
74
+ return false;
75
+ }
76
+ const { referenceQuestionAnswers, referenceQuestionId } = field.required;
77
+ const referencedField = allFields.find((field) => field.id == referenceQuestionId);
78
+ if (referencedField) {
79
+ (referencedField.fieldDependents || (referencedField.fieldDependents = new Set())).add(field.id);
80
+ return referenceQuestionAnswers?.includes(formValues[referenceQuestionId]);
81
+ }
82
+ return false;
83
+ }
84
+
85
+ export function evaluateDisabled(
86
+ node,
87
+ allFields: FormField[],
88
+ allValues: Record<string, any>,
89
+ sessionMode: SessionMode,
90
+ patient: fhir.Patient,
91
+ expressionRunnerFn,
92
+ ) {
93
+ const { value } = node;
94
+ const isDisabled = expressionRunnerFn(value['disabled']?.disableWhenExpression, node, allFields, allValues, {
95
+ mode: sessionMode,
96
+ patient,
97
+ });
98
+ return isDisabled;
99
+ }
100
+
101
+ export function evaluateHide(
102
+ node,
103
+ allFields: FormField[],
104
+ allValues: Record<string, any>,
105
+ sessionMode: SessionMode,
106
+ patient: fhir.Patient,
107
+ expressionRunnerFn,
108
+ ) {
109
+ const { value, type } = node;
110
+ const isHidden = expressionRunnerFn(value['hide']?.hideWhenExpression, node, allFields, allValues, {
111
+ mode: sessionMode,
112
+ patient,
113
+ });
114
+ node.value.isHidden = isHidden;
115
+ if (type == 'field' && node.value?.questions?.length) {
116
+ node.value?.questions.forEach((question) => {
117
+ question.isParentHidden = isHidden;
118
+ });
119
+ }
120
+ // cascade visibility
121
+ if (type == 'page') {
122
+ value['sections'].forEach((section) => {
123
+ section.isParentHidden = isHidden;
124
+ cascadeVisibilityToChildFields(isHidden, section, allFields);
125
+ });
126
+ }
127
+ if (type == 'section') {
128
+ cascadeVisibilityToChildFields(isHidden, value, allFields);
129
+ }
130
+ }
131
+
132
+ function cascadeVisibilityToChildFields(visibility: boolean, section: FormSection, allFields: Array<FormField>) {
133
+ const candidateIds = section.questions.map((q) => q.id);
134
+ allFields
135
+ .filter((field) => candidateIds.includes(field.id))
136
+ .forEach((field) => {
137
+ field.isParentHidden = visibility;
138
+ if (field.questionOptions.rendering == 'group') {
139
+ field.questions.forEach((member) => {
140
+ member.isParentHidden = visibility;
141
+ });
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Given a reference to a concept (either the uuid, or the source and reference term, ie "CIEL:1234") and a set of concepts, return matching concept, if any
148
+ *
149
+ * @param reference a uuid or source/term mapping, ie "3cd6f86c-26fe-102b-80cb-0017a47871b2" or "CIEL:1234"
150
+ * @param concepts
151
+ */
152
+ export function findConceptByReference(reference: string, concepts) {
153
+ if (reference?.includes(':')) {
154
+ // handle mapping
155
+ const [source, code] = reference.split(':');
156
+
157
+ return concepts?.find((concept) => {
158
+ return concept?.conceptMappings?.find((mapping) => {
159
+ return (
160
+ mapping?.conceptReferenceTerm?.conceptSource?.name.toUpperCase() === source.toUpperCase() &&
161
+ mapping?.conceptReferenceTerm?.code.toUpperCase() === code.toUpperCase()
162
+ );
163
+ });
164
+ });
165
+ } else {
166
+ // handle uuid
167
+ return concepts?.find((concept) => {
168
+ return concept.uuid === reference;
169
+ });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Retrieves the appropriate field control for a question, considering missing concepts.
175
+ * If the question is of type 'obs' and has a missing concept, it falls back to a disabled text input.
176
+ * Otherwise, it retrieves the registered control based on the rendering specified in the question.
177
+ * @param question - The FormField representing the question.
178
+ * @returns The field control to be used for rendering the question.
179
+ */
180
+ export function getFieldControlWithFallback(question: FormField) {
181
+ // Check if the question has a missing concept
182
+ if (hasMissingConcept(question)) {
183
+ // If so, render a disabled text input
184
+ question.disabled = true;
185
+ question.isDisabled = true;
186
+ return getRegisteredControl('text');
187
+ }
188
+
189
+ // Retrieve the registered control based on the specified rendering
190
+ return getRegisteredControl(question.questionOptions.rendering);
191
+ }
192
+
193
+ export function hasMissingConcept(question: FormField) {
194
+ return (
195
+ question.type == 'obs' && !question.questionOptions.concept && question.questionOptions.rendering !== 'fixed-value'
196
+ );
197
+ }
198
+
199
+ export function scrollIntoView(viewId: string, shouldFocus: boolean = false) {
200
+ const currentElement = document.getElementById(viewId);
201
+ currentElement?.scrollIntoView({
202
+ behavior: 'smooth',
203
+ block: 'center',
204
+ inline: 'center',
205
+ });
206
+
207
+ if (shouldFocus) {
208
+ currentElement?.focus();
209
+ }
210
+ }
@@ -0,0 +1,13 @@
1
+ import { type FormExpanded, type FormSection } from '../types';
2
+ import { isTrue } from './boolean-utils';
3
+
4
+ export function isSectionExpanded(section: FormSection, isFormExpanded: FormExpanded): FormExpanded {
5
+ if (isFormExpanded !== undefined) {
6
+ return isFormExpanded;
7
+ }
8
+
9
+ if (section?.isExpanded !== undefined && section?.isExpanded !== null) {
10
+ return isTrue(section.isExpanded);
11
+ }
12
+ return true;
13
+ }