@openmrs/esm-form-engine-lib 2.1.0-pre.1534 → 2.1.0-pre.1542
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/__mocks__/forms/rfe-forms/hide-pages-and-sections-form.json +93 -0
- package/__mocks__/forms/rfe-forms/js-expression-validation-form.json +54 -0
- package/package.json +1 -1
- package/src/components/renderer/field/fieldLogic.ts +3 -3
- package/src/form-engine.test.tsx +61 -1
- package/src/hooks/useEvaluateFormFieldExpressions.ts +2 -1
- package/src/hooks/useFormStateHelpers.ts +2 -1
- package/src/provider/form-factory-helper.ts +1 -1
- package/src/utils/common-utils.ts +13 -1
- package/src/validators/js-expression-validator.test.ts +9 -9
- package/src/validators/js-expression-validator.ts +2 -2
@@ -0,0 +1,93 @@
|
|
1
|
+
{
|
2
|
+
"encounterType": "e22e39fd-7db2-45e7-80f1-60fa0d5a4378",
|
3
|
+
"name": "Hide Pages and Sections",
|
4
|
+
"processor": "EncounterFormProcessor",
|
5
|
+
"referencedForms": [],
|
6
|
+
"uuid": "7c77485c-7a57-4646-ac21-11d92555a420",
|
7
|
+
"version": "1.0",
|
8
|
+
"pages": [
|
9
|
+
{
|
10
|
+
"label": "Page 1",
|
11
|
+
"sections": [
|
12
|
+
{
|
13
|
+
"label": "Section 1A",
|
14
|
+
"isExpanded": "true",
|
15
|
+
"questions": [
|
16
|
+
{
|
17
|
+
"id": "hideSection1B",
|
18
|
+
"label": "Hide Section 1B",
|
19
|
+
"type": "obs",
|
20
|
+
"questionOptions": {
|
21
|
+
"rendering": "text",
|
22
|
+
"concept": "7aef2620-76e0-4d88-b9cb-c47ba4f67bce"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
]
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"label": "Section 1B",
|
29
|
+
"isExpanded": "true",
|
30
|
+
"hide": {
|
31
|
+
"hideWhenExpression": "isEmpty(hideSection1B)"
|
32
|
+
},
|
33
|
+
"questions": [
|
34
|
+
{
|
35
|
+
"id": "hidePage2",
|
36
|
+
"label": "Hide Page 2",
|
37
|
+
"type": "obs",
|
38
|
+
"questionOptions": {
|
39
|
+
"rendering": "radio",
|
40
|
+
"concept": "1255AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
41
|
+
"answers": [
|
42
|
+
{
|
43
|
+
"concept": "1256AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
44
|
+
"label": "Choice 1",
|
45
|
+
"conceptMappings": []
|
46
|
+
},
|
47
|
+
{
|
48
|
+
"concept": "1258AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
49
|
+
"label": "Choice 2",
|
50
|
+
"conceptMappings": []
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"concept": "1259AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
54
|
+
"label": "Choice 3",
|
55
|
+
"conceptMappings": []
|
56
|
+
}
|
57
|
+
]
|
58
|
+
}
|
59
|
+
}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
]
|
63
|
+
},
|
64
|
+
{
|
65
|
+
"label": "Page 2",
|
66
|
+
"hide": {
|
67
|
+
"hideWhenExpression": "hidePage2 === '1258AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'"
|
68
|
+
},
|
69
|
+
"sections": [
|
70
|
+
{
|
71
|
+
"label": "Section 2A",
|
72
|
+
"isExpanded": "true",
|
73
|
+
"questions": [
|
74
|
+
{
|
75
|
+
"label": "Date",
|
76
|
+
"type": "obs",
|
77
|
+
"required": false,
|
78
|
+
"id": "date",
|
79
|
+
"datePickerFormat": "calendar",
|
80
|
+
"questionOptions": {
|
81
|
+
"rendering": "date",
|
82
|
+
"concept": "159599AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
83
|
+
"conceptMappings": []
|
84
|
+
},
|
85
|
+
"validators": []
|
86
|
+
}
|
87
|
+
]
|
88
|
+
}
|
89
|
+
]
|
90
|
+
}
|
91
|
+
],
|
92
|
+
"description": "Hide Pages and Sections"
|
93
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
{
|
2
|
+
"name": "Js expression validation",
|
3
|
+
"uuid": "xxxx",
|
4
|
+
"EncounterType": "xxxx",
|
5
|
+
"referencedForms": [],
|
6
|
+
"processor": "EncounterFormProcessor",
|
7
|
+
"pages": [
|
8
|
+
{
|
9
|
+
"label": "Page 1",
|
10
|
+
"sections": [
|
11
|
+
{
|
12
|
+
"label": "Section 1",
|
13
|
+
"questions": [
|
14
|
+
{
|
15
|
+
"label": "Question 1",
|
16
|
+
"type": "obs",
|
17
|
+
"required": false,
|
18
|
+
"id": "question1",
|
19
|
+
"questionOptions": {
|
20
|
+
"rendering": "text",
|
21
|
+
"concept": "166103AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
22
|
+
"conceptMappings": [
|
23
|
+
{
|
24
|
+
"relationship": "SAME-AS",
|
25
|
+
"type": "PIH",
|
26
|
+
"value": "2724"
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"relationship": "SAME-AS",
|
30
|
+
"type": "SNOMED CT",
|
31
|
+
"value": "397678008"
|
32
|
+
},
|
33
|
+
{
|
34
|
+
"relationship": "SAME-AS",
|
35
|
+
"type": "CIEL",
|
36
|
+
"value": "166103"
|
37
|
+
}
|
38
|
+
],
|
39
|
+
"showDate": true
|
40
|
+
},
|
41
|
+
"validators": [
|
42
|
+
{
|
43
|
+
"type": "js_expression",
|
44
|
+
"failsWhenExpression": "isEmpty(myValue)",
|
45
|
+
"message": "Empty value not allowed!"
|
46
|
+
}
|
47
|
+
]
|
48
|
+
}
|
49
|
+
]
|
50
|
+
}
|
51
|
+
]
|
52
|
+
}
|
53
|
+
]
|
54
|
+
}
|
package/package.json
CHANGED
@@ -206,9 +206,9 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
|
|
206
206
|
}
|
207
207
|
shouldUpdateForm = true;
|
208
208
|
});
|
209
|
+
}
|
209
210
|
|
210
|
-
|
211
|
-
|
212
|
-
}
|
211
|
+
if (shouldUpdateForm) {
|
212
|
+
setForm(formJson);
|
213
213
|
}
|
214
214
|
}
|
package/src/form-engine.test.tsx
CHANGED
@@ -12,7 +12,7 @@ import {
|
|
12
12
|
} from '@openmrs/esm-framework';
|
13
13
|
import { when } from 'jest-when';
|
14
14
|
import * as api from './api';
|
15
|
-
import { assertFormHasAllFields, findCheckboxGroup, findSelectInput } from './utils/test-utils';
|
15
|
+
import { assertFormHasAllFields, findCheckboxGroup, findSelectInput, findTextOrDateInput } from './utils/test-utils';
|
16
16
|
import { evaluatePostSubmissionExpression } from './utils/post-submission-action-helper';
|
17
17
|
import { mockPatient } from '__mocks__/patient.mock';
|
18
18
|
import { mockSessionDataResponse } from '__mocks__/session.mock';
|
@@ -42,6 +42,8 @@ import monthsOnArtForm from '__mocks__/forms/rfe-forms/months-on-art-form.json';
|
|
42
42
|
import nextVisitForm from '__mocks__/forms/rfe-forms/next-visit-test-form.json';
|
43
43
|
import viralLoadStatusForm from '__mocks__/forms/rfe-forms/viral-load-status-form.json';
|
44
44
|
import readOnlyValidationForm from '__mocks__/forms/rfe-forms/read-only-validation-form.json';
|
45
|
+
import jsExpressionValidationForm from '__mocks__/forms/rfe-forms/js-expression-validation-form.json';
|
46
|
+
import hidePagesAndSectionsForm from '__mocks__/forms/rfe-forms/hide-pages-and-sections-form.json';
|
45
47
|
|
46
48
|
import FormEngine from './form-engine.component';
|
47
49
|
import { type SessionMode } from './types';
|
@@ -262,6 +264,21 @@ describe('Form engine component', () => {
|
|
262
264
|
});
|
263
265
|
});
|
264
266
|
|
267
|
+
describe('js-expression based validation', () => {
|
268
|
+
it('should invoke validation when field value changes', async () => {
|
269
|
+
await act(async () => {
|
270
|
+
renderForm(null, jsExpressionValidationForm);
|
271
|
+
});
|
272
|
+
|
273
|
+
const textField = await findTextOrDateInput(screen, 'Question 1');
|
274
|
+
await user.type(textField, 'Some value');
|
275
|
+
// clear value
|
276
|
+
await user.clear(textField);
|
277
|
+
const errorMessage = await screen.findByText(/Empty value not allowed!/i);
|
278
|
+
expect(errorMessage).toBeInTheDocument();
|
279
|
+
});
|
280
|
+
});
|
281
|
+
|
265
282
|
describe('historical expressions', () => {
|
266
283
|
it('should ascertain getPreviousEncounter() returns an encounter and the historical expression displays on the UI', async () => {
|
267
284
|
renderForm(null, historicalExpressionsForm, 'COVID Assessment');
|
@@ -619,6 +636,49 @@ describe('Form engine component', () => {
|
|
619
636
|
});
|
620
637
|
});
|
621
638
|
|
639
|
+
describe('Hide pages and sections', () => {
|
640
|
+
it('should hide/show section based on field value', async () => {
|
641
|
+
await act(async () => renderForm(null, hidePagesAndSectionsForm));
|
642
|
+
|
643
|
+
// assert section "Section 1B" is hidden at initial render
|
644
|
+
try {
|
645
|
+
await screen.findByText('Section 1B');
|
646
|
+
fail('The section named "Section 1B" should be hidden');
|
647
|
+
} catch (err) {
|
648
|
+
expect(err.message.includes('Unable to find an element with the text: Section 1B')).toBeTruthy();
|
649
|
+
}
|
650
|
+
|
651
|
+
// user interactions to make section visible
|
652
|
+
const hideSection1bField = await findTextOrDateInput(screen, 'Hide Section 1B');
|
653
|
+
await user.type(hideSection1bField, 'Some value');
|
654
|
+
|
655
|
+
const section1b = await screen.findByText('Section 1B');
|
656
|
+
expect(section1b).toBeInTheDocument();
|
657
|
+
});
|
658
|
+
|
659
|
+
it('should hide/show page based on field value', async () => {
|
660
|
+
await act(async () => renderForm(null, hidePagesAndSectionsForm));
|
661
|
+
|
662
|
+
// assert page "Page 2" is visible at initial render
|
663
|
+
const page2 = await screen.findByText('Page 2');
|
664
|
+
expect(page2).toBeInTheDocument();
|
665
|
+
|
666
|
+
// user interactions to hide page
|
667
|
+
const hideSection1bField = await findTextOrDateInput(screen, 'Hide Section 1B');
|
668
|
+
await user.type(hideSection1bField, 'Some value');
|
669
|
+
const choice2RadioOption = screen.getByRole('radio', { name: /Choice 2/i });
|
670
|
+
await user.click(choice2RadioOption);
|
671
|
+
|
672
|
+
// assert page is hidden
|
673
|
+
try {
|
674
|
+
await screen.findByText('Page 2');
|
675
|
+
fail('The page named "Page 2" should be hidden');
|
676
|
+
} catch (err) {
|
677
|
+
expect(err.message.includes('Unable to find an element with the text: Page 2')).toBeTruthy();
|
678
|
+
}
|
679
|
+
});
|
680
|
+
});
|
681
|
+
|
622
682
|
describe('Calculated values', () => {
|
623
683
|
it('should evaluate BMI', async () => {
|
624
684
|
await act(async () => renderForm(null, bmiForm));
|
@@ -5,6 +5,7 @@ import { evalConditionalRequired, evaluateConditionalAnswered, evaluateHide } fr
|
|
5
5
|
import { isTrue } from '../utils/boolean-utils';
|
6
6
|
import { isEmpty } from '../validators/form-validator';
|
7
7
|
import { type QuestionAnswerOption } from '../types/schema';
|
8
|
+
import { updateFormSectionReferences } from '../utils/common-utils';
|
8
9
|
|
9
10
|
export const useEvaluateFormFieldExpressions = (
|
10
11
|
formValues: Record<string, any>,
|
@@ -125,7 +126,7 @@ export const useEvaluateFormFieldExpressions = (
|
|
125
126
|
}
|
126
127
|
});
|
127
128
|
});
|
128
|
-
setEvaluatedFormJson(factoryContext.formJson);
|
129
|
+
setEvaluatedFormJson(updateFormSectionReferences(factoryContext.formJson));
|
129
130
|
}, [factoryContext.formJson, formFields]);
|
130
131
|
|
131
132
|
return { evaluatedFormJson, evaluatedFields };
|
@@ -2,6 +2,7 @@ import { type Dispatch, useCallback } from 'react';
|
|
2
2
|
import { type FormField, type FormSchema } from '../types';
|
3
3
|
import { type Action } from '../components/renderer/form/state';
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
5
|
+
import { updateFormSectionReferences } from '../utils/common-utils';
|
5
6
|
|
6
7
|
export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: FormField[]) {
|
7
8
|
const addFormField = useCallback((field: FormField) => {
|
@@ -35,7 +36,7 @@ export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: Form
|
|
35
36
|
}, []);
|
36
37
|
|
37
38
|
const setForm = useCallback((formJson: FormSchema) => {
|
38
|
-
dispatch({ type: 'SET_FORM_JSON', value: formJson });
|
39
|
+
dispatch({ type: 'SET_FORM_JSON', value: updateFormSectionReferences(formJson) });
|
39
40
|
}, []);
|
40
41
|
|
41
42
|
return {
|
@@ -24,7 +24,7 @@ export function validateForm(context: FormContextProps) {
|
|
24
24
|
const validator = formFieldValidators[validatorConfig.type];
|
25
25
|
if (validator) {
|
26
26
|
const validationResults = validator.validate(field, values[field.id], {
|
27
|
-
|
27
|
+
formFields,
|
28
28
|
values,
|
29
29
|
expressionContext: {
|
30
30
|
patient,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import dayjs from 'dayjs';
|
2
|
-
import { type FormField, type OpenmrsObs, type RenderType } from '../types';
|
2
|
+
import { type FormSchema, type FormField, type OpenmrsObs, type RenderType } from '../types';
|
3
3
|
import { isEmpty } from '../validators/form-validator';
|
4
4
|
import { formatDate, type FormatDateOptions } from '@openmrs/esm-framework';
|
5
5
|
|
@@ -77,3 +77,15 @@ export function formatDateAsDisplayString(field: FormField, date: Date) {
|
|
77
77
|
}
|
78
78
|
return formatDate(date, options);
|
79
79
|
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Creates a new copy of `formJson` with updated references at the page and section levels.
|
83
|
+
* This ensures React re-renders properly by providing new references for nested arrays.
|
84
|
+
*/
|
85
|
+
export function updateFormSectionReferences(formJson: FormSchema) {
|
86
|
+
formJson.pages = formJson.pages.map((page) => {
|
87
|
+
page.sections = Array.from(page.sections);
|
88
|
+
return page;
|
89
|
+
});
|
90
|
+
return { ...formJson };
|
91
|
+
}
|
@@ -20,22 +20,22 @@ describe('ExpressionValidator - validate', () => {
|
|
20
20
|
it('should evaluate js expressions', () => {
|
21
21
|
// setup
|
22
22
|
const field = allFields.find((f) => f.id == 'htsProviderRemarks');
|
23
|
-
const failsWhenExpression = '!isEmpty(myValue) && isEmpty(
|
23
|
+
const failsWhenExpression = '!isEmpty(myValue) && isEmpty(referredToPreventionServices)';
|
24
24
|
|
25
25
|
// replay
|
26
26
|
let errors = ExpressionValidator.validate(field, 'Remarks..', {
|
27
27
|
failsWhenExpression,
|
28
28
|
expressionContext,
|
29
29
|
values,
|
30
|
-
message: '
|
31
|
-
|
30
|
+
message: 'At least one type of Prevention Services must be selected',
|
31
|
+
formFields: allFields,
|
32
32
|
});
|
33
33
|
|
34
34
|
// verify
|
35
35
|
expect(errors).toEqual([
|
36
36
|
{
|
37
37
|
errCode: 'value.invalid',
|
38
|
-
message: '
|
38
|
+
message: 'At least one type of Prevention Services must be selected',
|
39
39
|
resultType: 'error',
|
40
40
|
},
|
41
41
|
]);
|
@@ -49,14 +49,14 @@ describe('ExpressionValidator - validate', () => {
|
|
49
49
|
expressionContext,
|
50
50
|
values,
|
51
51
|
message: 'Atleast one type of Prevention Services must be selected',
|
52
|
-
|
52
|
+
formFields: allFields,
|
53
53
|
});
|
54
54
|
|
55
55
|
// verify
|
56
56
|
expect(errors).toEqual([]);
|
57
57
|
});
|
58
58
|
|
59
|
-
|
59
|
+
it('should fail if date value is not within the configured bounds', () => {
|
60
60
|
// setup
|
61
61
|
const dateField: FormField = {
|
62
62
|
label: 'Test Date',
|
@@ -81,7 +81,7 @@ describe('ExpressionValidator - validate', () => {
|
|
81
81
|
...dateField.validators[0],
|
82
82
|
expressionContext,
|
83
83
|
values,
|
84
|
-
|
84
|
+
formFields: allFields,
|
85
85
|
});
|
86
86
|
|
87
87
|
// verify
|
@@ -96,7 +96,7 @@ describe('ExpressionValidator - validate', () => {
|
|
96
96
|
...dateField.validators[0],
|
97
97
|
expressionContext,
|
98
98
|
values,
|
99
|
-
|
99
|
+
formFields: allFields,
|
100
100
|
});
|
101
101
|
|
102
102
|
// verify
|
@@ -109,7 +109,7 @@ describe('ExpressionValidator - validate', () => {
|
|
109
109
|
...dateField.validators[0],
|
110
110
|
expressionContext,
|
111
111
|
values,
|
112
|
-
|
112
|
+
formFields: allFields,
|
113
113
|
});
|
114
114
|
|
115
115
|
// verify
|
@@ -5,7 +5,7 @@ interface ExpressionValidatorConfig {
|
|
5
5
|
failsWhenExpression?: string;
|
6
6
|
warnsWhenExpression?: string;
|
7
7
|
message: string;
|
8
|
-
|
8
|
+
formFields: FormField[];
|
9
9
|
expressionContext: ExpressionContext;
|
10
10
|
values: Record<string, any>;
|
11
11
|
}
|
@@ -23,7 +23,7 @@ export const ExpressionValidator: FormFieldValidator = {
|
|
23
23
|
return evaluateExpression(
|
24
24
|
config[key],
|
25
25
|
{ value: field, type: 'field' },
|
26
|
-
config.
|
26
|
+
config.formFields,
|
27
27
|
{ ...config.values, [field.id]: value },
|
28
28
|
config.expressionContext,
|
29
29
|
)
|