@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,281 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ type FormField,
4
+ type RenderType,
5
+ type ValidationResult,
6
+ type FormFieldValidator,
7
+ type SessionMode,
8
+ type ValueAndDisplay,
9
+ } from '../../../types';
10
+ import { Controller, useWatch } from 'react-hook-form';
11
+ import { ToastNotification } from '@carbon/react';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { ErrorBoundary } from 'react-error-boundary';
14
+ import { type FormFieldValueAdapter, type FormFieldInputProps } from '../../../types';
15
+ import { hasRendering } from '../../../utils/common-utils';
16
+ import { useFormProviderContext } from '../../../provider/form-provider';
17
+ import { isEmpty } from '../../../validators/form-validator';
18
+ import PreviousValueReview from '../../previous-value-review/previous-value-review.component';
19
+ import { getRegisteredControl } from '../../../registry/registry';
20
+ import styles from './form-field-renderer.scss';
21
+ import { isTrue } from '../../../utils/boolean-utils';
22
+ import UnspecifiedField from '../../inputs/unspecified/unspecified.component';
23
+ import { getFieldControlWithFallback } from '../../../utils/form-helper';
24
+ import { handleFieldLogic } from './fieldLogic';
25
+
26
+ export interface FormFieldRendererProps {
27
+ field: FormField;
28
+ valueAdapter: FormFieldValueAdapter;
29
+ repeatOptions?: {
30
+ targetRendering: RenderType;
31
+ };
32
+ }
33
+
34
+ export const FormFieldRenderer = ({ field, valueAdapter, repeatOptions }: FormFieldRendererProps) => {
35
+ const [inputComponentWrapper, setInputComponentWrapper] = useState<{
36
+ value: React.ComponentType<FormFieldInputProps>;
37
+ }>(null);
38
+ const [errors, setErrors] = useState<ValidationResult[]>([]);
39
+ const [warnings, setWarnings] = useState<ValidationResult[]>([]);
40
+ const [historicalValue, setHistoricalValue] = useState<ValueAndDisplay>(null);
41
+ const context = useFormProviderContext();
42
+
43
+ const {
44
+ methods: { control, getValues, getFieldState },
45
+ patient,
46
+ sessionMode,
47
+ formFields,
48
+ formFieldValidators,
49
+ addInvalidField,
50
+ removeInvalidField,
51
+ } = context;
52
+
53
+ const fieldValue = useWatch({ control, name: field.id, exact: true });
54
+ const noop = () => {};
55
+
56
+ useEffect(() => {
57
+ if (hasRendering(field, 'repeating') && repeatOptions?.targetRendering) {
58
+ getRegisteredControl(repeatOptions.targetRendering).then((component) => {
59
+ if (component) {
60
+ setInputComponentWrapper({ value: component });
61
+ }
62
+ });
63
+ } else {
64
+ getFieldControlWithFallback(field).then((component) => {
65
+ if (component) {
66
+ setInputComponentWrapper({ value: component });
67
+ }
68
+ });
69
+ }
70
+ if (sessionMode === 'enter' && (field.historicalExpression || context.previousDomainObjectValue)) {
71
+ try {
72
+ context.processor.getHistoricalValue(field, context).then((value) => {
73
+ setHistoricalValue(value);
74
+ });
75
+ } catch (error) {
76
+ console.error(error);
77
+ }
78
+ }
79
+ }, []);
80
+
81
+ useEffect(() => {
82
+ const { isDirty, isTouched } = getFieldState(field.id);
83
+ const { submission, unspecified } = field.meta;
84
+ const { calculate, defaultValue } = field.questionOptions;
85
+ if (
86
+ !isEmpty(fieldValue) &&
87
+ !submission?.newValue &&
88
+ !isDirty &&
89
+ !unspecified &&
90
+ (calculate?.calculateExpression || defaultValue)
91
+ ) {
92
+ valueAdapter.transformFieldValue(field, fieldValue, context);
93
+ }
94
+ if (isDirty || isTouched) {
95
+ onAfterChange(fieldValue);
96
+ }
97
+ }, [fieldValue]);
98
+
99
+ useEffect(() => {
100
+ if (field.meta.submission?.errors) {
101
+ setErrors(field.meta.submission.errors);
102
+ }
103
+ if (field.meta.submission?.warnings) {
104
+ setWarnings(field.meta.submission.warnings);
105
+ }
106
+ }, [field.meta.submission]);
107
+
108
+ const onAfterChange = (value: any) => {
109
+ const { errors: validationErrors, warnings: validationWarnings } = validateFieldValue(
110
+ field,
111
+ value,
112
+ formFieldValidators,
113
+ {
114
+ fields: formFields,
115
+ values: getValues(),
116
+ expressionContext: { patient, mode: sessionMode },
117
+ },
118
+ );
119
+ if (errors.length && !validationErrors.length) {
120
+ removeInvalidField(field.id);
121
+ setErrors([]);
122
+ } else if (validationErrors.length) {
123
+ setErrors(validationErrors);
124
+ addInvalidField(field);
125
+ }
126
+ if (!validationErrors.length) {
127
+ valueAdapter.transformFieldValue(field, value, context);
128
+ }
129
+ setWarnings(validationWarnings);
130
+ handleFieldLogic(field, context);
131
+ };
132
+
133
+ if (!inputComponentWrapper) {
134
+ return null;
135
+ }
136
+
137
+ const InputComponent = inputComponentWrapper.value;
138
+
139
+ if (!repeatOptions?.targetRendering && isGroupField(field.questionOptions.rendering)) {
140
+ return (
141
+ <InputComponent
142
+ key={field.id}
143
+ field={field}
144
+ value={null}
145
+ errors={errors}
146
+ warnings={warnings}
147
+ setFieldValue={null}
148
+ />
149
+ );
150
+ }
151
+ return (
152
+ <ErrorBoundary FallbackComponent={ErrorFallback} onReset={noop}>
153
+ <Controller
154
+ control={control}
155
+ name={field.id}
156
+ render={({ field: { value, onChange, onBlur } }) => (
157
+ <div>
158
+ <InputComponent
159
+ key={`${field.id}-input-component`}
160
+ field={field}
161
+ value={value}
162
+ errors={errors}
163
+ warnings={warnings}
164
+ setFieldValue={(val) => {
165
+ onChange(val);
166
+ onAfterChange(val);
167
+ onBlur();
168
+ }}
169
+ />
170
+ {isUnspecifiedSupported(field) && (
171
+ <div className={styles.unspecifiedContainer}>
172
+ {field.unspecified && (
173
+ <UnspecifiedField
174
+ key={`${field.id}-unspecified`}
175
+ field={field}
176
+ setFieldValue={onChange}
177
+ onAfterChange={onAfterChange}
178
+ fieldValue={value}
179
+ />
180
+ )}
181
+ </div>
182
+ )}
183
+ {historicalValue?.value && (
184
+ <div>
185
+ <PreviousValueReview
186
+ key={`${field.id}-previous-value-review`}
187
+ previousValue={historicalValue.value}
188
+ displayText={historicalValue.display}
189
+ onAfterChange={onAfterChange}
190
+ field={field}
191
+ />
192
+ </div>
193
+ )}
194
+ </div>
195
+ )}
196
+ />
197
+ </ErrorBoundary>
198
+ );
199
+ };
200
+
201
+ function ErrorFallback({ error }) {
202
+ const { t } = useTranslation();
203
+ return (
204
+ <ToastNotification
205
+ aria-label={t('closesNotification', 'Closes notification')}
206
+ caption=""
207
+ hideCloseButton
208
+ lowContrast
209
+ onClose={function noRefCheck() {}}
210
+ onCloseButtonClick={function noRefCheck() {}}
211
+ statusIconDescription={t('notification', 'Notification')}
212
+ subtitle={error.message}
213
+ title={t('errorRenderingField', 'Error rendering field')}
214
+ />
215
+ );
216
+ }
217
+
218
+ export interface ValidatorConfig {
219
+ fields: FormField[];
220
+ values: Record<string, any>;
221
+ expressionContext: {
222
+ patient: fhir.Patient;
223
+ mode: SessionMode;
224
+ };
225
+ }
226
+
227
+ function validateFieldValue(
228
+ field: FormField,
229
+ value: any,
230
+ validators: Record<string, FormFieldValidator>,
231
+ context: ValidatorConfig,
232
+ ): { errors: ValidationResult[]; warnings: ValidationResult[] } {
233
+ const errors: ValidationResult[] = [];
234
+ const warnings: ValidationResult[] = [];
235
+
236
+ if (field.meta.submission?.unspecified) {
237
+ return { errors: [], warnings: [] };
238
+ }
239
+
240
+ try {
241
+ field.validators.forEach((validatorConfig) => {
242
+ const results = validators[validatorConfig.type]?.validate?.(field, value, {
243
+ ...validatorConfig,
244
+ ...context,
245
+ });
246
+ if (results) {
247
+ results.forEach((result) => {
248
+ if (result.resultType === 'error') {
249
+ errors.push(result);
250
+ } else if (result.resultType === 'warning') {
251
+ warnings.push(result);
252
+ }
253
+ });
254
+ }
255
+ });
256
+ } catch (error) {
257
+ console.error(error);
258
+ }
259
+
260
+ return { errors, warnings };
261
+ }
262
+
263
+ /**
264
+ * Determines whether a field can be unspecified
265
+ */
266
+ export function isUnspecifiedSupported(question: FormField) {
267
+ const { rendering } = question.questionOptions;
268
+ return (
269
+ isTrue(question.unspecified) &&
270
+ rendering != 'toggle' &&
271
+ rendering != 'group' &&
272
+ rendering != 'repeating' &&
273
+ rendering != 'markdown' &&
274
+ rendering != 'extension-widget' &&
275
+ rendering != 'workspace-launcher'
276
+ );
277
+ }
278
+
279
+ function isGroupField(rendering: RenderType) {
280
+ return rendering === 'group' || rendering === 'repeating';
281
+ }
@@ -0,0 +1,5 @@
1
+ .unspecifiedContainer {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-items: center;
5
+ }
@@ -0,0 +1,89 @@
1
+ import React, { useEffect, useMemo, useReducer } from 'react';
2
+ import { useForm } from 'react-hook-form';
3
+ import PageRenderer from '../page/page.renderer.component';
4
+ import FormProcessorFactory from '../../processor-factory/form-processor-factory.component';
5
+ import { formStateReducer, initialState } from './state';
6
+ import { useEvaluateFormFieldExpressions } from '../../../hooks/useEvaluateFormFieldExpressions';
7
+ import { useFormFactory } from '../../../provider/form-factory-provider';
8
+ import { FormProvider, type FormContextProps } from '../../../provider/form-provider';
9
+ import { isTrue } from '../../../utils/boolean-utils';
10
+ import { type FormProcessorContextProps } from '../../../types';
11
+ import { useFormStateHelpers } from '../../../hooks/useFormStateHelpers';
12
+
13
+ export type FormRendererProps = {
14
+ processorContext: FormProcessorContextProps;
15
+ initialValues: Record<string, any>;
16
+ setIsLoadingFormDependencies: (isLoading: boolean) => void;
17
+ };
18
+
19
+ export const FormRenderer = ({ processorContext, initialValues, setIsLoadingFormDependencies }: FormRendererProps) => {
20
+ const { evaluatedFields, evaluatedFormJson } = useEvaluateFormFieldExpressions(initialValues, processorContext);
21
+ const { registerForm, workspaceLayout } = useFormFactory();
22
+ const methods = useForm({
23
+ defaultValues: initialValues,
24
+ });
25
+ const [{ formFields, invalidFields, formJson }, dispatch] = useReducer(formStateReducer, {
26
+ ...initialState,
27
+ formFields: evaluatedFields,
28
+ formJson: evaluatedFormJson,
29
+ });
30
+
31
+ const {
32
+ addFormField,
33
+ updateFormField,
34
+ getFormField,
35
+ removeFormField,
36
+ setInvalidFields,
37
+ addInvalidField,
38
+ removeInvalidField,
39
+ setForm,
40
+ } = useFormStateHelpers(dispatch, formFields);
41
+
42
+ const context: FormContextProps = useMemo(() => {
43
+ return {
44
+ ...processorContext,
45
+ workspaceLayout,
46
+ methods,
47
+ formFields,
48
+ formJson,
49
+ invalidFields,
50
+ addFormField,
51
+ updateFormField,
52
+ getFormField,
53
+ removeFormField,
54
+ setInvalidFields,
55
+ addInvalidField,
56
+ removeInvalidField,
57
+ setForm,
58
+ };
59
+ }, [processorContext, workspaceLayout, methods, formFields, formJson, invalidFields]);
60
+
61
+ useEffect(() => {
62
+ registerForm(formJson.name, context);
63
+ }, [formJson.name, context]);
64
+
65
+ return (
66
+ <FormProvider {...context}>
67
+ {formJson.pages.map((page) => {
68
+ const pageHasNoVisibleContent =
69
+ page.sections?.every((section) => section.isHidden) ||
70
+ page.sections?.every((section) => section.questions?.every((question) => question.isHidden)) ||
71
+ isTrue(page.isHidden);
72
+ if (!page.isSubform && pageHasNoVisibleContent) {
73
+ return null;
74
+ }
75
+ if (page.isSubform && page.subform?.form) {
76
+ return (
77
+ <FormProcessorFactory
78
+ key={page.subform.form.uuid}
79
+ formJson={page.subform.form}
80
+ isSubForm={true}
81
+ setIsLoadingFormDependencies={setIsLoadingFormDependencies}
82
+ />
83
+ );
84
+ }
85
+ return <PageRenderer page={page} />;
86
+ })}
87
+ </FormProvider>
88
+ );
89
+ };
@@ -0,0 +1,54 @@
1
+ import { type FormField, type FormSchema } from '../../../types';
2
+
3
+ type FormState = {
4
+ formFields: FormField[];
5
+ invalidFields: FormField[];
6
+ formJson: FormSchema;
7
+ };
8
+
9
+ type Action =
10
+ | { type: 'SET_FORM_FIELDS'; value: FormField[] }
11
+ | { type: 'ADD_FORM_FIELD'; value: FormField }
12
+ | { type: 'UPDATE_FORM_FIELD'; value: FormField }
13
+ | { type: 'REMOVE_FORM_FIELD'; value: string }
14
+ | { type: 'SET_INVALID_FIELDS'; value: FormField[] }
15
+ | { type: 'ADD_INVALID_FIELD'; value: FormField }
16
+ | { type: 'REMOVE_INVALID_FIELD'; value: string }
17
+ | { type: 'CLEAR_INVALID_FIELDS' }
18
+ | { type: 'SET_FORM_JSON'; value: any };
19
+
20
+ const initialState: FormState = {
21
+ formFields: [],
22
+ invalidFields: [],
23
+ formJson: null,
24
+ };
25
+
26
+ const formStateReducer = (state: FormState, action: Action): FormState => {
27
+ switch (action.type) {
28
+ case 'SET_FORM_FIELDS':
29
+ return { ...state, formFields: action.value };
30
+ case 'ADD_FORM_FIELD':
31
+ return { ...state, formFields: [...state.formFields, action.value] };
32
+ case 'UPDATE_FORM_FIELD':
33
+ return {
34
+ ...state,
35
+ formFields: state.formFields.map((field) => (field.id === action.value.id ? action.value : field)),
36
+ };
37
+ case 'REMOVE_FORM_FIELD':
38
+ return { ...state, formFields: state.formFields.filter((field) => field.id !== action.value) };
39
+ case 'SET_INVALID_FIELDS':
40
+ return { ...state, invalidFields: action.value };
41
+ case 'ADD_INVALID_FIELD':
42
+ return { ...state, invalidFields: [...state.invalidFields, action.value] };
43
+ case 'REMOVE_INVALID_FIELD':
44
+ return { ...state, invalidFields: state.invalidFields.filter((field) => field.id !== action.value) };
45
+ case 'CLEAR_INVALID_FIELDS':
46
+ return { ...state, invalidFields: [] };
47
+ case 'SET_FORM_JSON':
48
+ return { ...state, formJson: action.value };
49
+ default:
50
+ return state;
51
+ }
52
+ };
53
+
54
+ export { formStateReducer, initialState, FormState, Action };
@@ -0,0 +1,50 @@
1
+ import React, { useMemo } from 'react';
2
+ import { type FormPage } from '../../../types';
3
+ import { isTrue } from '../../../utils/boolean-utils';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { SectionRenderer } from '../section/section-renderer.component';
6
+ import { Waypoint } from 'react-waypoint';
7
+ import styles from './page.renderer.scss';
8
+ import { Accordion, AccordionItem } from '@carbon/react';
9
+ import { useFormFactory } from '../../../provider/form-factory-provider';
10
+
11
+ interface PageRendererProps {
12
+ page: FormPage;
13
+ }
14
+
15
+ function PageRenderer({ page }: PageRendererProps) {
16
+ const { t } = useTranslation();
17
+ const pageId = useMemo(() => page.label.replace(/\s/g, ''), [page.label]);
18
+
19
+ const { setCurrentPage } = useFormFactory();
20
+ const visibleSections = page.sections.filter((section) => {
21
+ const hasVisibleQuestions = section.questions.some((question) => !isTrue(question.isHidden));
22
+ return !isTrue(section.isHidden) && hasVisibleQuestions;
23
+ });
24
+ return (
25
+ <div>
26
+ <Waypoint onEnter={() => setCurrentPage(pageId)} topOffset="50%" bottomOffset="60%">
27
+ <div className={styles.pageContent}>
28
+ <div className={styles.pageHeader}>
29
+ <p className={styles.pageTitle}>{t(page.label)}</p>
30
+ </div>
31
+ <Accordion>
32
+ {visibleSections.map((section) => (
33
+ <AccordionItem
34
+ title={t(section.label)}
35
+ open={true}
36
+ className={styles.sectionContainer}
37
+ key={`section-${section.label}`}>
38
+ <div className={styles.formSection}>
39
+ <SectionRenderer section={section} />
40
+ </div>
41
+ </AccordionItem>
42
+ ))}
43
+ </Accordion>
44
+ </div>
45
+ </Waypoint>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export default PageRenderer;
@@ -0,0 +1,36 @@
1
+ @use '@carbon/colors';
2
+
3
+ .pageContent:last-child > hr {
4
+ display: none;
5
+ }
6
+
7
+ .pageHeader {
8
+ display: flex;
9
+ flex-direction: row;
10
+ margin: 0.5rem 1rem;
11
+ }
12
+
13
+ .pageTitle {
14
+ font-size: 1.25rem;
15
+ font-weight: 600;
16
+ line-height: 1.4;
17
+ color: colors.$gray-100;
18
+ width: 100%;
19
+ }
20
+
21
+ .sectionContainer > div {
22
+ background-color: colors.$gray-10;
23
+ }
24
+
25
+ .formSection {
26
+ flex: 1 1 65%;
27
+ }
28
+
29
+ .formSection > div > fieldset {
30
+ margin-bottom: 0 !important;
31
+ }
32
+
33
+ // TODO: try removing this when upgrading @carbon/react. Added at 1.37
34
+ :global(.cds--accordion__wrapper) {
35
+ max-block-size: unset !important;
36
+ }
@@ -0,0 +1,21 @@
1
+ import React, { useMemo } from 'react';
2
+ import { type FormSection } from '../../../types';
3
+ import { useFormProviderContext } from '../../../provider/form-provider';
4
+ import { FormFieldRenderer } from '../field/form-field-renderer.component';
5
+ import styles from './section-renderer.scss';
6
+
7
+ export const SectionRenderer = ({ section }: { section: FormSection }) => {
8
+ const { formFieldAdapters } = useFormProviderContext();
9
+ const sectionId = useMemo(() => section.label.replace(/\s/g, ''), [section.label]);
10
+ return (
11
+ <div className={styles.section}>
12
+ {section.questions.map((question) =>
13
+ formFieldAdapters[question.type] ? (
14
+ <div key={`${sectionId}-${question.id}`} className={styles.sectionBody}>
15
+ <FormFieldRenderer key={question.id} field={question} valueAdapter={formFieldAdapters[question.type]} />
16
+ </div>
17
+ ) : null,
18
+ )}
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,19 @@
1
+ @use '@carbon/colors';
2
+
3
+ .section {
4
+ margin-top: 1rem;
5
+ width: 100%;
6
+ }
7
+
8
+ .sectionBody {
9
+ margin-top: 0.5rem;
10
+ margin-bottom: 1.5rem;
11
+ }
12
+
13
+ .flexFullWidth {
14
+ flex-basis: 100%;
15
+ }
16
+
17
+ .questionInfoDefault {
18
+ display: flex;
19
+ }
@@ -0,0 +1,29 @@
1
+ import { updateFieldIdInExpression } from './helpers';
2
+
3
+ describe('RepeatingFieldComponent - handleExpressionFieldIdUpdate', () => {
4
+ it('Should handle update of expression with ids in repeat group', () => {
5
+ const expression =
6
+ "infantStatus !== '151849AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' && infantStatus !== '154223AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'";
7
+ const fieldIds = ['birthDate', 'infantStatus', 'deathDate'];
8
+ const index = 2;
9
+
10
+ const updatedExpression = updateFieldIdInExpression(expression, index, fieldIds);
11
+
12
+ expect(updatedExpression).toEqual(
13
+ "infantStatus_2 !== '151849AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' && infantStatus_2 !== '154223AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'",
14
+ );
15
+ });
16
+
17
+ it('Should handle update of expression with ids not in repeat group', () => {
18
+ const expression =
19
+ "myValue > today() || myValue <= '1/1/1890' || myValue > useFieldValue('visit_date') || myValue < useFieldValue('visit_date')";
20
+ const fieldIds = ['birthDate', 'infantStatus', 'deathDate'];
21
+ const index = 1;
22
+
23
+ const updatedExpression = updateFieldIdInExpression(expression, index, fieldIds);
24
+
25
+ expect(updatedExpression).toEqual(
26
+ "myValue > today() || myValue <= '1/1/1890' || myValue > useFieldValue('visit_date') || myValue < useFieldValue('visit_date')",
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,68 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ import { type FormField } from '../../types';
3
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
4
+ import { isEmpty } from '../../validators/form-validator';
5
+ import { clearSubmission } from '../../utils/common-utils';
6
+
7
+ export function cloneRepeatField(srcField: FormField, value: OpenmrsResource, idSuffix: number) {
8
+ const originalGroupMembersIds: string[] = [];
9
+ const clonedField = cloneDeep(srcField) as FormField;
10
+ clonedField.questionOptions.repeatOptions = { ...(clonedField.questionOptions.repeatOptions ?? {}) };
11
+ clonedField.meta = { repeat: { ...(clonedField.meta ?? {}), isClone: true }, previousValue: value };
12
+ clonedField.id = `${clonedField.id}_${idSuffix}`;
13
+ clonedField.questions?.forEach((childField) => {
14
+ originalGroupMembersIds.push(childField.id);
15
+ childField.id = `${childField.id}_${idSuffix}`;
16
+ childField.meta.groupId = clonedField.id;
17
+ childField.meta.previousValue = null;
18
+ clearSubmission(childField);
19
+
20
+ // cleanup expressions
21
+
22
+ if (childField['hide'] && childField['hide'].hideWhenExpression) {
23
+ childField['hide'].hideWhenExpression = updateFieldIdInExpression(
24
+ childField['hide'].hideWhenExpression,
25
+ idSuffix,
26
+ originalGroupMembersIds,
27
+ );
28
+ }
29
+
30
+ if (childField.validators?.length) {
31
+ childField.validators.forEach((validator) => {
32
+ if (validator.type === 'js_expression') {
33
+ validator.failsWhenExpression = updateFieldIdInExpression(
34
+ validator.failsWhenExpression,
35
+ idSuffix,
36
+ originalGroupMembersIds,
37
+ );
38
+ }
39
+ });
40
+ }
41
+ if (childField.questionOptions.calculate?.calculateExpression) {
42
+ childField.questionOptions.calculate.calculateExpression = updateFieldIdInExpression(
43
+ childField.questionOptions.calculate?.calculateExpression,
44
+ idSuffix,
45
+ originalGroupMembersIds,
46
+ );
47
+ }
48
+ });
49
+ return clonedField;
50
+ }
51
+
52
+ export function updateFieldIdInExpression(expression: string, index: number, questionIds: string[]) {
53
+ let uniqueQuestionIds = [...new Set(questionIds)];
54
+ uniqueQuestionIds.forEach((id) => {
55
+ if (expression.match(id)) {
56
+ expression = expression.replace(new RegExp(id, 'g'), `${id}_${index}`);
57
+ }
58
+ });
59
+ return expression;
60
+ }
61
+
62
+ export function disableRepeatAddButton(limit: string | number, counter: number) {
63
+ const repeatLimit = Number(limit);
64
+ if (isEmpty(limit) || isNaN(repeatLimit)) {
65
+ return false;
66
+ }
67
+ return counter >= repeatLimit;
68
+ }