@openmrs/esm-form-engine-lib 3.0.1 → 3.1.0

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.
@@ -15,6 +15,7 @@ import { useFormFactory } from '../../provider/form-factory-provider';
15
15
  const renderingByTypeMap: Record<string, RenderType> = {
16
16
  obsGroup: 'group',
17
17
  testOrder: 'select',
18
+ diagnosis: 'ui-select-extended',
18
19
  };
19
20
 
20
21
  const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
@@ -32,6 +33,8 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
32
33
  methods: { getValues, setValue },
33
34
  addFormField,
34
35
  removeFormField,
36
+ deletedFields,
37
+ setDeletedFields,
35
38
  } = context;
36
39
 
37
40
  useEffect(() => {
@@ -113,6 +116,7 @@ const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
113
116
  clearSubmission(field);
114
117
  }
115
118
  setRows(rows.filter((q) => q.id !== field.id));
119
+ setDeletedFields([...deletedFields, field]);
116
120
  removeFormField(field.id);
117
121
  };
118
122
 
package/src/constants.ts CHANGED
@@ -5,6 +5,7 @@ export const encounterRepresentation =
5
5
  'custom:(uuid,encounterDatetime,encounterType:(uuid,name,description),location:(uuid,name),' +
6
6
  'patient:(uuid,display),encounterProviders:(uuid,provider:(uuid,name),encounterRole:(uuid,name)),' +
7
7
  'orders:(uuid,display,concept:(uuid,display),voided),' +
8
+ 'diagnoses:(uuid,certainty,condition,formFieldPath,formFieldNamespace,display,rank,voided,diagnosis:(coded:(uuid,display))),' +
8
9
  'obs:(uuid,obsDatetime,comment,voided,groupMembers,formFieldNamespace,formFieldPath,concept:(uuid,name:(uuid,name)),value:(uuid,name:(uuid,name),' +
9
10
  'names:(uuid,conceptNameType,name))))';
