@openmrs/esm-form-engine-lib 3.1.3-pre.1754 → 3.1.3-pre.1762
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/default-values-form.json +91 -0
- package/package.json +1 -1
- package/src/components/inputs/number/number.component.tsx +3 -13
- package/src/form-engine.test.tsx +43 -2
- package/src/processors/encounter/encounter-form-processor.ts +3 -0
- package/src/processors/encounter/encounter-processor-helper.ts +9 -2
@@ -0,0 +1,91 @@
|
|
1
|
+
{
|
2
|
+
"encounterType": "",
|
3
|
+
"name": "Sample Default Values Form",
|
4
|
+
"processor": "EncounterFormProcessor",
|
5
|
+
"referencedForms": [],
|
6
|
+
"uuid": "3620c971-58e5-4e70-83ce-98d0dcf4bb7b",
|
7
|
+
"version": "1.0",
|
8
|
+
"pages": [
|
9
|
+
{
|
10
|
+
"label": "First Page",
|
11
|
+
"sections": [
|
12
|
+
{
|
13
|
+
"label": "A Section",
|
14
|
+
"isExpanded": "true",
|
15
|
+
"questions": [
|
16
|
+
{
|
17
|
+
"id": "sampleQuestion",
|
18
|
+
"label": "Text field with Default Value",
|
19
|
+
"type": "obs",
|
20
|
+
"questionOptions": {
|
21
|
+
"rendering": "text",
|
22
|
+
"defaultValue": "Value text",
|
23
|
+
"concept": "f82ba2b7-3849-4ad0-b867-36881e59f5c8"
|
24
|
+
}
|
25
|
+
}
|
26
|
+
]
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"label": "Another Section",
|
30
|
+
"isExpanded": "true",
|
31
|
+
"questions": [
|
32
|
+
{
|
33
|
+
"id": "codedQuestion",
|
34
|
+
"label": "Dropdown with Default Value",
|
35
|
+
"type": "obs",
|
36
|
+
"questionOptions": {
|
37
|
+
"rendering": "select",
|
38
|
+
"concept": "8cdea80a-d167-431c-8278-246c7a1f913b",
|
39
|
+
"defaultValue": "6b4e859c-86ca-41e5-b1c4-017889653b59",
|
40
|
+
"answers": [
|
41
|
+
{
|
42
|
+
"concept": "2b4e859c-86ca-41e5-b1c4-017889653b50",
|
43
|
+
"label": "Choice 1",
|
44
|
+
"conceptMappings": []
|
45
|
+
},
|
46
|
+
{
|
47
|
+
"concept": "6b4e859c-86ca-41e5-b1c4-017889653b59",
|
48
|
+
"label": "Choice 2",
|
49
|
+
"conceptMappings": []
|
50
|
+
},
|
51
|
+
{
|
52
|
+
"concept": "5b4e859c-c6ca-41e5-b1c4-017889653b5k",
|
53
|
+
"label": "Choice 3",
|
54
|
+
"conceptMappings": []
|
55
|
+
}
|
56
|
+
]
|
57
|
+
}
|
58
|
+
},
|
59
|
+
{
|
60
|
+
"id": "codedQuestionWithInvalidDefaultValue",
|
61
|
+
"label": "Dropdown with an invalid Default Value",
|
62
|
+
"type": "obs",
|
63
|
+
"questionOptions": {
|
64
|
+
"rendering": "select",
|
65
|
+
"concept": "8cdea80a-d167-431c-8278-246c7a1f913b",
|
66
|
+
"defaultValue": "invalid-value",
|
67
|
+
"answers": [
|
68
|
+
{
|
69
|
+
"concept": "2b4e859c-86ca-41e5-b1c4-017889653b50",
|
70
|
+
"label": "Choice 1",
|
71
|
+
"conceptMappings": []
|
72
|
+
},
|
73
|
+
{
|
74
|
+
"concept": "6b4e859c-86ca-41e5-b1c4-017889653b59",
|
75
|
+
"label": "Choice 2",
|
76
|
+
"conceptMappings": []
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"concept": "5b4e859c-c6ca-41e5-b1c4-017889653b5k",
|
80
|
+
"label": "Choice 3",
|
81
|
+
"conceptMappings": []
|
82
|
+
}
|
83
|
+
]
|
84
|
+
}
|
85
|
+
}
|
86
|
+
]
|
87
|
+
}
|
88
|
+
]
|
89
|
+
}
|
90
|
+
]
|
91
|
+
}
|
package/package.json
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useCallback, useMemo
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
2
2
|
import { Layer, NumberInput } from '@carbon/react';
|
3
3
|
import classNames from 'classnames';
|
4
4
|
import { isTrue } from '../../../utils/boolean-utils';
|
@@ -13,7 +13,6 @@ import { isEmpty } from '../../../validators/form-validator';
|
|
13
13
|
|
14
14
|
const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings, setFieldValue }) => {
|
15
15
|
const { t } = useTranslation();
|
16
|
-
const [lastBlurredValue, setLastBlurredValue] = useState(value);
|
17
16
|
const { layoutType, sessionMode, workspaceLayout } = useFormProviderContext();
|
18
17
|
|
19
18
|
const numberValue = useMemo(() => {
|
@@ -23,16 +22,9 @@ const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
23
22
|
return value ?? '';
|
24
23
|
}, [value]);
|
25
24
|
|
26
|
-
const onBlur = (event) => {
|
27
|
-
event.preventDefault();
|
28
|
-
if (lastBlurredValue != value) {
|
29
|
-
setLastBlurredValue(value);
|
30
|
-
}
|
31
|
-
};
|
32
|
-
|
33
25
|
const handleChange = useCallback(
|
34
|
-
(event) => {
|
35
|
-
const parsedValue = isEmpty(
|
26
|
+
(event, { value }) => {
|
27
|
+
const parsedValue = isEmpty(value) ? undefined : Number(value);
|
36
28
|
setFieldValue(isNaN(parsedValue) ? undefined : parsedValue);
|
37
29
|
},
|
38
30
|
[setFieldValue],
|
@@ -67,11 +59,9 @@ const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
67
59
|
name={field.id}
|
68
60
|
value={numberValue}
|
69
61
|
onChange={handleChange}
|
70
|
-
onBlur={onBlur}
|
71
62
|
allowEmpty={true}
|
72
63
|
size="lg"
|
73
64
|
hideSteppers={field.hideSteppers ?? false}
|
74
|
-
onWheel={(e) => e.target.blur()}
|
75
65
|
disabled={field.isDisabled}
|
76
66
|
readOnly={isTrue(field.readonly)}
|
77
67
|
className={classNames(styles.controlWidthConstrained, styles.boldedLabel)}
|
package/src/form-engine.test.tsx
CHANGED
@@ -46,6 +46,7 @@ import readOnlyValidationForm from '__mocks__/forms/rfe-forms/read-only-validati
|
|
46
46
|
import jsExpressionValidationForm from '__mocks__/forms/rfe-forms/js-expression-validation-form.json';
|
47
47
|
import hidePagesAndSectionsForm from '__mocks__/forms/rfe-forms/hide-pages-and-sections-form.json';
|
48
48
|
import diagnosisForm from '__mocks__/forms/rfe-forms/diagnosis-test-form.json';
|
49
|
+
import defaultValuesForm from '__mocks__/forms/rfe-forms/default-values-form.json';
|
49
50
|
|
50
51
|
import FormEngine from './form-engine.component';
|
51
52
|
import { type FormSchema, type OpenmrsEncounter, type SessionMode } from './types';
|
@@ -358,7 +359,7 @@ describe('Form engine component', () => {
|
|
358
359
|
await act(async () => {
|
359
360
|
renderForm(null, requiredTestForm);
|
360
361
|
});
|
361
|
-
|
362
|
+
|
362
363
|
await user.click(screen.getByRole('button', { name: /save/i }));
|
363
364
|
|
364
365
|
const labels = screen.getAllByText(/Text question/i);
|
@@ -457,7 +458,9 @@ describe('Form engine component', () => {
|
|
457
458
|
expect(selectErrorMessage).toBeInTheDocument();
|
458
459
|
|
459
460
|
// Validate multi-select field
|
460
|
-
const multiSelectInputField = screen.getByText('If Unscheduled, actual scheduled reason multi-select', {
|
461
|
+
const multiSelectInputField = screen.getByText('If Unscheduled, actual scheduled reason multi-select', {
|
462
|
+
exact: true,
|
463
|
+
});
|
461
464
|
expect(multiSelectInputField).toBeInTheDocument();
|
462
465
|
const multiSelectErrorMessage = screen.getByText(
|
463
466
|
'Patient visit marked as unscheduled. Please provide the scheduled multi-select reason.',
|
@@ -699,6 +702,44 @@ describe('Form engine component', () => {
|
|
699
702
|
});
|
700
703
|
});
|
701
704
|
|
705
|
+
describe('Default values', () => {
|
706
|
+
it('should initialize fields with default values', async () => {
|
707
|
+
const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
|
708
|
+
|
709
|
+
await act(async () => renderForm(null, defaultValuesForm));
|
710
|
+
|
711
|
+
// text field
|
712
|
+
const textField = await findTextOrDateInput(screen, 'Text field with Default Value');
|
713
|
+
expect(textField).toHaveValue('Value text');
|
714
|
+
|
715
|
+
// dropdown field
|
716
|
+
const dropdownField = await findSelectInput(screen, 'Dropdown with Default Value');
|
717
|
+
expect(dropdownField.title).toBe('Choice 2');
|
718
|
+
|
719
|
+
// dropdown with an invalid default value
|
720
|
+
const invalidDropdownField = await findSelectInput(screen, 'Dropdown with an invalid Default Value');
|
721
|
+
expect(invalidDropdownField.title).toBe('Choose an option');
|
722
|
+
|
723
|
+
await user.click(screen.getByRole('button', { name: /save/i }));
|
724
|
+
|
725
|
+
const encounter = saveEncounterMock.mock.calls[0][1];
|
726
|
+
expect(encounter.obs).toEqual([
|
727
|
+
{
|
728
|
+
value: 'Value text',
|
729
|
+
concept: 'f82ba2b7-3849-4ad0-b867-36881e59f5c8',
|
730
|
+
formFieldNamespace: 'rfe-forms',
|
731
|
+
formFieldPath: 'rfe-forms-sampleQuestion',
|
732
|
+
},
|
733
|
+
{
|
734
|
+
value: '6b4e859c-86ca-41e5-b1c4-017889653b59',
|
735
|
+
concept: '8cdea80a-d167-431c-8278-246c7a1f913b',
|
736
|
+
formFieldNamespace: 'rfe-forms',
|
737
|
+
formFieldPath: 'rfe-forms-codedQuestion',
|
738
|
+
},
|
739
|
+
]);
|
740
|
+
});
|
741
|
+
});
|
742
|
+
|
702
743
|
describe('Calculated values', () => {
|
703
744
|
it('should evaluate BMI', async () => {
|
704
745
|
const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
|
@@ -285,6 +285,9 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
285
285
|
console.error(error);
|
286
286
|
}
|
287
287
|
}
|
288
|
+
if (field.questionOptions.defaultValue) {
|
289
|
+
initialValues[field.id] = inferInitialValueFromDefaultFieldValue(field);
|
290
|
+
}
|
288
291
|
if (field.questionOptions.calculate?.calculateExpression) {
|
289
292
|
fieldsWithCalculateExpressions.push(field);
|
290
293
|
}
|
@@ -280,10 +280,17 @@ export function inferInitialValueFromDefaultFieldValue(field: FormField) {
|
|
280
280
|
if (field.questionOptions.rendering == 'toggle' && typeof field.questionOptions.defaultValue != 'boolean') {
|
281
281
|
return field.questionOptions.defaultValue == ConceptTrue;
|
282
282
|
}
|
283
|
+
|
283
284
|
// validate default value
|
284
|
-
|
285
|
-
|
285
|
+
const errors = DefaultValueValidator.validate(field, field.questionOptions.defaultValue);
|
286
|
+
if (errors.length) {
|
287
|
+
console.error(
|
288
|
+
`Default value validation errors for field "${field.id}" with value "${field.questionOptions.defaultValue}":`,
|
289
|
+
errors,
|
290
|
+
);
|
291
|
+
return null;
|
286
292
|
}
|
293
|
+
return field.questionOptions.defaultValue;
|
287
294
|
}
|
288
295
|
|
289
296
|
export async function hydrateRepeatField(
|