@openmrs/esm-form-engine-lib 3.0.0 → 3.0.1
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/README.md +65 -30
- package/__mocks__/forms/rfe-forms/obs-group-test_form.json +188 -124
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +5 -4
- package/src/components/group/obs-group.component.tsx +5 -5
- package/src/components/inputs/content-switcher/content-switcher.component.tsx +6 -1
- package/src/components/inputs/date/date.component.tsx +1 -1
- package/src/components/inputs/multi-select/multi-select.component.tsx +4 -4
- package/src/components/inputs/number/number.component.tsx +2 -2
- package/src/components/inputs/radio/radio.component.tsx +1 -1
- package/src/components/inputs/select/dropdown.component.tsx +2 -2
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +39 -18
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +0 -1
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +3 -1
- package/src/components/sidebar/sidebar.component.tsx +5 -6
- package/src/components/sidebar/sidebar.scss +5 -0
- package/src/components/value/value.component.tsx +4 -1
- package/src/form-engine.component.tsx +17 -13
- package/src/form-engine.scss +8 -0
- package/src/form-engine.test.tsx +44 -1
- package/src/hooks/{useConcepts.tsx → useConcepts.ts} +1 -2
- package/src/hooks/useDataSourceDependentValue.ts +1 -1
- package/src/hooks/{useEncounter.tsx → useEncounter.ts} +9 -1
- package/src/hooks/{useFormJson.tsx → useFormJson.ts} +12 -2
- package/src/hooks/{usePatientData.tsx → usePatientData.ts} +1 -1
- package/src/hooks/usePatientPrograms.ts +13 -3
- package/src/hooks/useProcessorDependencies.ts +14 -4
- package/src/types/schema.ts +5 -0
- package/src/utils/common-expression-helpers.test.ts +1 -1
- package/src/utils/common-expression-helpers.ts +10 -7
- package/translations/en.json +3 -3
- package/src/hooks/useClobData.tsx +0 -21
- package/src/hooks/useFieldValidationResults.ts +0 -18
- package/src/hooks/useFormsConfig.tsx +0 -27
- package/src/hooks/useRestMaxResultsCount.ts +0 -5
- package/src/hooks/useSystemSetting.ts +0 -36
- package/src/hooks/{useEncounterRole.tsx → useEncounterRole.ts} +1 -1
- package/src/hooks/{useFormCollapse.tsx → useFormCollapse.ts} +1 -1
@@ -60,7 +60,12 @@ const ContentSwitcher: React.FC<FormFieldInputProps> = ({ field, value, errors,
|
|
60
60
|
className={styles.selectedOption}
|
61
61
|
size="md">
|
62
62
|
{field.questionOptions.answers.map((option, index) => (
|
63
|
-
<Switch
|
63
|
+
<Switch
|
64
|
+
name={option.concept || option.value}
|
65
|
+
text={t(option.label)}
|
66
|
+
key={index}
|
67
|
+
disabled={field.isDisabled}
|
68
|
+
/>
|
64
69
|
))}
|
65
70
|
</CdsContentSwitcher>
|
66
71
|
</FormGroup>
|
@@ -113,7 +113,7 @@ const DateField: React.FC<FormFieldInputProps> = ({ field, value: dateValue, err
|
|
113
113
|
id={field.id}
|
114
114
|
labelText={timePickerLabel}
|
115
115
|
placeholder="HH:MM"
|
116
|
-
pattern="(1[012]|[1-9]):[0-5][0-9]
|
116
|
+
pattern="(1[012]|[1-9]):[0-5][0-9]$"
|
117
117
|
type="time"
|
118
118
|
disabled={field.datePickerFormat === 'timer' ? field.isDisabled : !dateValue ? true : false}
|
119
119
|
invalid={errors.length > 0}
|
@@ -22,7 +22,7 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
22
22
|
.map((answer, index) => ({
|
23
23
|
id: `${field.id}-${answer.concept}`,
|
24
24
|
concept: answer.concept,
|
25
|
-
label: answer.label,
|
25
|
+
label: t(answer.label),
|
26
26
|
key: index,
|
27
27
|
disabled: answer.disable?.isDisabled,
|
28
28
|
readonly: isTrue(field.readonly),
|
@@ -97,14 +97,14 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
97
97
|
<Layer>
|
98
98
|
{isSearchable ? (
|
99
99
|
<FilterableMultiSelect
|
100
|
+
id={field.id}
|
101
|
+
key={field.id}
|
100
102
|
placeholder={t('search', 'Search') + '...'}
|
101
103
|
onChange={handleSelectItemsChange}
|
102
|
-
id={t(field.label)}
|
103
104
|
items={selectOptions}
|
104
105
|
initialSelectedItems={initiallySelectedQuestionItems}
|
105
106
|
label={''}
|
106
107
|
titleText={label}
|
107
|
-
key={field.id}
|
108
108
|
itemToString={(item) => (item ? item.label : ' ')}
|
109
109
|
disabled={field.isDisabled}
|
110
110
|
invalid={errors.length > 0}
|
@@ -120,7 +120,7 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
120
120
|
<Checkbox
|
121
121
|
key={`${field.id}-${value.concept}`}
|
122
122
|
className={styles.checkbox}
|
123
|
-
labelText={value.label}
|
123
|
+
labelText={t(value.label)}
|
124
124
|
id={`${field.id}-${value.concept}`}
|
125
125
|
onChange={() => {
|
126
126
|
handleSelectCheckbox(value);
|
@@ -70,14 +70,14 @@ const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
70
70
|
onBlur={onBlur}
|
71
71
|
allowEmpty={true}
|
72
72
|
size="lg"
|
73
|
-
hideSteppers={
|
73
|
+
hideSteppers={field.hideSteppers ?? false}
|
74
74
|
onWheel={(e) => e.target.blur()}
|
75
75
|
disabled={field.isDisabled}
|
76
76
|
readOnly={isTrue(field.readonly)}
|
77
77
|
className={classNames(styles.controlWidthConstrained, styles.boldedLabel)}
|
78
78
|
warn={warnings.length > 0}
|
79
79
|
warnText={warnings[0]?.message}
|
80
|
-
step={0.01}
|
80
|
+
step={field.questionOptions.step ?? 0.01}
|
81
81
|
/>
|
82
82
|
</Layer>
|
83
83
|
)
|
@@ -50,7 +50,7 @@ const Radio: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings,
|
|
50
50
|
return (
|
51
51
|
<RadioButton
|
52
52
|
id={`${field.id}-${answer.label}`}
|
53
|
-
labelText={answer.label ?? ''}
|
53
|
+
labelText={t(answer.label) ?? ''}
|
54
54
|
value={answer.concept}
|
55
55
|
key={index}
|
56
56
|
onClick={(e) => {
|
@@ -27,9 +27,9 @@ const Dropdown: React.FC<FormFieldInputProps> = ({ field, value, errors, warning
|
|
27
27
|
let answer = field.questionOptions.answers.find((opt) => {
|
28
28
|
return opt.value ? opt.value == item : opt.concept == item;
|
29
29
|
});
|
30
|
-
return answer
|
30
|
+
return answer ? t(answer.label) : '';
|
31
31
|
},
|
32
|
-
[field.questionOptions.answers],
|
32
|
+
[field.questionOptions.answers, t],
|
33
33
|
);
|
34
34
|
|
35
35
|
const items = useMemo(() => {
|
@@ -1,21 +1,21 @@
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2
2
|
import { debounce } from 'lodash-es';
|
3
3
|
import { ComboBox, DropdownSkeleton, Layer, InlineLoading } from '@carbon/react';
|
4
|
-
import { isTrue } from '../../../utils/boolean-utils';
|
5
4
|
import { useTranslation } from 'react-i18next';
|
6
|
-
import {
|
5
|
+
import { useWatch } from 'react-hook-form';
|
6
|
+
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
7
7
|
import { getControlTemplate } from '../../../registry/inbuilt-components/control-templates';
|
8
|
-
import {
|
8
|
+
import { getRegisteredDataSource } from '../../../registry/registry';
|
9
9
|
import { isEmpty } from '../../../validators/form-validator';
|
10
|
+
import { isTrue } from '../../../utils/boolean-utils';
|
11
|
+
import { isViewMode } from '../../../utils/common-utils';
|
10
12
|
import { shouldUseInlineLayout } from '../../../utils/form-helper';
|
11
|
-
import
|
12
|
-
import styles from './ui-select-extended.scss';
|
13
|
+
import { type DataSource, type FormFieldInputProps } from '../../../types';
|
13
14
|
import { useFormProviderContext } from '../../../provider/form-provider';
|
14
|
-
import FieldLabel from '../../field-label/field-label.component';
|
15
|
-
import { useWatch } from 'react-hook-form';
|
16
15
|
import useDataSourceDependentValue from '../../../hooks/useDataSourceDependentValue';
|
17
|
-
import
|
18
|
-
import
|
16
|
+
import FieldLabel from '../../field-label/field-label.component';
|
17
|
+
import FieldValueView from '../../value/view/field-value-view.component';
|
18
|
+
import styles from './ui-select-extended.scss';
|
19
19
|
|
20
20
|
const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnings, setFieldValue }) => {
|
21
21
|
const { t } = useTranslation();
|
@@ -49,6 +49,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
49
49
|
|
50
50
|
const debouncedSearch = debounce((searchTerm: string, dataSource: DataSource<OpenmrsResource>) => {
|
51
51
|
setIsSearching(true);
|
52
|
+
|
52
53
|
dataSource
|
53
54
|
.fetchData(searchTerm, config)
|
54
55
|
.then((dataItems) => {
|
@@ -86,22 +87,33 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
86
87
|
}, [field.questionOptions?.datasource]);
|
87
88
|
|
88
89
|
useEffect(() => {
|
90
|
+
let ignore = false;
|
91
|
+
|
89
92
|
// If not searchable, preload the items
|
90
93
|
if (dataSource && !isTrue(field.questionOptions.isSearchable)) {
|
91
94
|
setItems([]);
|
92
95
|
setIsLoading(true);
|
96
|
+
|
93
97
|
dataSource
|
94
98
|
.fetchData(null, { ...config, referencedValue: dataSourceDependentValue })
|
95
99
|
.then((dataItems) => {
|
96
|
-
|
97
|
-
|
100
|
+
if (!ignore) {
|
101
|
+
setItems(dataItems.map(dataSource.toUuidAndDisplay));
|
102
|
+
setIsLoading(false);
|
103
|
+
}
|
98
104
|
})
|
99
105
|
.catch((err) => {
|
100
|
-
|
101
|
-
|
102
|
-
|
106
|
+
if (!ignore) {
|
107
|
+
console.error(err);
|
108
|
+
setIsLoading(false);
|
109
|
+
setItems([]);
|
110
|
+
}
|
103
111
|
});
|
104
112
|
}
|
113
|
+
|
114
|
+
return () => {
|
115
|
+
ignore = true;
|
116
|
+
};
|
105
117
|
}, [dataSource, config, dataSourceDependentValue]);
|
106
118
|
|
107
119
|
useEffect(() => {
|
@@ -111,19 +123,28 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
111
123
|
}, [dataSource, searchTerm, config]);
|
112
124
|
|
113
125
|
useEffect(() => {
|
126
|
+
let ignore = false;
|
114
127
|
if (value && !isDirty && dataSource && isSearchable && sessionMode !== 'enter' && !items.length) {
|
115
128
|
// While in edit mode, search-based instances should fetch the initial item (previously selected value) to resolve its display property
|
116
129
|
setIsLoading(true);
|
117
130
|
try {
|
118
131
|
dataSource.fetchSingleItem(value).then((item) => {
|
119
|
-
|
120
|
-
|
132
|
+
if (!ignore) {
|
133
|
+
setItems([dataSource.toUuidAndDisplay(item)]);
|
134
|
+
setIsLoading(false);
|
135
|
+
}
|
121
136
|
});
|
122
137
|
} catch (error) {
|
123
|
-
|
124
|
-
|
138
|
+
if (!ignore) {
|
139
|
+
console.error(error);
|
140
|
+
setIsLoading(false);
|
141
|
+
}
|
125
142
|
}
|
126
143
|
}
|
144
|
+
|
145
|
+
return () => {
|
146
|
+
ignore = true;
|
147
|
+
};
|
127
148
|
}, [value, isDirty, sessionMode, dataSource, isSearchable, items]);
|
128
149
|
|
129
150
|
if (isLoading) {
|
@@ -14,7 +14,6 @@ const mockUsePatient = jest.mocked(usePatient);
|
|
14
14
|
const mockUseSession = jest.mocked(useSession);
|
15
15
|
global.ResizeObserver = require('resize-observer-polyfill');
|
16
16
|
|
17
|
-
jest.mock('../../../hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
|
18
17
|
jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
|
19
18
|
|
20
19
|
jest.mock('lodash-es', () => ({
|
@@ -28,7 +28,9 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
28
28
|
<div>
|
29
29
|
<div className={styles.label}>{t(field.label)}</div>
|
30
30
|
<div className={styles.workspaceButton}>
|
31
|
-
<Button disabled={isTrue(field.readonly)}
|
31
|
+
<Button disabled={isTrue(field.readonly)} onClick={handleLaunchWorkspace}>
|
32
|
+
{t(field.questionOptions?.buttonLabel) ?? t('launchWorkspace', 'Launch Workspace')}
|
33
|
+
</Button>
|
32
34
|
</div>
|
33
35
|
</div>
|
34
36
|
)
|
@@ -1,13 +1,12 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import { useTranslation } from 'react-i18next';
|
4
|
-
import { Button } from '@carbon/react';
|
5
|
-
import { type FormPage, type SessionMode } from '../../types';
|
6
|
-
import styles from './sidebar.scss';
|
7
|
-
import { usePageObserver } from './usePageObserver';
|
8
|
-
import { useCurrentActivePage } from './useCurrentActivePage';
|
4
|
+
import { Button, InlineLoading } from '@carbon/react';
|
9
5
|
import { isPageContentVisible } from '../../utils/form-helper';
|
10
|
-
import {
|
6
|
+
import { useCurrentActivePage } from './useCurrentActivePage';
|
7
|
+
import { usePageObserver } from './usePageObserver';
|
8
|
+
import type { FormPage, SessionMode } from '../../types';
|
9
|
+
import styles from './sidebar.scss';
|
11
10
|
|
12
11
|
interface SidebarProps {
|
13
12
|
defaultPage: string;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
@use '@carbon/colors';
|
2
|
+
@use '@carbon/layout';
|
2
3
|
@use '@carbon/type';
|
3
4
|
|
4
5
|
.pageLink {
|
@@ -95,6 +96,10 @@
|
|
95
96
|
.saveButton {
|
96
97
|
@extend .button;
|
97
98
|
margin-bottom: 0.625rem;
|
99
|
+
|
100
|
+
&:global(.cds--inline-loading) {
|
101
|
+
min-height: layout.$spacing-05;
|
102
|
+
}
|
98
103
|
}
|
99
104
|
|
100
105
|
.closeButton {
|
@@ -1,10 +1,13 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import styles from './value.scss';
|
3
|
+
import { useTranslation } from 'react-i18next';
|
3
4
|
|
4
5
|
export const ValueEmpty = () => {
|
6
|
+
const { t } = useTranslation();
|
7
|
+
|
5
8
|
return (
|
6
9
|
<div>
|
7
|
-
<span className={styles.empty}>(Blank)</span>
|
10
|
+
<span className={styles.empty}>({t('blank', 'Blank')})</span>
|
8
11
|
</div>
|
9
12
|
);
|
10
13
|
};
|
@@ -1,24 +1,24 @@
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2
|
-
import type { FormField, FormSchema, SessionMode } from './types';
|
3
|
-
import { useSession, type Visit } from '@openmrs/esm-framework';
|
4
|
-
import { isEmpty, useFormJson } from '.';
|
5
|
-
import FormProcessorFactory from './components/processor-factory/form-processor-factory.component';
|
6
|
-
import Loader from './components/loaders/loader.component';
|
7
|
-
import { usePatientData } from './hooks/usePatientData';
|
8
|
-
import { FormFactoryProvider } from './provider/form-factory-provider';
|
9
2
|
import classNames from 'classnames';
|
10
|
-
import styles from './form-engine.scss';
|
11
3
|
import { Button, ButtonSet, InlineLoading } from '@carbon/react';
|
12
4
|
import { I18nextProvider, useTranslation } from 'react-i18next';
|
13
|
-
import
|
14
|
-
import
|
5
|
+
import { useSession, type Visit } from '@openmrs/esm-framework';
|
6
|
+
import { FormFactoryProvider } from './provider/form-factory-provider';
|
15
7
|
import { init, teardown } from './lifecycle';
|
16
|
-
import {
|
8
|
+
import { isEmpty, useFormJson } from '.';
|
17
9
|
import { moduleName } from './globals';
|
10
|
+
import { reportError } from './utils/error-utils';
|
18
11
|
import { useFormCollapse } from './hooks/useFormCollapse';
|
19
|
-
import Sidebar from './components/sidebar/sidebar.component';
|
20
12
|
import { useFormWorkspaceSize } from './hooks/useFormWorkspaceSize';
|
21
13
|
import { usePageObserver } from './components/sidebar/usePageObserver';
|
14
|
+
import { usePatientData } from './hooks/usePatientData';
|
15
|
+
import type { FormField, FormSchema, SessionMode } from './types';
|
16
|
+
import FormProcessorFactory from './components/processor-factory/form-processor-factory.component';
|
17
|
+
import Loader from './components/loaders/loader.component';
|
18
|
+
import MarkdownWrapper from './components/inputs/markdown/markdown-wrapper.component';
|
19
|
+
import PatientBanner from './components/patient-banner/patient-banner.component';
|
20
|
+
import Sidebar from './components/sidebar/sidebar.component';
|
21
|
+
import styles from './form-engine.scss';
|
22
22
|
|
23
23
|
interface FormEngineProps {
|
24
24
|
patientUUID: string;
|
@@ -176,7 +176,11 @@ const FormEngine = ({
|
|
176
176
|
}}>
|
177
177
|
{mode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
|
178
178
|
</Button>
|
179
|
-
<Button
|
179
|
+
<Button
|
180
|
+
className={styles.saveButton}
|
181
|
+
disabled={isLoadingDependencies || isSubmitting || mode === 'view'}
|
182
|
+
kind="primary"
|
183
|
+
type="submit">
|
180
184
|
{isSubmitting ? (
|
181
185
|
<InlineLoading description={t('submitting', 'Submitting') + '...'} />
|
182
186
|
) : (
|
package/src/form-engine.scss
CHANGED
package/src/form-engine.test.tsx
CHANGED
@@ -92,7 +92,6 @@ jest.mock('../src/api', () => {
|
|
92
92
|
};
|
93
93
|
});
|
94
94
|
|
95
|
-
jest.mock('./hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
|
96
95
|
jest.mock('./hooks/useEncounterRole', () => ({
|
97
96
|
useEncounterRole: jest.fn().mockReturnValue({
|
98
97
|
isLoading: false,
|
@@ -877,6 +876,50 @@ describe('Form engine component', () => {
|
|
877
876
|
});
|
878
877
|
|
879
878
|
describe('Obs group', () => {
|
879
|
+
it('should not render empty obs group', async () => {
|
880
|
+
await act(async () => {
|
881
|
+
renderForm(null, obsGroupTestForm);
|
882
|
+
});
|
883
|
+
|
884
|
+
// Check that only one obs group is initially rendered
|
885
|
+
const initialGroups = screen.getAllByRole('group', { name: /My Group|Dependents Group/i });
|
886
|
+
expect(initialGroups.length).toBe(1);
|
887
|
+
const dependentTypeRadios = screen.queryAllByRole('radio', { name: /child|spouse/i });
|
888
|
+
expect(dependentTypeRadios.length).toBe(0);
|
889
|
+
|
890
|
+
// Select "Yes" for having dependents
|
891
|
+
const yesRadio = screen.getByRole('radio', { name: /yes/i });
|
892
|
+
await user.click(yesRadio);
|
893
|
+
|
894
|
+
// Now the dependent type radios should be visible
|
895
|
+
const visibleDependentTypeRadios = screen.getAllByRole('radio', { name: /child|spouse/i });
|
896
|
+
expect(visibleDependentTypeRadios.length).toBe(2);
|
897
|
+
|
898
|
+
// Check that the group label is still hidden since it only has one visible field
|
899
|
+
const dependentsGroupResults = screen.queryAllByRole('group', { name: /Dependents Group/i });
|
900
|
+
expect(dependentsGroupResults.length).toBe(0);
|
901
|
+
|
902
|
+
// Check that dependent name and age are still hidden
|
903
|
+
const hiddenDependentNameInput = screen.queryByRole('textbox', { name: /dependent name/i });
|
904
|
+
const hiddenDependentAgeInput = screen.queryByRole('spinbutton', { name: /dependent age/i });
|
905
|
+
expect(hiddenDependentNameInput).toBeNull();
|
906
|
+
expect(hiddenDependentAgeInput).toBeNull();
|
907
|
+
|
908
|
+
// Select "Child" as dependent type
|
909
|
+
await user.click(visibleDependentTypeRadios[0]);
|
910
|
+
|
911
|
+
// Check the visibility of the group label
|
912
|
+
const dependentsGroup = screen.getAllByRole('group', { name: /Dependents Group/i })[0];
|
913
|
+
expect(dependentsGroup).toBeInTheDocument();
|
914
|
+
|
915
|
+
// Check that dependent name and age are now visible
|
916
|
+
const dependentNameInput = screen.getByRole('textbox', { name: /dependent name/i });
|
917
|
+
const dependentAgeInput = screen.getByRole('spinbutton', { name: /dependent age/i });
|
918
|
+
|
919
|
+
expect(dependentNameInput).toBeInTheDocument();
|
920
|
+
expect(dependentAgeInput).toBeInTheDocument();
|
921
|
+
});
|
922
|
+
|
880
923
|
it('should save obs group on form submission', async () => {
|
881
924
|
const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
|
882
925
|
await act(async () => {
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { useMemo } from 'react';
|
2
|
-
import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
3
2
|
import useSWRInfinite from 'swr/infinite';
|
3
|
+
import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
4
4
|
|
5
5
|
type ConceptFetchResponse = FetchResponse<{ results: Array<OpenmrsResource> }>;
|
6
6
|
|
@@ -12,7 +12,6 @@ export function useConcepts(references: Set<string>): {
|
|
12
12
|
isLoading: boolean;
|
13
13
|
error: Error | undefined;
|
14
14
|
} {
|
15
|
-
|
16
15
|
const chunkSize = 100;
|
17
16
|
const totalCount = references.size;
|
18
17
|
const totalPages = Math.ceil(totalCount / chunkSize);
|
@@ -1,6 +1,6 @@
|
|
1
|
+
import { useWatch } from 'react-hook-form';
|
1
2
|
import { type FormField } from '../types';
|
2
3
|
import { useFormProviderContext } from '../provider/form-provider';
|
3
|
-
import { useWatch } from 'react-hook-form';
|
4
4
|
|
5
5
|
const useDataSourceDependentValue = (field: FormField) => {
|
6
6
|
const dependentField = field.questionOptions['config']?.referencedField;
|
@@ -11,8 +11,12 @@ export function useEncounter(formJson: FormSchema) {
|
|
11
11
|
const [error, setError] = useState(null);
|
12
12
|
|
13
13
|
useEffect(() => {
|
14
|
+
const abortController = new AbortController();
|
15
|
+
|
14
16
|
if (!isEmpty(formJson.encounter) && isString(formJson.encounter)) {
|
15
|
-
openmrsFetch(`${restBaseUrl}/encounter/${formJson.encounter}?v=${encounterRepresentation}
|
17
|
+
openmrsFetch(`${restBaseUrl}/encounter/${formJson.encounter}?v=${encounterRepresentation}`, {
|
18
|
+
signal: abortController.signal,
|
19
|
+
})
|
16
20
|
.then((response) => {
|
17
21
|
setEncounter(response.data);
|
18
22
|
setIsLoading(false);
|
@@ -26,6 +30,10 @@ export function useEncounter(formJson: FormSchema) {
|
|
26
30
|
} else {
|
27
31
|
setIsLoading(false);
|
28
32
|
}
|
33
|
+
|
34
|
+
return () => {
|
35
|
+
abortController.abort();
|
36
|
+
};
|
29
37
|
}, [formJson.encounter]);
|
30
38
|
|
31
39
|
return { encounter: encounter, error, isLoading };
|
@@ -9,7 +9,10 @@ import { moduleName } from '../globals';
|
|
9
9
|
export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: string, formSessionIntent: string) {
|
10
10
|
const [formJson, setFormJson] = useState<FormSchema>(null);
|
11
11
|
const [error, setError] = useState(validateFormsArgs(formUuid, rawFormJson));
|
12
|
+
|
12
13
|
useEffect(() => {
|
14
|
+
const abortController = new AbortController();
|
15
|
+
|
13
16
|
const setFormJsonWithTranslations = (formJson: FormSchema) => {
|
14
17
|
if (formJson?.translations) {
|
15
18
|
const language = window.i18next.language;
|
@@ -17,14 +20,21 @@ export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: s
|
|
17
20
|
}
|
18
21
|
setFormJson(formJson);
|
19
22
|
};
|
23
|
+
|
20
24
|
loadFormJson(formUuid, rawFormJson, formSessionIntent)
|
21
25
|
.then((formJson) => {
|
22
26
|
setFormJsonWithTranslations({ ...formJson, encounter: encounterUuid });
|
23
27
|
})
|
24
28
|
.catch((error) => {
|
25
|
-
|
26
|
-
|
29
|
+
if (error.name !== 'AbortError') {
|
30
|
+
console.error(error);
|
31
|
+
setError(new Error('Error loading form JSON: ' + error.message));
|
32
|
+
}
|
27
33
|
});
|
34
|
+
|
35
|
+
return () => {
|
36
|
+
abortController.abort();
|
37
|
+
};
|
28
38
|
}, [formSessionIntent, formUuid, rawFormJson, encounterUuid]);
|
29
39
|
|
30
40
|
return {
|
@@ -24,7 +24,7 @@ const patientGenderMap = {
|
|
24
24
|
export const usePatientData = (patientUuid) => {
|
25
25
|
const { patient, isLoading: isLoadingPatient, error: patientError } = usePatient(patientUuid);
|
26
26
|
if (patient && !isLoadingPatient) {
|
27
|
-
// This is
|
27
|
+
// This is for backward compatibility with the Angular form engine
|
28
28
|
patient['age'] = calculateAge(new Date(patient?.birthDate));
|
29
29
|
patient['sex'] = patientGenderMap[patient.gender] ?? 'U';
|
30
30
|
}
|
@@ -9,19 +9,29 @@ export const usePatientPrograms = (patientUuid: string, formJson: FormSchema) =>
|
|
9
9
|
const [error, setError] = useState(null);
|
10
10
|
|
11
11
|
useEffect(() => {
|
12
|
+
const abortController = new AbortController();
|
13
|
+
|
12
14
|
if (formJson.meta?.programs?.hasProgramFields) {
|
13
|
-
openmrsFetch(`${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}
|
15
|
+
openmrsFetch(`${restBaseUrl}/programenrollment?patient=${patientUuid}&v=${customRepresentation}`, {
|
16
|
+
signal: abortController.signal,
|
17
|
+
})
|
14
18
|
.then((response) => {
|
15
19
|
setPatientPrograms(response.data.results.filter((enrollment) => enrollment.dateCompleted === null));
|
16
20
|
setIsLoading(false);
|
17
21
|
})
|
18
22
|
.catch((error) => {
|
19
|
-
|
20
|
-
|
23
|
+
if (error.name !== 'AbortError') {
|
24
|
+
setError(error);
|
25
|
+
setIsLoading(false);
|
26
|
+
}
|
21
27
|
});
|
22
28
|
} else {
|
23
29
|
setIsLoading(false);
|
24
30
|
}
|
31
|
+
|
32
|
+
return () => {
|
33
|
+
abortController.abort();
|
34
|
+
};
|
25
35
|
}, [formJson]);
|
26
36
|
|
27
37
|
return {
|
@@ -13,17 +13,27 @@ const useProcessorDependencies = (
|
|
13
13
|
const { loadDependencies } = formProcessor;
|
14
14
|
|
15
15
|
useEffect(() => {
|
16
|
+
let ignore = false;
|
17
|
+
|
16
18
|
if (loadDependencies) {
|
17
19
|
setIsLoading(true);
|
18
20
|
loadDependencies(context, setContext)
|
19
|
-
.then((
|
20
|
-
|
21
|
+
.then(() => {
|
22
|
+
if (!ignore) {
|
23
|
+
setIsLoading(false);
|
24
|
+
}
|
21
25
|
})
|
22
26
|
.catch((error) => {
|
23
|
-
|
24
|
-
|
27
|
+
if (!ignore) {
|
28
|
+
setError(error);
|
29
|
+
reportError(error, 'Encountered error while loading dependencies');
|
30
|
+
}
|
25
31
|
});
|
26
32
|
}
|
33
|
+
|
34
|
+
return () => {
|
35
|
+
ignore = true;
|
36
|
+
};
|
27
37
|
}, [loadDependencies]);
|
28
38
|
|
29
39
|
return { isLoading, error };
|
package/src/types/schema.ts
CHANGED
@@ -84,6 +84,7 @@ export interface FormField {
|
|
84
84
|
questionInfo?: string;
|
85
85
|
historicalExpression?: string;
|
86
86
|
constrainMaxWidth?: boolean;
|
87
|
+
hideSteppers?: boolean;
|
87
88
|
/** @deprecated */
|
88
89
|
inlineMultiCheckbox?: boolean;
|
89
90
|
meta?: QuestionMetaProps;
|
@@ -143,6 +144,10 @@ export interface FormQuestionOptions {
|
|
143
144
|
*/
|
144
145
|
max?: string;
|
145
146
|
min?: string;
|
147
|
+
/**
|
148
|
+
* specifies the increment or decrement step for number field values
|
149
|
+
*/
|
150
|
+
step?: number;
|
146
151
|
/**
|
147
152
|
* maxLength and maxLength are used to validate text field length
|
148
153
|
*/
|
@@ -171,7 +171,7 @@ describe('CommonExpressionHelpers', () => {
|
|
171
171
|
it('should return the correct number of months on ART', () => {
|
172
172
|
const artStartDate = new Date('2020-01-01');
|
173
173
|
const today = new Date();
|
174
|
-
const monthsOnART =
|
174
|
+
const monthsOnART = dayjs(today).diff(dayjs(artStartDate), 'months');
|
175
175
|
expect(helpers.calcMonthsOnART(artStartDate)).toBe(monthsOnART);
|
176
176
|
});
|
177
177
|
|
@@ -118,19 +118,22 @@ export class CommonExpressionHelpers {
|
|
118
118
|
};
|
119
119
|
|
120
120
|
calcMonthsOnART = (artStartDate: Date) => {
|
121
|
-
if (artStartDate == null)
|
121
|
+
if (artStartDate == null) {
|
122
|
+
return null;
|
123
|
+
}
|
122
124
|
|
123
125
|
if (!(artStartDate instanceof Date)) {
|
124
126
|
throw new Error('DateFormatException: value passed is not a valid date');
|
125
127
|
}
|
126
128
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
if (
|
131
|
-
|
129
|
+
const today = new Date();
|
130
|
+
const artInDays = Math.round((today.getTime() - artStartDate.getTime()) / 86400000);
|
131
|
+
|
132
|
+
if (artInDays < 30) {
|
133
|
+
return 0;
|
132
134
|
}
|
133
|
-
|
135
|
+
|
136
|
+
return dayjs(today).diff(artStartDate, 'month');
|
134
137
|
};
|
135
138
|
|
136
139
|
calcViralLoadStatus = (viralLoadCount: number) => {
|