Skip to content

Commit c175478

Browse files
authored
(feat) O3-4137: Add support for nested obsGroup (#427)
* feat: add support for nested obsgroups * fix: improve flatten fields
1 parent 2abfe5f commit c175478

File tree

9 files changed

+437
-96
lines changed

9 files changed

+437
-96
lines changed

src/adapters/obs-adapter.test.ts

Lines changed: 278 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type FormContextProps } from '../provider/form-provider';
22
import { type FormField } from '../types';
3-
import { hasPreviousObsValueChanged, findObsByFormField, ObsAdapter } from './obs-adapter';
3+
import { findObsByFormField, hasPreviousObsValueChanged, ObsAdapter } from './obs-adapter';
44

55
const formContext = {
66
methods: null,
@@ -944,3 +944,280 @@ describe('findObsByFormField', () => {
944944
expect(matchedObs[0]).toBe(obsList[3]);
945945
});
946946
});
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+
});
Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,54 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import classNames from 'classnames';
33
import { type FormFieldInputProps } from '../../types';
44
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';
66
import { useFormProviderContext } from '../../provider/form-provider';
7+
import { FormGroup } from '@carbon/react';
8+
import { useTranslation } from 'react-i18next';
79

8-
export const ObsGroup: React.FC<FormFieldInputProps> = ({ field }) => {
10+
export const ObsGroup: React.FC<FormFieldInputProps> = ({ field, ...restProps }) => {
11+
const { t } = useTranslation();
912
const { formFieldAdapters } = useFormProviderContext();
13+
const showLabel = useMemo(() => field.questions?.length > 1, [field]);
1014

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}`;
2521

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+
);
2752
};
2853

2954
export default ObsGroup;

src/components/group/obs-group.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@
1010
.groupContainer {
1111
margin: 0.5rem 0;
1212
}
13+
14+
.boldLegend > legend {
15+
font-weight: bolder;
16+
}

src/components/renderer/field/form-field-renderer.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,6 @@ export function isUnspecifiedSupported(question: FormField) {
235235
);
236236
}
237237

238-
function isGroupField(rendering: RenderType) {
238+
export function isGroupField(rendering: RenderType) {
239239
return rendering === 'group' || rendering === 'repeating';
240240
}

0 commit comments

Comments
 (0)