10
11
  export const FormsStore = 'forms-engine-store';
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import dayjs from 'dayjs';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { act, cleanup, render, screen, within } from '@testing-library/react';
4
+ import { act, cleanup, render, screen, waitFor, within } from '@testing-library/react';
5
5
  import {
6
6
  ExtensionSlot,
7
7
  OpenmrsDatePicker,
@@ -44,9 +44,11 @@ import viralLoadStatusForm from '__mocks__/forms/rfe-forms/viral-load-status-for
44
44
  import readOnlyValidationForm from '__mocks__/forms/rfe-forms/read-only-validation-form.json';
45
45
  import jsExpressionValidationForm from '__mocks__/forms/rfe-forms/js-expression-validation-form.json';
46
46
  import hidePagesAndSectionsForm from '__mocks__/forms/rfe-forms/hide-pages-and-sections-form.json';
47
+ import diagnosisForm from '__mocks__/forms/rfe-forms/diagnosis-test-form.json';
47
48
 
48
49
  import FormEngine from './form-engine.component';
49
- import { type SessionMode } from './types';
50
+ import { type FormSchema, type OpenmrsEncounter, type SessionMode } from './types';
51
+ import { useEncounter } from './hooks/useEncounter';
50
52
 
51
53
  const patientUUID = '8673ee4f-e2ab-4077-ba55-4980f408773e';
52
54
  const visit = mockVisit;
@@ -59,6 +61,7 @@ const mockExtensionSlot = jest.mocked(ExtensionSlot);
59
61
  const mockUsePatient = jest.mocked(usePatient);
60
62
  const mockUseSession = jest.mocked(useSession);
61
63
  const mockOpenmrsDatePicker = jest.mocked(OpenmrsDatePicker);
64
+ const mockUseEncounter = jest.mocked(useEncounter);
62
65
 
63
66
  mockOpenmrsDatePicker.mockImplementation(({ id, labelText, value, onChange, isInvalid, invalidText }) => {
64
67
  return (
@@ -79,6 +82,48 @@ mockOpenmrsDatePicker.mockImplementation(({ id, labelText, value, onChange, isIn
79
82
  when(mockOpenmrsFetch).calledWith(formsResourcePath).mockReturnValue({ data: demoHtsOpenmrsForm });
80
83
  when(mockOpenmrsFetch).calledWith(clobDataResourcePath).mockReturnValue({ data: demoHtsForm });
81
84
 
85
+ jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
86
+
87
+ jest.mock('lodash-es', () => ({
88
+ ...jest.requireActual('lodash-es'),
89
+ debounce: jest.fn((fn) => fn),
90
+ }));
91
+
92
+ jest.mock('./registry/registry', () => {
93
+ const originalModule = jest.requireActual('./registry/registry');
94
+ return {
95
+ ...originalModule,
96
+ getRegisteredDataSource: jest.fn().mockResolvedValue({
97
+ fetchData: jest.fn().mockImplementation((...args) => {
98
+ if (args[1].class?.length && !args[1].referencedValue?.key) {
99
+ // concept DS
100
+ return Promise.resolve([
101
+ {
102
+ uuid: 'stage-1-uuid',
103
+ display: 'stage 1',
104
+ },
105
+ {
106
+ uuid: 'stage-2-uuid',
107
+ display: 'stage 2',
108
+ },
109
+ {
110
+ uuid: 'stage-3-uuid',
111
+ display: 'stage 3',
112
+ },
113
+ ]);
114
+ }
115
+ }),
116
+ fetchSingleItem: jest.fn().mockImplementation((uuid: string) => {
117
+ return Promise.resolve({
118
+ uuid,
119
+ display: 'stage 1',
120
+ });
121
+ }),
122
+ toUuidAndDisplay: (data) => data,
123
+ }),
124
+ };
125
+ });
126
+
82
127
  jest.mock('../src/api', () => {
83
128
  const originalModule = jest.requireActual('../src/api');
84
129
 
@@ -117,6 +162,16 @@ jest.mock('./hooks/useConcepts', () => ({
117
162
  }),
118
163
  }));
119
164
 
165
+ jest.mock('./hooks/useEncounter', () => ({
166
+ useEncounter: jest.fn().mockImplementation((formJson: FormSchema) => {
167
+ return {
168
+ encounter: formJson.encounter ? (mockHxpEncounter as OpenmrsEncounter) : null,
169
+ isLoading: false,
170
+ error: undefined,
171
+ };
172
+ }),
173
+ }));
174
+
120
175
  describe('Form engine component', () => {
121
176
  const user = userEvent.setup();
122
177
 
@@ -1047,7 +1102,118 @@ describe('Form engine component', () => {
1047
1102
  });
1048
1103
  });
1049
1104
 
1050
- function renderForm(formUUID, formJson, intent?: string, mode?: SessionMode) {
1105
+ describe('Encounter diagnosis', () => {
1106
+ it('should test addition of a diagnosis', async () => {
1107
+ await act(async () => {
1108
+ renderForm(null, diagnosisForm);
1109
+ });
1110
+
1111
+ const testDiagnosis1AddButton = screen.getAllByRole('button', { name: 'Add' })[0];
1112
+ await user.click(testDiagnosis1AddButton);
1113
+
1114
+ await waitFor(() => {
1115
+ expect(screen.getAllByRole('combobox', { name: /^test diagnosis 1$/i }).length).toEqual(2);
1116
+ });
1117
+
1118
+ expect(screen.getByRole('button', { name: /Remove/i })).toBeInTheDocument();
1119
+ });
1120
+
1121
+ it('should render all diagnosis fields', async () => {
1122
+ await act(async () => {
1123
+ renderForm(null, diagnosisForm);
1124
+ });
1125
+ const diagnosisFields = screen.getAllByRole('combobox', { name: /test diagnosis 1|test diagnosis 2/i });
1126
+ expect(diagnosisFields.length).toBe(2);
1127
+ });
1128
+
1129
+ it('should be possible to delete cloned fields', async () => {
1130
+ await act(async () => {
1131
+ renderForm(null, diagnosisForm);
1132
+ });
1133
+
1134
+ const testDiagnosis1AddButton = screen.getAllByRole('button', { name: 'Add' })[0];
1135
+ await user.click(testDiagnosis1AddButton);
1136
+
1137
+ await waitFor(() => {
1138
+ expect(screen.getAllByRole('combobox', { name: /^test diagnosis 1$/i }).length).toEqual(2);
1139
+ });
1140
+ const removeButton = screen.getByRole('button', { name: /Remove/i });
1141
+
1142
+ await user.click(removeButton);
1143
+
1144
+ expect(removeButton).not.toBeInTheDocument();
1145
+ });
1146
+
1147
+ it('should save diagnosis field on form submission', async () => {
1148
+ await act(async () => {
1149
+ renderForm(null, diagnosisForm);
1150
+ });
1151
+
1152
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
1153
+ const combobox = await findSelectInput(screen, 'Test Diagnosis 1');
1154
+ expect(combobox).toHaveAttribute('placeholder', 'Search...');
1155
+
1156
+ await user.click(combobox);
1157
+ await user.type(combobox, 'stage');
1158
+
1159
+ expect(screen.getByText(/stage 1/)).toBeInTheDocument();
1160
+ expect(screen.getByText(/stage 2/)).toBeInTheDocument();
1161
+ expect(screen.getByText(/stage 3/)).toBeInTheDocument();
1162
+
1163
+ await user.click(screen.getByText('stage 1'));
1164
+ await user.click(screen.getByRole('button', { name: /save/i }));
1165
+ expect(saveEncounterMock).toHaveBeenCalledTimes(1);
1166
+ const [_, encounter] = saveEncounterMock.mock.calls[0];
1167
+ expect(encounter.diagnoses.length).toBe(1);
1168
+ expect(encounter.diagnoses[0]).toEqual({
1169
+ patient: '8673ee4f-e2ab-4077-ba55-4980f408773e',
1170
+ condition: null,
1171
+ diagnosis: {
1172
+ coded: 'stage-1-uuid',
1173
+ },
1174
+ certainty: 'CONFIRMED',
1175
+ rank: 1,
1176
+ formFieldPath: `rfe-forms-diagnosis1`,
1177
+ formFieldNamespace: 'rfe-forms',
1178
+ });
1179
+ });
1180
+
1181
+ it('should edit diagnosis field on form submission', async () => {
1182
+ await act(async () => {
1183
+ renderForm(null, diagnosisForm, null, 'edit', mockHxpEncounter.uuid);
1184
+ });
1185
+ mockUseEncounter.mockImplementation(() => ({ encounter: mockHxpEncounter, error: null, isLoading: false }));
1186
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
1187
+
1188
+ const field1 = await findSelectInput(screen, 'Test Diagnosis 1');
1189
+ expect(field1).toHaveValue('stage 1');
1190
+
1191
+ await user.click(field1);
1192
+ await user.type(field1, 'stage');
1193
+ expect(screen.getByText(/stage 1/)).toBeInTheDocument();
1194
+ expect(screen.getByText(/stage 2/)).toBeInTheDocument();
1195
+ expect(screen.getByText(/stage 3/)).toBeInTheDocument();
1196
+ await user.click(screen.getByText(/stage 3/));
1197
+ await user.click(screen.getByRole('button', { name: /save/i }));
1198
+ expect(saveEncounterMock).toHaveBeenCalledTimes(1);
1199
+ const [_, encounter] = saveEncounterMock.mock.calls[0];
1200
+ expect(encounter.diagnoses.length).toBe(1);
1201
+ expect(encounter.diagnoses[0]).toEqual({
1202
+ patient: '8673ee4f-e2ab-4077-ba55-4980f408773e',
1203
+ condition: null,
1204
+ diagnosis: {
1205
+ coded: 'stage-3-uuid',
1206
+ },
1207
+ certainty: 'CONFIRMED',
1208
+ rank: 1,
1209
+ formFieldPath: `rfe-forms-diagnosis1`,
1210
+ formFieldNamespace: 'rfe-forms',
1211
+ uuid: '95690fb4-0398-42d9-9ffc-8a134e6d829d',
1212
+ });
1213
+ });
1214
+ });
1215
+
1216
+ function renderForm(formUUID, formJson, intent?: string, mode?: SessionMode, encounterUUID?: string) {
1051
1217
  render(
1052
1218
  <FormEngine
1053
1219
  formJson={formJson}
@@ -1055,6 +1221,7 @@ describe('Form engine component', () => {
1055
1221
  patientUUID={patientUUID}
1056
1222
  formSessionIntent={intent}
1057
1223
  visit={visit}
1224
+ encounterUUID={encounterUUID}
1058
1225
  mode={mode ? mode : 'enter'}
1059
1226
  />,
1060
1227
  );
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  import useSWRInfinite from 'swr/infinite';
3
- import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
3
+ import { type FetchResponse, openmrsFetch, type OpenmrsResource, restBaseUrl } from '@openmrs/esm-framework';
4
+ import { useRestApiMaxResults } from './useRestApiMaxResults';
4
5
 
5
6
  type ConceptFetchResponse = FetchResponse<{ results: Array<OpenmrsResource> }>;
6
7
 
@@ -12,23 +13,21 @@ export function useConcepts(references: Set<string>): {
12
13
  isLoading: boolean;
13
14
  error: Error | undefined;
14
15
  } {
15
- const chunkSize = 100;
16
+ const { maxResults } = useRestApiMaxResults();
16
17
  const totalCount = references.size;
17
- const totalPages = Math.ceil(totalCount / chunkSize);
18
+ const totalPages = Math.ceil(totalCount / maxResults);
18
19
 
19
- const getUrl = (index, prevPageData: ConceptFetchResponse) => {
20
+ const getUrl = (index: number, prevPageData: ConceptFetchResponse) => {
20
21
  if (index >= totalPages) {
21
22
  return null;
22
23
  }
23
24
 
24
- if (!chunkSize) {
25
- return null;
26
- }
27
-
28
- const start = index * chunkSize;
29
- const end = start + chunkSize;
25
+ const start = index * maxResults;
26
+ const end = start + maxResults;
30
27
  const referenceChunk = Array.from(references).slice(start, end);
31
- return `${restBaseUrl}/concept?references=${referenceChunk.join(',')}&v=${conceptRepresentation}`;
28
+ return `${restBaseUrl}/concept?references=${referenceChunk.join(
29
+ ',',
30
+ )}&v=${conceptRepresentation}&limit=${maxResults}`;
32
31
  };
33
32
 
34
33
  const { data, error, isLoading } = useSWRInfinite<ConceptFetchResponse, Error>(getUrl, openmrsFetch, {
@@ -54,6 +54,10 @@ export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: Form
54
54
  dispatch({ type: 'SET_FORM_JSON', value: updateFormSectionReferences(formJson) });
55
55
  }, []);
56
56
 
57
+ const setDeletedFields = useCallback((fields: FormField[]) => {
58
+ dispatch({ type: 'SET_DELETED_FIELDS', value: fields });
59
+ }, []);
60
+
57
61
  return {
58
62
  addFormField,
59
63
  updateFormField,
@@ -63,5 +67,6 @@ export function useFormStateHelpers(dispatch: Dispatch<Action>, formFields: Form
63
67
  addInvalidField,
64
68
  removeInvalidField,
65
69
  setForm,
70
+ setDeletedFields,
66
71
  };
67
72
  }
@@ -1,42 +1,38 @@
1
+ import useSWR from 'swr';
1
2
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
- import { useEffect, useState } from 'react';
3
- import { type FormSchema, type PatientProgram } from '../types';
4
- const customRepresentation = `custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(startDate,endDate,state:(uuid,name,retired,concept:(uuid),programWorkflow:(uuid)))`;
3
+ import type { FormSchema, ProgramsFetchResponse } from '../types';
5
4
 
6
- export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) => {
7
- const [patientPrograms, setPatientPrograms] = useState<Array<PatientProgram>>([]);
8
- const [isLoading, setIsLoading] = useState(true);
9
- const [error, setError] = useState(null);
5
+ const useActiveProgramEnrollments = (patientUuid: string) => {
6
+ const customRepresentation = `custom:(uuid,display,program:(uuid,name,allWorkflows),dateEnrolled,dateCompleted,location:(uuid,display),states:(startDate,endDate,state:(uuid,name,retired,concept:(uuid),programWorkflow:(uuid)))`;
7
+ const apiUrl = `${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`;
10
8
 
11
- useEffect(() => {
12
- const abortController = new AbortController();
9
+ const { data, error, isLoading } = useSWR<{ data: ProgramsFetchResponse }, Error>(
10
+ patientUuid ? apiUrl : null,
11
+ openmrsFetch,
12
+ );
13
13
 
14
- if (formJson.meta?.programs?.hasProgramFields) {
15
- openmrsFetch(`${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`, {
16
- signal: abortController.signal,
17
- })
18
- .then((response) => {
19
- setPatientPrograms(response.data.results.filter((enrollment) => enrollment.dateCompleted === null));
20
- setIsLoading(false);
21
- })
22
- .catch((error) => {
23
- if (error.name !== 'AbortError') {
24
- setError(error);
25
- setIsLoading(false);
26
- }
27
- });
28
- } else {
29
- setIsLoading(false);
30
- }
14
+ const sortedEnrollments =
15
+ data?.data?.results.length > 0
16
+ ? data?.data.results.sort((a, b) => (b.dateEnrolled > a.dateEnrolled ? 1 : -1))
17
+ : null;
31
18
 
32
- return () => {
33
- abortController.abort();
34
- };
35
- }, [formJson]);
19
+ const activePrograms = sortedEnrollments?.filter((enrollment) => !enrollment.dateCompleted);
36
20
 
37
21
  return {
38
- patientPrograms,
22
+ activePrograms,
39
23
  error,
40
24
  isLoading,
41
25
  };
42
26
  };
27
+
28
+ export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) => {
29
+ const { activePrograms, error, isLoading } = useActiveProgramEnrollments(
30
+ formJson.meta?.programs?.hasProgramFields ? patientUuid : null,
31
+ );
32
+
33
+ return {
34
+ patientPrograms: activePrograms,
35
+ errorFetchingPatientPrograms: error,
36
+ isLoadingPatientPrograms: isLoading,
37
+ };
38
+ };
@@ -0,0 +1,35 @@
1
+ import { useMemo } from 'react';
2
+ import useSWR from 'swr';
3
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
4
+
5
+ type GlobalPropertyResponse = FetchResponse<{
6
+ results: Array<{ property: string; value: string }>;
7
+ }>;
8
+
9
+ const DEFAULT_CHUNK_SIZE = 100;
10
+
11
+ export function useRestApiMaxResults() {
12
+ const { data, error, isLoading } = useSWR<GlobalPropertyResponse, Error>(
13
+ `${restBaseUrl}/systemsetting?q=webservices.rest.maxResultsAbsolute&v=custom:(property,value)`,
14
+ openmrsFetch,
15
+ );
16
+
17
+ const maxResults = useMemo(() => {
18
+ try {
19
+ const maxResultsValue = data?.data.results.find(
20
+ (prop) => prop.property === 'webservices.rest.maxResultsAbsolute',
21
+ )?.value;
22
+
23
+ const parsedValue = parseInt(maxResultsValue ?? '');
24
+ return !isNaN(parsedValue) && parsedValue > 0 ? parsedValue : DEFAULT_CHUNK_SIZE;
25
+ } catch {
26
+ return DEFAULT_CHUNK_SIZE;
27
+ }
28
+ }, [data]);
29
+
30
+ return {
31
+ maxResults,
32
+ error,
33
+ isLoading,
34
+ };
35
+ }
@@ -1,17 +1,5 @@
1
- import {
2
- type FormField,
3
- type FormPage,
4
- type FormProcessorContextProps,
5
- type FormSchema,
6
- type FormSection,
7
- type ValueAndDisplay,
8
- } from '../../types';
9
- import { usePatientPrograms } from '../../hooks/usePatientPrograms';
10
1
  import { useEffect, useState } from 'react';
11
- import { useEncounter } from '../../hooks/useEncounter';
12
- import { isEmpty } from '../../validators/form-validator';
13
- import { type FormContextProps } from '../../provider/form-provider';
14
- import { FormProcessor } from '../form-processor';
2
+ import { type OpenmrsResource, showSnackbar, translateFrom } from '@openmrs/esm-framework';
15
3
  import {
16
4
  getMutableSessionProps,
17
5
  hydrateRepeatField,
@@ -23,23 +11,32 @@ import {
23
11
  savePatientIdentifiers,
24
12
  savePatientPrograms,
25
13
  } from './encounter-processor-helper';
26
- import { type OpenmrsResource, showSnackbar, translateFrom } from '@openmrs/esm-framework';
27
- import { moduleName } from '../../globals';
14
+ import {
15
+ type FormField,
16
+ type FormPage,
17
+ type FormProcessorContextProps,
18
+ type FormSchema,
19
+ type FormSection,
20
+ type ValueAndDisplay,
21
+ } from '../../types';
22
+ import { evaluateAsyncExpression, type FormNode } from '../../utils/expression-runner';
28
23
  import { extractErrorMessagesFromResponse } from '../../utils/error-utils';
24
+ import { extractObsValueAndDisplay } from '../../utils/form-helper';
25
+ import { FormProcessor } from '../form-processor';
29
26
  import { getPreviousEncounter, saveEncounter } from '../../api';
30
- import { useEncounterRole } from '../../hooks/useEncounterRole';
31
- import { evaluateAsyncExpression, type FormNode } from '../../utils/expression-runner';
32
27
  import { hasRendering } from '../../utils/common-utils';
33
- import { extractObsValueAndDisplay } from '../../utils/form-helper';
28
+ import { isEmpty } from '../../validators/form-validator';
29
+ import { moduleName } from '../../globals';
30
+ import { type FormContextProps } from '../../provider/form-provider';
31
+ import { useEncounter } from '../../hooks/useEncounter';
32
+ import { useEncounterRole } from '../../hooks/useEncounterRole';
33
+ import { usePatientPrograms } from '../../hooks/usePatientPrograms';
34
34
 
35
35
  function useCustomHooks(context: Partial<FormProcessorContextProps>) {
36
36
  const [isLoading, setIsLoading] = useState(true);
37
37
  const { encounter, isLoading: isLoadingEncounter } = useEncounter(context.formJson);
38
38
  const { encounterRole, isLoading: isLoadingEncounterRole } = useEncounterRole();
39
- const { isLoading: isLoadingPatientPrograms, patientPrograms } = usePatientPrograms(
40
- context.patient?.id,
41
- context.formJson,
42
- );
39
+ const { isLoadingPatientPrograms, patientPrograms } = usePatientPrograms(context.patient?.id, context.formJson);
43
40
 
44
41
  useEffect(() => {
45
42
  setIsLoading(isLoadingPatientPrograms || isLoadingEncounter || isLoadingEncounterRole);
@@ -165,11 +162,21 @@ export class EncounterFormProcessor extends FormProcessor {
165
162
  // save encounter
166
163
  try {
167
164
  const { data: savedEncounter } = await saveEncounter(abortController, encounter, encounter.uuid);
168
- const saveOrders = savedEncounter.orders.map((order) => order.orderNumber);
169
- if (saveOrders.length) {
165
+ const savedOrders = savedEncounter.orders.map((order) => order.orderNumber);
166
+ const savedDiagnoses = savedEncounter.diagnoses.map((diagnosis) => diagnosis.display);
167
+ if (savedOrders.length) {
170
168
  showSnackbar({
171
169
  title: translateFn('ordersSaved', 'Order(s) saved successfully'),
172
- subtitle: saveOrders.join(', '),
170
+ subtitle: savedOrders.join(', '),
171
+ kind: 'success',
172
+ isLowContrast: true,
173
+ });
174
+ }
175
+ // handle diagnoses
176
+ if (savedDiagnoses.length) {
177
+ showSnackbar({
178
+ title: translateFn('diagnosisSaved', 'Diagnosis(es) saved successfully'),
179
+ subtitle: savedDiagnoses.join(', '),
173
180
  kind: 'success',
174
181
  isLowContrast: true,
175
182
  });
@@ -17,6 +17,7 @@ import { DefaultValueValidator } from '../../validators/default-value-validator'
17
17
  import { cloneRepeatField } from '../../components/repeat/helpers';
18
18
  import { assignedOrderIds } from '../../adapters/orders-adapter';
19
19
  import { type OpenmrsResource } from '@openmrs/esm-framework';
20
+ import { assignedDiagnosesIds } from '../../adapters/encounter-diagnosis-adapter';
20
21
 
21
22
  export function prepareEncounter(
22
23
  context: FormContextProps,
@@ -25,10 +26,13 @@ export function prepareEncounter(
25
26
  encounterProvider: string,
26
27
  location: string,
27
28
  ) {
28
- const { patient, formJson, domainObjectValue: encounter, formFields, visit } = context;
29
+ const { patient, formJson, domainObjectValue: encounter, formFields, visit, deletedFields } = context;
30
+ const allFormFields = [...formFields, ...deletedFields];
29
31
  const obsForSubmission = [];
30
- prepareObs(obsForSubmission, formFields);
31
- const ordersForSubmission = prepareOrders(formFields);
32
+ prepareObs(obsForSubmission, allFormFields);
33
+ const ordersForSubmission = prepareOrders(allFormFields);
34
+ const diagnosesForSubmission = prepareDiagnosis(allFormFields);
35
+
32
36
  let encounterForSubmission: OpenmrsEncounter = {};
33
37
 
34
38
  if (encounter) {
@@ -58,6 +62,7 @@ export function prepareEncounter(
58
62
  }
59
63
  encounterForSubmission.obs = obsForSubmission;
60
64
  encounterForSubmission.orders = ordersForSubmission;
65
+ encounterForSubmission.diagnoses = diagnosesForSubmission;
61
66
  } else {
62
67
  encounterForSubmission = {
63
68
  patient: patient.id,
@@ -76,6 +81,7 @@ export function prepareEncounter(
76
81
  },
77
82
  visit: visit?.uuid,
78
83
  orders: ordersForSubmission,
84
+ diagnoses: diagnosesForSubmission,
79
85
  };
80
86
  }
81
87
  return encounterForSubmission;
@@ -313,6 +319,33 @@ export async function hydrateRepeatField(
313
319
  }),
314
320
  );
315
321
  }
322
+
323
+ const unMappedDiagnoses = encounter.diagnoses.filter((diagnosis) => {
324
+ return (
325
+ !diagnosis.voided &&
326
+ !assignedDiagnosesIds.includes(diagnosis?.diagnosis?.coded.uuid) &&
327
+ diagnosis.formFieldPath.startsWith(`rfe-forms-${field.id}_`)
328
+ );
329
+ });
330
+
331
+ if (field.type === 'diagnosis') {
332
+ return Promise.all(
333
+ unMappedDiagnoses.map(async (diagnosis) => {
334
+ const idSuffix = parseInt(diagnosis.formFieldPath.split('_')[1]);
335
+ const clone = cloneRepeatField(field, diagnosis, idSuffix);
336
+ initialValues[clone.id] = await formFieldAdapters[field.type].getInitialValue(
337
+ clone,
338
+ { diagnoses: [diagnosis] } as any,
339
+ context,
340
+ );
341
+ if (!assignedDiagnosesIds.includes(diagnosis.diagnosis.coded.uuid)) {
342
+ assignedDiagnosesIds.push(diagnosis.diagnosis.coded.uuid);
343
+ }
344
+
345
+ return clone;
346
+ }),
347
+ );
348
+ }
316
349
  // handle obs groups
317
350
  return Promise.all(
318
351
  unMappedGroups.map(async (group) => {
@@ -331,3 +364,12 @@ export async function hydrateRepeatField(
331
364
  }),
332
365
  ).then((results) => results.flat());
333
366
  }
367
+
368
+ function prepareDiagnosis(fields: FormField[]) {
369
+ const diagnoses = fields
370
+ .filter((field) => field.type === 'diagnosis' && hasSubmission(field))
371
+ .map((field) => field.meta.submission.newValue || field.meta.submission.voidedValue)
372
+ .filter((o) => o);
373
+
374
+ return diagnoses;
375
+ }
@@ -7,6 +7,7 @@ export interface FormContextProps extends FormProcessorContextProps {
7
7
  methods: UseFormReturn<any>;
8
8
  workspaceLayout: 'minimized' | 'maximized';
9
9
  isSubmitting?: boolean;
10
+ deletedFields: FormField[];
10
11
  getFormField?: (field: string) => FormField;
11
12
  addFormField?: (field: FormField) => void;
12
13
  updateFormField?: (field: FormField) => void;
@@ -15,6 +16,7 @@ export interface FormContextProps extends FormProcessorContextProps {
15
16
  removeInvalidField?: (fieldId: string) => void;
16
17
  setInvalidFields?: (fields: FormField[]) => void;
17
18
  setForm?: (formJson: FormSchema) => void;
19
+ setDeletedFields?: (fields: FormField[]) => void;
18
20
  }
19
21
 
20
22
  export interface FormProviderProps extends FormContextProps {
@@ -10,6 +10,7 @@ import { ObsCommentAdapter } from '../../adapters/obs-comment-adapter';
10
10
  import { OrdersAdapter } from '../../adapters/orders-adapter';
11
11
  import { PatientIdentifierAdapter } from '../../adapters/patient-identifier-adapter';
12
12
  import { ProgramStateAdapter } from '../../adapters/program-state-adapter';
13
+ import { EncounterDiagnosisAdapter } from '../../adapters/encounter-diagnosis-adapter';
13
14
  import { type FormFieldValueAdapter } from '../../types';
14
15
 
15
16
  export const inbuiltFieldValueAdapters: RegistryItem<FormFieldValueAdapter>[] = [
@@ -61,4 +62,8 @@ export const inbuiltFieldValueAdapters: RegistryItem<FormFieldValueAdapter>[] =
61
62
  type: 'patientIdentifier',
62
63
  component: PatientIdentifierAdapter,
63
64
  },
65
+ {
66
+ type: 'diagnosis',
67
+ component: EncounterDiagnosisAdapter,
68
+ },
64
69
  ];
@@ -148,6 +148,9 @@ function transformByType(question: FormField) {
148
148
  ? 'date'
149
149
  : question.questionOptions.rendering;
150
150
  break;
151
+ case 'diagnosis':
152
+ handleDiagnosis(question);
153
+ break;
151
154
  }
152
155
  }
153
156
 
@@ -276,3 +279,32 @@ function handleQuestionsWithObsComments(sectionQuestions: Array<FormField>): Arr
276
279
 
277
280
  return augmentedQuestions;
278
281
  }
282
+
283
+ function handleDiagnosis(question: FormField) {
284
+ if (
285
+ ('dataSource' in question.questionOptions && question.questionOptions['dataSource'] === 'diagnoses') ||
286
+ question.type === 'diagnosis'
287
+ ) {
288
+ question.questionOptions.datasource = {
289
+ name: 'problem_datasource',
290
+ config: {
291
+ class: question.questionOptions.diagnosis?.conceptClasses,
292
+ },
293
+ };
294
+ if (question.questionOptions.diagnosis?.conceptSet) {
295
+ question.questionOptions = {
296
+ ...question.questionOptions,
297
+ concept: question.questionOptions.diagnosis?.conceptSet,
298
+ datasource: {
299
+ name: 'problem_datasource',
300
+ config: {
301
+ useSetMembersByConcept: true,
302
+ },
303
+ },
304
+ };
305
+ }
306
+ question.questionOptions.isSearchable = true;
307
+
308
+ delete question.questionOptions['dataSource'];
309
+ }
310
+ }