@openmrs/esm-form-engine-lib 3.0.1 → 3.1.0-pre.1684
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/__mocks__/forms/rfe-forms/diagnosis-test-form.json +60 -0
- package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +40 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +1 -1
- package/src/adapters/encounter-diagnosis-adapter.test.ts +232 -0
- package/src/adapters/encounter-diagnosis-adapter.ts +116 -0
- package/src/adapters/obs-adapter.test.ts +2 -0
- package/src/adapters/program-state-adapter.test.ts +2 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +0 -7
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +14 -5
- package/src/components/renderer/form/form-renderer.component.tsx +5 -2
- package/src/components/renderer/form/state.ts +6 -1
- package/src/components/repeat/repeat.component.tsx +4 -0
- package/src/constants.ts +1 -0
- package/src/form-engine.test.tsx +170 -3
- package/src/hooks/useConcepts.ts +10 -11
- package/src/hooks/useFormStateHelpers.ts +5 -0
- package/src/hooks/usePatientPrograms.ts +27 -31
- package/src/hooks/useRestApiMaxResults.ts +35 -0
- package/src/processors/encounter/encounter-form-processor.ts +32 -25
- package/src/processors/encounter/encounter-processor-helper.ts +45 -3
- package/src/provider/form-provider.tsx +2 -0
- package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +5 -0
- package/src/transformers/default-schema-transformer.ts +32 -0
- package/src/types/domain.ts +36 -0
- package/src/types/schema.ts +6 -0
@@ -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';
|
package/src/form-engine.test.tsx
CHANGED
@@ -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
|
-
|
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
|
);
|
package/src/hooks/useConcepts.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import { useMemo } from 'react';
|
2
2
|
import useSWRInfinite from 'swr/infinite';
|
3
|
-
import { type FetchResponse, type OpenmrsResource,
|
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
|
16
|
+
const { maxResults } = useRestApiMaxResults();
|
16
17
|
const totalCount = references.size;
|
17
|
-
const totalPages = Math.ceil(totalCount /
|
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
|
-
|
25
|
-
|
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(
|
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 {
|
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
|
-
|
7
|
-
const
|
8
|
-
const
|
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
|
-
|
12
|
-
|
9
|
+
const { data, error, isLoading } = useSWR<{ data: ProgramsFetchResponse }, Error>(
|
10
|
+
patientUuid ? apiUrl : null,
|
11
|
+
openmrsFetch,
|
12
|
+
);
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
33
|
-
abortController.abort();
|
34
|
-
};
|
35
|
-
}, [formJson]);
|
19
|
+
const activePrograms = sortedEnrollments?.filter((enrollment) => !enrollment.dateCompleted);
|
36
20
|
|
37
21
|
return {
|
38
|
-
|
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 {
|
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 {
|
27
|
-
|
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 {
|
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 {
|
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
|
169
|
-
|
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:
|
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,
|
31
|
-
const ordersForSubmission = prepareOrders(
|
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
|
+
}
|