@openmrs/esm-form-engine-lib 2.1.0-pre.1585 → 2.1.0-pre.1597

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.1585",
3
+ "version": "2.1.0-pre.1597",
4
4
  "description": "React Form Engine for O3",
5
5
  "browser": "dist/openmrs-esm-form-engine-lib.js",
6
6
  "main": "src/index.ts",
@@ -83,7 +83,7 @@ export const ObsAdapter: FormFieldValueAdapter = {
83
83
  return null;
84
84
  }
85
85
  if (hasRendering(field, 'checkbox')) {
86
- return handleMultiSelect(field, value);
86
+ return handleMultiSelect(field, Array.isArray(value) ? value : [value]);
87
87
  }
88
88
  if (!isEmpty(value) && hasPreviousObsValueChanged(field, value)) {
89
89
  return gracefullySetSubmission(field, editObs(field, value), undefined);
package/src/api/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
  import { encounterRepresentation } from '../constants';
3
- import { type OpenmrsForm, type PatientIdentifier, type PatientProgramPayload } from '../types';
3
+ import type { FHIRObsResource, OpenmrsForm, PatientIdentifier, PatientProgramPayload } from '../types';
4
4
  import { isUuid } from '../utils/boolean-utils';
5
5
 
6
6
  export function saveEncounter(abortController: AbortController, payload, encounterUuid?: string) {
@@ -76,15 +76,18 @@ export async function getPreviousEncounter(patientUuid: string, encounterType: s
76
76
  return null;
77
77
  }
78
78
 
79
- export function getLatestObs(patientUuid: string, conceptUuid: string, encounterTypeUuid?: string) {
79
+ export async function getLatestObs(
80
+ patientUuid: string,
81
+ conceptUuid: string,
82
+ encounterTypeUuid?: string,
83
+ ): Promise<FHIRObsResource> {
80
84
  let params = `patient=${patientUuid}&code=${conceptUuid}${
81
85
  encounterTypeUuid ? `&encounter.type=${encounterTypeUuid}` : ''
82
86
  }`;
83
87
  // the latest obs
84
88
  params += '&_sort=-date&_count=1';
85
- return openmrsFetch(`${fhirBaseUrl}/Observation?${params}`).then(({ data }) => {
86
- return data.entry?.length ? data.entry[0].resource : null;
87
- });
89
+ const { data } = await openmrsFetch(`${fhirBaseUrl}/Observation?${params}`);
90
+ return data.entry?.length ? data.entry[0].resource : null;
88
91
  }
89
92
 
90
93
  /**
@@ -64,30 +64,40 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
64
64
  mode: sessionMode,
65
65
  patient,
66
66
  },
67
- ).then((result) => {
68
- setValue(dependent.id, result);
69
- // validate calculated value
70
- const { errors, warnings } = validateFieldValue(dependent, result, context.formFieldValidators, {
71
- formFields,
72
- values,
73
- expressionContext: { patient, mode: sessionMode },
67
+ )
68
+ .then((result) => {
69
+ setValue(dependent.id, result);
70
+ // validate calculated value
71
+ const { errors, warnings } = validateFieldValue(dependent, result, context.formFieldValidators, {
72
+ formFields,
73
+ values,
74
+ expressionContext: { patient, mode: sessionMode },
75
+ });
76
+ if (!dependent.meta.submission) {
77
+ dependent.meta.submission = {};
78
+ }
79
+ dependent.meta.submission.errors = errors;
80
+ dependent.meta.submission.warnings = warnings;
81
+ if (!errors.length) {
82
+ context.formFieldAdapters[dependent.type].transformFieldValue(dependent, result, context);
83
+ }
84
+ updateFormField(dependent);
85
+ })
86
+ .catch((error) => {
87
+ reportError(error, 'Error evaluating calculate expression');
74
88
  });
75
- if (!dependent.meta.submission) {
76
- dependent.meta.submission = {};
77
- }
78
- dependent.meta.submission.errors = errors;
79
- dependent.meta.submission.warnings = warnings;
80
- if (!errors.length) {
81
- context.formFieldAdapters[dependent.type].transformFieldValue(dependent, result, context);
82
- }
83
- updateFormField(dependent);
84
- }).catch((error) => {
85
- reportError(error, 'Error evaluating calculate expression');
86
- });
87
89
  }
88
90
  // evaluate hide
89
91
  if (dependent.hide) {
90
- evaluateHide({ value: dependent, type: 'field' }, formFields, values, sessionMode, patient, evaluateExpression);
92
+ evaluateHide(
93
+ { value: dependent, type: 'field' },
94
+ formFields,
95
+ values,
96
+ sessionMode,
97
+ patient,
98
+ evaluateExpression,
99
+ updateFormField,
100
+ );
91
101
  }
92
102
  // evaluate disabled
93
103
  if (typeof dependent.disabled === 'object' && dependent.disabled.disableWhenExpression) {
@@ -197,12 +207,8 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
197
207
  sessionMode,
198
208
  patient,
199
209
  evaluateExpression,
210
+ updateFormField,
200
211
  );
201
- if (isTrue(section.isHidden)) {
202
- section.questions.forEach((field) => {
203
- field.isParentHidden = true;
204
- });
205
- }
206
212
  shouldUpdateForm = true;
207
213
  break;
208
214
  }
@@ -214,14 +220,15 @@ function evaluateFieldDependents(field: FormField, values: any, context: FormCon
214
220
  if (field.pageDependents) {
215
221
  field.pageDependents?.forEach((dep) => {
216
222
  const dependent = formJson.pages.find((f) => f.label == dep);
217
- evaluateHide({ value: dependent, type: 'page' }, formFields, values, sessionMode, patient, evaluateExpression);
218
- if (isTrue(dependent.isHidden)) {
219
- dependent.sections.forEach((section) => {
220
- section.questions.forEach((field) => {
221
- field.isParentHidden = true;
222
- });
223
- });
224
- }
223
+ evaluateHide(
224
+ { value: dependent, type: 'page' },
225
+ formFields,
226
+ values,
227
+ sessionMode,
228
+ patient,
229
+ evaluateExpression,
230
+ updateFormField,
231
+ );
225
232
  shouldUpdateForm = true;
226
233
  });
227
234
  }
@@ -109,7 +109,15 @@ export const useEvaluateFormFieldExpressions = (
109
109
  useEffect(() => {
110
110
  factoryContext.formJson?.pages?.forEach((page) => {
111
111
  if (page.hide) {
112
- evaluateHide({ value: page, type: 'page' }, formFields, formValues, sessionMode, patient, evaluateExpression);
112
+ evaluateHide(
113
+ { value: page, type: 'page' },
114
+ formFields,
115
+ formValues,
116
+ sessionMode,
117
+ patient,
118
+ evaluateExpression,
119
+ null,
120
+ );
113
121
  } else {
114
122
  page.isHidden = false;
115
123
  }
@@ -122,6 +130,7 @@ export const useEvaluateFormFieldExpressions = (
122
130
  sessionMode,
123
131
  patient,
124
132
  evaluateExpression,
133
+ null,
125
134
  );
126
135
  } else {
127
136
  section.isHidden = false;
@@ -15,20 +15,80 @@ export interface OpenmrsEncounter {
15
15
  }
16
16
 
17
17
  export interface OpenmrsObs extends OpenmrsResource {
18
- concept: any;
19
- obsDatetime: string | Date;
20
- obsGroup: OpenmrsObs;
21
- groupMembers: Array<OpenmrsObs>;
22
- comment: string;
23
- location: OpenmrsResource;
24
- order: OpenmrsResource;
25
- encounter: OpenmrsResource;
26
- voided: boolean;
27
- value: any;
28
- formFieldPath: string;
29
- formFieldNamespace: string;
30
- status: string;
31
- interpretation: string;
18
+ concept?: any;
19
+ obsDatetime?: string | Date;
20
+ obsGroup?: OpenmrsObs;
21
+ groupMembers?: Array<OpenmrsObs>;
22
+ comment?: string;
23
+ location?: OpenmrsResource;
24
+ order?: OpenmrsResource;
25
+ encounter?: OpenmrsResource;
26
+ voided?: boolean;
27
+ value?: any;
28
+ formFieldPath?: string;
29
+ formFieldNamespace?: string;
30
+ status?: string;
31
+ interpretation?: string;
32
+ }
33
+
34
+ export interface FHIRObsResource {
35
+ resourceType: string;
36
+ id: string;
37
+ category: Array<{
38
+ coding: Array<{
39
+ system: string;
40
+ code: string;
41
+ display: string;
42
+ }>;
43
+ }>;
44
+ code: {
45
+ coding: Array<{
46
+ code: string;
47
+ display: string;
48
+ }>;
49
+ text: string;
50
+ };
51
+ encounter?: {
52
+ reference: string;
53
+ type: string;
54
+ };
55
+ effectiveDateTime: string;
56
+ issued: string;
57
+ valueBoolean?: boolean;
58
+ valueString?: string;
59
+ valueDateTime?: string;
60
+ valueQuantity?: {
61
+ value: number;
62
+ unit: string;
63
+ system: string;
64
+ code: string;
65
+ };
66
+ valueCodeableConcept?: {
67
+ coding: [
68
+ {
69
+ code: string;
70
+ display: string;
71
+ },
72
+ ];
73
+ text: string;
74
+ };
75
+ referenceRange: Array<{
76
+ low?: {
77
+ value: number;
78
+ };
79
+ high?: {
80
+ value: number;
81
+ };
82
+ type: {
83
+ coding: Array<{
84
+ system: string;
85
+ code: string;
86
+ }>;
87
+ };
88
+ }>;
89
+ hasMember?: Array<{
90
+ reference: string;
91
+ }>;
32
92
  }
33
93
 
34
94
  export interface OpenmrsForm {
@@ -1,14 +1,12 @@
1
1
  import {
2
2
  findConceptByReference,
3
- evaluateConditionalAnswered,
4
3
  evaluateFieldReadonlyProp,
5
4
  evaluateDisabled,
6
5
  isPageContentVisible,
6
+ extractObsValueAndDisplay,
7
7
  } from './form-helper';
8
- import { DefaultValueValidator } from '../validators/default-value-validator';
9
- import { type LayoutType } from '@openmrs/esm-framework';
10
8
  import { ConceptTrue } from '../constants';
11
- import { type FormPage, type FormField, type OpenmrsEncounter, type SessionMode } from '../types';
9
+ import type { FormPage, FormField } from '../types';
12
10
 
13
11
  jest.mock('../validators/default-value-validator');
14
12
 
@@ -507,4 +505,160 @@ describe('Form Engine Helper', () => {
507
505
  expect(isPageContentVisible(page)).toBe(false);
508
506
  });
509
507
  });
508
+
509
+ describe('extractObsValueAndDisplay', () => {
510
+ // Mock form field types
511
+ const mockFormFields = {
512
+ codedField: {
513
+ questionOptions: {
514
+ rendering: 'select',
515
+ answers: [{ concept: '2395de62-f5a6-49b6-ab0f-57a21a9029c1', label: 'Sneezing Symptom' }],
516
+ },
517
+ },
518
+ toggleField: {
519
+ questionOptions: {
520
+ rendering: 'toggle',
521
+ answers: [],
522
+ },
523
+ },
524
+ dateField: {
525
+ questionOptions: {
526
+ rendering: 'date',
527
+ },
528
+ },
529
+ stringField: {
530
+ questionOptions: {
531
+ rendering: 'text',
532
+ },
533
+ },
534
+ } as any;
535
+
536
+ // Primitive value tests
537
+ describe('Primitive Value Handling', () => {
538
+ it('should handle string primitive value', () => {
539
+ const result = extractObsValueAndDisplay(mockFormFields.stringField, 'Hello World');
540
+ expect(result).toEqual({
541
+ value: 'Hello World',
542
+ display: 'Hello World',
543
+ });
544
+ });
545
+
546
+ it('should handle number primitive value', () => {
547
+ const result = extractObsValueAndDisplay(mockFormFields.stringField, 42);
548
+ expect(result).toEqual({
549
+ value: 42,
550
+ display: 42,
551
+ });
552
+ });
553
+ });
554
+
555
+ // FHIR Observation tests
556
+ describe('FHIR Observation Handling', () => {
557
+ const codedFHIRObs = {
558
+ resourceType: 'Observation',
559
+ code: {
560
+ coding: {
561
+ code: '1095de62-b5a6-49v6-ab0f-57a21a9029cb',
562
+ },
563
+ },
564
+ valueCodeableConcept: {
565
+ coding: [
566
+ {
567
+ code: '2395de62-f5a6-49b6-ab0f-57a21a9029c1',
568
+ display: 'Sneezing',
569
+ },
570
+ ],
571
+ },
572
+ };
573
+
574
+ const booleanFHIRObs = {
575
+ resourceType: 'Observation',
576
+ code: {
577
+ coding: {
578
+ code: 'b095deb2-b5a6-49v6-ab0f-57a21a9029cx',
579
+ },
580
+ },
581
+ valueBoolean: true,
582
+ };
583
+
584
+ const dateFHIRObs = {
585
+ resourceType: 'Observation',
586
+ code: {
587
+ coding: {
588
+ code: 'e095de62-b5a6-49v6-ab0f-57a21a9029cy',
589
+ },
590
+ },
591
+ valueDateTime: '2024-07-31T01:33:19+00:00',
592
+ };
593
+
594
+ it('should handle coded FHIR observation', () => {
595
+ const result = extractObsValueAndDisplay(mockFormFields.codedField, codedFHIRObs);
596
+ expect(result).toEqual({
597
+ value: '2395de62-f5a6-49b6-ab0f-57a21a9029c1',
598
+ display: 'Sneezing Symptom',
599
+ });
600
+ });
601
+
602
+ it('should handle boolean FHIR observation', () => {
603
+ const result = extractObsValueAndDisplay(mockFormFields.toggleField, booleanFHIRObs);
604
+ expect(result).toEqual({
605
+ value: ConceptTrue,
606
+ display: undefined,
607
+ });
608
+ });
609
+
610
+ it('should handle date FHIR observation', () => {
611
+ const result = extractObsValueAndDisplay(mockFormFields.dateField, dateFHIRObs);
612
+ expect(result).toEqual({
613
+ value: expect.any(Date),
614
+ display: expect.stringContaining('2024-07-31'),
615
+ });
616
+ });
617
+ });
618
+
619
+ // OpenMRS Observation tests
620
+ describe('OpenMRS Observation Handling', () => {
621
+ const codedOpenMRSObs = {
622
+ value: {
623
+ uuid: '2395de62-f5a6-49b6-ab0f-57a21a9029c1',
624
+ name: { name: 'Sneezing' },
625
+ },
626
+ };
627
+
628
+ const booleanOpenMRSObs = {
629
+ value: {
630
+ uuid: 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3',
631
+ name: { name: 'True' },
632
+ },
633
+ };
634
+
635
+ const dateOpenMRSObs = {
636
+ value: '2024-07-31T01:33:19+00:00',
637
+ };
638
+
639
+ it('should handle coded OpenMRS observation', () => {
640
+ const result = extractObsValueAndDisplay(mockFormFields.codedField, codedOpenMRSObs);
641
+ expect(result).toEqual({
642
+ value: '2395de62-f5a6-49b6-ab0f-57a21a9029c1',
643
+ display: 'Sneezing Symptom',
644
+ });
645
+ });
646
+
647
+ it('should handle boolean OpenMRS observation', () => {
648
+ const result = extractObsValueAndDisplay(mockFormFields.toggleField, booleanOpenMRSObs);
649
+ expect(result).toEqual({
650
+ value: 'cf82933b-3f3f-45e7-a5ab-5d31aaee3da3',
651
+ display: 'True',
652
+ });
653
+ });
654
+
655
+ it('should handle date OpenMRS observation', () => {
656
+ const result = extractObsValueAndDisplay(mockFormFields.dateField, dateOpenMRSObs);
657
+ expect(result).toEqual({
658
+ value: expect.any(Date),
659
+ display: expect.stringMatching(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/),
660
+ });
661
+ });
662
+ });
663
+ });
510
664
  });
@@ -1,8 +1,9 @@
1
1
  import { type LayoutType } from '@openmrs/esm-framework';
2
- import { type OpenmrsObs, type FormField, type FormPage, type FormSection, type SessionMode } from '../types';
2
+ import type { FormField, FormPage, FormSection, SessionMode, FHIRObsResource, RenderType } from '../types';
3
3
  import { isEmpty } from '../validators/form-validator';
4
4
  import { parseToLocalDateTime } from './common-utils';
5
5
  import dayjs from 'dayjs';
6
+ import { ConceptFalse, ConceptTrue } from '../constants';
6
7
 
7
8
  export function shouldUseInlineLayout(
8
9
  renderingType: 'single-line' | 'multiline' | 'automatic',
@@ -94,6 +95,7 @@ export function evaluateHide(
94
95
  sessionMode: SessionMode,
95
96
  patient: fhir.Patient,
96
97
  expressionRunnerFn,
98
+ updateFormFieldFn: (field: FormField) => void | null,
97
99
  ) {
98
100
  const { value, type } = node;
99
101
  const isHidden = expressionRunnerFn(value['hide']?.hideWhenExpression, node, allFields, allValues, {
@@ -110,15 +112,20 @@ export function evaluateHide(
110
112
  if (type == 'page') {
111
113
  value['sections'].forEach((section) => {
112
114
  section.isParentHidden = isHidden;
113
- cascadeVisibilityToChildFields(isHidden, section, allFields);
115
+ cascadeVisibilityToChildFields(isHidden, section, allFields, updateFormFieldFn);
114
116
  });
115
117
  }
116
118
  if (type == 'section') {
117
- cascadeVisibilityToChildFields(isHidden, value, allFields);
119
+ cascadeVisibilityToChildFields(isHidden, value, allFields, updateFormFieldFn);
118
120
  }
119
121
  }
120
122
 
121
- function cascadeVisibilityToChildFields(visibility: boolean, section: FormSection, allFields: Array<FormField>) {
123
+ function cascadeVisibilityToChildFields(
124
+ visibility: boolean,
125
+ section: FormSection,
126
+ allFields: Array<FormField>,
127
+ updateFormFieldFn: (field: FormField) => void,
128
+ ) {
122
129
  const candidateIds = section.questions.map((q) => q.id);
123
130
  allFields
124
131
  .filter((field) => candidateIds.includes(field.id))
@@ -129,6 +136,7 @@ function cascadeVisibilityToChildFields(visibility: boolean, section: FormSectio
129
136
  member.isParentHidden = visibility;
130
137
  });
131
138
  }
139
+ updateFormFieldFn?.(field);
132
140
  });
133
141
  }
134
142
 
@@ -172,24 +180,30 @@ export function scrollIntoView(viewId: string, shouldFocus: boolean = false) {
172
180
  }
173
181
  }
174
182
 
175
- export const extractObsValueAndDisplay = (field: FormField, obs: OpenmrsObs) => {
183
+ export const extractObsValueAndDisplay = (field: FormField, obs: any) => {
176
184
  const rendering = field.questionOptions.rendering;
177
-
178
- if (typeof obs.value === 'string' || typeof obs.value === 'number') {
185
+ if (typeof obs !== 'object') {
186
+ return { value: obs, display: obs };
187
+ }
188
+ const omrsObs = obs.resourceType === 'Observation' ? mapFHIRObsToOpenMRS(obs, rendering) : obs;
189
+ if (!omrsObs) {
190
+ return { value: null, display: null };
191
+ }
192
+ if (typeof omrsObs.value === 'string' || typeof omrsObs.value === 'number') {
179
193
  if (rendering === 'date' || rendering === 'datetime') {
180
- const dateObj = parseToLocalDateTime(`${obs.value}`);
194
+ const dateObj = parseToLocalDateTime(`${omrsObs.value}`);
181
195
  return { value: dateObj, display: dayjs(dateObj).format('YYYY-MM-DD HH:mm') };
182
196
  }
183
- return { value: obs.value, display: obs.value };
197
+ return { value: omrsObs.value, display: omrsObs.value };
184
198
  } else if (['toggle', 'checkbox'].includes(rendering)) {
185
199
  return {
186
- value: obs.value?.uuid,
187
- display: obs.value?.name?.name,
200
+ value: omrsObs.value?.uuid,
201
+ display: omrsObs.value?.name?.name,
188
202
  };
189
203
  } else {
190
204
  return {
191
- value: obs.value?.uuid,
192
- display: field.questionOptions.answers?.find((option) => option.concept === obs.value?.uuid)?.label,
205
+ value: omrsObs.value?.uuid,
206
+ display: field.questionOptions.answers?.find((option) => option.concept === omrsObs.value?.uuid)?.label,
193
207
  };
194
208
  }
195
209
  };
@@ -212,3 +226,48 @@ export function isPageContentVisible(page: FormPage) {
212
226
  }) ?? false
213
227
  );
214
228
  }
229
+
230
+ function mapFHIRObsToOpenMRS(fhirObs: FHIRObsResource, rendering: RenderType) {
231
+ try {
232
+ return {
233
+ obsDatetime: fhirObs.effectiveDateTime,
234
+ uuid: fhirObs.id,
235
+ concept: {
236
+ uuid: fhirObs.code.coding[0]?.code,
237
+ display: fhirObs.code.coding[0]?.display,
238
+ },
239
+ value: extractFHIRObsValue(fhirObs, rendering),
240
+ };
241
+ } catch (error) {
242
+ console.error('Error converting FHIR Obs to OpenMRS modelling', error);
243
+ return null;
244
+ }
245
+ }
246
+
247
+ function extractFHIRObsValue(fhirObs: FHIRObsResource, rendering: RenderType) {
248
+ switch (rendering) {
249
+ case 'toggle':
250
+ return fhirObs.valueBoolean ? { uuid: ConceptTrue } : { uuid: ConceptFalse };
251
+
252
+ case 'date':
253
+ case 'datetime':
254
+ return fhirObs.valueDateTime;
255
+
256
+ case 'number':
257
+ return fhirObs.valueQuantity?.value ?? null;
258
+
259
+ case 'radio':
260
+ case 'checkbox':
261
+ case 'select':
262
+ case 'content-switcher':
263
+ return fhirObs.valueCodeableConcept?.coding[0]
264
+ ? {
265
+ uuid: fhirObs.valueCodeableConcept?.coding[0].code,
266
+ name: { name: fhirObs.valueCodeableConcept?.coding[0].display },
267
+ }
268
+ : null;
269
+
270
+ default:
271
+ return fhirObs.valueString;
272
+ }
273
+ }