@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,37 @@
1
+ import { showToast } from '@openmrs/esm-framework';
2
+
3
+ export function reportError(error: Error, title: string): void {
4
+ if (error) {
5
+ const errorMessage = extractErrorMessagesFromResponse(error).join(', ');
6
+ console.error(error);
7
+ showToast({
8
+ description: errorMessage,
9
+ title: title,
10
+ kind: 'error',
11
+ critical: true,
12
+ });
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Extracts error messages from a given error response object.
18
+ * If fieldErrors are present, it extracts the error messages from each field.
19
+ * Otherwise, it returns the top-level error message.
20
+ *
21
+ * @param {object} errorObject - The error response object.
22
+ * @returns {string[]} An array of error messages.
23
+ */
24
+ export function extractErrorMessagesFromResponse(errorObject) {
25
+ const fieldErrors = errorObject?.responseBody?.error?.fieldErrors;
26
+ const globalErrors = errorObject?.responseBody?.error?.globalErrors;
27
+
28
+ if ((!fieldErrors || Object.keys(fieldErrors).length === 0) && !globalErrors) {
29
+ return [errorObject?.responseBody?.error?.message ?? errorObject?.message];
30
+ }
31
+
32
+ if (globalErrors?.length) {
33
+ return globalErrors.flatMap((error) => error.message);
34
+ } else {
35
+ return Object.values(fieldErrors).flatMap((errors: Array<Error>) => errors.map((error) => error.message));
36
+ }
37
+ }
@@ -0,0 +1,308 @@
1
+ import { type FormField } from '../types';
2
+ import { ConceptFalse } from '../constants';
3
+ import {
4
+ extractArgs,
5
+ findAndRegisterReferencedFields,
6
+ hasParentheses,
7
+ linkReferencedFieldValues,
8
+ parseExpression,
9
+ replaceFieldRefWithValuePath,
10
+ } from './expression-parser';
11
+ import { testFields } from './expression-runner.test';
12
+
13
+ describe('Expression parsing', () => {
14
+ it('should split expression 1 into parts correctly', () => {
15
+ const input =
16
+ "isDateBefore(myValue, '1980-01-01') || myValue < useFieldValue('initiationDate', null) && getOtherValue('arg1', 'arg2')";
17
+ const expectedOutput = [
18
+ "isDateBefore(myValue, '1980-01-01')",
19
+ '||',
20
+ 'myValue',
21
+ '<',
22
+ "useFieldValue('initiationDate', null)",
23
+ '&&',
24
+ "getOtherValue('arg1', 'arg2')",
25
+ ];
26
+
27
+ expect(parseExpression(input)).toEqual(expectedOutput);
28
+ });
29
+
30
+ it('should split expression 2 into parts correctly', () => {
31
+ const input = "isDateBefore(myValue, '1980-01-01') || myValue < useFieldValue('initiationDate', null)";
32
+ const expectedOutput = [
33
+ "isDateBefore(myValue, '1980-01-01')",
34
+ '||',
35
+ 'myValue',
36
+ '<',
37
+ "useFieldValue('initiationDate', null)",
38
+ ];
39
+
40
+ expect(parseExpression(input)).toEqual(expectedOutput);
41
+ });
42
+
43
+ it('should split expression 3 into parts correctly', () => {
44
+ const input =
45
+ "isDateBefore(myValue, '1980-01-01') != myValue && useFieldValue('initiationDate', null) && getOtherValue('Some string', 'Some other string')";
46
+ const expectedOutput = [
47
+ "isDateBefore(myValue, '1980-01-01')",
48
+ '!=',
49
+ 'myValue',
50
+ '&&',
51
+ "useFieldValue('initiationDate', null)",
52
+ '&&',
53
+ "getOtherValue('Some string', 'Some other string')",
54
+ ];
55
+
56
+ expect(parseExpression(input)).toEqual(expectedOutput);
57
+ });
58
+
59
+ it('should split expression 4 into parts correctly', () => {
60
+ const input = "getValue('some id') ? 'was truthy' : 'was false'";
61
+ const expectedOutput = ["getValue('some id')", '?', "'was truthy'", ':', "'was false'"];
62
+
63
+ expect(parseExpression(input)).toEqual(expectedOutput);
64
+ });
65
+ });
66
+
67
+ describe('replaceFieldRefWithValuePath', () => {
68
+ const field1: FormField = {
69
+ label: 'Visit Count',
70
+ type: 'obs',
71
+ questionOptions: {
72
+ rendering: 'number',
73
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
74
+ answers: [],
75
+ },
76
+ id: 'htsVisitCount',
77
+ };
78
+
79
+ const field2: FormField = {
80
+ label: 'Notes',
81
+ type: 'obs',
82
+ questionOptions: {
83
+ rendering: 'text',
84
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
85
+ answers: [],
86
+ },
87
+ id: 'notes',
88
+ };
89
+
90
+ const field3: FormField = {
91
+ label: 'Was HIV tested?',
92
+ type: 'obs',
93
+ questionOptions: {
94
+ rendering: 'toggle',
95
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
96
+ answers: [],
97
+ },
98
+ id: 'wasHivTested',
99
+ };
100
+
101
+ it("should replace 'htsVisitCount' with value path", () => {
102
+ // setup
103
+ const token = "isEmpty('htsVisitCount')";
104
+ // replay
105
+ const result = replaceFieldRefWithValuePath(field1, 10, token);
106
+ // verify
107
+ expect(result).toEqual('isEmpty(fieldValues.htsVisitCount)');
108
+ });
109
+
110
+ it('should replace "notes" with value path', () => {
111
+ // setup
112
+ const token = 'api.getValue(notes)';
113
+ // replay
114
+ const result = replaceFieldRefWithValuePath(field2, 'Some notes', token);
115
+ // verify
116
+ expect(result).toEqual('api.getValue(fieldValues.notes)');
117
+ });
118
+
119
+ it('should replace "wasHivTested" with the system encoded boolean value for toggle rendering types', () => {
120
+ const token = "isEmpty('wasHivTested')";
121
+ const result = replaceFieldRefWithValuePath(field3, false, token);
122
+ expect(result).toEqual(`isEmpty('${ConceptFalse}')`);
123
+ });
124
+ });
125
+
126
+ describe('linkReferencedFieldValues', () => {
127
+ const field1: FormField = {
128
+ label: 'Visit Count',
129
+ type: 'obs',
130
+ questionOptions: {
131
+ rendering: 'number',
132
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
133
+ answers: [],
134
+ },
135
+ id: 'htsVisitCount',
136
+ };
137
+
138
+ const field2: FormField = {
139
+ label: 'Notes',
140
+ type: 'obs',
141
+ questionOptions: {
142
+ rendering: 'text',
143
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
144
+ answers: [],
145
+ },
146
+ id: 'notes',
147
+ };
148
+
149
+ const field3: FormField = {
150
+ label: 'Was HIV tested?',
151
+ type: 'obs',
152
+ questionOptions: {
153
+ rendering: 'toggle',
154
+ concept: '162576AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
155
+ answers: [],
156
+ },
157
+ id: 'wasHivTested',
158
+ };
159
+
160
+ const valuesMap = {
161
+ htsVisitCount: 10,
162
+ notes: 'Some notes',
163
+ wasHivTested: false,
164
+ };
165
+
166
+ it("should replace 'htsVisitCount' with value path", () => {
167
+ // setup
168
+ const expression = "htsVisitCount && helpFn1(htsVisitCount) && helpFn2('htsVisitCount')";
169
+ // replay
170
+ const result = linkReferencedFieldValues([field1], valuesMap, parseExpression(expression));
171
+ // verify
172
+ expect(result).toEqual(
173
+ 'fieldValues.htsVisitCount && helpFn1(fieldValues.htsVisitCount) && helpFn2(fieldValues.htsVisitCount)',
174
+ );
175
+ });
176
+
177
+ it('should support complex expressions', () => {
178
+ // setup
179
+ const expression =
180
+ 'htsVisitCount > 2 ? resolve(api.getByConcept(wasHivTested)) : resolve(api.call2ndApi(wasHivTested, htsVisitCount))';
181
+ // replay
182
+ const result = linkReferencedFieldValues([field1, field2, field3], valuesMap, parseExpression(expression));
183
+ // verify
184
+ expect(result).toEqual(
185
+ `fieldValues.htsVisitCount > 2 ? resolve(api.getByConcept('${ConceptFalse}')) : resolve(api.call2ndApi('${ConceptFalse}', fieldValues.htsVisitCount))`,
186
+ );
187
+ });
188
+
189
+ it('should ignore ref to useFieldValue', () => {
190
+ // setup
191
+ const expression =
192
+ "htsVisitCount > 2 ? resolve(api.getByConcept(useFieldValue('wasHivTested'))) : resolve(api.call2ndApi(wasHivTested, useFieldValue('htsVisitCount')))";
193
+ // replay
194
+ const result = linkReferencedFieldValues([field1, field2, field3], valuesMap, parseExpression(expression));
195
+ // verify
196
+ expect(result).toEqual(
197
+ `fieldValues.htsVisitCount > 2 ? resolve(api.getByConcept(useFieldValue('wasHivTested'))) : resolve(api.call2ndApi('${ConceptFalse}', useFieldValue('htsVisitCount')))`,
198
+ );
199
+ });
200
+ });
201
+
202
+ describe('findAndRegisterReferencedFields', () => {
203
+ it('should register field dependents', () => {
204
+ // setup
205
+ const expression = "linkedToCare == 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3' && !isEmpty(htsProviderRemarks)";
206
+ const patientIdentificationNumberField = testFields.find((f) => f.id === 'patientIdentificationNumber');
207
+
208
+ // replay
209
+ findAndRegisterReferencedFields(
210
+ { value: patientIdentificationNumberField, type: 'field' },
211
+ parseExpression(expression),
212
+ testFields,
213
+ );
214
+
215
+ // verify
216
+ const linkedToCare = testFields.find((f) => f.id === 'linkedToCare');
217
+ const htsProviderRemarks = testFields.find((f) => f.id === 'htsProviderRemarks');
218
+ expect(linkedToCare.fieldDependents).toStrictEqual(new Set(['patientIdentificationNumber']));
219
+ expect(htsProviderRemarks.fieldDependents).toStrictEqual(new Set(['patientIdentificationNumber']));
220
+ });
221
+ });
222
+
223
+ describe('extractArgs', () => {
224
+ it('should extract single argument correctly', () => {
225
+ const expression = "('arg1')";
226
+ const expectedOutput = ['arg1'];
227
+ expect(extractArgs(expression)).toEqual(expectedOutput);
228
+ });
229
+
230
+ it('should extract multiple arguments correctly', () => {
231
+ const expression = "('arg1', 'arg2', 'arg3')";
232
+ const expectedOutput = ['arg1', 'arg2', 'arg3'];
233
+ expect(extractArgs(expression)).toEqual(expectedOutput);
234
+ });
235
+
236
+ it('should handle arguments with spaces correctly', () => {
237
+ const expression = "('arg with spaces', 'another arg')";
238
+ const expectedOutput = ['arg with spaces', 'another arg'];
239
+ expect(extractArgs(expression)).toEqual(expectedOutput);
240
+ });
241
+
242
+ it('should handle arguments with special characters correctly', () => {
243
+ const expression = "('arg!@#$', 'another$%^&arg')";
244
+ const expectedOutput = ['arg!@#$', 'another$%^&arg'];
245
+ expect(extractArgs(expression)).toEqual(expectedOutput);
246
+ });
247
+
248
+ it('should handle no arguments correctly', () => {
249
+ const expression = '()';
250
+ const expectedOutput = [];
251
+ expect(extractArgs(expression)).toEqual(expectedOutput);
252
+ });
253
+
254
+ it('should handle arguments with escaped quotes correctly', () => {
255
+ const expression = "('arg\\'with\\'escaped\\'quotes', 'another\\'arg')";
256
+ const expectedOutput = ["arg'with'escaped'quotes", "another'arg"];
257
+ expect(extractArgs(expression)).toEqual(expectedOutput);
258
+ });
259
+
260
+ it('should handle complex expressions with various argument types', () => {
261
+ const expression = "('string', 123, true, 'another string')";
262
+ const expectedOutput = ['string', '123', 'true', 'another string'];
263
+ expect(extractArgs(expression)).toEqual(expectedOutput);
264
+ });
265
+
266
+ it('should handle arguments with no quotes correctly', () => {
267
+ const expression = '(arg1, arg2)';
268
+ const expectedOutput = ['arg1', 'arg2'];
269
+ expect(extractArgs(expression)).toEqual(expectedOutput);
270
+ });
271
+ });
272
+
273
+ describe('hasParentheses', () => {
274
+ it('should return true for expression with single set of parentheses', () => {
275
+ const expression = 'myFunction(arg1, arg2)';
276
+ expect(hasParentheses(expression)).toBe(true);
277
+ });
278
+
279
+ it('should return true for expression with multiple sets of parentheses', () => {
280
+ const expression = '(arg1 && (arg2 || arg3))';
281
+ expect(hasParentheses(expression)).toBe(true);
282
+ });
283
+
284
+ it('should return true for expression with nested parentheses', () => {
285
+ const expression = 'outerFunction(innerFunction(arg1, arg2))';
286
+ expect(hasParentheses(expression)).toBe(true);
287
+ });
288
+
289
+ it('should return false for expression without parentheses', () => {
290
+ const expression = 'arg1 && arg2 || arg3';
291
+ expect(hasParentheses(expression)).toBe(false);
292
+ });
293
+
294
+ it('should return true for expression with parentheses inside quotes', () => {
295
+ const expression = "myFunction('arg(with)parentheses')";
296
+ expect(hasParentheses(expression)).toBe(true);
297
+ });
298
+
299
+ it('should return true for expression with mixed characters and parentheses', () => {
300
+ const expression = 'a + b * (c - d)';
301
+ expect(hasParentheses(expression)).toBe(true);
302
+ });
303
+
304
+ it('should return true for complex expression with multiple types of parentheses', () => {
305
+ const expression = 'func1(arg1, (func2(arg2) && func3(arg3)))';
306
+ expect(hasParentheses(expression)).toBe(true);
307
+ });
308
+ });
@@ -0,0 +1,158 @@
1
+ import { type FormField } from '../types';
2
+ import { ConceptFalse, ConceptTrue } from '../constants';
3
+ import { registerDependency } from './common-expression-helpers';
4
+ import { type FormNode } from './expression-runner';
5
+
6
+ /**
7
+ * Parses a complex expression string into an array of tokens, ignoring operators found within quotes and within parentheses.
8
+ *
9
+ * @param expression The expression string to parse.
10
+ * @returns An array of tokens representing the individual elements of the expression.
11
+ */
12
+ export function parseExpression(expression: string): string[] {
13
+ const tokens = [];
14
+ let currentToken = '';
15
+ let inQuote = false;
16
+ let openParensCount = 0;
17
+
18
+ for (let i = 0; i < expression.length; i++) {
19
+ const char = expression.charAt(i);
20
+
21
+ if (char === "'" || char === '"') {
22
+ if (inQuote) {
23
+ inQuote = false;
24
+ } else if (openParensCount === 0) {
25
+ inQuote = true;
26
+ }
27
+ }
28
+ if (inQuote) {
29
+ currentToken += char;
30
+ } else {
31
+ if (char === '(') {
32
+ openParensCount++;
33
+ } else if (char === ')') {
34
+ openParensCount--;
35
+ }
36
+ if (openParensCount === 0) {
37
+ if (char === ' ' || char === '\t' || char === '\n') {
38
+ if (currentToken.length > 0) {
39
+ tokens.push(currentToken);
40
+ currentToken = '';
41
+ }
42
+ } else {
43
+ currentToken += char;
44
+ }
45
+ } else {
46
+ currentToken += char;
47
+ }
48
+ }
49
+ }
50
+ if (currentToken.length > 0) {
51
+ tokens.push(currentToken);
52
+ }
53
+ return tokens;
54
+ }
55
+
56
+ /**
57
+ * Links field references within expression fragments to the actual field values
58
+ * @returns The expression with linked field references
59
+ */
60
+ export function linkReferencedFieldValues(
61
+ fields: FormField[],
62
+ fieldValues: Record<string, any>,
63
+ tokens: string[],
64
+ ): string {
65
+ const processedTokens = [];
66
+ tokens.forEach((token) => {
67
+ if (hasParentheses(token)) {
68
+ let tokenWithUnresolvedArgs = token;
69
+ extractArgs(token).forEach((arg) => {
70
+ const referencedField = findReferencedFieldIfExists(arg, fields);
71
+ if (referencedField) {
72
+ tokenWithUnresolvedArgs = replaceFieldRefWithValuePath(
73
+ referencedField,
74
+ fieldValues[referencedField.id],
75
+ tokenWithUnresolvedArgs,
76
+ );
77
+ }
78
+ });
79
+ processedTokens.push(tokenWithUnresolvedArgs);
80
+ } else {
81
+ const referencedField = findReferencedFieldIfExists(token, fields);
82
+ if (referencedField) {
83
+ processedTokens.push(replaceFieldRefWithValuePath(referencedField, fieldValues[referencedField.id], token));
84
+ } else {
85
+ // push token as is
86
+ processedTokens.push(token);
87
+ }
88
+ }
89
+ });
90
+ return processedTokens.join(' ');
91
+ }
92
+
93
+ /**
94
+ * Extracts the arguments or parameters to a function within an arbitrary expression.
95
+ *
96
+ * @param {string} expression - The expression to extract arguments from.
97
+ * @returns {string[]} An array of the extracted arguments.
98
+ */
99
+ export function extractArgs(expression: string): string[] {
100
+ const args = [];
101
+ // eslint-disable-next-line no-useless-escape
102
+ const regx = /(?:\w+|'(?:\\'|[^'\n])*')(?=[,\)]|\s*(?=\)))/g;
103
+ let match;
104
+ while ((match = regx.exec(expression))) {
105
+ args.push(match[0].replace(/\\'/g, "'").replace(/(^'|'$)/g, ''));
106
+ }
107
+ return args;
108
+ }
109
+
110
+ /**
111
+ * Checks if an expression contains opening and closing parentheses.
112
+ *
113
+ * @param {string} expression - The expression to check.
114
+ * @returns {boolean} `true` if the expression contains parentheses, otherwise `false`.
115
+ */
116
+ export function hasParentheses(expression: string): boolean {
117
+ const re = /[()]/;
118
+ return re.test(expression);
119
+ }
120
+
121
+ export function replaceFieldRefWithValuePath(field: FormField, value: any, token: string): string {
122
+ if (token.includes(`useFieldValue('${field.id}')`)) {
123
+ return token;
124
+ }
125
+ // strip quotes
126
+ token = token.replace(new RegExp(`['"]${field.id}['"]`, 'g'), field.id);
127
+ if (field.questionOptions.rendering == 'toggle' && typeof value == 'boolean') {
128
+ // TODO: reference ConceptTrue and ConceptFalse through config patterns
129
+ return token.replace(field.id, `${value ? `'${ConceptTrue}'` : `'${ConceptFalse}'`}`);
130
+ }
131
+ return token.replace(field.id, `fieldValues.${field.id}`);
132
+ }
133
+
134
+ /**
135
+ * Finds and registers referenced fields in the expression
136
+ * @param fieldNode The field node
137
+ * @param tokens Expression tokens
138
+ * @param fields All fields
139
+ */
140
+ export function findAndRegisterReferencedFields(fieldNode: FormNode, tokens: string[], fields: Array<FormField>): void {
141
+ tokens.forEach((token) => {
142
+ if (hasParentheses(token)) {
143
+ extractArgs(token).forEach((arg) => {
144
+ registerDependency(fieldNode, findReferencedFieldIfExists(arg, fields));
145
+ });
146
+ } else {
147
+ registerDependency(fieldNode, findReferencedFieldIfExists(token, fields));
148
+ }
149
+ });
150
+ }
151
+
152
+ function findReferencedFieldIfExists(fieldId: string, fields: FormField[]): FormField | undefined {
153
+ // check if field id has trailing quotes
154
+ if (/^'+|'+$/.test(fieldId)) {
155
+ fieldId = fieldId.replace(/^'|'$/g, '');
156
+ }
157
+ return fields.find((field) => field.id === fieldId);
158
+ }