@openmrs/esm-form-engine-lib 2.1.0-pre.1542 → 2.1.0-pre.1561
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/package.json +1 -1
- package/src/adapters/obs-adapter.test.ts +278 -1
- package/src/adapters/obs-adapter.ts +66 -30
- package/src/components/group/obs-group.component.tsx +43 -18
- package/src/components/group/obs-group.scss +4 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +1 -1
- package/src/components/repeat/repeat.component.tsx +2 -10
- package/src/hooks/useFormFields.ts +33 -23
- 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 +8 -3
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 createNestedObs = () => ({
|
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 = createNestedObs();
|
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,23 +1,23 @@
|
|
1
1
|
import dayjs from 'dayjs';
|
2
|
-
import {
|
2
|
+
import { codedTypes, ConceptTrue } from '../constants';
|
3
3
|
import {
|
4
|
-
type
|
4
|
+
type Attachment,
|
5
|
+
type AttachmentResponse,
|
5
6
|
type FormField,
|
7
|
+
type FormFieldValueAdapter,
|
6
8
|
type OpenmrsEncounter,
|
7
|
-
type
|
8
|
-
type Attachment,
|
9
|
+
type OpenmrsObs,
|
9
10
|
type ValueAndDisplay,
|
10
11
|
} from '../types';
|
11
12
|
import {
|
12
|
-
hasRendering,
|
13
|
-
gracefullySetSubmission,
|
14
13
|
clearSubmission,
|
15
14
|
flattenObsList,
|
16
|
-
parseToLocalDateTime,
|
17
15
|
formatDateAsDisplayString,
|
16
|
+
gracefullySetSubmission,
|
17
|
+
hasRendering,
|
18
|
+
parseToLocalDateTime,
|
18
19
|
} from '../utils/common-utils';
|
19
20
|
import { type FormContextProps } from '../provider/form-provider';
|
20
|
-
import { type FormFieldValueAdapter } from '../types';
|
21
21
|
import { isEmpty } from '../validators/form-validator';
|
22
22
|
import { getAttachmentByUuid } from '../api';
|
23
23
|
import { formatDate, restBaseUrl } from '@openmrs/esm-framework';
|
@@ -60,40 +60,45 @@ export const ObsAdapter: FormFieldValueAdapter = {
|
|
60
60
|
if (isEmpty(value)) {
|
61
61
|
return value;
|
62
62
|
}
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
if (rendering == 'checkbox') {
|
67
|
-
return value.map(
|
68
|
-
(selected) => field.questionOptions.answers?.find((option) => option.concept == selected)?.label,
|
69
|
-
);
|
70
|
-
}
|
71
|
-
if (rendering === 'toggle') {
|
72
|
-
return value ? field.questionOptions.toggleOptions.labelTrue : field.questionOptions.toggleOptions.labelFalse;
|
73
|
-
}
|
74
|
-
if (codedTypes.includes(rendering)) {
|
75
|
-
return field.questionOptions.answers?.find((option) => option.concept == value)?.label;
|
63
|
+
|
64
|
+
if (Array.isArray(value)) {
|
65
|
+
return value.map((val) => getDisplayValueForSingleObs(field, val)).join(', ');
|
76
66
|
}
|
77
|
-
|
67
|
+
|
68
|
+
return getDisplayValueForSingleObs(field, value);
|
78
69
|
},
|
79
70
|
transformFieldValue: (field: FormField, value: any, context: FormContextProps) => {
|
80
71
|
// clear previous submission
|
81
72
|
clearSubmission(field);
|
73
|
+
|
82
74
|
if (!field.meta.previousValue && isEmpty(value)) {
|
83
75
|
return null;
|
84
76
|
}
|
77
|
+
|
78
|
+
// Handle `obsGroup` recursively
|
79
|
+
if (field.type === 'obsGroup' && Array.isArray(field.questions)) {
|
80
|
+
const groupObs = field.questions.map((nestedField) =>
|
81
|
+
ObsAdapter.transformFieldValue(nestedField, nestedField.value, context),
|
82
|
+
);
|
83
|
+
return gracefullySetSubmission(field, { groupMembers: groupObs }, undefined);
|
84
|
+
}
|
85
|
+
|
85
86
|
if (hasRendering(field, 'checkbox')) {
|
86
87
|
return handleMultiSelect(field, value);
|
87
88
|
}
|
89
|
+
|
88
90
|
if (!isEmpty(value) && hasPreviousObsValueChanged(field, value)) {
|
89
91
|
return gracefullySetSubmission(field, editObs(field, value), undefined);
|
90
92
|
}
|
93
|
+
|
91
94
|
if (field.meta.previousValue && isEmpty(value)) {
|
92
95
|
return gracefullySetSubmission(field, undefined, voidObs(field.meta.previousValue));
|
93
96
|
}
|
97
|
+
|
94
98
|
if (!isEmpty(value)) {
|
95
99
|
return gracefullySetSubmission(field, constructObs(field, value), undefined);
|
96
100
|
}
|
101
|
+
|
97
102
|
return null;
|
98
103
|
},
|
99
104
|
tearDown: function (): void {
|
@@ -145,22 +150,46 @@ function extractFieldValue(field: FormField, obsList: OpenmrsObs[] = [], makeFie
|
|
145
150
|
return '';
|
146
151
|
}
|
147
152
|
|
153
|
+
/**
|
154
|
+
* Extracts field's display value
|
155
|
+
*/
|
156
|
+
|
157
|
+
function getDisplayValueForSingleObs(field: FormField, value: any) {
|
158
|
+
const rendering = field.questionOptions.rendering;
|
159
|
+
|
160
|
+
if (value instanceof Date) {
|
161
|
+
return formatDateAsDisplayString(field, value);
|
162
|
+
}
|
163
|
+
if (rendering == 'checkbox') {
|
164
|
+
return value.map((selected) => field.questionOptions.answers?.find((option) => option.concept == selected)?.label);
|
165
|
+
}
|
166
|
+
if (rendering === 'toggle') {
|
167
|
+
return value ? field.questionOptions.toggleOptions.labelTrue : field.questionOptions.toggleOptions.labelFalse;
|
168
|
+
}
|
169
|
+
if (codedTypes.includes(rendering)) {
|
170
|
+
return field.questionOptions.answers?.find((option) => option.concept == value)?.label;
|
171
|
+
}
|
172
|
+
return value;
|
173
|
+
}
|
174
|
+
|
148
175
|
export function constructObs(field: FormField, value: any): Partial<OpenmrsObs> {
|
149
176
|
if (isEmpty(value) && field.type !== 'obsGroup') {
|
150
177
|
return null;
|
151
178
|
}
|
152
|
-
|
153
|
-
|
154
|
-
? { groupMembers: [] }
|
155
|
-
: {
|
156
|
-
value: field.questionOptions.rendering.startsWith('date') ? formatDateByPickerType(field, value) : value,
|
157
|
-
};
|
158
|
-
return {
|
159
|
-
...draftObs,
|
179
|
+
|
180
|
+
const draftObs: Partial<OpenmrsObs> = {
|
160
181
|
concept: field.questionOptions.concept,
|
161
182
|
formFieldNamespace: 'rfe-forms',
|
162
183
|
formFieldPath: `rfe-forms-${field.id}`,
|
163
184
|
};
|
185
|
+
|
186
|
+
if (field.type === 'obsGroup') {
|
187
|
+
draftObs.groupMembers = value?.map((v: any) => constructObs(field, v)) || [];
|
188
|
+
} else {
|
189
|
+
draftObs.value = field.questionOptions.rendering.startsWith('date') ? formatDateByPickerType(field, value) : value;
|
190
|
+
}
|
191
|
+
|
192
|
+
return draftObs;
|
164
193
|
}
|
165
194
|
|
166
195
|
export function voidObs(obs: OpenmrsObs) {
|
@@ -222,6 +251,11 @@ function handleMultiSelect(field: FormField, values: Array<string> = []) {
|
|
222
251
|
// 2. a mix of both (previous and current)
|
223
252
|
// 3. we only have a current value
|
224
253
|
|
254
|
+
// For `obsGroup` types, handle nested groups
|
255
|
+
if (field.type === 'obsGroup' && Array.isArray(field.questions)) {
|
256
|
+
return field.questions.map((nestedField) => handleMultiSelect(nestedField, nestedField.value));
|
257
|
+
}
|
258
|
+
|
225
259
|
if (field.meta.previousValue && isEmpty(values)) {
|
226
260
|
// we assume the user cleared the existing value(s)
|
227
261
|
// so we void all previous values
|
@@ -231,6 +265,7 @@ function handleMultiSelect(field: FormField, values: Array<string> = []) {
|
|
231
265
|
field.meta.previousValue.map((previousValue) => voidObs(previousValue)),
|
232
266
|
);
|
233
267
|
}
|
268
|
+
|
234
269
|
if (field.meta.previousValue && !isEmpty(values)) {
|
235
270
|
const toBeVoided = field.meta.previousValue.filter((obs) => !values.includes(obs.value.uuid));
|
236
271
|
const toBeCreated = values.filter((v) => !field.meta.previousValue.some((obs) => obs.value.uuid === v));
|
@@ -240,6 +275,7 @@ function handleMultiSelect(field: FormField, values: Array<string> = []) {
|
|
240
275
|
toBeVoided.map((obs) => voidObs(obs)),
|
241
276
|
);
|
242
277
|
}
|
278
|
+
|
243
279
|
return gracefullySetSubmission(
|
244
280
|
field,
|
245
281
|
values.map((value) => constructObs(field, value)),
|
@@ -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 keyId = `${child.id}_${index}`;
|
25
21
|
|
26
|
-
|
22
|
+
if (child.type === 'obsGroup' && isGroupField(child.questionOptions.rendering)) {
|
23
|
+
return (
|
24
|
+
<div key={keyId} 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={keyId}>
|
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;
|
@@ -282,6 +282,6 @@ export function isUnspecifiedSupported(question: FormField) {
|
|
282
282
|
);
|
283
283
|
}
|
284
284
|
|
285
|
-
function isGroupField(rendering: RenderType) {
|
285
|
+
export function isGroupField(rendering: RenderType) {
|
286
286
|
return rendering === 'group' || rendering === 'repeating';
|
287
287
|
}
|
@@ -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';
|
@@ -78,6 +77,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
78
77
|
});
|
79
78
|
}
|
80
79
|
}
|
80
|
+
|
81
81
|
const clonedField = cloneRepeatField(field, null, counter);
|
82
82
|
// run necessary expressions
|
83
83
|
if (clonedField.type === 'obsGroup') {
|
@@ -168,15 +168,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
168
168
|
|
169
169
|
return (
|
170
170
|
<React.Fragment>
|
171
|
-
{
|
172
|
-
<div className={styles.container}>
|
173
|
-
<FormGroup legendText={t(field.label)} className={styles.boldLegend}>
|
174
|
-
{nodes}
|
175
|
-
</FormGroup>
|
176
|
-
</div>
|
177
|
-
) : (
|
178
|
-
<div>{nodes}</div>
|
179
|
-
)}
|
171
|
+
<div>{nodes}</div>
|
180
172
|
</React.Fragment>
|
181
173
|
);
|
182
174
|
};
|
@@ -1,35 +1,45 @@
|
|
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
|
+
const flattenFields = (fields: FormField[]) => {
|
10
|
+
fields.forEach((field) => {
|
11
|
+
flattenedFieldsTemp.push(field);
|
12
|
+
|
13
|
+
// If the field is an obsGroup, we need to flatten its nested questions
|
14
|
+
if (field.type === 'obsGroup' && field.questions) {
|
15
|
+
field.questions.forEach((groupedField) => {
|
16
|
+
groupedField.meta.groupId = field.id;
|
17
|
+
flattenFields([groupedField]);
|
18
|
+
});
|
19
|
+
}
|
20
|
+
|
21
|
+
// Collect concept references
|
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
|
+
});
|
33
|
+
};
|
34
|
+
|
8
35
|
form.pages?.forEach((page) =>
|
9
36
|
page.sections?.forEach((section) => {
|
10
|
-
section.questions
|
11
|
-
|
12
|
-
|
13
|
-
question.questions.forEach((groupedField) => {
|
14
|
-
groupedField.meta.groupId = question.id;
|
15
|
-
flattenedFieldsTemp.push(groupedField);
|
16
|
-
});
|
17
|
-
}
|
18
|
-
});
|
37
|
+
if (section.questions) {
|
38
|
+
flattenFields(section.questions);
|
39
|
+
}
|
19
40
|
}),
|
20
41
|
);
|
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
|
-
});
|
42
|
+
|
33
43
|
return [flattenedFieldsTemp, conceptReferencesTemp];
|
34
44
|
}, [form]);
|
35
45
|
|
@@ -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[]) {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { type FormField, type
|
1
|
+
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType } from '../types';
|
2
2
|
import { isTrue } from '../utils/boolean-utils';
|
3
3
|
import { hasRendering } from '../utils/common-utils';
|
4
4
|
|
@@ -39,8 +39,13 @@ function handleQuestion(question: FormField, form: FormSchema) {
|
|
39
39
|
setFieldValidators(question);
|
40
40
|
transformByType(question);
|
41
41
|
transformByRendering(question);
|
42
|
-
|
43
|
-
|
42
|
+
|
43
|
+
if (question.questions?.length) {
|
44
|
+
if (question.type === 'obsGroup' && question.questions.length) {
|
45
|
+
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, form));
|
46
|
+
} else {
|
47
|
+
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, form));
|
48
|
+
}
|
44
49
|
}
|
45
50
|
} catch (error) {
|
46
51
|
console.error(error);
|