@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,237 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { type FormSchemaTransformer, type FormSchema, type FormSection, type ReferencedForm } from '../types';
3
+ import { isTrue } from '../utils/boolean-utils';
4
+ import { applyFormIntent } from '../utils/forms-loader';
5
+ import { fetchOpenMRSForm, fetchClobData } from '../api';
6
+ import { getRegisteredFormSchemaTransformers } from '../registry/registry';
7
+ import { moduleName } from '../globals';
8
+
9
+ export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: string, formSessionIntent: string) {
10
+ const [formJson, setFormJson] = useState<FormSchema>(null);
11
+ const [error, setError] = useState(validateFormsArgs(formUuid, rawFormJson));
12
+ useEffect(() => {
13
+ const setFormJsonWithTranslations = (formJson: FormSchema) => {
14
+ if (formJson?.translations) {
15
+ const language = window.i18next.language;
16
+ window.i18next.addResourceBundle(language, moduleName, formJson.translations, true, true);
17
+ }
18
+ setFormJson(formJson);
19
+ };
20
+ loadFormJson(formUuid, rawFormJson, formSessionIntent)
21
+ .then((formJson) => {
22
+ setFormJsonWithTranslations({ ...formJson, encounter: encounterUuid });
23
+ })
24
+ .catch((error) => {
25
+ console.error(error);
26
+ setError(new Error('Error loading form JSON: ' + error.message));
27
+ });
28
+ }, [formSessionIntent, formUuid, rawFormJson, encounterUuid]);
29
+
30
+ return {
31
+ formJson,
32
+ isLoading: !formJson,
33
+ formError: error,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Fetches a form JSON schema from OpenMRS and recursively fetches its subForms if they available.
39
+ *
40
+ * If `rawFormJson` is provided, it will be used as the raw form JSON object. Otherwise, the form JSON will be fetched from OpenMRS using the `formIdentifier` parameter.
41
+ *
42
+ * @param rawFormJson The raw form JSON object to be used if `formIdentifier` is not provided.
43
+ * @param formIdentifier The UUID or name of the form to be fetched from OpenMRS if `rawFormJson` is not provided.
44
+ * @param formSessionIntent An optional parameter that represents the current intent.
45
+ * @returns A well-built form object that might include subForms.
46
+ */
47
+ export async function loadFormJson(
48
+ formIdentifier: string,
49
+ rawFormJson?: FormSchema,
50
+ formSessionIntent?: string,
51
+ ): Promise<FormSchema> {
52
+ const openmrsFormResponse = await fetchOpenMRSForm(formIdentifier);
53
+ const clobDataResponse = await fetchClobData(openmrsFormResponse);
54
+ const transformers = await getRegisteredFormSchemaTransformers();
55
+ const formJson: FormSchema = clobDataResponse
56
+ ? { ...clobDataResponse, uuid: openmrsFormResponse.uuid }
57
+ : parseFormJson(rawFormJson);
58
+
59
+ // Sub forms
60
+ const subFormRefs = extractSubFormRefs(formJson);
61
+ const subForms = await loadSubForms(subFormRefs, formSessionIntent);
62
+ updateFormJsonWithSubForms(formJson, subForms);
63
+
64
+ // Form components
65
+ const formComponentsRefs = getReferencedForms(formJson);
66
+ const resolvedFormComponents = await loadFormComponents(formComponentsRefs);
67
+ const formNameToAliasMap = formComponentsRefs.reduce((acc, form) => {
68
+ acc[form.formName] = form.alias;
69
+ return acc;
70
+ }, {});
71
+
72
+ const formComponents = mapFormComponents(resolvedFormComponents);
73
+ updateFormJsonWithComponents(formJson, formComponents, formNameToAliasMap);
74
+ return refineFormJson(formJson, transformers, formSessionIntent);
75
+ }
76
+
77
+ function extractSubFormRefs(formJson: FormSchema): string[] {
78
+ return formJson.pages
79
+ .filter((page) => page.isSubform && !page.subform.form && page.subform?.name)
80
+ .map((page) => page.subform?.name);
81
+ }
82
+
83
+ async function loadSubForms(subFormRefs: string[], formSessionIntent?: string): Promise<FormSchema[]> {
84
+ return Promise.all(subFormRefs.map((subForm) => loadFormJson(subForm, null, formSessionIntent)));
85
+ }
86
+
87
+ function updateFormJsonWithSubForms(formJson: FormSchema, subForms: FormSchema[]): void {
88
+ subForms.forEach((subForm) => {
89
+ const matchingPage = formJson.pages.find((page) => page.subform?.name === subForm.name);
90
+ if (matchingPage) {
91
+ matchingPage.subform.form = subForm;
92
+ }
93
+ });
94
+ }
95
+
96
+ function validateFormsArgs(formUuid: string, rawFormJson: any): Error {
97
+ if (!formUuid && !rawFormJson) {
98
+ return new Error('InvalidArgumentsErr: Neither formUuid nor formJson was provided');
99
+ }
100
+ if (formUuid && rawFormJson) {
101
+ return new Error('InvalidArgumentsErr: Both formUuid and formJson cannot be provided at the same time.');
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Refines the input form JSON object by parsing it, removing inline sub forms, applying form schema transformers, setting the encounter type, and applying form intents if provided.
107
+ * @param {any} formJson - The input form JSON object or string.
108
+ * @param {string} [formSessionIntent] - The optional form session intent.
109
+ * @returns {FormSchema} - The refined form JSON object of type FormSchema.
110
+ */
111
+ function refineFormJson(
112
+ formJson: any,
113
+ schemaTransformers: FormSchemaTransformer[] = [],
114
+ formSessionIntent?: string,
115
+ ): FormSchema {
116
+ removeInlineSubForms(formJson, formSessionIntent);
117
+ // apply form schema transformers
118
+ schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm), formJson);
119
+ setEncounterType(formJson);
120
+ return applyFormIntent(formSessionIntent, formJson);
121
+ }
122
+
123
+ /**
124
+ * Parses the input form JSON and returns a deep copy of the object.
125
+ * @param {any} formJson - The input form JSON object or string.
126
+ * @returns {FormSchema} - The parsed form JSON object of type FormSchema.
127
+ */
128
+ function parseFormJson(formJson: any): FormSchema {
129
+ return typeof formJson === 'string' ? JSON.parse(formJson) : JSON.parse(JSON.stringify(formJson));
130
+ }
131
+
132
+ /**
133
+ * Removes inline sub forms from the form JSON and replaces them with their pages if the encounter type matches.
134
+ * @param {FormSchema} formJson - The input form JSON object of type FormSchema.
135
+ * @param {string} formSessionIntent - The form session intent.
136
+ */
137
+ function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): void {
138
+ for (let i = formJson.pages.length - 1; i >= 0; i--) {
139
+ const page = formJson.pages[i];
140
+ if (
141
+ isTrue(page.isSubform) &&
142
+ !isTrue(page.isHidden) &&
143
+ page.subform?.form?.encounterType === formJson.encounterType
144
+ ) {
145
+ const nonSubformPages = page.subform.form.pages.filter((page) => !isTrue(page.isSubform));
146
+ formJson.pages.splice(i, 1, ...refineFormJson(page.subform.form, [], formSessionIntent).pages);
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Sets the encounter type for the form JSON if it's provided through the `encounter` attribute.
153
+ * @param {FormSchema} formJson - The input form JSON object of type FormSchema.
154
+ */
155
+ function setEncounterType(formJson: FormSchema): void {
156
+ if (formJson.encounter && typeof formJson.encounter === 'string' && !formJson.encounterType) {
157
+ formJson.encounterType = formJson.encounter;
158
+ delete formJson.encounter;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Functions to support reusable Form Components
164
+ */
165
+ function getReferencedForms(formJson: FormSchema): Array<ReferencedForm> {
166
+ const referencedForms: Array<any> = formJson?.referencedForms;
167
+ if (!referencedForms) {
168
+ return [];
169
+ }
170
+ return referencedForms;
171
+ }
172
+
173
+ async function loadFormComponents(formComponentRefs: Array<ReferencedForm>): Promise<FormSchema[]> {
174
+ return Promise.all(formComponentRefs.map((formComponent) => loadFormJson(formComponent.formName, null, null)));
175
+ }
176
+
177
+ function mapFormComponents(formComponents: Array<FormSchema>): Map<string, FormSchema> {
178
+ const formComponentsMap: Map<string, FormSchema> = new Map();
179
+
180
+ formComponents.forEach((formComponent) => {
181
+ formComponentsMap.set(formComponent.name, formComponent);
182
+ });
183
+
184
+ return formComponentsMap;
185
+ }
186
+
187
+ function updateFormJsonWithComponents(
188
+ formJson: FormSchema,
189
+ formComponents: Map<string, FormSchema>,
190
+ formNameToAliasMap: Record<string, string>,
191
+ ): void {
192
+ formComponents.forEach((component, targetFormName) => {
193
+ //loop through pages and search sections for reference key
194
+ formJson.pages.forEach((page) => {
195
+ if (page.sections) {
196
+ page.sections.forEach((section) => {
197
+ if (
198
+ section.reference &&
199
+ (section.reference.form === targetFormName || section.reference.form === formNameToAliasMap[targetFormName])
200
+ ) {
201
+ // resolve referenced component section
202
+ let resolvedFormSection = getReferencedFormSection(section, component);
203
+ // add resulting referenced component section to section
204
+ Object.assign(section, resolvedFormSection);
205
+ }
206
+ });
207
+ }
208
+ });
209
+ });
210
+ }
211
+
212
+ function getReferencedFormSection(formSection: FormSection, formComponent: FormSchema): FormSection {
213
+ let referencedFormSection: FormSection;
214
+
215
+ // search for component page and section reference from component
216
+ let matchingComponentPage = formComponent.pages.filter((page) => page.label === formSection.reference.page);
217
+ if (matchingComponentPage.length > 0) {
218
+ let matchingComponentSection = matchingComponentPage[0].sections.filter(
219
+ (componentSection) => componentSection.label === formSection.reference.section,
220
+ );
221
+ if (matchingComponentSection.length > 0) {
222
+ referencedFormSection = matchingComponentSection[0];
223
+ }
224
+ }
225
+
226
+ return filterExcludedQuestions(referencedFormSection, formSection.reference);
227
+ }
228
+
229
+ function filterExcludedQuestions(formSection: FormSection, reference: any): FormSection {
230
+ if (reference?.excludeQuestions) {
231
+ const excludeQuestions = reference.excludeQuestions;
232
+ formSection.questions = formSection.questions.filter((question) => {
233
+ return !excludeQuestions.includes(question.id);
234
+ });
235
+ }
236
+ return formSection;
237
+ }
@@ -0,0 +1,50 @@
1
+ import { type Dispatch, useCallback } from 'react';
2
+ import { type FormSchema, type FormField } from '../types';
3
+ import { type Action } from '../components/renderer/form/state';
4
+
5
+ export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: FormField[]) {
6
+ const addFormField = useCallback((field: FormField) => {
7
+ dispatch({ type: 'ADD_FORM_FIELD', value: field });
8
+ }, []);
9
+ const updateFormField = useCallback((field: FormField) => {
10
+ dispatch({ type: 'UPDATE_FORM_FIELD', value: field });
11
+ }, []);
12
+
13
+ const getFormField = useCallback(
14
+ (fieldId: string) => {
15
+ return formFields.find((field) => field.id === fieldId);
16
+ },
17
+ [formFields.length],
18
+ );
19
+
20
+ const removeFormField = useCallback((fieldId: string) => {
21
+ dispatch({ type: 'REMOVE_FORM_FIELD', value: fieldId });
22
+ }, []);
23
+
24
+ const setInvalidFields = useCallback((fields: FormField[]) => {
25
+ dispatch({ type: 'SET_INVALID_FIELDS', value: fields });
26
+ }, []);
27
+
28
+ const addInvalidField = useCallback((field: FormField) => {
29
+ dispatch({ type: 'ADD_INVALID_FIELD', value: field });
30
+ }, []);
31
+
32
+ const removeInvalidField = useCallback((fieldId: string) => {
33
+ dispatch({ type: 'REMOVE_INVALID_FIELD', value: fieldId });
34
+ }, []);
35
+
36
+ const setForm = useCallback((formJson: FormSchema) => {
37
+ dispatch({ type: 'SET_FORM_JSON', value: formJson });
38
+ }, []);
39
+
40
+ return {
41
+ addFormField,
42
+ updateFormField,
43
+ getFormField,
44
+ removeFormField,
45
+ setInvalidFields,
46
+ addInvalidField,
47
+ removeInvalidField,
48
+ setForm,
49
+ };
50
+ }
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from 'react';
2
+ import get from 'lodash-es/get';
3
+ import { getConfig } from '@openmrs/esm-framework';
4
+ import { ConceptTrue, ConceptFalse } from '../constants';
5
+
6
+ export interface FormsConfig {
7
+ conceptTrue: string;
8
+ conceptFalse: string;
9
+ }
10
+ const defaultOptions: FormsConfig = {
11
+ conceptTrue: ConceptTrue,
12
+ conceptFalse: ConceptFalse,
13
+ };
14
+
15
+ export function useFormsConfig(moduleName: string, configPath: string) {
16
+ const [config, setConfig] = useState<FormsConfig>(defaultOptions);
17
+
18
+ useEffect(() => {
19
+ if (moduleName && configPath) {
20
+ getConfig(moduleName).then((c) => {
21
+ setConfig({ config, ...get(c, configPath, config) });
22
+ });
23
+ }
24
+ }, [moduleName, configPath, config]);
25
+
26
+ return config;
27
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { type FormProcessorContextProps } from '../types';
3
+ import { type FormProcessor } from '../processors/form-processor';
4
+
5
+ const useInitialValues = (
6
+ formProcessor: FormProcessor,
7
+ isLoadingContextDependencies: boolean,
8
+ context: FormProcessorContextProps,
9
+ ) => {
10
+ const [isLoadingInitialValues, setIsLoadingInitialValues] = useState(true);
11
+ const [initialValues, setInitialValues] = useState({});
12
+ const [error, setError] = useState(null);
13
+
14
+ useEffect(() => {
15
+ if (
16
+ formProcessor &&
17
+ !isLoadingContextDependencies &&
18
+ context.formFields?.length &&
19
+ Object.keys(context.formFieldAdapters).length &&
20
+ !Object.keys(initialValues).length
21
+ ) {
22
+ formProcessor
23
+ .getInitialValues(context)
24
+ .then((values) => {
25
+ setInitialValues(values);
26
+ setIsLoadingInitialValues(false);
27
+ })
28
+ .catch((error) => {
29
+ console.error(error);
30
+ setError(error);
31
+ });
32
+ }
33
+ }, [formProcessor, isLoadingContextDependencies, context]);
34
+
35
+ return { isLoadingInitialValues, initialValues, error };
36
+ };
37
+
38
+ export default useInitialValues;
@@ -0,0 +1,32 @@
1
+ import { usePatient } from '@openmrs/esm-framework';
2
+
3
+ function calculateAge(birthDate: Date): number {
4
+ const today = new Date();
5
+ const yearsDiff = today.getFullYear() - birthDate.getFullYear();
6
+ if (
7
+ today.getMonth() < birthDate.getMonth() ||
8
+ (today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate())
9
+ ) {
10
+ // subtract one year if the current date is before the birth date this year
11
+ return yearsDiff - 1;
12
+ } else {
13
+ return yearsDiff;
14
+ }
15
+ }
16
+
17
+ const patientGenderMap = {
18
+ female: 'F',
19
+ male: 'M',
20
+ other: 'O',
21
+ unknown: 'U',
22
+ };
23
+
24
+ export const usePatientData = (patientUuid) => {
25
+ const { patient, isLoading: isLoadingPatient, error: patientError } = usePatient(patientUuid);
26
+ if (patient && !isLoadingPatient) {
27
+ // This is to support backward compatibility with the AMPATH JSON format
28
+ patient['age'] = calculateAge(new Date(patient?.birthDate));
29
+ patient['sex'] = patientGenderMap[patient.gender] ?? 'U';
30
+ }
31
+ return { patient, isLoadingPatient, patientError };
32
+ };
@@ -0,0 +1,32 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { useEffect, useState } from 'react';
3
+ import { type FormSchema, type PatientProgram } from '../types';
4
+ const customRepresentation = `custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(startDate,endDate,state:(uuid,name,retired,concept:(uuid),programWorkflow:(uuid)))`;
5
+
6
+ export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) => {
7
+ const [patientPrograms, setPatientPrograms] = useState<Array<PatientProgram>>([]);
8
+ const [isLoading, setIsLoading] = useState(true);
9
+ const [error, setError] = useState(null);
10
+
11
+ useEffect(() => {
12
+ if (formJson.meta?.programs?.hasProgramFields) {
13
+ openmrsFetch(`${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`)
14
+ .then((response) => {
15
+ setPatientPrograms(response.data.results.filter((enrollment) => enrollment.dateCompleted === null));
16
+ setIsLoading(false);
17
+ })
18
+ .catch((error) => {
19
+ setError(error);
20
+ setIsLoading(false);
21
+ });
22
+ } else {
23
+ setIsLoading(false);
24
+ }
25
+ }, [formJson]);
26
+
27
+ return {
28
+ patientPrograms,
29
+ error,
30
+ isLoading,
31
+ };
32
+ };
@@ -0,0 +1,42 @@
1
+ import { renderHook, act } from '@testing-library/react';
2
+ import { usePostSubmissionActions } from './usePostSubmissionActions';
3
+
4
+ // Mock the getRegisteredPostSubmissionAction function
5
+ jest.mock('../registry/registry', () => ({
6
+ getRegisteredPostSubmissionAction: jest.fn(),
7
+ }));
8
+
9
+ describe('usePostSubmissionActions', () => {
10
+ // Mock the actual post-submission action function
11
+ const mockPostAction = jest.fn();
12
+
13
+ // Sample action references
14
+ const actionRefs = [
15
+ { actionId: 'action1', config: { param1: 'value1' } },
16
+ { actionId: 'action2', config: { param2: 'value2' } },
17
+ ];
18
+
19
+ // Set up the mock implementation for getRegisteredPostSubmissionAction
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ jest.spyOn(global.console, 'error').mockImplementation(() => {});
23
+ jest.requireMock('../registry/registry').getRegisteredPostSubmissionAction.mockImplementation((actionId) => {
24
+ if (actionId === 'action1') {
25
+ return Promise.resolve(mockPostAction);
26
+ }
27
+ return Promise.resolve(null);
28
+ });
29
+ });
30
+
31
+ it('should fetch post-submission actions and return them', async () => {
32
+ const { result } = renderHook(() => usePostSubmissionActions(actionRefs));
33
+
34
+ // Wait for the effect to complete
35
+ await act(async () => {});
36
+
37
+ expect(result.current).toEqual([
38
+ { postAction: mockPostAction, config: { param1: 'value1' }, actionId: 'action1' },
39
+ { postAction: null, config: { param2: 'value2' }, actionId: 'action2' },
40
+ ]);
41
+ });
42
+ });
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { getRegisteredPostSubmissionAction } from '../registry/registry';
3
+ import { type PostSubmissionAction } from '../types';
4
+
5
+ export interface PostSubmissionActionMeta {
6
+ postAction: PostSubmissionAction;
7
+ actionId: string;
8
+ config: Record<string, any>;
9
+ enabled?: string;
10
+ }
11
+
12
+ export function usePostSubmissionActions(
13
+ actionRefs: Array<{ actionId: string; enabled?: string; config?: Record<string, any> }>,
14
+ ): Array<PostSubmissionActionMeta> {
15
+ const [actions, setActions] = useState<Array<PostSubmissionActionMeta>>([]);
16
+
17
+ useEffect(() => {
18
+ const actionArray: Array<PostSubmissionActionMeta> = [];
19
+ if (actionRefs?.length) {
20
+ actionRefs.map((ref) => {
21
+ const actionId = typeof ref === 'string' ? ref : ref.actionId;
22
+ getRegisteredPostSubmissionAction(actionId)?.then((action) =>
23
+ actionArray.push({ postAction: action, config: ref.config, actionId: actionId, enabled: ref.enabled }),
24
+ );
25
+ });
26
+ }
27
+ setActions(actionArray);
28
+ }, [actionRefs]);
29
+
30
+ return actions;
31
+ }
@@ -0,0 +1,30 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { type FormProcessorContextProps } from '../types';
3
+ import { type FormProcessor } from '../processors/form-processor';
4
+
5
+ const useProcessorDependencies = (
6
+ formProcessor: FormProcessor,
7
+ context: Partial<FormProcessorContextProps>,
8
+ setContext: (context: FormProcessorContextProps) => void,
9
+ ) => {
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const [error, setError] = useState('');
12
+ const { loadDependencies } = formProcessor;
13
+
14
+ useEffect(() => {
15
+ if (loadDependencies) {
16
+ setIsLoading(true);
17
+ loadDependencies(context, setContext)
18
+ .then((results) => {
19
+ setIsLoading(false);
20
+ })
21
+ .catch((error) => {
22
+ setError(error);
23
+ });
24
+ }
25
+ }, []);
26
+
27
+ return { isLoading, error };
28
+ };
29
+
30
+ export default useProcessorDependencies;
@@ -0,0 +1,5 @@
1
+ import useSystemSetting from './useSystemSetting';
2
+
3
+ export default function useRestMaxResultsCount() {
4
+ return useSystemSetting('webservices.rest.maxResultsDefault');
5
+ }
@@ -0,0 +1,36 @@
1
+ import { openmrsFetch, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
2
+ import { useEffect } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import useSWRImmutable from 'swr/immutable';
5
+
6
+ export interface SystemSetting {
7
+ uuid: string;
8
+ property: string;
9
+ value: string;
10
+ }
11
+
12
+ export default function useSystemSetting(setting: string) {
13
+ const { t } = useTranslation();
14
+ const apiUrl = `${restBaseUrl}/systemsetting/${setting}?v=custom:(value)`;
15
+ const { data, error, isLoading } = useSWRImmutable<{ data: SystemSetting }, Error>(apiUrl, openmrsFetch);
16
+
17
+ useEffect(() => {
18
+ if (error) {
19
+ showSnackbar({
20
+ title: t('error', 'Error'),
21
+ subtitle: error?.message,
22
+ kind: 'error',
23
+ isLowContrast: false,
24
+ });
25
+ }
26
+ }, [error]);
27
+
28
+ return {
29
+ systemSetting: data?.data,
30
+ error: error,
31
+ isLoading: isLoading,
32
+ isValueUuid:
33
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data?.data?.value) ||
34
+ /^[0-9a-f]{36}$/i.test(data?.data?.value),
35
+ };
36
+ }
@@ -0,0 +1,29 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+
3
+ /**
4
+ * This hook evaluates the layout of the current workspace based on the width of the container element
5
+ */
6
+ export function useWorkspaceLayout(rootRef): 'minimized' | 'maximized' {
7
+ const [layout, setLayout] = useState<'minimized' | 'maximized'>('minimized');
8
+ const TABLET_MAX = 1023;
9
+ useLayoutEffect(() => {
10
+ const handleResize = () => {
11
+ const containerWidth = rootRef.current?.parentElement?.offsetWidth;
12
+ containerWidth && setLayout(containerWidth > TABLET_MAX ? 'maximized' : 'minimized');
13
+ };
14
+ handleResize();
15
+ const resizeObserver = new ResizeObserver((entries) => {
16
+ handleResize();
17
+ });
18
+
19
+ if (rootRef.current) {
20
+ resizeObserver.observe(rootRef.current?.parentElement);
21
+ }
22
+
23
+ return () => {
24
+ resizeObserver.disconnect();
25
+ };
26
+ }, [rootRef]);
27
+
28
+ return layout;
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from './types';
2
+ export * from './utils/forms-loader';
3
+ export * from './registry/registry';
4
+ export * from './constants';
5
+ export * from './utils/boolean-utils';
6
+ export * from './validators/form-validator';
7
+ export * from './utils/form-helper';
8
+ export * from './form-context';
9
+ export * from './components/value/view/field-value-view.component';
10
+ export * from './components/previous-value-review/previous-value-review.component';
11
+ export * from './hooks/useFormJson';
12
+ export { default as FormEngine } from './form-engine.component';
@@ -0,0 +1,33 @@
1
+ import setupFormEngineLibI18n from './setupI18n';
2
+ import { type FormFieldValueAdapter } from './types';
3
+
4
+ const formFieldAdapters = new Set<FormFieldValueAdapter>();
5
+
6
+ export function registerFormFieldAdaptersForCleanUp(formFieldAdaptersMap: Record<string, FormFieldValueAdapter>) {
7
+ if (formFieldAdaptersMap) {
8
+ Object.values(formFieldAdaptersMap).forEach((adapter) => {
9
+ formFieldAdapters.add(adapter);
10
+ });
11
+ }
12
+ }
13
+ /**
14
+ * Invoked on mounting the "FormEngine" component
15
+ */
16
+ export function init() {
17
+ // Setting up the i18n for the form engine library
18
+ setupFormEngineLibI18n();
19
+ }
20
+
21
+ /**
22
+ * Invoked on unmounting the "FormEngine" component
23
+ */
24
+ export function teardown() {
25
+ formFieldAdapters.forEach((adapter) => {
26
+ try {
27
+ adapter.tearDown();
28
+ } catch (error) {
29
+ // pass
30
+ }
31
+ });
32
+ formFieldAdapters.clear();
33
+ }