@openmrs/esm-form-engine-lib 2.1.0-pre.1581 → 2.1.0-pre.1585
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/083bcecacf9ae0b3/083bcecacf9ae0b3.gz +0 -0
- package/1bdd6eb63dcfc148/1bdd6eb63dcfc148.gz +0 -0
- package/5a22470d28efd31b/5a22470d28efd31b.gz +0 -0
- package/bfdd668cd7e4ec20/bfdd668cd7e4ec20.gz +0 -0
- package/package.json +1 -1
- package/src/adapters/obs-adapter.test.ts +278 -1
- package/src/components/group/obs-group.component.tsx +43 -18
- package/src/components/group/obs-group.scss +4 -0
- package/src/components/inputs/text/text.component.tsx +3 -1
- package/src/components/inputs/text/text.test.tsx +3 -9
- package/src/components/renderer/field/form-field-renderer.component.tsx +1 -1
- package/src/components/renderer/form/form-renderer.component.tsx +0 -1
- package/src/components/repeat/helpers.ts +1 -0
- package/src/components/repeat/repeat.component.tsx +14 -18
- package/src/hooks/useFormFields.ts +51 -23
- package/src/hooks/useFormStateHelpers.ts +18 -3
- package/src/processors/encounter/encounter-form-processor.ts +1 -1
- package/src/processors/encounter/encounter-processor-helper.ts +50 -40
- package/src/transformers/default-schema-transformer.ts +7 -2
Binary file
|
Binary file
|
Binary file
|
Binary file
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { type FormContextProps } from '../provider/form-provider';
|
2
2
|
import { type FormField } from '../types';
|
3
|
-
import {
|
3
|
+
import { findObsByFormField, hasPreviousObsValueChanged, ObsAdapter } from './obs-adapter';
|
4
4
|
|
5
5
|
const formContext = {
|
6
6
|
methods: null,
|
@@ -944,3 +944,280 @@ describe('findObsByFormField', () => {
|
|
944
944
|
expect(matchedObs[0]).toBe(obsList[3]);
|
945
945
|
});
|
946
946
|
});
|
947
|
+
|
948
|
+
describe('ObsAdapter - handling nested obsGroups', () => {
|
949
|
+
const createNestedFields = (): FormField => ({
|
950
|
+
label: 'Parent obsGroup',
|
951
|
+
type: 'obsGroup',
|
952
|
+
required: false,
|
953
|
+
id: 'parentObsgroup',
|
954
|
+
questionOptions: {
|
955
|
+
rendering: 'group',
|
956
|
+
concept: '163770AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
957
|
+
},
|
958
|
+
questions: [
|
959
|
+
{
|
960
|
+
label: 'Health Center',
|
961
|
+
type: 'obs',
|
962
|
+
required: false,
|
963
|
+
id: 'healthCenter',
|
964
|
+
questionOptions: {
|
965
|
+
rendering: 'select',
|
966
|
+
concept: '1745AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
967
|
+
answers: [
|
968
|
+
{
|
969
|
+
concept: '1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
970
|
+
label: 'Family member',
|
971
|
+
},
|
972
|
+
{
|
973
|
+
concept: '1588AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
974
|
+
label: 'Health clinic/post',
|
975
|
+
},
|
976
|
+
{
|
977
|
+
concept: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
978
|
+
label: 'Other',
|
979
|
+
},
|
980
|
+
],
|
981
|
+
},
|
982
|
+
},
|
983
|
+
{
|
984
|
+
label: 'Nested obsGroup',
|
985
|
+
type: 'obsGroup',
|
986
|
+
required: false,
|
987
|
+
id: 'nestedObsgroup',
|
988
|
+
questionOptions: {
|
989
|
+
rendering: 'group',
|
990
|
+
concept: '3f824eeb-8452-4df0-b346-6ed056cbc5b9',
|
991
|
+
},
|
992
|
+
questions: [
|
993
|
+
{
|
994
|
+
label: 'Comment',
|
995
|
+
type: 'obs',
|
996
|
+
required: false,
|
997
|
+
id: 'comment',
|
998
|
+
questionOptions: {
|
999
|
+
rendering: 'textarea',
|
1000
|
+
concept: '161011AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1001
|
+
},
|
1002
|
+
},
|
1003
|
+
{
|
1004
|
+
label: 'Other Diagnoses',
|
1005
|
+
type: 'obs',
|
1006
|
+
required: false,
|
1007
|
+
id: 'otherDiagnoses',
|
1008
|
+
questionOptions: {
|
1009
|
+
rendering: 'select',
|
1010
|
+
concept: '159947AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1011
|
+
answers: [
|
1012
|
+
{
|
1013
|
+
concept: '159394AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1014
|
+
label: 'Diagnosis certainty',
|
1015
|
+
},
|
1016
|
+
],
|
1017
|
+
},
|
1018
|
+
},
|
1019
|
+
],
|
1020
|
+
},
|
1021
|
+
],
|
1022
|
+
});
|
1023
|
+
|
1024
|
+
const createEncounterWithNestedObs = () => ({
|
1025
|
+
uuid: 'encounter-uuid',
|
1026
|
+
obs: [
|
1027
|
+
{
|
1028
|
+
uuid: 'parent-group-uuid',
|
1029
|
+
concept: {
|
1030
|
+
uuid: '163770AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1031
|
+
},
|
1032
|
+
groupMembers: [
|
1033
|
+
{
|
1034
|
+
uuid: 'health-center-uuid',
|
1035
|
+
concept: {
|
1036
|
+
uuid: '1745AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1037
|
+
},
|
1038
|
+
value: {
|
1039
|
+
uuid: '1588AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1040
|
+
},
|
1041
|
+
formFieldPath: 'rfe-forms-healthCenter',
|
1042
|
+
},
|
1043
|
+
{
|
1044
|
+
uuid: 'nested-group-uuid',
|
1045
|
+
concept: {
|
1046
|
+
uuid: '3f824eeb-8452-4df0-b346-6ed056cbc5b9',
|
1047
|
+
},
|
1048
|
+
groupMembers: [
|
1049
|
+
{
|
1050
|
+
uuid: 'comment-uuid',
|
1051
|
+
concept: {
|
1052
|
+
uuid: '161011AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1053
|
+
},
|
1054
|
+
value: 'Test comment for nested group',
|
1055
|
+
formFieldPath: 'rfe-forms-comment',
|
1056
|
+
},
|
1057
|
+
{
|
1058
|
+
uuid: 'diagnosis-uuid',
|
1059
|
+
concept: {
|
1060
|
+
uuid: '159947AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1061
|
+
},
|
1062
|
+
value: {
|
1063
|
+
uuid: '159394AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1064
|
+
},
|
1065
|
+
formFieldPath: 'rfe-forms-otherDiagnoses',
|
1066
|
+
},
|
1067
|
+
],
|
1068
|
+
},
|
1069
|
+
],
|
1070
|
+
},
|
1071
|
+
],
|
1072
|
+
});
|
1073
|
+
|
1074
|
+
beforeEach(() => {
|
1075
|
+
formContext.domainObjectValue = createEncounterWithNestedObs();
|
1076
|
+
ObsAdapter.tearDown();
|
1077
|
+
});
|
1078
|
+
|
1079
|
+
it('should get initial values from nested obs groups', async () => {
|
1080
|
+
const fields = createNestedFields();
|
1081
|
+
|
1082
|
+
const healthCenterField = fields.questions[0];
|
1083
|
+
const healthCenterValue = await ObsAdapter.getInitialValue(
|
1084
|
+
healthCenterField,
|
1085
|
+
formContext.domainObjectValue,
|
1086
|
+
formContext,
|
1087
|
+
);
|
1088
|
+
expect(healthCenterValue).toBe('1588AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
1089
|
+
|
1090
|
+
const commentField = fields.questions[1].questions[0];
|
1091
|
+
const commentValue = await ObsAdapter.getInitialValue(commentField, formContext.domainObjectValue, formContext);
|
1092
|
+
expect(commentValue).toBe('Test comment for nested group');
|
1093
|
+
|
1094
|
+
const diagnosisField = fields.questions[1].questions[1];
|
1095
|
+
const diagnosisValue = await ObsAdapter.getInitialValue(diagnosisField, formContext.domainObjectValue, formContext);
|
1096
|
+
expect(diagnosisValue).toBe('159394AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
1097
|
+
});
|
1098
|
+
|
1099
|
+
it('should transform values in nested groups', () => {
|
1100
|
+
const fields = createNestedFields();
|
1101
|
+
|
1102
|
+
const healthCenterField = fields.questions[0];
|
1103
|
+
const healthCenterObs = ObsAdapter.transformFieldValue(
|
1104
|
+
healthCenterField,
|
1105
|
+
'1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1106
|
+
formContext,
|
1107
|
+
);
|
1108
|
+
expect(healthCenterObs).toEqual({
|
1109
|
+
concept: '1745AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1110
|
+
formFieldNamespace: 'rfe-forms',
|
1111
|
+
formFieldPath: 'rfe-forms-healthCenter',
|
1112
|
+
value: '1560AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1113
|
+
});
|
1114
|
+
|
1115
|
+
const commentField = fields.questions[1].questions[0];
|
1116
|
+
const commentObs = ObsAdapter.transformFieldValue(commentField, 'New test comment', formContext);
|
1117
|
+
expect(commentObs).toEqual({
|
1118
|
+
concept: '161011AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1119
|
+
formFieldNamespace: 'rfe-forms',
|
1120
|
+
formFieldPath: 'rfe-forms-comment',
|
1121
|
+
value: 'New test comment',
|
1122
|
+
});
|
1123
|
+
});
|
1124
|
+
|
1125
|
+
it('should edit existing values in nested groups', () => {
|
1126
|
+
formContext.sessionMode = 'edit';
|
1127
|
+
const fields = createNestedFields();
|
1128
|
+
|
1129
|
+
const healthCenterField = fields.questions[0];
|
1130
|
+
healthCenterField.meta = {
|
1131
|
+
previousValue: {
|
1132
|
+
uuid: 'health-center-uuid',
|
1133
|
+
value: {
|
1134
|
+
uuid: '1588AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1135
|
+
},
|
1136
|
+
},
|
1137
|
+
};
|
1138
|
+
|
1139
|
+
const healthCenterObs = ObsAdapter.transformFieldValue(
|
1140
|
+
healthCenterField,
|
1141
|
+
'5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1142
|
+
formContext,
|
1143
|
+
);
|
1144
|
+
|
1145
|
+
expect(healthCenterObs).toEqual({
|
1146
|
+
uuid: 'health-center-uuid',
|
1147
|
+
formFieldNamespace: 'rfe-forms',
|
1148
|
+
formFieldPath: 'rfe-forms-healthCenter',
|
1149
|
+
value: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1150
|
+
});
|
1151
|
+
|
1152
|
+
const commentField = fields.questions[1].questions[0];
|
1153
|
+
commentField.meta = {
|
1154
|
+
previousValue: {
|
1155
|
+
uuid: 'comment-uuid',
|
1156
|
+
value: 'Test comment for nested group',
|
1157
|
+
},
|
1158
|
+
};
|
1159
|
+
|
1160
|
+
const commentObs = ObsAdapter.transformFieldValue(commentField, 'Updated comment text', formContext);
|
1161
|
+
|
1162
|
+
expect(commentObs).toEqual({
|
1163
|
+
uuid: 'comment-uuid',
|
1164
|
+
formFieldNamespace: 'rfe-forms',
|
1165
|
+
formFieldPath: 'rfe-forms-comment',
|
1166
|
+
value: 'Updated comment text',
|
1167
|
+
});
|
1168
|
+
});
|
1169
|
+
|
1170
|
+
it('should void deleted values in nested groups', () => {
|
1171
|
+
formContext.sessionMode = 'edit';
|
1172
|
+
const fields = createNestedFields();
|
1173
|
+
|
1174
|
+
const commentField = fields.questions[1].questions[0];
|
1175
|
+
commentField.meta = {
|
1176
|
+
previousValue: {
|
1177
|
+
uuid: 'comment-uuid',
|
1178
|
+
value: 'Test comment for nested group',
|
1179
|
+
},
|
1180
|
+
};
|
1181
|
+
|
1182
|
+
ObsAdapter.transformFieldValue(commentField, '', formContext);
|
1183
|
+
expect(commentField.meta.submission.voidedValue).toEqual({
|
1184
|
+
uuid: 'comment-uuid',
|
1185
|
+
voided: true,
|
1186
|
+
});
|
1187
|
+
expect(commentField.meta.submission.newValue).toBe(null);
|
1188
|
+
|
1189
|
+
const diagnosisField = fields.questions[1].questions[1];
|
1190
|
+
diagnosisField.meta = {
|
1191
|
+
previousValue: {
|
1192
|
+
uuid: 'diagnosis-uuid',
|
1193
|
+
value: {
|
1194
|
+
uuid: '159394AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
1195
|
+
},
|
1196
|
+
},
|
1197
|
+
};
|
1198
|
+
|
1199
|
+
ObsAdapter.transformFieldValue(diagnosisField, null, formContext);
|
1200
|
+
expect(diagnosisField.meta.submission.voidedValue).toEqual({
|
1201
|
+
uuid: 'diagnosis-uuid',
|
1202
|
+
voided: true,
|
1203
|
+
});
|
1204
|
+
expect(diagnosisField.meta.submission.newValue).toBe(null);
|
1205
|
+
});
|
1206
|
+
|
1207
|
+
it('should handle empty nested groups', async () => {
|
1208
|
+
const emptyEncounter = {
|
1209
|
+
uuid: 'encounter-uuid',
|
1210
|
+
obs: [],
|
1211
|
+
};
|
1212
|
+
|
1213
|
+
const fields = createNestedFields();
|
1214
|
+
|
1215
|
+
const healthCenterField = fields.questions[0];
|
1216
|
+
const healthCenterValue = await ObsAdapter.getInitialValue(healthCenterField, emptyEncounter, formContext);
|
1217
|
+
expect(healthCenterValue).toBe('');
|
1218
|
+
|
1219
|
+
const commentField = fields.questions[1].questions[0];
|
1220
|
+
const commentValue = await ObsAdapter.getInitialValue(commentField, emptyEncounter, formContext);
|
1221
|
+
expect(commentValue).toBe('');
|
1222
|
+
});
|
1223
|
+
});
|
@@ -1,29 +1,54 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, { useMemo } from 'react';
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import { type FormFieldInputProps } from '../../types';
|
4
4
|
import styles from './obs-group.scss';
|
5
|
-
import { FormFieldRenderer } from '../renderer/field/form-field-renderer.component';
|
5
|
+
import { FormFieldRenderer, isGroupField } from '../renderer/field/form-field-renderer.component';
|
6
6
|
import { useFormProviderContext } from '../../provider/form-provider';
|
7
|
+
import { FormGroup } from '@carbon/react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
7
9
|
|
8
|
-
export const ObsGroup: React.FC<FormFieldInputProps> = ({ field }) => {
|
10
|
+
export const ObsGroup: React.FC<FormFieldInputProps> = ({ field, ...restProps }) => {
|
11
|
+
const { t } = useTranslation();
|
9
12
|
const { formFieldAdapters } = useFormProviderContext();
|
13
|
+
const showLabel = useMemo(() => field.questions?.length > 1, [field]);
|
10
14
|
|
11
|
-
const
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
<div className={classNames(styles.flexColumn)} key={keyId}>
|
18
|
-
<div className={styles.groupContainer}>
|
19
|
-
<FormFieldRenderer fieldId={child.id} valueAdapter={formFieldAdapters[child.type]} />
|
20
|
-
</div>
|
21
|
-
</div>
|
22
|
-
);
|
23
|
-
}
|
24
|
-
});
|
15
|
+
const content = useMemo(
|
16
|
+
() =>
|
17
|
+
field.questions
|
18
|
+
?.filter((child) => !child.isHidden)
|
19
|
+
.map((child, index) => {
|
20
|
+
const key = `${child.id}_${index}`;
|
25
21
|
|
26
|
-
|
22
|
+
if (child.type === 'obsGroup' && isGroupField(child.questionOptions.rendering)) {
|
23
|
+
return (
|
24
|
+
<div key={key} className={styles.nestedGroupContainer}>
|
25
|
+
<ObsGroup field={child} {...restProps} />
|
26
|
+
</div>
|
27
|
+
);
|
28
|
+
} else if (formFieldAdapters[child.type]) {
|
29
|
+
return (
|
30
|
+
<div className={classNames(styles.flexColumn)} key={key}>
|
31
|
+
<div className={styles.groupContainer}>
|
32
|
+
<FormFieldRenderer fieldId={child.id} valueAdapter={formFieldAdapters[child.type]} />
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
);
|
36
|
+
}
|
37
|
+
}),
|
38
|
+
[field],
|
39
|
+
);
|
40
|
+
|
41
|
+
return (
|
42
|
+
<div className={styles.groupContainer}>
|
43
|
+
{showLabel ? (
|
44
|
+
<FormGroup legendText={t(field.label)} className={styles.boldLegend}>
|
45
|
+
{content}
|
46
|
+
</FormGroup>
|
47
|
+
) : (
|
48
|
+
content
|
49
|
+
)}
|
50
|
+
</div>
|
51
|
+
);
|
27
52
|
};
|
28
53
|
|
29
54
|
export default ObsGroup;
|
@@ -42,7 +42,9 @@ const TextField: React.FC<FormFieldInputProps> = ({ field, value, errors, warnin
|
|
42
42
|
<TextInput
|
43
43
|
id={field.id}
|
44
44
|
labelText={<FieldLabel field={field} />}
|
45
|
-
onChange={
|
45
|
+
onChange={(event) => {
|
46
|
+
setFieldValue(event.target.value);
|
47
|
+
}}
|
46
48
|
onBlur={onBlur}
|
47
49
|
name={field.id}
|
48
50
|
value={value}
|
@@ -161,17 +161,11 @@ describe('Text field input', () => {
|
|
161
161
|
it('should record new obs', async () => {
|
162
162
|
await renderForm(textValues);
|
163
163
|
const inputField = screen.getByLabelText('Indicate your notes');
|
164
|
-
|
165
|
-
await user.
|
164
|
+
await user.click(inputField);
|
165
|
+
await user.paste('Updated patient notes');
|
166
166
|
|
167
167
|
await act(async () => {
|
168
|
-
expect(mockSetFieldValue).toHaveBeenCalledWith(
|
169
|
-
expect.objectContaining({
|
170
|
-
target: expect.objectContaining({
|
171
|
-
value: 'Updated patient notes',
|
172
|
-
}),
|
173
|
-
}),
|
174
|
-
);
|
168
|
+
expect(mockSetFieldValue).toHaveBeenCalledWith('Updated patient notes');
|
175
169
|
});
|
176
170
|
});
|
177
171
|
|
@@ -235,6 +235,6 @@ export function isUnspecifiedSupported(question: FormField) {
|
|
235
235
|
);
|
236
236
|
}
|
237
237
|
|
238
|
-
function isGroupField(rendering: RenderType) {
|
238
|
+
export function isGroupField(rendering: RenderType) {
|
239
239
|
return rendering === 'group' || rendering === 'repeating';
|
240
240
|
}
|
@@ -6,7 +6,6 @@ import { formStateReducer, initialState } from './state';
|
|
6
6
|
import { useEvaluateFormFieldExpressions } from '../../../hooks/useEvaluateFormFieldExpressions';
|
7
7
|
import { useFormFactory } from '../../../provider/form-factory-provider';
|
8
8
|
import { FormProvider, type FormContextProps } from '../../../provider/form-provider';
|
9
|
-
import { isTrue } from '../../../utils/boolean-utils';
|
10
9
|
import { type FormProcessorContextProps } from '../../../types';
|
11
10
|
import { useFormStateHelpers } from '../../../hooks/useFormStateHelpers';
|
12
11
|
import { pageObserver } from '../../sidebar/page-observer';
|
@@ -15,6 +15,7 @@ export function cloneRepeatField(srcField: FormField, value: OpenmrsResource, id
|
|
15
15
|
childField.id = `${childField.id}_${idSuffix}`;
|
16
16
|
childField.meta.groupId = clonedField.id;
|
17
17
|
childField.meta.previousValue = null;
|
18
|
+
childField.fieldDependents = new Set();
|
18
19
|
clearSubmission(childField);
|
19
20
|
|
20
21
|
// cleanup expressions
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2
|
-
import { FormGroup } from '@carbon/react';
|
3
2
|
import { useTranslation } from 'react-i18next';
|
4
3
|
import type { FormField, FormFieldInputProps, RenderType } from '../../types';
|
5
4
|
import { evaluateAsyncExpression, evaluateExpression } from '../../utils/expression-runner';
|
@@ -48,12 +47,13 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
48
47
|
|
49
48
|
const handleAdd = useCallback(
|
50
49
|
(counter: number) => {
|
50
|
+
const clonedFieldsBuffer: FormField[] = [];
|
51
51
|
function evaluateExpressions(field: FormField) {
|
52
52
|
if (field.hide?.hideWhenExpression) {
|
53
53
|
field.isHidden = evaluateExpression(
|
54
54
|
field.hide.hideWhenExpression,
|
55
55
|
{ value: field, type: 'field' },
|
56
|
-
formFields,
|
56
|
+
[...formFields, ...clonedFieldsBuffer],
|
57
57
|
getValues(),
|
58
58
|
{
|
59
59
|
mode: sessionMode,
|
@@ -65,7 +65,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
65
65
|
evaluateAsyncExpression(
|
66
66
|
field.questionOptions.calculate?.calculateExpression,
|
67
67
|
{ value: field, type: 'field' },
|
68
|
-
formFields,
|
68
|
+
[...formFields, ...clonedFieldsBuffer],
|
69
69
|
getValues(),
|
70
70
|
{
|
71
71
|
mode: sessionMode,
|
@@ -79,17 +79,21 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
79
79
|
});
|
80
80
|
}
|
81
81
|
}
|
82
|
+
|
82
83
|
const clonedField = cloneRepeatField(field, null, counter);
|
83
|
-
|
84
|
+
clonedFieldsBuffer.push(clonedField);
|
85
|
+
|
86
|
+
// Handle nested questions
|
84
87
|
if (clonedField.type === 'obsGroup') {
|
85
88
|
clonedField.questions?.forEach((childField) => {
|
86
|
-
|
87
|
-
addFormField(childField);
|
89
|
+
clonedFieldsBuffer.push(childField);
|
88
90
|
});
|
89
|
-
} else {
|
90
|
-
evaluateExpressions(clonedField);
|
91
91
|
}
|
92
|
-
|
92
|
+
|
93
|
+
clonedFieldsBuffer.forEach((field) => {
|
94
|
+
evaluateExpressions(field);
|
95
|
+
addFormField(field);
|
96
|
+
});
|
93
97
|
setRows([...rows, clonedField]);
|
94
98
|
},
|
95
99
|
[formFields, field, rows, context],
|
@@ -170,15 +174,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
170
174
|
|
171
175
|
return (
|
172
176
|
<React.Fragment>
|
173
|
-
{
|
174
|
-
<div className={styles.container}>
|
175
|
-
<FormGroup legendText={t(field.label)} className={styles.boldLegend}>
|
176
|
-
{nodes}
|
177
|
-
</FormGroup>
|
178
|
-
</div>
|
179
|
-
) : (
|
180
|
-
<div>{nodes}</div>
|
181
|
-
)}
|
177
|
+
<div>{nodes}</div>
|
182
178
|
</React.Fragment>
|
183
179
|
);
|
184
180
|
};
|
@@ -1,35 +1,63 @@
|
|
1
1
|
import { useMemo } from 'react';
|
2
|
-
import { type
|
2
|
+
import { type FormField, type FormSchema } from '../types';
|
3
3
|
|
4
4
|
export function useFormFields(form: FormSchema): { formFields: FormField[]; conceptReferences: Set<string> } {
|
5
5
|
const [flattenedFields, conceptReferences] = useMemo(() => {
|
6
|
-
const flattenedFieldsTemp = [];
|
6
|
+
const flattenedFieldsTemp: FormField[] = [];
|
7
7
|
const conceptReferencesTemp = new Set<string>();
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
|
9
|
+
const processFlattenedFields = (
|
10
|
+
fields: FormField[],
|
11
|
+
): {
|
12
|
+
flattenedFields: FormField[];
|
13
|
+
conceptReferences: Set<string>;
|
14
|
+
} => {
|
15
|
+
const flattenedFields: FormField[] = [];
|
16
|
+
const conceptReferences = new Set<string>();
|
17
|
+
|
18
|
+
const processField = (field: FormField, parentGroupId?: string) => {
|
19
|
+
// Add group ID to nested fields if applicable
|
20
|
+
const processedField = parentGroupId ? { ...field, meta: { ...field.meta, groupId: parentGroupId } } : field;
|
21
|
+
|
22
|
+
// Add field to flattened list
|
23
|
+
flattenedFields.push(processedField);
|
24
|
+
|
25
|
+
// Collect concept references
|
26
|
+
if (processedField.questionOptions?.concept) {
|
27
|
+
conceptReferences.add(processedField.questionOptions.concept);
|
28
|
+
}
|
29
|
+
|
30
|
+
// Collect concept references from answers
|
31
|
+
processedField.questionOptions?.answers?.forEach((answer) => {
|
32
|
+
if (answer.concept) {
|
33
|
+
conceptReferences.add(answer.concept);
|
17
34
|
}
|
18
35
|
});
|
36
|
+
|
37
|
+
// Recursively process nested questions for obsGroup
|
38
|
+
if (processedField.type === 'obsGroup' && processedField.questions) {
|
39
|
+
processedField.questions.forEach((nestedField) => {
|
40
|
+
processField(nestedField, processedField.id);
|
41
|
+
});
|
42
|
+
}
|
43
|
+
};
|
44
|
+
|
45
|
+
// Process all input fields
|
46
|
+
fields.forEach((field) => processField(field));
|
47
|
+
|
48
|
+
return { flattenedFields, conceptReferences };
|
49
|
+
};
|
50
|
+
|
51
|
+
form.pages?.forEach((page) =>
|
52
|
+
page.sections?.forEach((section) => {
|
53
|
+
if (section.questions) {
|
54
|
+
const { flattenedFields, conceptReferences } = processFlattenedFields(section.questions);
|
55
|
+
flattenedFieldsTemp.push(...flattenedFields);
|
56
|
+
conceptReferences.forEach((conceptReference) => conceptReferencesTemp.add(conceptReference));
|
57
|
+
}
|
19
58
|
}),
|
20
59
|
);
|
21
|
-
|
22
|
-
if (field.questionOptions?.concept) {
|
23
|
-
conceptReferencesTemp.add(field.questionOptions.concept);
|
24
|
-
}
|
25
|
-
if (field.questionOptions?.answers) {
|
26
|
-
field.questionOptions.answers.forEach((answer) => {
|
27
|
-
if (answer.concept) {
|
28
|
-
conceptReferencesTemp.add(answer.concept);
|
29
|
-
}
|
30
|
-
});
|
31
|
-
}
|
32
|
-
});
|
60
|
+
|
33
61
|
return [flattenedFieldsTemp, conceptReferencesTemp];
|
34
62
|
}, [form]);
|
35
63
|
|
@@ -8,9 +8,24 @@ export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: Form
|
|
8
8
|
const addFormField = useCallback((field: FormField) => {
|
9
9
|
dispatch({ type: 'ADD_FORM_FIELD', value: field });
|
10
10
|
}, []);
|
11
|
-
const updateFormField = useCallback(
|
12
|
-
|
13
|
-
|
11
|
+
const updateFormField = useCallback(
|
12
|
+
(field: FormField) => {
|
13
|
+
if (field.meta.groupId) {
|
14
|
+
const group = formFields.find((f) => f.id === field.meta.groupId);
|
15
|
+
if (group) {
|
16
|
+
group.questions = group.questions.map((child) => {
|
17
|
+
if (child.id === field.id) {
|
18
|
+
return field;
|
19
|
+
}
|
20
|
+
return child;
|
21
|
+
});
|
22
|
+
updateFormField(group);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
dispatch({ type: 'UPDATE_FORM_FIELD', value: cloneDeep(field) });
|
26
|
+
},
|
27
|
+
[formFields],
|
28
|
+
);
|
14
29
|
|
15
30
|
const getFormField = useCallback(
|
16
31
|
(fieldId: string) => {
|
@@ -100,7 +100,7 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
100
100
|
field.meta.fixedValue = field.value;
|
101
101
|
delete field.value;
|
102
102
|
}
|
103
|
-
if (field.questionOptions?.rendering == 'group') {
|
103
|
+
if (field.questionOptions?.rendering == 'group' || field.type === 'obsGroup') {
|
104
104
|
field.questions?.forEach((child) => {
|
105
105
|
child.readonly = child.readonly ?? field.readonly;
|
106
106
|
return prepareFormField(child, section, page, schema);
|
@@ -1,16 +1,16 @@
|
|
1
1
|
import {
|
2
|
-
type PatientProgram,
|
3
2
|
type FormField,
|
3
|
+
type FormProcessorContextProps,
|
4
4
|
type OpenmrsEncounter,
|
5
5
|
type OpenmrsObs,
|
6
6
|
type PatientIdentifier,
|
7
|
+
type PatientProgram,
|
7
8
|
type PatientProgramPayload,
|
8
|
-
type FormProcessorContextProps,
|
9
9
|
} from '../../types';
|
10
10
|
import { saveAttachment, savePatientIdentifier, saveProgramEnrollment } from '../../api';
|
11
11
|
import { hasRendering, hasSubmission } from '../../utils/common-utils';
|
12
12
|
import dayjs from 'dayjs';
|
13
|
-
import {
|
13
|
+
import { assignedObsIds, constructObs, voidObs } from '../../adapters/obs-adapter';
|
14
14
|
import { type FormContextProps } from '../../provider/form-provider';
|
15
15
|
import { ConceptTrue } from '../../constants';
|
16
16
|
import { DefaultValueValidator } from '../../validators/default-value-validator';
|
@@ -185,43 +185,53 @@ export function getMutableSessionProps(context: FormContextProps) {
|
|
185
185
|
// Helpers
|
186
186
|
|
187
187
|
function prepareObs(obsForSubmission: OpenmrsObs[], fields: FormField[]) {
|
188
|
-
fields
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
188
|
+
fields.filter((field) => hasSubmittableObs(field)).forEach((field) => processObsField(obsForSubmission, field));
|
189
|
+
}
|
190
|
+
|
191
|
+
function processObsField(obsForSubmission: OpenmrsObs[], field: FormField) {
|
192
|
+
if ((field.isHidden || field.isParentHidden) && field.meta.previousValue) {
|
193
|
+
const valuesArray = Array.isArray(field.meta.previousValue) ? field.meta.previousValue : [field.meta.previousValue];
|
194
|
+
addObsToList(
|
195
|
+
obsForSubmission,
|
196
|
+
valuesArray.map((obs) => voidObs(obs)),
|
197
|
+
);
|
198
|
+
return;
|
199
|
+
}
|
200
|
+
|
201
|
+
if (field.type === 'obsGroup') {
|
202
|
+
processObsGroup(obsForSubmission, field);
|
203
|
+
} else if (hasSubmission(field)) {
|
204
|
+
// For non-group obs with a submission
|
205
|
+
addObsToList(obsForSubmission, field.meta.submission.newValue);
|
206
|
+
addObsToList(obsForSubmission, field.meta.submission.voidedValue);
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
function processObsGroup(obsForSubmission: OpenmrsObs[], groupField: FormField) {
|
211
|
+
if (groupField.meta.submission?.voidedValue) {
|
212
|
+
addObsToList(obsForSubmission, groupField.meta.submission.voidedValue);
|
213
|
+
return;
|
214
|
+
}
|
215
|
+
|
216
|
+
const obsGroup = constructObs(groupField, null);
|
217
|
+
if (groupField.meta.previousValue) {
|
218
|
+
obsGroup.uuid = groupField.meta.previousValue.uuid;
|
219
|
+
}
|
220
|
+
|
221
|
+
groupField.questions.forEach((nestedField) => {
|
222
|
+
if (nestedField.type === 'obsGroup') {
|
223
|
+
const nestedObsGroup: OpenmrsObs[] = [];
|
224
|
+
processObsGroup(nestedObsGroup, nestedField);
|
225
|
+
addObsToList(obsGroup.groupMembers, nestedObsGroup);
|
226
|
+
} else if (hasSubmission(nestedField)) {
|
227
|
+
addObsToList(obsGroup.groupMembers, nestedField.meta.submission.newValue);
|
228
|
+
addObsToList(obsGroup.groupMembers, nestedField.meta.submission.voidedValue);
|
229
|
+
}
|
230
|
+
});
|
231
|
+
|
232
|
+
if (obsGroup.groupMembers?.length || obsGroup.voided) {
|
233
|
+
addObsToList(obsForSubmission, obsGroup);
|
234
|
+
}
|
225
235
|
}
|
226
236
|
|
227
237
|
function prepareOrders(fields: FormField[]) {
|
@@ -41,8 +41,13 @@ function handleQuestion(question: FormField, page: FormPage, form: FormSchema) {
|
|
41
41
|
setFieldValidators(question);
|
42
42
|
transformByType(question);
|
43
43
|
transformByRendering(question);
|
44
|
-
|
45
|
-
|
44
|
+
|
45
|
+
if (question.questions?.length) {
|
46
|
+
if (question.type === 'obsGroup' && question.questions.length) {
|
47
|
+
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
|
48
|
+
} else {
|
49
|
+
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
|
50
|
+
}
|
46
51
|
}
|
47
52
|
question.meta.pageId = page.id;
|
48
53
|
} catch (error) {
|