@openmrs/esm-form-engine-lib 2.1.0-pre.1577 → 2.1.0-pre.1584

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "2.1.0-pre.1577",
3
+ "version": "2.1.0-pre.1584",
4
4
  "description": "React Form Engine for O3",
5
5
  "browser": "dist/openmrs-esm-form-engine-lib.js",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  import { type FormContextProps } from '../provider/form-provider';
2
2
  import { type FormField } from '../types';
3
- import { hasPreviousObsValueChanged, findObsByFormField, ObsAdapter } from './obs-adapter';
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
+ });
@@ -7,6 +7,7 @@ import {
7
7
  type AttachmentResponse,
8
8
  type Attachment,
9
9
  type ValueAndDisplay,
10
+ type FormFieldValueAdapter,
10
11
  } from '../types';
11
12
  import {
12
13
  hasRendering,
@@ -17,7 +18,6 @@ import {
17
18
  formatDateAsDisplayString,
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';
@@ -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 groupContent = field.questions
12
- ?.filter((child) => !child.isHidden)
13
- .map((child, index) => {
14
- const keyId = child.id + '_' + index;
15
- if (formFieldAdapters[child.type]) {
16
- return (
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
- return <div className={styles.flexRow}>{groupContent}</div>;
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;
@@ -10,3 +10,7 @@
10
10
  .groupContainer {
11
11
  margin: 0.5rem 0;
12
12
  }
13
+
14
+ .boldLegend > legend {
15
+ font-weight: bolder;
16
+ }
@@ -6,6 +6,7 @@ import { hasRendering } from '../../../utils/common-utils';
6
6
  import { evaluateAsyncExpression, evaluateExpression } from '../../../utils/expression-runner';
7
7
  import { evalConditionalRequired, evaluateDisabled, evaluateHide } from '../../../utils/form-helper';
8
8
  import { isEmpty } from '../../../validators/form-validator';
9
+ import { reportError } from '../../../utils/error-utils';
9
10
 
10
11
  export function handleFieldLogic(field: FormField, context: FormContextProps) {
11
12
  const {
@@ -80,6 +81,8 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
80
81
  context.formFieldAdapters[dependent.type].transformFieldValue(dependent, result, context);
81
82
  }
82
83
  updateFormField(dependent);
84
+ }).catch((error) => {
85
+ reportError(error, 'Error evaluating calculate expression');
83
86
  });
84
87
  }
85
88
  // evaluate hide
@@ -2,10 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  type FormField,
4
4
  type FormFieldInputProps,
5
- type FormFieldValidator,
6
5
  type FormFieldValueAdapter,
7
6
  type RenderType,
8
- type SessionMode,
9
7
  type ValidationResult,
10
8
  type ValueAndDisplay,
11
9
  } from '../../../types';
@@ -237,6 +235,6 @@ export function isUnspecifiedSupported(question: FormField) {
237
235
  );
238
236
  }
239
237
 
240
- function isGroupField(rendering: RenderType) {
238
+ export function isGroupField(rendering: RenderType) {
241
239
  return rendering === 'group' || rendering === 'repeating';
242
240
  }
@@ -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';
@@ -79,6 +78,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
79
78
  });
80
79
  }
81
80
  }
81
+
82
82
  const clonedField = cloneRepeatField(field, null, counter);
83
83
  // run necessary expressions
84
84
  if (clonedField.type === 'obsGroup') {
@@ -170,15 +170,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
170
170
 
171
171
  return (
172
172
  <React.Fragment>
173
- {isGrouped ? (
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
- )}
173
+ <div>{nodes}</div>
182
174
  </React.Fragment>
183
175
  );
184
176
  };
@@ -1,35 +1,63 @@
1
1
  import { useMemo } from 'react';
2
- import { type FormSchema, type FormField } from '../types';
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
- form.pages?.forEach((page) =>
9
- page.sections?.forEach((section) => {
10
- section.questions?.forEach((question) => {
11
- flattenedFieldsTemp.push(question);
12
- if (question.type == 'obsGroup') {
13
- question.questions.forEach((groupedField) => {
14
- groupedField.meta.groupId = question.id;
15
- flattenedFieldsTemp.push(groupedField);
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
- flattenedFieldsTemp.forEach((field) => {
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
 
@@ -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);
@@ -318,7 +318,7 @@ export class EncounterFormProcessor extends FormProcessor {
318
318
  patient: patient,
319
319
  previousEncounter: previousDomainObjectValue,
320
320
  });
321
- return extractObsValueAndDisplay(field, value);
321
+ return value ? extractObsValueAndDisplay(field, value) : null;
322
322
  }
323
323
  if (previousDomainObjectValue && field.questionOptions.enablePreviousValue) {
324
324
  return await adapter.getPreviousValue(field, previousDomainObjectValue, context);
@@ -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 { voidObs, constructObs, assignedObsIds } from '../../adapters/obs-adapter';
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
- .filter((field) => hasSubmittableObs(field))
190
- .forEach((field) => {
191
- if ((field.isHidden || field.isParentHidden) && field.meta.previousValue) {
192
- const valuesArray = Array.isArray(field.meta.previousValue)
193
- ? field.meta.previousValue
194
- : [field.meta.previousValue];
195
- addObsToList(
196
- obsForSubmission,
197
- valuesArray.map((obs) => voidObs(obs)),
198
- );
199
- return;
200
- }
201
- if (field.type == 'obsGroup') {
202
- if (field.meta.submission?.voidedValue) {
203
- addObsToList(obsForSubmission, field.meta.submission.voidedValue);
204
- return;
205
- }
206
- const obsGroup = constructObs(field, null);
207
- if (field.meta.previousValue) {
208
- obsGroup.uuid = field.meta.previousValue.uuid;
209
- }
210
- field.questions.forEach((groupedField) => {
211
- if (hasSubmission(groupedField)) {
212
- addObsToList(obsGroup.groupMembers, groupedField.meta.submission.newValue);
213
- addObsToList(obsGroup.groupMembers, groupedField.meta.submission.voidedValue);
214
- }
215
- });
216
- if (obsGroup.groupMembers.length || obsGroup.voided) {
217
- addObsToList(obsForSubmission, obsGroup);
218
- }
219
- }
220
- if (hasSubmission(field)) {
221
- addObsToList(obsForSubmission, field.meta.submission.newValue);
222
- addObsToList(obsForSubmission, field.meta.submission.voidedValue);
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
- if (question?.questions?.length) {
45
- question.questions.forEach((question) => handleQuestion(question, page, form));
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) {