@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.
- package/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +58 -0
- package/.husky/pre-commit +6 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +4 -0
- package/LICENSE.txt +401 -0
- package/README.md +136 -0
- package/__mocks__/concepts.mock.json +140 -0
- package/__mocks__/forms/afe-forms/component_art.json +38 -0
- package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
- package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
- package/__mocks__/forms/afe-forms/form-component.json +38 -0
- package/__mocks__/forms/afe-forms/mini-form.json +31 -0
- package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
- package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
- package/__mocks__/forms/afe-forms/test-orders.json +72 -0
- package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
- package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
- package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
- package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
- package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
- package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
- package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
- package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
- package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
- package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
- package/__mocks__/forms/rfe-forms/form-component.json +43 -0
- package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
- package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
- package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
- package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
- package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
- package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
- package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
- package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
- package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
- package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
- package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
- package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
- package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
- package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
- package/__mocks__/forms/rfe-forms/required-form.json +50 -0
- package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
- package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
- package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
- package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
- package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
- package/__mocks__/packages/test-forms-registry.ts +12 -0
- package/__mocks__/patient.mock.ts +173 -0
- package/__mocks__/react-i18next.js +49 -0
- package/__mocks__/react-markdown.tsx +5 -0
- package/__mocks__/session.mock.ts +117 -0
- package/__mocks__/single-spa-react.js +11 -0
- package/__mocks__/use-initial-values/encounter.mock.json +963 -0
- package/__mocks__/use-initial-values/patient.mock.json +73 -0
- package/__mocks__/visit.mock.ts +19 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -0
- package/jest.config.js +30 -0
- package/package.json +104 -0
- package/prettier.config.js +8 -0
- package/readme/form-engine.jpeg +0 -0
- package/src/adapters/control-adapter.ts +29 -0
- package/src/adapters/encounter-datetime-adapter.ts +38 -0
- package/src/adapters/encounter-location-adapter.ts +39 -0
- package/src/adapters/encounter-provider-adapter.ts +48 -0
- package/src/adapters/encounter-role-adapter.ts +54 -0
- package/src/adapters/inline-date-adapter.ts +58 -0
- package/src/adapters/obs-adapter.ts +280 -0
- package/src/adapters/obs-comment-adapter.ts +60 -0
- package/src/adapters/orders-adapter.ts +75 -0
- package/src/adapters/patient-identifier-adapter.ts +40 -0
- package/src/adapters/program-state-adapter.ts +52 -0
- package/src/api/index.ts +178 -0
- package/src/components/error/error-modal.component.tsx +37 -0
- package/src/components/error/error.scss +4 -0
- package/src/components/extension/extension-parcel.component.tsx +32 -0
- package/src/components/field-label/field-label.component.tsx +32 -0
- package/src/components/field-label/field-label.scss +11 -0
- package/src/components/group/obs-group.component.tsx +29 -0
- package/src/components/group/obs-group.scss +12 -0
- package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
- package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
- package/src/components/inputs/date/date.component.tsx +149 -0
- package/src/components/inputs/date/date.scss +36 -0
- package/src/components/inputs/file/camera/camera.component.tsx +34 -0
- package/src/components/inputs/file/camera/camera.scss +3 -0
- package/src/components/inputs/file/file.component.tsx +159 -0
- package/src/components/inputs/file/file.scss +101 -0
- package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
- package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
- package/src/components/inputs/markdown/markdown.component.tsx +8 -0
- package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
- package/src/components/inputs/multi-select/multi-select.scss +25 -0
- package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
- package/src/components/inputs/number/number.component.tsx +69 -0
- package/src/components/inputs/number/number.scss +15 -0
- package/src/components/inputs/radio/radio.component.tsx +79 -0
- package/src/components/inputs/radio/radio.scss +36 -0
- package/src/components/inputs/select/dropdown.component.tsx +73 -0
- package/src/components/inputs/select/dropdown.scss +11 -0
- package/src/components/inputs/select/dropdown.test.tsx +120 -0
- package/src/components/inputs/text/text.component.tsx +65 -0
- package/src/components/inputs/text/text.scss +15 -0
- package/src/components/inputs/text/text.test.tsx +104 -0
- package/src/components/inputs/text-area/text-area.component.tsx +63 -0
- package/src/components/inputs/text-area/text-area.scss +11 -0
- package/src/components/inputs/toggle/toggle.component.tsx +66 -0
- package/src/components/inputs/toggle/toggle.scss +12 -0
- package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
- package/src/components/inputs/tooltip/tooltip.scss +8 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
- package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
- package/src/components/inputs/unspecified/unspecified.scss +7 -0
- package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
- package/src/components/label/label.component.tsx +20 -0
- package/src/components/label/label.scss +11 -0
- package/src/components/loaders/loader.component.tsx +16 -0
- package/src/components/loaders/loader.scss +20 -0
- package/src/components/patient-banner/patient-banner.component.tsx +20 -0
- package/src/components/patient-banner/patient-banner.scss +12 -0
- package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
- package/src/components/previous-value-review/previous-value-review.scss +36 -0
- package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
- package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
- package/src/components/renderer/field/fieldLogic.ts +214 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
- package/src/components/renderer/field/form-field-renderer.scss +5 -0
- package/src/components/renderer/form/form-renderer.component.tsx +89 -0
- package/src/components/renderer/form/state.ts +54 -0
- package/src/components/renderer/page/page.renderer.component.tsx +50 -0
- package/src/components/renderer/page/page.renderer.scss +36 -0
- package/src/components/renderer/section/section-renderer.component.tsx +21 -0
- package/src/components/renderer/section/section-renderer.scss +19 -0
- package/src/components/repeat/helpers.test.ts +29 -0
- package/src/components/repeat/helpers.ts +68 -0
- package/src/components/repeat/repeat-controls.component.tsx +38 -0
- package/src/components/repeat/repeat-controls.scss +7 -0
- package/src/components/repeat/repeat.component.tsx +201 -0
- package/src/components/repeat/repeat.scss +30 -0
- package/src/components/repeat/repeat.test.ts +29 -0
- package/src/components/sidebar/sidebar.component.tsx +134 -0
- package/src/components/sidebar/sidebar.scss +121 -0
- package/src/components/value/value.component.tsx +27 -0
- package/src/components/value/value.scss +17 -0
- package/src/components/value/view/field-value-view.component.tsx +33 -0
- package/src/components/value/view/field-value-view.scss +31 -0
- package/src/constants.ts +12 -0
- package/src/datasources/concept-data-source.ts +42 -0
- package/src/datasources/data-source.ts +23 -0
- package/src/datasources/encounter-role-datasource.ts +15 -0
- package/src/datasources/historical-data-source.ts +11 -0
- package/src/datasources/location-data-source.ts +27 -0
- package/src/datasources/provider-datasource.ts +15 -0
- package/src/datasources/select-concept-answers-datasource.ts +15 -0
- package/src/declarations.d.ts +4 -0
- package/src/external-function-context.tsx +8 -0
- package/src/form-context.tsx +42 -0
- package/src/form-engine.component.tsx +178 -0
- package/src/form-engine.scss +140 -0
- package/src/form-engine.test.tsx +817 -0
- package/src/globals.ts +1 -0
- package/src/hooks/useClobData.tsx +21 -0
- package/src/hooks/useConcepts.tsx +55 -0
- package/src/hooks/useDatasourceDependentValue.ts +16 -0
- package/src/hooks/useEncounter.tsx +32 -0
- package/src/hooks/useEncounterRole.tsx +15 -0
- package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
- package/src/hooks/useFieldValidationResults.ts +18 -0
- package/src/hooks/useFormCollapse.tsx +36 -0
- package/src/hooks/useFormFieldValidators.ts +22 -0
- package/src/hooks/useFormFieldValueAdapters.ts +24 -0
- package/src/hooks/useFormFields.ts +37 -0
- package/src/hooks/useFormFieldsMeta.ts +48 -0
- package/src/hooks/useFormJson.test.tsx +173 -0
- package/src/hooks/useFormJson.tsx +237 -0
- package/src/hooks/useFormStateHelpers.ts +50 -0
- package/src/hooks/useFormsConfig.tsx +27 -0
- package/src/hooks/useInitialValues.ts +38 -0
- package/src/hooks/usePatientData.tsx +32 -0
- package/src/hooks/usePatientPrograms.ts +32 -0
- package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
- package/src/hooks/usePostSubmissionActions.ts +31 -0
- package/src/hooks/useProcessorDependencies.ts +30 -0
- package/src/hooks/useRestMaxResultsCount.ts +5 -0
- package/src/hooks/useSystemSetting.ts +36 -0
- package/src/hooks/useWorkspaceLayout.ts +29 -0
- package/src/index.ts +12 -0
- package/src/lifecycle.ts +33 -0
- package/src/post-submission-actions/program-enrollment-action.ts +138 -0
- package/src/processors/encounter/encounter-form-processor.ts +337 -0
- package/src/processors/encounter/encounter-processor-helper.ts +320 -0
- package/src/processors/form-processor.ts +41 -0
- package/src/provider/form-factory-helper.ts +100 -0
- package/src/provider/form-factory-provider.tsx +169 -0
- package/src/provider/form-provider.tsx +37 -0
- package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
- package/src/registry/inbuilt-components/control-templates.ts +57 -0
- package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
- package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
- package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
- package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
- package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
- package/src/registry/inbuilt-components/template-component-map.ts +28 -0
- package/src/registry/registry.test.ts +20 -0
- package/src/registry/registry.ts +261 -0
- package/src/routes.json +1 -0
- package/src/setupI18n.ts +16 -0
- package/src/setupTests.ts +5 -0
- package/src/transformers/default-schema-transformer.test.ts +155 -0
- package/src/transformers/default-schema-transformer.ts +239 -0
- package/src/types/domain.ts +129 -0
- package/src/types/index.ts +130 -0
- package/src/types/schema.ts +238 -0
- package/src/typings.d.ts +9 -0
- package/src/utils/boolean-utils.ts +25 -0
- package/src/utils/common-expression-helpers.ts +503 -0
- package/src/utils/common-utils.test.ts +136 -0
- package/src/utils/common-utils.ts +55 -0
- package/src/utils/error-utils.ts +37 -0
- package/src/utils/expression-parser.test.ts +308 -0
- package/src/utils/expression-parser.ts +158 -0
- package/src/utils/expression-runner.test.ts +387 -0
- package/src/utils/expression-runner.ts +219 -0
- package/src/utils/form-helper.test.ts +482 -0
- package/src/utils/form-helper.ts +210 -0
- package/src/utils/form-page-utils.ts +13 -0
- package/src/utils/forms-loader.test.ts +323 -0
- package/src/utils/forms-loader.ts +306 -0
- package/src/utils/post-submission-action-helper.ts +71 -0
- package/src/utils/test-utils.ts +54 -0
- package/src/utils/zscore-service.ts +59 -0
- package/src/validators/conditional-answered-validator.test.ts +61 -0
- package/src/validators/conditional-answered-validator.ts +17 -0
- package/src/validators/date-validator.test.ts +46 -0
- package/src/validators/date-validator.ts +19 -0
- package/src/validators/default-value-validator.test.ts +90 -0
- package/src/validators/default-value-validator.ts +36 -0
- package/src/validators/form-validator.test.ts +188 -0
- package/src/validators/form-validator.ts +95 -0
- package/src/validators/js-expression-validator.test.ts +118 -0
- package/src/validators/js-expression-validator.ts +44 -0
- package/src/validators/schema.ts +34 -0
- package/src/zscore/bfa_boys_5_above.json +2522 -0
- package/src/zscore/bfa_girls_5_above.json +2522 -0
- package/src/zscore/hfa_boys_5_above.json +2186 -0
- package/src/zscore/hfa_boys_below5.json +22286 -0
- package/src/zscore/hfa_girls_5_above.json +2186 -0
- package/src/zscore/hfa_girls_below5.json +22286 -0
- package/src/zscore/wfl_boys_below5.json +7814 -0
- package/src/zscore/wfl_girls_below5.json +7814 -0
- package/src/zscore-tests/bmi-age.test.tsx +88 -0
- package/src/zscore-tests/height-age.test.tsx +96 -0
- package/src/zscore-tests/weight-height.test.tsx +87 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/en.json +47 -0
- package/translations/es.json +38 -0
- package/translations/fr.json +38 -0
- package/translations/km.json +38 -0
- package/tsconfig.json +19 -0
- package/turbo.json +15 -0
- 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
|
+
}
|