@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "2.1.0-pre.1534",
3
+ "version": "2.1.0-pre.1542",
4
4
  "description": "React Form Engine for O3",
5
5
  "browser": "dist/openmrs-esm-form-engine-lib.js",
6
6
  "main": "src/index.ts",
@@ -206,9 +206,9 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
206
206
  }
207
207
  shouldUpdateForm = true;
208
208
  });
209
+ }
209
210
 
210
- if (shouldUpdateForm) {
211
- setForm({ ...formJson });
212
- }
211
+ if (shouldUpdateForm) {
212
+ setForm(formJson);
213
213
  }
214
214
  }
@@ -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
- fields: formFields,
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(`referredToPreventionServices`)';
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: 'Atleast one type of Prevention Services must be selected',
31
- fields: allFields,
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: 'Atleast one type of Prevention Services must be selected',
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
- fields: allFields,
52
+ formFields: allFields,
53
53
  });
54
54
 
55
55
  // verify
56
56
  expect(errors).toEqual([]);
57
57
  });
58
58
 
59
- fit('should fail if date value is not within the configured bounds', () => {
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
- fields: allFields,
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
- fields: allFields,
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
- fields: allFields,
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
- fields: FormField[];
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.fields,
26
+ config.formFields,
27
27
  { ...config.values, [field.id]: value },
28
28
  config.expressionContext,
29
29
  )