Skip to content

Commit 0ecfe24

Browse files
authored
(fix) O3-4543 Update section visibility when questions visibility changes (#476)
* (fix) O3-4543 Update section visibility when questions visibility changes * pr changes * clean up * add optional to isSectionVisible * fix test * (tests) Adds fieldLogic.test.ts
1 parent 68966f9 commit 0ecfe24

File tree

3 files changed

+189
-5
lines changed

3 files changed

+189
-5
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { handleFieldLogic, validateFieldValue } from './fieldLogic';
2+
import { evaluateAsyncExpression, evaluateExpression } from '../../../utils/expression-runner';
3+
import { type FormField } from '../../../types';
4+
import { type FormContextProps } from '../../../provider/form-provider';
5+
6+
jest.mock('../../../utils/expression-runner', () => ({
7+
evaluateExpression: jest.fn(),
8+
evaluateAsyncExpression: jest.fn().mockResolvedValue({ result: 'mockedResult' }),
9+
}));
10+
11+
describe('handleFieldLogic', () => {
12+
let mockContext: FormContextProps;
13+
let mockFieldCoded: FormField;
14+
15+
beforeEach(() => {
16+
mockContext = {
17+
methods: {
18+
getValues: jest.fn().mockReturnValue({}),
19+
setValue: jest.fn(),
20+
},
21+
formFields: [],
22+
sessionMode: 'edit',
23+
patient: {},
24+
formFieldValidators: {},
25+
formFieldAdapters: {
26+
obs: {
27+
transformFieldValue: jest.fn(),
28+
}
29+
},
30+
formJson: { pages: [] },
31+
updateFormField: jest.fn(),
32+
setForm: jest.fn(),
33+
} as unknown as FormContextProps;
34+
35+
mockFieldCoded = {
36+
id: 'testField',
37+
label: 'Test Field',
38+
type: 'obs',
39+
questionOptions: {
40+
rendering: 'radio',
41+
answers: [
42+
{
43+
label: 'Test Answer',
44+
concept: 'testConcept',
45+
disable: {
46+
disableWhenExpression: 'myValue > 10',
47+
},
48+
},
49+
],
50+
},
51+
fieldDependents: [],
52+
sectionDependents: [],
53+
pageDependents: [],
54+
validators: [],
55+
} as unknown as FormField;
56+
});
57+
58+
it('should evaluate field answer disabled logic', () => {
59+
(evaluateExpression as jest.Mock).mockReturnValue(true);
60+
61+
handleFieldLogic(mockFieldCoded, mockContext);
62+
63+
expect(evaluateExpression).toHaveBeenCalledWith(
64+
'myValue > 10',
65+
{ value: mockFieldCoded, type: 'field' },
66+
mockContext.formFields,
67+
mockContext.methods.getValues(),
68+
{
69+
mode: mockContext.sessionMode,
70+
patient: mockContext.patient,
71+
},
72+
);
73+
expect(mockFieldCoded.questionOptions.answers[0].disable.isDisabled).toBe(true);
74+
});
75+
76+
it('should handle field dependents logic', () => {
77+
mockFieldCoded.fieldDependents = new Set(['dependentField']);
78+
mockContext.formFields = [
79+
{
80+
id: 'dependentField',
81+
type: 'obs',
82+
questionOptions: {
83+
calculate: {
84+
calculateExpression: '2 + 2',
85+
},
86+
},
87+
validators: [],
88+
meta: {},
89+
} as unknown as FormField,
90+
];
91+
handleFieldLogic(mockFieldCoded, mockContext);
92+
93+
expect(mockContext.updateFormField).toHaveBeenCalled();
94+
});
95+
});
96+
97+
describe('validateFieldValue', () => {
98+
let mockField: FormField;
99+
let mockValidators: Record<string, any>;
100+
let mockContext: any;
101+
102+
beforeEach(() => {
103+
mockField = {
104+
id: 'testField',
105+
validators: [
106+
{
107+
type: 'required',
108+
},
109+
],
110+
meta: {},
111+
} as unknown as FormField;
112+
113+
mockValidators = {
114+
required: {
115+
validate: jest.fn().mockReturnValue([
116+
{ resultType: 'error', message: 'Field is required' },
117+
]),
118+
},
119+
};
120+
121+
mockContext = {
122+
formFields: [],
123+
values: {},
124+
expressionContext: {
125+
patient: {},
126+
mode: 'edit',
127+
},
128+
};
129+
});
130+
131+
it('should validate field value and return errors and warnings', () => {
132+
const result = validateFieldValue(mockField, '', mockValidators, mockContext);
133+
134+
expect(mockValidators.required.validate).toHaveBeenCalledWith(
135+
mockField,
136+
'',
137+
expect.objectContaining(mockContext),
138+
);
139+
expect(result.errors).toEqual([{ resultType: 'error', message: 'Field is required' }]);
140+
expect(result.warnings).toEqual([]);
141+
});
142+
143+
it('should return empty errors and warnings if field submission is unspecified', () => {
144+
mockField.meta.submission = { unspecified: true };
145+
146+
const result = validateFieldValue(mockField, '', mockValidators, mockContext);
147+
148+
expect(result.errors).toEqual([]);
149+
expect(result.warnings).toEqual([]);
150+
});
151+
});

src/components/renderer/field/fieldLogic.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { codedTypes } from '../../../constants';
22
import { type FormContextProps } from '../../../provider/form-provider';
33
import { type FormFieldValidator, type SessionMode, type ValidationResult, type FormField } from '../../../types';
4-
import { isTrue } from '../../../utils/boolean-utils';
54
import { hasRendering } from '../../../utils/common-utils';
65
import { evaluateAsyncExpression, evaluateExpression } from '../../../utils/expression-runner';
7-
import { evalConditionalRequired, evaluateDisabled, evaluateHide } from '../../../utils/form-helper';
6+
import { evalConditionalRequired, evaluateDisabled, evaluateHide, findFieldSection } from '../../../utils/form-helper';
87
import { isEmpty } from '../../../validators/form-validator';
98
import { reportError } from '../../../utils/error-utils';
109

@@ -49,6 +48,8 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
4948
updateFormField,
5049
setForm,
5150
} = context;
51+
52+
let shouldUpdateForm = false;
5253
// handle fields
5354
if (field.fieldDependents) {
5455
field.fieldDependents.forEach((dep) => {
@@ -89,6 +90,9 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
8990
}
9091
// evaluate hide
9192
if (dependent.hide) {
93+
const targetSection = findFieldSection(formJson, dependent);
94+
const isSectionVisible = targetSection?.questions.some((question) => !question.isHidden);
95+
9296
evaluateHide(
9397
{ value: dependent, type: 'field' },
9498
formFields,
@@ -98,6 +102,27 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
98102
evaluateExpression,
99103
updateFormField,
100104
);
105+
106+
if (targetSection) {
107+
targetSection.questions = targetSection?.questions.map((question) => {
108+
if (question.id === dependent.id) {
109+
return dependent;
110+
}
111+
return question;
112+
});
113+
const isDependentFieldHidden = dependent.isHidden;
114+
const sectionHasVisibleFieldAfterEvaluation = [...targetSection.questions, dependent].some(
115+
(field) => !field.isHidden,
116+
);
117+
118+
if (!isSectionVisible && !isDependentFieldHidden) {
119+
targetSection.isHidden = false;
120+
shouldUpdateForm = true;
121+
} else if (isSectionVisible && !sectionHasVisibleFieldAfterEvaluation) {
122+
targetSection.isHidden = true;
123+
shouldUpdateForm = true;
124+
}
125+
}
101126
}
102127
// evaluate disabled
103128
if (typeof dependent.disabled === 'object' && dependent.disabled.disableWhenExpression) {
@@ -192,8 +217,6 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
192217
});
193218
}
194219

195-
let shouldUpdateForm = false;
196-
197220
// handle sections
198221
if (field.sectionDependents) {
199222
field.sectionDependents.forEach((sectionId) => {

src/utils/form-helper.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type LayoutType } from '@openmrs/esm-framework';
2-
import type { FormField, FormPage, FormSection, SessionMode, FHIRObsResource, RenderType } from '../types';
2+
import type { FormField, FormPage, FormSection, SessionMode, FHIRObsResource, RenderType, FormSchema } from '../types';
33
import { isEmpty } from '../validators/form-validator';
44
import { parseToLocalDateTime } from './common-utils';
55
import dayjs from 'dayjs';
@@ -271,3 +271,13 @@ function extractFHIRObsValue(fhirObs: FHIRObsResource, rendering: RenderType) {
271271
return fhirObs.valueString;
272272
}
273273
}
274+
275+
/**
276+
* Find formField section
277+
* @param formJson FormSchema
278+
* @param field FormField
279+
*/
280+
export function findFieldSection(formJson: FormSchema, field: FormField) {
281+
let page = formJson.pages.find((page) => field.meta.pageId === page.id);
282+
return page.sections.find((section) => section.questions.find((question) => question.id === field.id));
283+
}

0 commit comments

Comments
 (0)