@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,280 @@
1
+ import dayjs from 'dayjs';
2
+ import { ConceptTrue, codedTypes } from '../constants';
3
+ import {
4
+ type OpenmrsObs,
5
+ type FormField,
6
+ type OpenmrsEncounter,
7
+ type AttachmentResponse,
8
+ type Attachment,
9
+ type ValueAndDisplay,
10
+ } from '../types';
11
+ import { hasRendering, gracefullySetSubmission, clearSubmission, flattenObsList } from '../utils/common-utils';
12
+ import { parseToLocalDateTime } from '../utils/form-helper';
13
+ import { type FormContextProps } from '../provider/form-provider';
14
+ import { type FormFieldValueAdapter } from '../types';
15
+ import { isEmpty } from '../validators/form-validator';
16
+ import { getAttachmentByUuid } from '../api';
17
+ import { formatDate, restBaseUrl } from '@openmrs/esm-framework';
18
+
19
+ // Temporarily holds observations that have already been bound with matching fields
20
+ export let assignedObsIds: string[] = [];
21
+
22
+ export const ObsAdapter: FormFieldValueAdapter = {
23
+ async getInitialValue(field: FormField, sourceObject: any, context: FormContextProps) {
24
+ const encounter = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter);
25
+ if (hasRendering(field, 'file')) {
26
+ const ac = new AbortController();
27
+ const attachmentsResponse = await getAttachmentByUuid(context.patient.id, encounter.uuid, ac);
28
+ // TODO: This seems like a violation of the data model.
29
+ // I think we should instead use something like `formFieldPath` to do the mapping.
30
+ const rawAttachment = attachmentsResponse.results?.find((attachment) => attachment.comment === field.id);
31
+ return rawAttachment ? generateAttachment(rawAttachment) : null;
32
+ }
33
+ return extractFieldValue(field, findObsByFormField(flattenObsList(encounter.obs), assignedObsIds, field), true);
34
+ },
35
+ async getPreviousValue(field: FormField, sourceObject: any, context: FormContextProps): Promise<ValueAndDisplay> {
36
+ const encounter = sourceObject ?? (context.previousDomainObjectValue as OpenmrsEncounter);
37
+ if (encounter) {
38
+ const value = extractFieldValue(
39
+ field,
40
+ findObsByFormField(flattenObsList(encounter.obs), assignedObsIds, field),
41
+ true,
42
+ );
43
+ if (!isEmpty(value)) {
44
+ return {
45
+ value,
46
+ display: this.getDisplayValue(field, value),
47
+ };
48
+ }
49
+ }
50
+ return null;
51
+ },
52
+ getDisplayValue: (field: FormField, value: any) => {
53
+ const rendering = field.questionOptions.rendering;
54
+ if (isEmpty(value)) {
55
+ return value;
56
+ }
57
+ if (rendering == 'checkbox') {
58
+ return value.map(
59
+ (selected) => field.questionOptions.answers?.find((option) => option.concept == selected)?.label,
60
+ );
61
+ }
62
+ if (rendering === 'toggle') {
63
+ return value ? field.questionOptions.toggleOptions.labelTrue : field.questionOptions.toggleOptions.labelFalse;
64
+ }
65
+ if (codedTypes.includes(rendering)) {
66
+ return field.questionOptions.answers?.find((option) => option.concept == value)?.label;
67
+ }
68
+ return value;
69
+ },
70
+ transformFieldValue: (field: FormField, value: any, context: FormContextProps) => {
71
+ // clear previous submission
72
+ clearSubmission(field);
73
+ if (!field.meta.previousValue && isEmpty(value)) {
74
+ return null;
75
+ }
76
+ if (hasRendering(field, 'checkbox')) {
77
+ return handleMultiSelect(field, value);
78
+ }
79
+ if (!isEmpty(value) && hasPreviousObsValueChanged(field, value)) {
80
+ return gracefullySetSubmission(field, editObs(field, value), undefined);
81
+ }
82
+ if (field.meta.previousValue && isEmpty(value)) {
83
+ return gracefullySetSubmission(field, undefined, voidObs(field.meta.previousValue));
84
+ }
85
+ if (!isEmpty(value)) {
86
+ return gracefullySetSubmission(field, constructObs(field, value), undefined);
87
+ }
88
+ return null;
89
+ },
90
+ tearDown: function (): void {
91
+ assignedObsIds = [];
92
+ },
93
+ };
94
+
95
+ // Helpers
96
+
97
+ /**
98
+ * Extracts field's primitive value from obs
99
+ */
100
+ function extractFieldValue(field: FormField, obsList: OpenmrsObs[] = [], makeFieldDirty = false) {
101
+ const rendering = field.questionOptions.rendering;
102
+ if (!field.meta) {
103
+ field.meta = {
104
+ previousValue: null,
105
+ };
106
+ }
107
+ if (obsList.length) {
108
+ if (rendering == 'checkbox') {
109
+ assignedObsIds.push(...obsList.map((obs) => obs.uuid));
110
+ field.meta.previousValue = makeFieldDirty ? obsList : null;
111
+ return obsList.map((o) => o.value.uuid);
112
+ }
113
+ const obs = obsList[0];
114
+ if (makeFieldDirty) {
115
+ field.meta.previousValue = { ...obs };
116
+ }
117
+ assignedObsIds.push(obs.uuid);
118
+ if (typeof obs.value === 'string' || typeof obs.value === 'number') {
119
+ if (rendering.startsWith('date')) {
120
+ const dateObject = parseToLocalDateTime(obs.value as string);
121
+ if (makeFieldDirty) {
122
+ field.meta.previousValue.value = dayjs(dateObject).format('YYYY-MM-DD HH:mm');
123
+ }
124
+ return dateObject;
125
+ }
126
+ return obs.value;
127
+ }
128
+ if (rendering == 'toggle') {
129
+ return obs.value.uuid === ConceptTrue;
130
+ }
131
+ if (rendering == 'fixed-value') {
132
+ return field['fixedValue'];
133
+ }
134
+ return obs.value?.uuid;
135
+ }
136
+ return '';
137
+ }
138
+
139
+ export function constructObs(field: FormField, value: any): Partial<OpenmrsObs> {
140
+ if (isEmpty(value) && field.type !== 'obsGroup') {
141
+ return null;
142
+ }
143
+ const draftObs =
144
+ field.type === 'obsGroup'
145
+ ? { groupMembers: [] }
146
+ : {
147
+ value: field.questionOptions.rendering.startsWith('date') ? formatDateByPickerType(field, value) : value,
148
+ };
149
+ return {
150
+ ...draftObs,
151
+ concept: field.questionOptions.concept,
152
+ formFieldNamespace: 'rfe-forms',
153
+ formFieldPath: `rfe-forms-${field.id}`,
154
+ };
155
+ }
156
+
157
+ export function voidObs(obs: OpenmrsObs) {
158
+ return { uuid: obs.uuid, voided: true };
159
+ }
160
+
161
+ export function editObs(field: FormField, newValue: any) {
162
+ const oldObs = field.meta.previousValue;
163
+ const formattedValue = field.questionOptions.rendering.startsWith('date')
164
+ ? formatDateByPickerType(field, newValue)
165
+ : newValue;
166
+ return {
167
+ uuid: oldObs.uuid,
168
+ value: formattedValue,
169
+ formFieldNamespace: 'rfe-forms',
170
+ formFieldPath: `rfe-forms-${field.id}`,
171
+ };
172
+ }
173
+
174
+ function formatDateByPickerType(field: FormField, value: Date) {
175
+ if (field.datePickerFormat) {
176
+ switch (field.datePickerFormat) {
177
+ case 'calendar':
178
+ return dayjs(value).format('YYYY-MM-DD');
179
+ case 'timer':
180
+ return dayjs(value).format('HH:mm');
181
+ case 'both':
182
+ return dayjs(value).format('YYYY-MM-DD HH:mm');
183
+ default:
184
+ return dayjs(value).format('YYYY-MM-DD');
185
+ }
186
+ }
187
+ return value;
188
+ }
189
+
190
+ export function hasPreviousObsValueChanged(field: FormField, newValue: any) {
191
+ const previousObs = field.meta.previousValue;
192
+ if (isEmpty(previousObs)) {
193
+ return false;
194
+ }
195
+ if (codedTypes.includes(field.questionOptions.rendering)) {
196
+ return previousObs.value.uuid !== newValue;
197
+ }
198
+ if (hasRendering(field, 'date')) {
199
+ return dayjs(newValue).diff(dayjs(previousObs.value), 'D') !== 0;
200
+ }
201
+ if (hasRendering(field, 'datetime') || field.datePickerFormat === 'both') {
202
+ return dayjs(newValue).diff(dayjs(previousObs.value), 'minute') !== 0;
203
+ }
204
+ if (hasRendering(field, 'toggle')) {
205
+ return (previousObs.value.uuid === ConceptTrue) !== newValue;
206
+ }
207
+ return previousObs.value !== newValue;
208
+ }
209
+
210
+ function handleMultiSelect(field: FormField, values: Array<string> = []) {
211
+ // three possible scenarios
212
+ // 1. we have a previous value and an empty current value
213
+ // 2. a mix of both (previous and current)
214
+ // 3. we only have a current value
215
+
216
+ if (field.meta.previousValue && isEmpty(values)) {
217
+ // we assume the user cleared the existing value(s)
218
+ // so we void all previous values
219
+ return gracefullySetSubmission(
220
+ field,
221
+ null,
222
+ field.meta.previousValue.map((previousValue) => voidObs(previousValue)),
223
+ );
224
+ }
225
+ if (field.meta.previousValue && !isEmpty(values)) {
226
+ const toBeVoided = field.meta.previousValue.filter((obs) => !values.includes(obs.value.uuid));
227
+ const toBeCreated = values.filter((v) => !field.meta.previousValue.some((obs) => obs.value.uuid === v));
228
+ return gracefullySetSubmission(
229
+ field,
230
+ toBeCreated.map((value) => constructObs(field, value)),
231
+ toBeVoided.map((obs) => voidObs(obs)),
232
+ );
233
+ }
234
+ return gracefullySetSubmission(
235
+ field,
236
+ values.map((value) => constructObs(field, value)),
237
+ undefined,
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Retrieves a list of observations from a given `obsList` that correspond to the specified field.
243
+ *
244
+ * Notes:
245
+ * If the query by field-path returns an empty list, the function falls back to querying
246
+ * by concept and uses `claimedObsIds` to exclude already assigned observations.
247
+ */
248
+ export function findObsByFormField(
249
+ obsList: Array<OpenmrsObs>,
250
+ claimedObsIds: string[],
251
+ field: FormField,
252
+ ): OpenmrsObs[] {
253
+ const obs = obsList.filter(
254
+ (o) => o.formFieldPath == `rfe-forms-${field.id}` && o.concept.uuid == field.questionOptions.concept,
255
+ );
256
+
257
+ // We shall fall back to mapping by the associated concept
258
+ // That being said, we shall find all matching obs and pick the one that wasn't previously claimed.
259
+ if (!obs?.length) {
260
+ const obsByConcept = obsList.filter((obs) => obs.concept.uuid == field.questionOptions.concept);
261
+ return claimedObsIds?.length ? obsByConcept.filter((obs) => !claimedObsIds.includes(obs.uuid)) : obsByConcept;
262
+ }
263
+
264
+ return obs;
265
+ }
266
+
267
+ function generateAttachment(rawAttachment: AttachmentResponse): Attachment {
268
+ const attachmentUrl = `${restBaseUrl}/attachment`;
269
+ return {
270
+ id: rawAttachment.uuid,
271
+ src: `${window.openmrsBase}${attachmentUrl}/${rawAttachment.uuid}/bytes`,
272
+ title: rawAttachment.comment,
273
+ description: '',
274
+ dateTime: formatDate(new Date(rawAttachment.dateTime), {
275
+ mode: 'wide',
276
+ }),
277
+ bytesMimeType: rawAttachment.bytesMimeType,
278
+ bytesContentFamily: rawAttachment.bytesContentFamily,
279
+ };
280
+ }
@@ -0,0 +1,60 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import { type FormContextProps } from '../provider/form-provider';
3
+ import { type FormField, type FormFieldValueAdapter, type FormProcessorContextProps } from '../types';
4
+ import { hasSubmission } from '../utils/common-utils';
5
+ import { isEmpty } from '../validators/form-validator';
6
+ import { editObs, hasPreviousObsValueChanged } from './obs-adapter';
7
+
8
+ export const ObsCommentAdapter: FormFieldValueAdapter = {
9
+ transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
10
+ const targetField = context.getFormField(field.meta.targetField);
11
+ const targetFieldCurrentValue = context.methods.getValues(targetField.id);
12
+
13
+ if (targetField.meta.submission?.newValue) {
14
+ if (isEmpty(value) && !isNewSubmissionEffective(targetField, targetFieldCurrentValue)) {
15
+ // clear submission
16
+ targetField.meta.submission.newValue = null;
17
+ } else {
18
+ targetField.meta.submission.newValue.comment = value;
19
+ }
20
+ } else if (!hasSubmission(targetField) && targetField.meta.previousValue) {
21
+ if (isEmpty(value) && isEmpty(targetField.meta.previousValue.comment)) {
22
+ return null;
23
+ }
24
+ // generate submission
25
+ const newSubmission = editObs(targetField, targetFieldCurrentValue);
26
+ targetField.meta.submission = {
27
+ newValue: {
28
+ ...newSubmission,
29
+ comment: value,
30
+ },
31
+ };
32
+ }
33
+ return null;
34
+ },
35
+ getInitialValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
36
+ const encounter = sourceObject ?? context.domainObjectValue;
37
+ if (encounter) {
38
+ const targetFieldId = field.id.split('_obs_comment')[0];
39
+ const targetField = context.formFields.find((field) => field.id === targetFieldId);
40
+ return targetField?.meta.previousValue?.comment;
41
+ }
42
+ return null;
43
+ },
44
+ getPreviousValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
45
+ return null;
46
+ },
47
+ getDisplayValue: function (field: FormField, value: string) {
48
+ return value;
49
+ },
50
+ tearDown: function (): void {
51
+ return;
52
+ },
53
+ };
54
+
55
+ export function isNewSubmissionEffective(targetField: FormField, targetFieldCurrentValue: any) {
56
+ return (
57
+ hasPreviousObsValueChanged(targetField, targetFieldCurrentValue) ||
58
+ !isEmpty(targetField.meta.submission.newValue.obsDatetime)
59
+ );
60
+ }
@@ -0,0 +1,75 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import { type FormFieldValueAdapter, type FormProcessorContextProps } from '..';
3
+ import { type FormContextProps } from '../provider/form-provider';
4
+ import { type FormField } from '../types';
5
+ import { clearSubmission, gracefullySetSubmission } from '../utils/common-utils';
6
+
7
+ export let assignedOrderIds: string[] = [];
8
+ const defaultOrderType = 'testorder';
9
+ const defaultCareSetting = '6f0c9a92-6f24-11e3-af88-005056821db0';
10
+
11
+ export const OrdersAdapter: FormFieldValueAdapter = {
12
+ transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
13
+ if (context.sessionMode == 'edit' && field.meta?.previousValue?.uuid) {
14
+ return editOrder(value, field, context.currentProvider.uuid);
15
+ }
16
+ const newValue = constructNewOrder(value, field, context.currentProvider.uuid);
17
+ gracefullySetSubmission(field, newValue, null);
18
+ return newValue;
19
+ },
20
+ getInitialValue: function (
21
+ field: FormField,
22
+ sourceObject: OpenmrsResource,
23
+ context: FormProcessorContextProps,
24
+ ): Promise<any> {
25
+ const availableOrderables = field.questionOptions.answers?.map((answer) => answer.concept) || [];
26
+ const matchedOrder = sourceObject?.orders
27
+ .filter((order) => !assignedOrderIds.includes(order.uuid) && !order.voided)
28
+ .find((order) => availableOrderables.includes(order.concept.uuid));
29
+ if (matchedOrder) {
30
+ field.meta = { ...(field.meta || {}), previousValue: matchedOrder };
31
+ assignedOrderIds.push(matchedOrder.uuid);
32
+ return matchedOrder.concept.uuid;
33
+ }
34
+ return null;
35
+ },
36
+ getPreviousValue: function (
37
+ field: FormField,
38
+ sourceObject: OpenmrsResource,
39
+ context: FormProcessorContextProps,
40
+ ): Promise<any> {
41
+ return null;
42
+ },
43
+ getDisplayValue: (field: FormField, value: any) => {
44
+ return field.questionOptions.answers?.find((option) => option.concept == value)?.label || value;
45
+ },
46
+ tearDown: function (): void {
47
+ assignedOrderIds = [];
48
+ },
49
+ };
50
+
51
+ function constructNewOrder(value: any, field: FormField, orderer: string) {
52
+ if (!value) {
53
+ return null;
54
+ }
55
+ return {
56
+ action: 'NEW',
57
+ concept: value,
58
+ type: field?.questionOptions?.orderType || defaultOrderType,
59
+ careSetting: field?.questionOptions?.orderSettingUuid || defaultCareSetting,
60
+ orderer: orderer,
61
+ };
62
+ }
63
+
64
+ function editOrder(newOrder: any, field: FormField, orderer: string) {
65
+ if (newOrder === field.meta.previousValue?.concept?.uuid) {
66
+ clearSubmission(field);
67
+ return null;
68
+ }
69
+ const voided = {
70
+ uuid: field.meta.previousValue?.uuid,
71
+ voided: true,
72
+ };
73
+ gracefullySetSubmission(field, constructNewOrder(newOrder, field, orderer), voided);
74
+ return field.meta.submission.newValue || null;
75
+ }
@@ -0,0 +1,40 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import { type FormContextProps } from '../provider/form-provider';
3
+ import { type FormField, type FormFieldValueAdapter, type FormProcessorContextProps } from '../types';
4
+ import { clearSubmission } from '../utils/common-utils';
5
+ import { isEmpty } from '../validators/form-validator';
6
+
7
+ export const PatientIdentifierAdapter: FormFieldValueAdapter = {
8
+ transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
9
+ clearSubmission(field);
10
+ if (field.meta?.previousValue?.value === value || isEmpty(value)) {
11
+ return null;
12
+ }
13
+ field.meta.submission.newValue = {
14
+ identifier: value,
15
+ identifierType: field.questionOptions.identifierType,
16
+ uuid: field.meta.previousValue?.id,
17
+ location: context.location,
18
+ };
19
+ return field.meta.submission.newValue;
20
+ },
21
+ getInitialValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
22
+ const latestIdentifier = context.patient?.identifier?.find(
23
+ (identifier) => identifier.type?.coding[0]?.code === field.questionOptions.identifierType,
24
+ );
25
+ field.meta = { ...(field.meta || {}), previousValue: latestIdentifier };
26
+ return latestIdentifier?.value;
27
+ },
28
+ getPreviousValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
29
+ return null;
30
+ },
31
+ getDisplayValue: function (field: FormField, value: any) {
32
+ if (value?.display) {
33
+ return value.display;
34
+ }
35
+ return value;
36
+ },
37
+ tearDown: function (): void {
38
+ return;
39
+ },
40
+ };
@@ -0,0 +1,52 @@
1
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
+ import { type FormContextProps } from '../provider/form-provider';
3
+ import { type FormField, type FormProcessorContextProps, type FormFieldValueAdapter } from '../types';
4
+ import dayjs from 'dayjs';
5
+ import { clearSubmission } from '../utils/common-utils';
6
+ import { isEmpty } from '../validators/form-validator';
7
+
8
+ export const ProgramStateAdapter: FormFieldValueAdapter = {
9
+ transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
10
+ clearSubmission(field);
11
+ if (field.meta?.previousValue?.uuid === value || isEmpty(value)) {
12
+ return null;
13
+ }
14
+ field.meta.submission.newValue = {
15
+ state: value,
16
+ startDate: dayjs().format(),
17
+ };
18
+ },
19
+ getInitialValue: function (
20
+ field: FormField,
21
+ sourceObject: OpenmrsResource,
22
+ context: FormProcessorContextProps,
23
+ ): Promise<any> {
24
+ const program = context.customDependencies.patientPrograms?.find(
25
+ (program) => program.program.uuid === field.questionOptions.programUuid,
26
+ );
27
+ if (program?.states?.length > 0) {
28
+ const currentState = program.states
29
+ .filter((state) => !state.endDate)
30
+ .find((state) => state.state.programWorkflow?.uuid === field.questionOptions.workflowUuid)?.state;
31
+ field.meta = { ...(field.meta || {}), previousValue: currentState };
32
+ return currentState.uuid;
33
+ }
34
+ return null;
35
+ },
36
+ getPreviousValue: function (
37
+ field: FormField,
38
+ sourceObject: OpenmrsResource,
39
+ context: FormProcessorContextProps,
40
+ ): Promise<any> {
41
+ return null;
42
+ },
43
+ getDisplayValue: function (field: FormField, value: any) {
44
+ if (value?.display) {
45
+ return value.display;
46
+ }
47
+ return value;
48
+ },
49
+ tearDown: function (): void {
50
+ return;
51
+ },
52
+ };
@@ -0,0 +1,178 @@
1
+ import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { encounterRepresentation } from '../constants';
3
+ import { type OpenmrsForm, type PatientIdentifier, type PatientProgramPayload } from '../types';
4
+ import { isUuid } from '../utils/boolean-utils';
5
+
6
+ export function saveEncounter(abortController: AbortController, payload, encounterUuid?: string) {
7
+ const url = encounterUuid
8
+ ? `${restBaseUrl}/encounter/${encounterUuid}?v=${encounterRepresentation}`
9
+ : `${restBaseUrl}/encounter?v=${encounterRepresentation}`;
10
+
11
+ return openmrsFetch(url, {
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ method: 'POST',
16
+ body: payload,
17
+ signal: abortController.signal,
18
+ });
19
+ }
20
+
21
+ export function saveAttachment(patientUuid, field, conceptUuid, date, encounterUUID, abortController) {
22
+ const url = `${restBaseUrl}/attachment`;
23
+
24
+ const content = field.meta.submission?.newValue?.value;
25
+ const cameraUploadType = typeof content === 'string' && content?.split(';')[0].split(':')[1].split('/')[1];
26
+
27
+ const formData = new FormData();
28
+ const fileCaption = field.id;
29
+
30
+ formData.append('fileCaption', fileCaption);
31
+ formData.append('patient', patientUuid);
32
+
33
+ if (typeof content === 'object') {
34
+ formData.append('file', content);
35
+ } else {
36
+ formData.append('file', new File([''], `camera-upload.${cameraUploadType}`), `camera-upload.${cameraUploadType}`);
37
+ formData.append('base64Content', content);
38
+ }
39
+ formData.append('encounter', encounterUUID);
40
+ formData.append('obsDatetime', date);
41
+
42
+ return openmrsFetch(url, {
43
+ method: 'POST',
44
+ signal: abortController.signal,
45
+ body: formData,
46
+ });
47
+ }
48
+
49
+ export function getAttachmentByUuid(patientUuid: string, encounterUuid: string, abortController: AbortController) {
50
+ const attachmentUrl = `${restBaseUrl}/attachment`;
51
+ return openmrsFetch(`${attachmentUrl}?patient=${patientUuid}&encounter=${encounterUuid}`, {
52
+ signal: abortController.signal,
53
+ }).then((response) => response.data);
54
+ }
55
+
56
+ export function getConcept(conceptUuid: string, v: string) {
57
+ return openmrsFetch(`${restBaseUrl}/concept/${conceptUuid}?v=${v}`).then(({ data }) => data.results);
58
+ }
59
+
60
+ export function getLocationsByTag(tag: string) {
61
+ return openmrsFetch(`${restBaseUrl}/location?tag=${tag}&v=custom:(uuid,display)`).then(({ data }) => data.results);
62
+ }
63
+
64
+ export function getAllLocations() {
65
+ return openmrsFetch<{ results }>(`${restBaseUrl}/location?v=custom:(uuid,display)`).then(({ data }) => data.results);
66
+ }
67
+
68
+ export async function getPreviousEncounter(patientUuid: string, encounterType: string) {
69
+ const query = `patient=${patientUuid}&_sort=-date&_count=1&type=${encounterType}`;
70
+ let response = await openmrsFetch(`${fhirBaseUrl}/Encounter?${query}`);
71
+ if (response?.data?.entry?.length) {
72
+ const latestEncounter = response.data.entry[0].resource.id;
73
+ response = await openmrsFetch(`${restBaseUrl}/encounter/${latestEncounter}?v=${encounterRepresentation}`);
74
+ return response.data;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export function getLatestObs(patientUuid: string, conceptUuid: string, encounterTypeUuid?: string) {
80
+ let params = `patient=${patientUuid}&code=${conceptUuid}${
81
+ encounterTypeUuid ? `&encounter.type=${encounterTypeUuid}` : ''
82
+ }`;
83
+ // the latest obs
84
+ params += '&_sort=-date&_count=1';
85
+ return openmrsFetch(`${fhirBaseUrl}/Observation?${params}`).then(({ data }) => {
86
+ return data.entry?.length ? data.entry[0].resource : null;
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Fetches an OpenMRS form using either its name or UUID.
92
+ * @param {string} nameOrUUID - The form's name or UUID.
93
+ * @returns {Promise<OpenmrsForm | null>} - A Promise that resolves to the fetched OpenMRS form or null if not found.
94
+ */
95
+ export async function fetchOpenMRSForm(nameOrUUID: string): Promise<OpenmrsForm | null> {
96
+ if (!nameOrUUID) {
97
+ return null;
98
+ }
99
+
100
+ const { url, isUUID } = isUuid(nameOrUUID)
101
+ ? { url: `${restBaseUrl}/form/${nameOrUUID}?v=full`, isUUID: true }
102
+ : { url: `${restBaseUrl}/form?q=${nameOrUUID}&v=full`, isUUID: false };
103
+
104
+ const { data: openmrsFormResponse } = await openmrsFetch(url);
105
+ if (isUUID) {
106
+ return openmrsFormResponse;
107
+ }
108
+
109
+ return openmrsFormResponse.results?.length
110
+ ? openmrsFormResponse.results.find((form) => form.retired === false)
111
+ : new Error(`Form with ${nameOrUUID} was not found`);
112
+ }
113
+
114
+ /**
115
+ * Fetches ClobData for a given OpenMRS form.
116
+ * @param {OpenmrsForm} form - The OpenMRS form object.
117
+ * @returns {Promise<any | null>} - A Promise that resolves to the fetched ClobData or null if not found.
118
+ */
119
+ export async function fetchClobData(form: OpenmrsForm): Promise<any | null> {
120
+ if (!form) {
121
+ return null;
122
+ }
123
+
124
+ const jsonSchemaResource = form.resources.find(({ name }) => name === 'JSON schema');
125
+ if (!jsonSchemaResource) {
126
+ return null;
127
+ }
128
+
129
+ const clobDataUrl = `${restBaseUrl}/clobdata/${jsonSchemaResource.valueReference}`;
130
+ const { data: clobDataResponse } = await openmrsFetch(clobDataUrl);
131
+
132
+ return clobDataResponse;
133
+ }
134
+
135
+ // Program Enrollment
136
+ export function getPatientEnrolledPrograms(patientUuid: string) {
137
+ return openmrsFetch(
138
+ `${restBaseUrl}/programenrollment?patient=${patientUuid}&v=custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(state:(uuid,name,concept:(uuid),programWorkflow:(uuid)))`,
139
+ ).then(({ data }) => {
140
+ if (data) {
141
+ return data;
142
+ }
143
+ return null;
144
+ });
145
+ }
146
+
147
+ export function saveProgramEnrollment(payload: PatientProgramPayload, abortController: AbortController) {
148
+ if (!payload) {
149
+ throw new Error('Program enrollment cannot be created because no payload is supplied');
150
+ }
151
+ const url = payload.uuid ? `${restBaseUrl}/programenrollment/${payload.uuid}` : `${restBaseUrl}/programenrollment`;
152
+ return openmrsFetch(url, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ },
157
+ body: JSON.stringify(payload),
158
+ signal: abortController.signal,
159
+ });
160
+ }
161
+
162
+ export function savePatientIdentifier(patientIdentifier: PatientIdentifier, patientUuid: string) {
163
+ let url: string;
164
+
165
+ if (patientIdentifier.uuid) {
166
+ url = `${restBaseUrl}/patient/${patientUuid}/identifier/${patientIdentifier.uuid}`;
167
+ } else {
168
+ url = `${restBaseUrl}/patient/${patientUuid}/identifier`;
169
+ }
170
+
171
+ return openmrsFetch(url, {
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ },
175
+ method: 'POST',
176
+ body: JSON.stringify(patientIdentifier),
177
+ });
178
+ }