@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,320 @@
1
+ import {
2
+ type PatientProgram,
3
+ type FormField,
4
+ type OpenmrsEncounter,
5
+ type OpenmrsObs,
6
+ type PatientIdentifier,
7
+ type PatientProgramPayload,
8
+ type FormProcessorContextProps,
9
+ } from '../../types';
10
+ import { saveAttachment, savePatientIdentifier, saveProgramEnrollment } from '../../api';
11
+ import { hasRendering, hasSubmission } from '../../utils/common-utils';
12
+ import dayjs from 'dayjs';
13
+ import { voidObs, constructObs, assignedObsIds } from '../../adapters/obs-adapter';
14
+ import { type FormContextProps } from '../../provider/form-provider';
15
+ import { ConceptTrue } from '../../constants';
16
+ import { DefaultValueValidator } from '../../validators/default-value-validator';
17
+ import { cloneRepeatField } from '../../components/repeat/helpers';
18
+ import { assignedOrderIds } from '../../adapters/orders-adapter';
19
+
20
+ export function prepareEncounter(
21
+ context: FormContextProps,
22
+ encounterDate: Date,
23
+ encounterRole: string,
24
+ encounterProvider: string,
25
+ location: string,
26
+ ) {
27
+ const { patient, formJson, domainObjectValue: encounter, formFields, visit } = context;
28
+ const obsForSubmission = [];
29
+ prepareObs(obsForSubmission, formFields);
30
+ const ordersForSubmission = prepareOrders(formFields);
31
+ let encounterForSubmission: OpenmrsEncounter = {};
32
+
33
+ if (encounter) {
34
+ Object.assign(encounterForSubmission, encounter);
35
+ // update encounter providers
36
+ const hasCurrentProvider =
37
+ encounterForSubmission.encounterProviders.findIndex(
38
+ (encProvider) => encProvider.provider.uuid == encounterProvider,
39
+ ) !== -1;
40
+ if (!hasCurrentProvider) {
41
+ encounterForSubmission.encounterProviders = [
42
+ ...encounterForSubmission.encounterProviders,
43
+ {
44
+ provider: encounterProvider,
45
+ encounterRole,
46
+ },
47
+ ];
48
+ }
49
+ // TODO: Question: Should we be editing the location, form and visit here?
50
+ encounterForSubmission.encounterDatetime = encounterDate;
51
+ encounterForSubmission.location = location;
52
+ encounterForSubmission.form = {
53
+ uuid: formJson.uuid,
54
+ };
55
+ if (visit) {
56
+ encounterForSubmission.visit = visit.uuid;
57
+ }
58
+ encounterForSubmission.obs = obsForSubmission;
59
+ encounterForSubmission.orders = ordersForSubmission;
60
+ } else {
61
+ encounterForSubmission = {
62
+ patient: patient.id,
63
+ encounterDatetime: encounterDate,
64
+ location: location,
65
+ encounterType: formJson.encounterType,
66
+ encounterProviders: [
67
+ {
68
+ provider: encounterProvider,
69
+ encounterRole,
70
+ },
71
+ ],
72
+ obs: obsForSubmission,
73
+ form: {
74
+ uuid: formJson.uuid,
75
+ },
76
+ visit: visit?.uuid,
77
+ orders: ordersForSubmission,
78
+ };
79
+ }
80
+ return encounterForSubmission;
81
+ }
82
+
83
+ export function preparePatientIdentifiers(fields: FormField[], encounterLocation: string): PatientIdentifier[] {
84
+ return fields
85
+ .filter((field) => field.type === 'patientIdentifier' && hasSubmission(field))
86
+ .map((field) => field.meta.submission.newValue);
87
+ }
88
+
89
+ export function savePatientIdentifiers(patient: fhir.Patient, identifiers: PatientIdentifier[]) {
90
+ return identifiers.map((patientIdentifier) => {
91
+ return savePatientIdentifier(patientIdentifier, patient.id);
92
+ });
93
+ }
94
+
95
+ export function preparePatientPrograms(
96
+ fields: FormField[],
97
+ patient: fhir.Patient,
98
+ currentPatientPrograms: Array<PatientProgram>,
99
+ ): Array<PatientProgramPayload> {
100
+ const programStateFields = fields.filter((field) => field.type === 'programState' && hasSubmission(field));
101
+ const programMap = new Map<string, PatientProgramPayload>();
102
+ programStateFields.forEach((field) => {
103
+ const programUuid = field.questionOptions.programUuid;
104
+ const newState = field.meta.submission.newValue;
105
+ const existingProgramEnrollment = currentPatientPrograms.find((program) => program.program.uuid === programUuid);
106
+
107
+ if (existingProgramEnrollment) {
108
+ if (programMap.has(programUuid)) {
109
+ programMap.get(programUuid).states.push(newState);
110
+ } else {
111
+ programMap.set(programUuid, {
112
+ uuid: existingProgramEnrollment.uuid,
113
+ states: [newState],
114
+ });
115
+ }
116
+ } else {
117
+ if (programMap.has(programUuid)) {
118
+ programMap.get(programUuid).states.push(newState);
119
+ } else {
120
+ programMap.set(programUuid, {
121
+ patient: patient.id,
122
+ program: programUuid,
123
+ states: [newState],
124
+ dateEnrolled: dayjs().format(),
125
+ });
126
+ }
127
+ }
128
+ });
129
+ return Array.from(programMap.values());
130
+ }
131
+
132
+ export function savePatientPrograms(patientPrograms: PatientProgramPayload[]) {
133
+ const ac = new AbortController();
134
+ return Promise.all(patientPrograms.map((programPayload) => saveProgramEnrollment(programPayload, ac)));
135
+ }
136
+
137
+ export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter, abortController: AbortController) {
138
+ const complexFields = fields?.filter((field) => field?.questionOptions.rendering === 'file' && hasSubmission(field));
139
+
140
+ if (!complexFields?.length) return [];
141
+
142
+ return complexFields.map((field) => {
143
+ const patientUuid = typeof encounter?.patient === 'string' ? encounter?.patient : encounter?.patient?.uuid;
144
+ return saveAttachment(
145
+ patientUuid,
146
+ field,
147
+ field?.questionOptions.concept,
148
+ new Date().toISOString(),
149
+ encounter?.uuid,
150
+ abortController,
151
+ );
152
+ });
153
+ }
154
+
155
+ export function getMutableSessionProps(context: FormContextProps) {
156
+ const {
157
+ formFields,
158
+ location,
159
+ currentProvider,
160
+ sessionDate,
161
+ customDependencies,
162
+ domainObjectValue: encounter,
163
+ } = context;
164
+ const { defaultEncounterRole } = customDependencies;
165
+ const encounterRole =
166
+ formFields.find((field) => field.type === 'encounterRole')?.meta.submission?.newValue || defaultEncounterRole?.uuid;
167
+ const encounterProvider =
168
+ formFields.find((field) => field.type === 'encounterProvider')?.meta.submission?.newValue || currentProvider.uuid;
169
+ const encounterDate =
170
+ formFields.find((field) => field.type === 'encounterDatetime')?.meta.submission?.newValue ||
171
+ encounter?.encounterDatetime ||
172
+ sessionDate;
173
+ const encounterLocation =
174
+ formFields.find((field) => field.type === 'encounterLocation')?.meta.submission?.newValue ||
175
+ encounter?.location?.uuid ||
176
+ location.uuid;
177
+ return {
178
+ encounterRole: encounterRole as string,
179
+ encounterProvider: encounterProvider as string,
180
+ encounterDate: encounterDate as Date,
181
+ encounterLocation: encounterLocation as string,
182
+ };
183
+ }
184
+
185
+ // Helpers
186
+
187
+ function prepareObs(obsForSubmission: OpenmrsObs[], fields: FormField[]) {
188
+ fields
189
+ .filter((field) => hasSubmittableObs(field))
190
+ .forEach((field) => {
191
+ if ((field.isHidden || field.isParentHidden) && field.meta.previousValue) {
192
+ const valuesArray = Array.isArray(field.meta.previousValue)
193
+ ? field.meta.previousValue
194
+ : [field.meta.previousValue];
195
+ addObsToList(
196
+ obsForSubmission,
197
+ valuesArray.map((obs) => voidObs(obs)),
198
+ );
199
+ return;
200
+ }
201
+ if (field.type == 'obsGroup') {
202
+ if (field.meta.submission?.voidedValue) {
203
+ addObsToList(obsForSubmission, field.meta.submission.voidedValue);
204
+ return;
205
+ }
206
+ const obsGroup = constructObs(field, null);
207
+ if (field.meta.previousValue) {
208
+ obsGroup.uuid = field.meta.previousValue.uuid;
209
+ }
210
+ field.questions.forEach((groupedField) => {
211
+ if (hasSubmission(groupedField)) {
212
+ addObsToList(obsGroup.groupMembers, groupedField.meta.submission.newValue);
213
+ addObsToList(obsGroup.groupMembers, groupedField.meta.submission.voidedValue);
214
+ }
215
+ });
216
+ if (obsGroup.groupMembers.length || obsGroup.voided) {
217
+ addObsToList(obsForSubmission, obsGroup);
218
+ }
219
+ }
220
+ if (hasSubmission(field)) {
221
+ addObsToList(obsForSubmission, field.meta.submission.newValue);
222
+ addObsToList(obsForSubmission, field.meta.submission.voidedValue);
223
+ }
224
+ });
225
+ }
226
+
227
+ function prepareOrders(fields: FormField[]) {
228
+ return fields
229
+ .filter((field) => field.type === 'testOrder' && hasSubmission(field))
230
+ .flatMap((field) => [field.meta.submission.newValue, field.meta.submission.voidedValue])
231
+ .filter((o) => o);
232
+ }
233
+
234
+ function addObsToList(obsList: Array<Partial<OpenmrsObs>>, obs: Partial<OpenmrsObs>) {
235
+ if (!obs) {
236
+ return;
237
+ }
238
+ if (Array.isArray(obs)) {
239
+ obsList.push(...obs);
240
+ } else {
241
+ obsList.push(obs);
242
+ }
243
+ }
244
+
245
+ function hasSubmittableObs(field: FormField) {
246
+ const {
247
+ questionOptions: { isTransient },
248
+ type,
249
+ } = field;
250
+
251
+ if (isTransient || !['obs', 'obsGroup'].includes(type) || hasRendering(field, 'file') || field.meta.groupId) {
252
+ return false;
253
+ }
254
+ if ((field.isHidden || field.isParentHidden) && field.meta.previousValue) {
255
+ return true;
256
+ }
257
+ return !field.isHidden && !field.isParentHidden && (type === 'obsGroup' || hasSubmission(field));
258
+ }
259
+
260
+ export function inferInitialValueFromDefaultFieldValue(field: FormField) {
261
+ if (field.questionOptions.rendering == 'toggle' && typeof field.questionOptions.defaultValue != 'boolean') {
262
+ return field.questionOptions.defaultValue == ConceptTrue;
263
+ }
264
+ // validate default value
265
+ if (!DefaultValueValidator.validate(field, field.questionOptions.defaultValue).length) {
266
+ return field.questionOptions.defaultValue;
267
+ }
268
+ }
269
+
270
+ export async function hydrateRepeatField(
271
+ field: FormField,
272
+ encounter: OpenmrsEncounter,
273
+ initialValues: Record<string, any>,
274
+ context: FormProcessorContextProps,
275
+ ): Promise<FormField[]> {
276
+ let counter = 1;
277
+ const { formFieldAdapters } = context;
278
+ const unMappedGroups = encounter.obs.filter(
279
+ (obs) =>
280
+ obs.concept.uuid === field.questionOptions.concept &&
281
+ obs.uuid != field.meta.previousValue?.uuid &&
282
+ !assignedObsIds.includes(obs.uuid),
283
+ );
284
+ const unMappedOrders = encounter.orders.filter((order) => {
285
+ const availableOrderables = field.questionOptions.answers?.map((answer) => answer.concept) || [];
286
+ return availableOrderables.includes(order.concept?.uuid) && !assignedOrderIds.includes(order.uuid);
287
+ });
288
+ if (field.type === 'testOrder') {
289
+ return Promise.all(
290
+ unMappedOrders
291
+ .filter((order) => !order.voided)
292
+ .map(async (order) => {
293
+ const clone = cloneRepeatField(field, order, counter++);
294
+ initialValues[clone.id] = await formFieldAdapters[field.type].getInitialValue(
295
+ clone,
296
+ { orders: [order] } as any,
297
+ context,
298
+ );
299
+ return clone;
300
+ }),
301
+ );
302
+ }
303
+ // handle obs groups
304
+ return Promise.all(
305
+ unMappedGroups.map(async (group) => {
306
+ const clone = cloneRepeatField(field, group, counter++);
307
+ await Promise.all(
308
+ clone.questions.map(async (childField) => {
309
+ initialValues[childField.id] = await formFieldAdapters[field.type].getInitialValue(
310
+ childField,
311
+ { obs: [group] } as any,
312
+ context,
313
+ );
314
+ }),
315
+ );
316
+ assignedObsIds.push(group.uuid);
317
+ return [clone, ...clone.questions];
318
+ }),
319
+ ).then((results) => results.flat());
320
+ }
@@ -0,0 +1,41 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import { type FormContextProps } from '../provider/form-provider';
3
+ import { type ValueAndDisplay, type FormField, type FormSchema } from '../types';
4
+ import { type FormProcessorContextProps } from '../types';
5
+
6
+ export type FormProcessorConstructor = new (...args: ConstructorParameters<typeof FormProcessor>) => FormProcessor;
7
+
8
+ export type GetCustomHooksResponse = {
9
+ useCustomHooks: (context: Partial<FormProcessorContextProps>) => {
10
+ data: any;
11
+ isLoading: boolean;
12
+ error: any;
13
+ updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>) => void;
14
+ };
15
+ };
16
+
17
+ export abstract class FormProcessor {
18
+ formJson: FormSchema;
19
+ domainObjectValue: OpenmrsResource;
20
+
21
+ constructor(formJson: FormSchema) {
22
+ this.formJson = formJson;
23
+ }
24
+
25
+ getDomainObject() {
26
+ return this.domainObjectValue;
27
+ }
28
+
29
+ async loadDependencies(
30
+ context: Partial<FormProcessorContextProps>,
31
+ setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>,
32
+ ): Promise<Record<string, any>> {
33
+ return Promise.resolve({});
34
+ }
35
+
36
+ abstract getHistoricalValue(field: FormField, context: FormContextProps): Promise<ValueAndDisplay>;
37
+ abstract processSubmission(context: FormContextProps, abortController: AbortController): Promise<OpenmrsResource>;
38
+ abstract getInitialValues(context: FormProcessorContextProps): Promise<Record<string, any>>;
39
+ abstract getCustomHooks(): GetCustomHooksResponse;
40
+ abstract prepareFormSchema(schema: FormSchema): FormSchema;
41
+ }
@@ -0,0 +1,100 @@
1
+ import { type OpenmrsResource, showSnackbar } from '@openmrs/esm-framework';
2
+ import { type FormContextProps } from './form-provider';
3
+ import { extractErrorMessagesFromResponse } from '../utils/error-utils';
4
+ import { evaluatePostSubmissionExpression } from '../utils/post-submission-action-helper';
5
+ import { type PostSubmissionActionMeta } from '../hooks/usePostSubmissionActions';
6
+ import { type TFunction } from 'react-i18next';
7
+ import { type SessionMode } from '../types';
8
+
9
+ export function validateForm(context: FormContextProps) {
10
+ const {
11
+ formFields,
12
+ formFieldValidators,
13
+ patient,
14
+ sessionMode,
15
+ addInvalidField,
16
+ methods: { getValues, trigger },
17
+ } = context;
18
+ const values = getValues();
19
+ const errors = formFields
20
+ .flatMap((field) =>
21
+ field.validators?.flatMap((validatorConfig) => {
22
+ const validator = formFieldValidators[validatorConfig.type];
23
+ if (validator) {
24
+ const validationResults = validator.validate(field, values[field.id], {
25
+ fields: formFields,
26
+ values,
27
+ expressionContext: {
28
+ patient,
29
+ mode: sessionMode,
30
+ },
31
+ ...validatorConfig,
32
+ });
33
+ const errors = validationResults.filter((result) => result.resultType === 'error');
34
+ if (errors.length) {
35
+ field.meta.submission = { ...field.meta.submission, errors };
36
+ trigger(field.id);
37
+ addInvalidField(field);
38
+ }
39
+ return errors;
40
+ }
41
+ }),
42
+ )
43
+ .filter((error) => Boolean(error));
44
+ return errors.length === 0;
45
+ }
46
+
47
+ export async function processPostSubmissionActions(
48
+ postSubmissionHandlers: PostSubmissionActionMeta[],
49
+ submissionResults: OpenmrsResource[],
50
+ patient: fhir.Patient,
51
+ sessionMode: SessionMode,
52
+ t: TFunction,
53
+ ) {
54
+ return Promise.all(
55
+ postSubmissionHandlers.map(async ({ postAction, config, actionId, enabled }) => {
56
+ try {
57
+ const encounterData = [];
58
+ if (submissionResults) {
59
+ submissionResults.forEach((result) => {
60
+ if (result?.data) {
61
+ encounterData.push(result.data);
62
+ }
63
+ if (result?.uuid) {
64
+ encounterData.push(result);
65
+ }
66
+ });
67
+
68
+ if (encounterData.length) {
69
+ const isActionEnabled = enabled ? evaluatePostSubmissionExpression(enabled, encounterData) : true;
70
+ if (isActionEnabled) {
71
+ await postAction.applyAction(
72
+ {
73
+ patient,
74
+ sessionMode,
75
+ encounters: encounterData,
76
+ },
77
+ config,
78
+ );
79
+ }
80
+ } else {
81
+ throw new Error('No encounter data to process post submission action');
82
+ }
83
+ } else {
84
+ throw new Error('No handlers available to process post submission action');
85
+ }
86
+ } catch (error) {
87
+ const errorMessages = extractErrorMessagesFromResponse(error);
88
+ showSnackbar({
89
+ title: t(
90
+ 'errorDescriptionTitle',
91
+ actionId ? actionId.replace(/([a-z])([A-Z])/g, '$1 $2') : 'Post Submission Error',
92
+ ),
93
+ subtitle: t('errorDescription', '{{errors}}', { errors: errorMessages.join(', ') }),
94
+ kind: 'error',
95
+ isLowContrast: false,
96
+ });
97
+ }
98
+ }),
99
+ );
100
+ }
@@ -0,0 +1,169 @@
1
+ import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
2
+ import { type FormField, type FormSchema, type SessionMode } from '../types';
3
+ import { EncounterFormProcessor } from '../processors/encounter/encounter-form-processor';
4
+ import {
5
+ type LayoutType,
6
+ useLayoutType,
7
+ type OpenmrsResource,
8
+ showSnackbar,
9
+ showToast,
10
+ type ToastDescriptor,
11
+ } from '@openmrs/esm-framework';
12
+ import { type FormProcessorConstructor } from '../processors/form-processor';
13
+ import { type FormContextProps } from './form-provider';
14
+ import { processPostSubmissionActions, validateForm } from './form-factory-helper';
15
+ import { useTranslation } from 'react-i18next';
16
+ import { usePostSubmissionActions } from '../hooks/usePostSubmissionActions';
17
+ import { reportError } from '../utils/error-utils';
18
+
19
+ interface FormFactoryProviderContextProps {
20
+ patient: fhir.Patient;
21
+ sessionMode: SessionMode;
22
+ sessionDate: Date;
23
+ formJson: FormSchema;
24
+ formProcessors: Record<string, FormProcessorConstructor>;
25
+ layoutType: LayoutType;
26
+ workspaceLayout: 'minimized' | 'maximized';
27
+ visit: OpenmrsResource;
28
+ location: OpenmrsResource;
29
+ provider: OpenmrsResource;
30
+ registerForm: (formId: string, context: FormContextProps) => void;
31
+ setCurrentPage: (page: string) => void;
32
+ handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
33
+ }
34
+
35
+ interface FormFactoryProviderProps {
36
+ patient: fhir.Patient;
37
+ sessionMode: SessionMode;
38
+ sessionDate: Date;
39
+ formJson: FormSchema;
40
+ workspaceLayout: 'minimized' | 'maximized';
41
+ location: OpenmrsResource;
42
+ provider: OpenmrsResource;
43
+ visit: OpenmrsResource;
44
+ children: React.ReactNode;
45
+ formSubmissionProps: {
46
+ isSubmitting: boolean;
47
+ setIsSubmitting: (isSubmitting: boolean) => void;
48
+ onSubmit: (data: any) => void;
49
+ onError: (error: any) => void;
50
+ handleClose: () => void;
51
+ };
52
+ setCurrentPage: (page: string) => void;
53
+ handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
54
+ }
55
+
56
+ const FormFactoryProviderContext = createContext<FormFactoryProviderContextProps | undefined>(undefined);
57
+
58
+ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
59
+ patient,
60
+ sessionMode,
61
+ sessionDate,
62
+ formJson,
63
+ workspaceLayout,
64
+ location,
65
+ provider,
66
+ visit,
67
+ children,
68
+ formSubmissionProps,
69
+ setCurrentPage,
70
+ handleConfirmQuestionDeletion,
71
+ }) => {
72
+ const { t } = useTranslation();
73
+ const rootForm = useRef<FormContextProps>();
74
+ const subForms = useRef<Record<string, FormContextProps>>({});
75
+ const layoutType = useLayoutType();
76
+ const { isSubmitting, setIsSubmitting, onSubmit, onError, handleClose } = formSubmissionProps;
77
+ const postSubmissionHandlers = usePostSubmissionActions(formJson.postSubmissionActions);
78
+
79
+ const abortController = new AbortController();
80
+
81
+ const registerForm = useCallback((formId: string, context: FormContextProps) => {
82
+ if (!rootForm.current) {
83
+ rootForm.current = context;
84
+ } else {
85
+ subForms.current[formId] = context;
86
+ }
87
+ }, []);
88
+
89
+ // TODO: Manage and load processors from the registry
90
+ const formProcessors = useRef<Record<string, FormProcessorConstructor>>({
91
+ EncounterFormProcessor: EncounterFormProcessor,
92
+ });
93
+
94
+ useEffect(() => {
95
+ if (isSubmitting) {
96
+ // TODO: find a dynamic way of managing the form processing order
97
+ const forms = [rootForm.current, ...Object.values(subForms.current)];
98
+ // validate all forms
99
+ const isValid = forms.every((formContext) => validateForm(formContext));
100
+ if (isValid) {
101
+ Promise.all(forms.map((formContext) => formContext.processor.processSubmission(formContext, abortController)))
102
+ .then(async (results) => {
103
+ formSubmissionProps.setIsSubmitting(false);
104
+ if (sessionMode === 'edit') {
105
+ showSnackbar({
106
+ title: t('updatedRecord', 'Record updated'),
107
+ subtitle: t('updatedRecordDescription', 'The patient encounter was updated'),
108
+ kind: 'success',
109
+ isLowContrast: true,
110
+ });
111
+ } else {
112
+ showSnackbar({
113
+ title: t('createdRecord', 'Record created'),
114
+ subtitle: t('createdRecordDescription', 'A new encounter was created'),
115
+ kind: 'success',
116
+ isLowContrast: true,
117
+ });
118
+ }
119
+ if (postSubmissionHandlers) {
120
+ await processPostSubmissionActions(postSubmissionHandlers, results, patient, sessionMode, t);
121
+ }
122
+ if (onSubmit) {
123
+ onSubmit(results);
124
+ } else {
125
+ handleClose();
126
+ }
127
+ })
128
+ .catch((toastErrorObject: ToastDescriptor) => {
129
+ setIsSubmitting(false);
130
+ showToast(toastErrorObject);
131
+ });
132
+ } else {
133
+ setIsSubmitting(false);
134
+ }
135
+ }
136
+ return () => {
137
+ abortController.abort();
138
+ };
139
+ }, [isSubmitting]);
140
+
141
+ return (
142
+ <FormFactoryProviderContext.Provider
143
+ value={{
144
+ patient,
145
+ sessionMode,
146
+ sessionDate,
147
+ formJson,
148
+ formProcessors: formProcessors.current,
149
+ layoutType,
150
+ workspaceLayout,
151
+ visit,
152
+ location,
153
+ provider,
154
+ registerForm,
155
+ setCurrentPage,
156
+ handleConfirmQuestionDeletion,
157
+ }}>
158
+ {formProcessors.current && children}
159
+ </FormFactoryProviderContext.Provider>
160
+ );
161
+ };
162
+
163
+ export const useFormFactory = () => {
164
+ const context = useContext(FormFactoryProviderContext);
165
+ if (!context) {
166
+ throw new Error('useFormFactoryContext must be used within a FormFactoryProvider');
167
+ }
168
+ return context;
169
+ };
@@ -0,0 +1,37 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import { type UseFormReturn } from 'react-hook-form';
3
+ import { type FormProcessorContextProps } from '../types';
4
+ import { type FormSchema, type FormField } from '../types/schema';
5
+
6
+ export interface FormContextProps extends FormProcessorContextProps {
7
+ methods: UseFormReturn<any>;
8
+ workspaceLayout: 'minimized' | 'maximized';
9
+ isSubmitting?: boolean;
10
+ getFormField?: (field: string) => FormField;
11
+ addFormField?: (field: FormField) => void;
12
+ updateFormField?: (field: FormField) => void;
13
+ removeFormField?: (fieldId: string) => void;
14
+ addInvalidField?: (field: FormField) => void;
15
+ removeInvalidField?: (fieldId: string) => void;
16
+ setInvalidFields?: (fields: FormField[]) => void;
17
+ setForm?: (formJson: FormSchema) => void;
18
+ }
19
+
20
+ export interface FormProviderProps extends FormContextProps {
21
+ children: ReactNode;
22
+ }
23
+
24
+ export const FormContext = React.createContext<FormContextProps | undefined>(undefined);
25
+
26
+ export const FormProvider = ({ methods, children, ...contextProps }: FormProviderProps) => {
27
+ return <FormContext.Provider value={{ ...contextProps, methods }}>{children}</FormContext.Provider>;
28
+ };
29
+
30
+ export const useFormProviderContext = () => {
31
+ const context = React.useContext(FormContext);
32
+ if (!context) {
33
+ throw new Error('FormProviderContext must be used within a FormProviderContext');
34
+ }
35
+
36
+ return context;
37
+ };
@@ -0,0 +1,9 @@
1
+ import { type PostSubmissionAction } from '../../types';
2
+ import { type ComponentRegistration } from '../registry';
3
+
4
+ export const inbuiltPostSubmissionActions: Array<ComponentRegistration<PostSubmissionAction>> = [
5
+ {
6
+ name: 'ProgramEnrollmentSubmissionAction',
7
+ load: () => import('../../post-submission-actions/program-enrollment-action'),
8
+ },
9
+ ];