@openmrs/esm-form-engine-lib 2.1.0-pre.1362
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/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +58 -0
- package/.husky/pre-commit +6 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +4 -0
- package/LICENSE.txt +401 -0
- package/README.md +136 -0
- package/__mocks__/concepts.mock.json +140 -0
- package/__mocks__/forms/afe-forms/component_art.json +38 -0
- package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
- package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
- package/__mocks__/forms/afe-forms/form-component.json +38 -0
- package/__mocks__/forms/afe-forms/mini-form.json +31 -0
- package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
- package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
- package/__mocks__/forms/afe-forms/test-orders.json +72 -0
- package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
- package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
- package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
- package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
- package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
- package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
- package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
- package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
- package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
- package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
- package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
- package/__mocks__/forms/rfe-forms/form-component.json +43 -0
- package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
- package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
- package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
- package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
- package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
- package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
- package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
- package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
- package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
- package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
- package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
- package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
- package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
- package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
- package/__mocks__/forms/rfe-forms/required-form.json +50 -0
- package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
- package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
- package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
- package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
- package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
- package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
- package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
- package/__mocks__/packages/test-forms-registry.ts +12 -0
- package/__mocks__/patient.mock.ts +173 -0
- package/__mocks__/react-i18next.js +49 -0
- package/__mocks__/react-markdown.tsx +5 -0
- package/__mocks__/session.mock.ts +117 -0
- package/__mocks__/single-spa-react.js +11 -0
- package/__mocks__/use-initial-values/encounter.mock.json +963 -0
- package/__mocks__/use-initial-values/patient.mock.json +73 -0
- package/__mocks__/visit.mock.ts +19 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -0
- package/jest.config.js +30 -0
- package/package.json +104 -0
- package/prettier.config.js +8 -0
- package/readme/form-engine.jpeg +0 -0
- package/src/adapters/control-adapter.ts +29 -0
- package/src/adapters/encounter-datetime-adapter.ts +38 -0
- package/src/adapters/encounter-location-adapter.ts +39 -0
- package/src/adapters/encounter-provider-adapter.ts +48 -0
- package/src/adapters/encounter-role-adapter.ts +54 -0
- package/src/adapters/inline-date-adapter.ts +58 -0
- package/src/adapters/obs-adapter.ts +280 -0
- package/src/adapters/obs-comment-adapter.ts +60 -0
- package/src/adapters/orders-adapter.ts +75 -0
- package/src/adapters/patient-identifier-adapter.ts +40 -0
- package/src/adapters/program-state-adapter.ts +52 -0
- package/src/api/index.ts +178 -0
- package/src/components/error/error-modal.component.tsx +37 -0
- package/src/components/error/error.scss +4 -0
- package/src/components/extension/extension-parcel.component.tsx +32 -0
- package/src/components/field-label/field-label.component.tsx +32 -0
- package/src/components/field-label/field-label.scss +11 -0
- package/src/components/group/obs-group.component.tsx +29 -0
- package/src/components/group/obs-group.scss +12 -0
- package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
- package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
- package/src/components/inputs/date/date.component.tsx +149 -0
- package/src/components/inputs/date/date.scss +36 -0
- package/src/components/inputs/file/camera/camera.component.tsx +34 -0
- package/src/components/inputs/file/camera/camera.scss +3 -0
- package/src/components/inputs/file/file.component.tsx +159 -0
- package/src/components/inputs/file/file.scss +101 -0
- package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
- package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
- package/src/components/inputs/markdown/markdown.component.tsx +8 -0
- package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
- package/src/components/inputs/multi-select/multi-select.scss +25 -0
- package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
- package/src/components/inputs/number/number.component.tsx +69 -0
- package/src/components/inputs/number/number.scss +15 -0
- package/src/components/inputs/radio/radio.component.tsx +79 -0
- package/src/components/inputs/radio/radio.scss +36 -0
- package/src/components/inputs/select/dropdown.component.tsx +73 -0
- package/src/components/inputs/select/dropdown.scss +11 -0
- package/src/components/inputs/select/dropdown.test.tsx +120 -0
- package/src/components/inputs/text/text.component.tsx +65 -0
- package/src/components/inputs/text/text.scss +15 -0
- package/src/components/inputs/text/text.test.tsx +104 -0
- package/src/components/inputs/text-area/text-area.component.tsx +63 -0
- package/src/components/inputs/text-area/text-area.scss +11 -0
- package/src/components/inputs/toggle/toggle.component.tsx +66 -0
- package/src/components/inputs/toggle/toggle.scss +12 -0
- package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
- package/src/components/inputs/tooltip/tooltip.scss +8 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
- package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
- package/src/components/inputs/unspecified/unspecified.scss +7 -0
- package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
- package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
- package/src/components/label/label.component.tsx +20 -0
- package/src/components/label/label.scss +11 -0
- package/src/components/loaders/loader.component.tsx +16 -0
- package/src/components/loaders/loader.scss +20 -0
- package/src/components/patient-banner/patient-banner.component.tsx +20 -0
- package/src/components/patient-banner/patient-banner.scss +12 -0
- package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
- package/src/components/previous-value-review/previous-value-review.scss +36 -0
- package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
- package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
- package/src/components/renderer/field/fieldLogic.ts +214 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
- package/src/components/renderer/field/form-field-renderer.scss +5 -0
- package/src/components/renderer/form/form-renderer.component.tsx +89 -0
- package/src/components/renderer/form/state.ts +54 -0
- package/src/components/renderer/page/page.renderer.component.tsx +50 -0
- package/src/components/renderer/page/page.renderer.scss +36 -0
- package/src/components/renderer/section/section-renderer.component.tsx +21 -0
- package/src/components/renderer/section/section-renderer.scss +19 -0
- package/src/components/repeat/helpers.test.ts +29 -0
- package/src/components/repeat/helpers.ts +68 -0
- package/src/components/repeat/repeat-controls.component.tsx +38 -0
- package/src/components/repeat/repeat-controls.scss +7 -0
- package/src/components/repeat/repeat.component.tsx +201 -0
- package/src/components/repeat/repeat.scss +30 -0
- package/src/components/repeat/repeat.test.ts +29 -0
- package/src/components/sidebar/sidebar.component.tsx +134 -0
- package/src/components/sidebar/sidebar.scss +121 -0
- package/src/components/value/value.component.tsx +27 -0
- package/src/components/value/value.scss +17 -0
- package/src/components/value/view/field-value-view.component.tsx +33 -0
- package/src/components/value/view/field-value-view.scss +31 -0
- package/src/constants.ts +12 -0
- package/src/datasources/concept-data-source.ts +42 -0
- package/src/datasources/data-source.ts +23 -0
- package/src/datasources/encounter-role-datasource.ts +15 -0
- package/src/datasources/historical-data-source.ts +11 -0
- package/src/datasources/location-data-source.ts +27 -0
- package/src/datasources/provider-datasource.ts +15 -0
- package/src/datasources/select-concept-answers-datasource.ts +15 -0
- package/src/declarations.d.ts +4 -0
- package/src/external-function-context.tsx +8 -0
- package/src/form-context.tsx +42 -0
- package/src/form-engine.component.tsx +178 -0
- package/src/form-engine.scss +140 -0
- package/src/form-engine.test.tsx +817 -0
- package/src/globals.ts +1 -0
- package/src/hooks/useClobData.tsx +21 -0
- package/src/hooks/useConcepts.tsx +55 -0
- package/src/hooks/useDatasourceDependentValue.ts +16 -0
- package/src/hooks/useEncounter.tsx +32 -0
- package/src/hooks/useEncounterRole.tsx +15 -0
- package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
- package/src/hooks/useFieldValidationResults.ts +18 -0
- package/src/hooks/useFormCollapse.tsx +36 -0
- package/src/hooks/useFormFieldValidators.ts +22 -0
- package/src/hooks/useFormFieldValueAdapters.ts +24 -0
- package/src/hooks/useFormFields.ts +37 -0
- package/src/hooks/useFormFieldsMeta.ts +48 -0
- package/src/hooks/useFormJson.test.tsx +173 -0
- package/src/hooks/useFormJson.tsx +237 -0
- package/src/hooks/useFormStateHelpers.ts +50 -0
- package/src/hooks/useFormsConfig.tsx +27 -0
- package/src/hooks/useInitialValues.ts +38 -0
- package/src/hooks/usePatientData.tsx +32 -0
- package/src/hooks/usePatientPrograms.ts +32 -0
- package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
- package/src/hooks/usePostSubmissionActions.ts +31 -0
- package/src/hooks/useProcessorDependencies.ts +30 -0
- package/src/hooks/useRestMaxResultsCount.ts +5 -0
- package/src/hooks/useSystemSetting.ts +36 -0
- package/src/hooks/useWorkspaceLayout.ts +29 -0
- package/src/index.ts +12 -0
- package/src/lifecycle.ts +33 -0
- package/src/post-submission-actions/program-enrollment-action.ts +138 -0
- package/src/processors/encounter/encounter-form-processor.ts +337 -0
- package/src/processors/encounter/encounter-processor-helper.ts +320 -0
- package/src/processors/form-processor.ts +41 -0
- package/src/provider/form-factory-helper.ts +100 -0
- package/src/provider/form-factory-provider.tsx +169 -0
- package/src/provider/form-provider.tsx +37 -0
- package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
- package/src/registry/inbuilt-components/control-templates.ts +57 -0
- package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
- package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
- package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
- package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
- package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
- package/src/registry/inbuilt-components/template-component-map.ts +28 -0
- package/src/registry/registry.test.ts +20 -0
- package/src/registry/registry.ts +261 -0
- package/src/routes.json +1 -0
- package/src/setupI18n.ts +16 -0
- package/src/setupTests.ts +5 -0
- package/src/transformers/default-schema-transformer.test.ts +155 -0
- package/src/transformers/default-schema-transformer.ts +239 -0
- package/src/types/domain.ts +129 -0
- package/src/types/index.ts +130 -0
- package/src/types/schema.ts +238 -0
- package/src/typings.d.ts +9 -0
- package/src/utils/boolean-utils.ts +25 -0
- package/src/utils/common-expression-helpers.ts +503 -0
- package/src/utils/common-utils.test.ts +136 -0
- package/src/utils/common-utils.ts +55 -0
- package/src/utils/error-utils.ts +37 -0
- package/src/utils/expression-parser.test.ts +308 -0
- package/src/utils/expression-parser.ts +158 -0
- package/src/utils/expression-runner.test.ts +387 -0
- package/src/utils/expression-runner.ts +219 -0
- package/src/utils/form-helper.test.ts +482 -0
- package/src/utils/form-helper.ts +210 -0
- package/src/utils/form-page-utils.ts +13 -0
- package/src/utils/forms-loader.test.ts +323 -0
- package/src/utils/forms-loader.ts +306 -0
- package/src/utils/post-submission-action-helper.ts +71 -0
- package/src/utils/test-utils.ts +54 -0
- package/src/utils/zscore-service.ts +59 -0
- package/src/validators/conditional-answered-validator.test.ts +61 -0
- package/src/validators/conditional-answered-validator.ts +17 -0
- package/src/validators/date-validator.test.ts +46 -0
- package/src/validators/date-validator.ts +19 -0
- package/src/validators/default-value-validator.test.ts +90 -0
- package/src/validators/default-value-validator.ts +36 -0
- package/src/validators/form-validator.test.ts +188 -0
- package/src/validators/form-validator.ts +95 -0
- package/src/validators/js-expression-validator.test.ts +118 -0
- package/src/validators/js-expression-validator.ts +44 -0
- package/src/validators/schema.ts +34 -0
- package/src/zscore/bfa_boys_5_above.json +2522 -0
- package/src/zscore/bfa_girls_5_above.json +2522 -0
- package/src/zscore/hfa_boys_5_above.json +2186 -0
- package/src/zscore/hfa_boys_below5.json +22286 -0
- package/src/zscore/hfa_girls_5_above.json +2186 -0
- package/src/zscore/hfa_girls_below5.json +22286 -0
- package/src/zscore/wfl_boys_below5.json +7814 -0
- package/src/zscore/wfl_girls_below5.json +7814 -0
- package/src/zscore-tests/bmi-age.test.tsx +88 -0
- package/src/zscore-tests/height-age.test.tsx +96 -0
- package/src/zscore-tests/weight-height.test.tsx +87 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/en.json +47 -0
- package/translations/es.json +38 -0
- package/translations/fr.json +38 -0
- package/translations/km.json +38 -0
- package/tsconfig.json +19 -0
- package/turbo.json +15 -0
- package/webpack.config.js +1 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
2
|
+
import debounce from 'lodash-es/debounce';
|
3
|
+
import { ComboBox, DropdownSkeleton, Layer } from '@carbon/react';
|
4
|
+
import { isTrue } from '../../../utils/boolean-utils';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
import { getRegisteredDataSource } from '../../../registry/registry';
|
7
|
+
import { getControlTemplate } from '../../../registry/inbuilt-components/control-templates';
|
8
|
+
import { type FormFieldInputProps } from '../../../types';
|
9
|
+
import { isEmpty } from '../../../validators/form-validator';
|
10
|
+
import { shouldUseInlineLayout } from '../../../utils/form-helper';
|
11
|
+
import FieldValueView from '../../value/view/field-value-view.component';
|
12
|
+
import styles from './ui-select-extended.scss';
|
13
|
+
import { useFormProviderContext } from '../../../provider/form-provider';
|
14
|
+
import FieldLabel from '../../field-label/field-label.component';
|
15
|
+
import useDataSourceDependentValue from '../../../hooks/useDatasourceDependentValue';
|
16
|
+
import { useWatch } from 'react-hook-form';
|
17
|
+
|
18
|
+
const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnings, setFieldValue }) => {
|
19
|
+
const { t } = useTranslation();
|
20
|
+
const [items, setItems] = useState([]);
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
22
|
+
const [searchTerm, setSearchTerm] = useState('');
|
23
|
+
const isProcessingSelection = useRef(false);
|
24
|
+
const [dataSource, setDataSource] = useState(null);
|
25
|
+
const [config, setConfig] = useState({});
|
26
|
+
const [savedSearchableItem, setSavedSearchableItem] = useState({});
|
27
|
+
const dataSourceDependentValue = useDataSourceDependentValue(field);
|
28
|
+
const {
|
29
|
+
layoutType,
|
30
|
+
sessionMode,
|
31
|
+
workspaceLayout,
|
32
|
+
methods: { control },
|
33
|
+
} = useFormProviderContext();
|
34
|
+
|
35
|
+
const value = useWatch({ control, name: field.id, exact: true });
|
36
|
+
|
37
|
+
const isInline = useMemo(() => {
|
38
|
+
if (['view', 'embedded-view'].includes(sessionMode) || isTrue(field.readonly)) {
|
39
|
+
return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode);
|
40
|
+
}
|
41
|
+
return false;
|
42
|
+
}, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]);
|
43
|
+
|
44
|
+
useEffect(() => {
|
45
|
+
const dataSource = field.questionOptions?.datasource?.name;
|
46
|
+
setConfig(
|
47
|
+
dataSource
|
48
|
+
? field.questionOptions.datasource?.config
|
49
|
+
: getControlTemplate(field.questionOptions.rendering)?.datasource?.config,
|
50
|
+
);
|
51
|
+
getRegisteredDataSource(dataSource ? dataSource : field.questionOptions.rendering).then((ds) => setDataSource(ds));
|
52
|
+
}, [field.questionOptions?.datasource]);
|
53
|
+
|
54
|
+
const selectedItem = useMemo(() => items.find((item) => item.uuid == value), [items, value]);
|
55
|
+
|
56
|
+
const debouncedSearch = debounce((searchTerm, dataSource) => {
|
57
|
+
setItems([]);
|
58
|
+
setIsLoading(true);
|
59
|
+
dataSource
|
60
|
+
.fetchData(searchTerm, config)
|
61
|
+
.then((dataItems) => {
|
62
|
+
setItems(dataItems.map(dataSource.toUuidAndDisplay));
|
63
|
+
setIsLoading(false);
|
64
|
+
})
|
65
|
+
.catch((err) => {
|
66
|
+
console.error(err);
|
67
|
+
setIsLoading(false);
|
68
|
+
setItems([]);
|
69
|
+
});
|
70
|
+
}, 300);
|
71
|
+
|
72
|
+
const processSearchableValues = (value) => {
|
73
|
+
dataSource
|
74
|
+
.fetchData(null, config, value)
|
75
|
+
.then((dataItem) => {
|
76
|
+
setSavedSearchableItem(dataItem);
|
77
|
+
setIsLoading(false);
|
78
|
+
})
|
79
|
+
.catch((err) => {
|
80
|
+
console.error(err);
|
81
|
+
setIsLoading(false);
|
82
|
+
setItems([]);
|
83
|
+
});
|
84
|
+
};
|
85
|
+
|
86
|
+
useEffect(() => {
|
87
|
+
// If not searchable, preload the items
|
88
|
+
if (dataSource && !isTrue(field.questionOptions.isSearchable)) {
|
89
|
+
setItems([]);
|
90
|
+
setIsLoading(true);
|
91
|
+
dataSource
|
92
|
+
.fetchData(null, { ...config, referencedValue: dataSourceDependentValue })
|
93
|
+
.then((dataItems) => {
|
94
|
+
setItems(dataItems.map(dataSource.toUuidAndDisplay));
|
95
|
+
setIsLoading(false);
|
96
|
+
})
|
97
|
+
.catch((err) => {
|
98
|
+
console.error(err);
|
99
|
+
setIsLoading(false);
|
100
|
+
setItems([]);
|
101
|
+
});
|
102
|
+
}
|
103
|
+
}, [dataSource, config, dataSourceDependentValue]);
|
104
|
+
|
105
|
+
useEffect(() => {
|
106
|
+
if (dataSource && isTrue(field.questionOptions.isSearchable) && !isEmpty(searchTerm)) {
|
107
|
+
debouncedSearch(searchTerm, dataSource);
|
108
|
+
}
|
109
|
+
}, [dataSource, searchTerm, config]);
|
110
|
+
|
111
|
+
useEffect(() => {
|
112
|
+
if (
|
113
|
+
dataSource &&
|
114
|
+
isTrue(field.questionOptions.isSearchable) &&
|
115
|
+
isEmpty(searchTerm) &&
|
116
|
+
value &&
|
117
|
+
!Object.keys(savedSearchableItem).length
|
118
|
+
) {
|
119
|
+
setIsLoading(true);
|
120
|
+
processSearchableValues(value);
|
121
|
+
}
|
122
|
+
}, [value]);
|
123
|
+
|
124
|
+
if (isLoading) {
|
125
|
+
return <DropdownSkeleton />;
|
126
|
+
}
|
127
|
+
|
128
|
+
return sessionMode == 'view' || sessionMode == 'embedded-view' || isTrue(field.readonly) ? (
|
129
|
+
<FieldValueView
|
130
|
+
label={t(field.label)}
|
131
|
+
value={value ? items.find((item) => item.uuid == value)?.display : value}
|
132
|
+
conceptName={field.meta?.concept?.display}
|
133
|
+
isInline={isInline}
|
134
|
+
/>
|
135
|
+
) : (
|
136
|
+
!field.isHidden && (
|
137
|
+
<div className={styles.boldedLabel}>
|
138
|
+
<Layer>
|
139
|
+
<ComboBox
|
140
|
+
id={field.id}
|
141
|
+
titleText={<FieldLabel field={field} />}
|
142
|
+
items={items}
|
143
|
+
itemToString={(item) => item?.display}
|
144
|
+
selectedItem={selectedItem}
|
145
|
+
shouldFilterItem={({ item, inputValue }) => {
|
146
|
+
if (!inputValue) {
|
147
|
+
// Carbon's initial call at component mount
|
148
|
+
return true;
|
149
|
+
}
|
150
|
+
return item.display?.toLowerCase().includes(inputValue.toLowerCase());
|
151
|
+
}}
|
152
|
+
onChange={({ selectedItem }) => {
|
153
|
+
isProcessingSelection.current = true;
|
154
|
+
setFieldValue(selectedItem?.uuid);
|
155
|
+
}}
|
156
|
+
disabled={field.isDisabled}
|
157
|
+
readOnly={field.readonly}
|
158
|
+
invalid={errors.length > 0}
|
159
|
+
invalidText={errors.length && errors[0].message}
|
160
|
+
onInputChange={(value) => {
|
161
|
+
if (isProcessingSelection.current) {
|
162
|
+
// Notes:
|
163
|
+
// When the user selects a value, both the onChange and onInputChange functions are invoked sequentially.
|
164
|
+
// Issue: onInputChange modifies the search term, unnecessarily triggering a search.
|
165
|
+
isProcessingSelection.current = false;
|
166
|
+
return;
|
167
|
+
}
|
168
|
+
if (field.questionOptions['isSearchable']) {
|
169
|
+
setSearchTerm(value);
|
170
|
+
}
|
171
|
+
}}
|
172
|
+
onBlur={(event) => {
|
173
|
+
// Notes:
|
174
|
+
// There is an issue with the onBlur event where the value is not persistently set to null when the user clears the input field.
|
175
|
+
// This is a workaround to ensure that the value is set to null when the user clears the input field.
|
176
|
+
if (!event.target.value) {
|
177
|
+
setFieldValue(null);
|
178
|
+
}
|
179
|
+
}}
|
180
|
+
/>
|
181
|
+
</Layer>
|
182
|
+
</div>
|
183
|
+
)
|
184
|
+
);
|
185
|
+
};
|
186
|
+
|
187
|
+
export default UiSelectExtended;
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
3
|
+
import UiSelectExtended from './ui-select-extended.component';
|
4
|
+
import { type EncounterContext, FormContext } from '../../../form-context';
|
5
|
+
import { type FormField } from '../../../types';
|
6
|
+
|
7
|
+
const questions: FormField[] = [
|
8
|
+
{
|
9
|
+
label: 'Transfer Location',
|
10
|
+
type: 'obs',
|
11
|
+
questionOptions: {
|
12
|
+
rendering: 'ui-select-extended',
|
13
|
+
concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
14
|
+
datasource: {
|
15
|
+
name: 'location_datasource',
|
16
|
+
config: {
|
17
|
+
tag: 'test-tag',
|
18
|
+
},
|
19
|
+
},
|
20
|
+
},
|
21
|
+
meta: {},
|
22
|
+
id: 'patient_transfer_location',
|
23
|
+
},
|
24
|
+
{
|
25
|
+
label: 'Select criteria for new WHO stage:',
|
26
|
+
type: 'obs',
|
27
|
+
questionOptions: {
|
28
|
+
concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372',
|
29
|
+
rendering: 'select-concept-answers',
|
30
|
+
datasource: {
|
31
|
+
name: 'select_concept_answers_datasource',
|
32
|
+
config: {
|
33
|
+
concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372',
|
34
|
+
},
|
35
|
+
},
|
36
|
+
},
|
37
|
+
validators: [],
|
38
|
+
id: '__sq5ELJr7p',
|
39
|
+
},
|
40
|
+
];
|
41
|
+
|
42
|
+
const encounterContext: EncounterContext = {
|
43
|
+
patient: {
|
44
|
+
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
45
|
+
},
|
46
|
+
location: {
|
47
|
+
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
|
48
|
+
},
|
49
|
+
encounter: {
|
50
|
+
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
|
51
|
+
obs: [],
|
52
|
+
},
|
53
|
+
sessionMode: 'enter',
|
54
|
+
encounterDate: new Date(2023, 8, 29),
|
55
|
+
setEncounterDate: (value) => {},
|
56
|
+
encounterProvider: '2c95f6f5-788e-4e73-9079-5626911231fa',
|
57
|
+
setEncounterProvider: jest.fn,
|
58
|
+
setEncounterLocation: jest.fn,
|
59
|
+
encounterRole: '8cb3a399-d18b-4b62-aefb-5a0f948a3809',
|
60
|
+
setEncounterRole: jest.fn,
|
61
|
+
};
|
62
|
+
|
63
|
+
const renderForm = (initialValues) => {
|
64
|
+
render(<></>);
|
65
|
+
};
|
66
|
+
|
67
|
+
// Mock the data source fetch behavior
|
68
|
+
jest.mock('../../../registry/registry', () => ({
|
69
|
+
getRegisteredDataSource: jest.fn().mockResolvedValue({
|
70
|
+
fetchData: jest.fn().mockImplementation((...args) => {
|
71
|
+
if (args[1].concept) {
|
72
|
+
return Promise.resolve([
|
73
|
+
{
|
74
|
+
uuid: 'stage-1-uuid',
|
75
|
+
display: 'stage 1',
|
76
|
+
},
|
77
|
+
{
|
78
|
+
uuid: 'stage-2-uuid',
|
79
|
+
display: 'stage 2',
|
80
|
+
},
|
81
|
+
]);
|
82
|
+
}
|
83
|
+
|
84
|
+
return Promise.resolve([
|
85
|
+
{
|
86
|
+
uuid: 'aaa-1',
|
87
|
+
display: 'Kololo',
|
88
|
+
},
|
89
|
+
{
|
90
|
+
uuid: 'aaa-2',
|
91
|
+
display: 'Naguru',
|
92
|
+
},
|
93
|
+
{
|
94
|
+
uuid: 'aaa-3',
|
95
|
+
display: 'Muyenga',
|
96
|
+
},
|
97
|
+
]);
|
98
|
+
}),
|
99
|
+
toUuidAndDisplay: (data) => data,
|
100
|
+
}),
|
101
|
+
}));
|
102
|
+
|
103
|
+
describe.skip('UiSelectExtended Component', () => {
|
104
|
+
it('renders with items from the datasource', async () => {
|
105
|
+
await act(async () => {
|
106
|
+
await renderForm({});
|
107
|
+
});
|
108
|
+
|
109
|
+
// setup
|
110
|
+
const uiSelectExtendedWidget = screen.getByLabelText('Transfer Location');
|
111
|
+
|
112
|
+
// assert initial values
|
113
|
+
expect(questions[0].meta.submission).toBe(undefined);
|
114
|
+
|
115
|
+
//Click on the UiSelectExtendedWidget to open the dropdown
|
116
|
+
fireEvent.click(uiSelectExtendedWidget);
|
117
|
+
|
118
|
+
// Assert that all three items are displayed
|
119
|
+
expect(screen.getByText('Kololo')).toBeInTheDocument();
|
120
|
+
expect(screen.getByText('Naguru')).toBeInTheDocument();
|
121
|
+
expect(screen.getByText('Muyenga')).toBeInTheDocument();
|
122
|
+
});
|
123
|
+
|
124
|
+
it('renders with items from the datasource of select-concept-answers rendering', async () => {
|
125
|
+
await act(async () => {
|
126
|
+
await renderForm({});
|
127
|
+
});
|
128
|
+
|
129
|
+
const uiSelectExtendedWidget = screen.getByLabelText(/Select criteria for new WHO stage:/i);
|
130
|
+
fireEvent.click(uiSelectExtendedWidget);
|
131
|
+
|
132
|
+
// Assert that all items are displayed
|
133
|
+
expect(screen.getByText('stage 1')).toBeInTheDocument();
|
134
|
+
expect(screen.getByText('stage 2')).toBeInTheDocument();
|
135
|
+
});
|
136
|
+
|
137
|
+
it('Selects a value from the list', async () => {
|
138
|
+
await act(async () => {
|
139
|
+
await renderForm({});
|
140
|
+
});
|
141
|
+
|
142
|
+
// setup
|
143
|
+
const uiSelectExtendedWidget = screen.getByLabelText('Transfer Location');
|
144
|
+
|
145
|
+
//Click on the UiSelectExtendedWidget to open the dropdown
|
146
|
+
fireEvent.click(uiSelectExtendedWidget);
|
147
|
+
|
148
|
+
// Find the list item for 'Naguru' and click it to select
|
149
|
+
const naguruOption = screen.getByText('Naguru');
|
150
|
+
fireEvent.click(naguruOption);
|
151
|
+
|
152
|
+
// verify
|
153
|
+
await act(async () => {
|
154
|
+
expect(questions[0].meta.submission.newValue).toEqual({
|
155
|
+
concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
156
|
+
formFieldNamespace: 'rfe-forms',
|
157
|
+
formFieldPath: 'rfe-forms-patient_transfer_location',
|
158
|
+
value: 'aaa-2',
|
159
|
+
});
|
160
|
+
});
|
161
|
+
});
|
162
|
+
|
163
|
+
it('Filters items based on user input', async () => {
|
164
|
+
await act(async () => {
|
165
|
+
await renderForm({});
|
166
|
+
});
|
167
|
+
|
168
|
+
// setup
|
169
|
+
const uiSelectExtendedWidget = screen.getByLabelText('Transfer Location');
|
170
|
+
|
171
|
+
//Click on the UiSelectExtendedWidget to open the dropdown
|
172
|
+
fireEvent.click(uiSelectExtendedWidget);
|
173
|
+
|
174
|
+
// Type 'Nag' in the input field to filter items
|
175
|
+
fireEvent.change(uiSelectExtendedWidget, { target: { value: 'Nag' } });
|
176
|
+
|
177
|
+
// Wait for the filtered items to appear in the dropdown
|
178
|
+
await waitFor(() => {
|
179
|
+
// Verify that 'Naguru' is in the filtered items
|
180
|
+
expect(screen.getByText('Naguru')).toBeInTheDocument();
|
181
|
+
|
182
|
+
// Verify that 'Kololo' and 'Muyenga' are not in the filtered items
|
183
|
+
expect(screen.queryByText('Kololo')).not.toBeInTheDocument();
|
184
|
+
expect(screen.queryByText('Muyenga')).not.toBeInTheDocument();
|
185
|
+
});
|
186
|
+
});
|
187
|
+
|
188
|
+
it('Should set the correct value for the config parameter', async () => {
|
189
|
+
// Mock the data source fetch behavior
|
190
|
+
const expectedConfigValue = {
|
191
|
+
tag: 'test-tag',
|
192
|
+
};
|
193
|
+
|
194
|
+
// Mock the getRegisteredDataSource function
|
195
|
+
jest.mock('../../../registry/registry', () => ({
|
196
|
+
getRegisteredDataSource: jest.fn().mockResolvedValue({
|
197
|
+
fetchData: jest.fn().mockResolvedValue([]),
|
198
|
+
toUuidAndDisplay: (data) => data,
|
199
|
+
config: expectedConfigValue,
|
200
|
+
}),
|
201
|
+
}));
|
202
|
+
|
203
|
+
await act(async () => {
|
204
|
+
await renderForm({});
|
205
|
+
});
|
206
|
+
const config = questions[0].questionOptions.datasource.config;
|
207
|
+
|
208
|
+
// Assert that the config is set with the expected configuration value
|
209
|
+
expect(config).toEqual(expectedConfigValue);
|
210
|
+
});
|
211
|
+
});
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
2
|
+
import { Checkbox } from '@carbon/react';
|
3
|
+
import { useTranslation } from 'react-i18next';
|
4
|
+
import { isEmpty } from '../../../validators/form-validator';
|
5
|
+
import { type FormField } from '../../../types';
|
6
|
+
import { isTrue } from '../../../utils/boolean-utils';
|
7
|
+
|
8
|
+
import styles from './unspecified.scss';
|
9
|
+
import { useFormProviderContext } from '../../../provider/form-provider';
|
10
|
+
import { isViewMode } from '../../../utils/common-utils';
|
11
|
+
|
12
|
+
interface UnspecifiedFieldProps {
|
13
|
+
field: FormField;
|
14
|
+
fieldValue: any;
|
15
|
+
setFieldValue: (value: any) => void;
|
16
|
+
onAfterChange: (value: any) => void;
|
17
|
+
}
|
18
|
+
|
19
|
+
const UnspecifiedField: React.FC<UnspecifiedFieldProps> = ({ field, fieldValue, setFieldValue, onAfterChange }) => {
|
20
|
+
const { t } = useTranslation();
|
21
|
+
const [isUnspecified, setIsUnspecified] = useState(false);
|
22
|
+
const { sessionMode, updateFormField } = useFormProviderContext();
|
23
|
+
|
24
|
+
useEffect(() => {
|
25
|
+
if (isEmpty(fieldValue) && sessionMode === 'edit') {
|
26
|
+
// we assume that the field was previously unspecified
|
27
|
+
setIsUnspecified(true);
|
28
|
+
}
|
29
|
+
}, []);
|
30
|
+
|
31
|
+
useEffect(() => {
|
32
|
+
if (field.meta.submission?.newValue) {
|
33
|
+
setIsUnspecified(false);
|
34
|
+
field.meta.submission.unspecified = false;
|
35
|
+
updateFormField({ ...field });
|
36
|
+
}
|
37
|
+
}, [field.meta?.submission]);
|
38
|
+
|
39
|
+
const handleOnChange = useCallback(
|
40
|
+
(value) => {
|
41
|
+
const rendering = field.questionOptions.rendering;
|
42
|
+
if (value.target.checked) {
|
43
|
+
const emptyValue = rendering === 'checkbox' ? [] : '';
|
44
|
+
field.meta.submission = { ...field.meta.submission, unspecified: true };
|
45
|
+
updateFormField({ ...field });
|
46
|
+
setIsUnspecified(true);
|
47
|
+
setFieldValue(emptyValue);
|
48
|
+
onAfterChange(emptyValue);
|
49
|
+
} else {
|
50
|
+
setIsUnspecified(false);
|
51
|
+
}
|
52
|
+
},
|
53
|
+
[field.questionOptions.rendering],
|
54
|
+
);
|
55
|
+
|
56
|
+
return (
|
57
|
+
!field.isHidden &&
|
58
|
+
!isTrue(field.readonly) &&
|
59
|
+
!isViewMode(sessionMode) && (
|
60
|
+
<div className={styles.unspecified}>
|
61
|
+
<Checkbox
|
62
|
+
id={`${field.id}-unspecified`}
|
63
|
+
labelText={t('unspecified', 'Unspecified')}
|
64
|
+
value={t('unspecified', 'Unspecified')}
|
65
|
+
onChange={handleOnChange}
|
66
|
+
checked={isUnspecified}
|
67
|
+
disabled={field.isDisabled}
|
68
|
+
/>
|
69
|
+
</div>
|
70
|
+
)
|
71
|
+
);
|
72
|
+
};
|
73
|
+
|
74
|
+
export default UnspecifiedField;
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import dayjs from 'dayjs';
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
4
|
+
import { OpenmrsDatePicker } from '@openmrs/esm-framework';
|
5
|
+
import { type FormField, type EncounterContext } from '../../..';
|
6
|
+
import { findTextOrDateInput } from '../../../utils/test-utils';
|
7
|
+
|
8
|
+
const mockOpenmrsDatePicker = jest.mocked(OpenmrsDatePicker);
|
9
|
+
|
10
|
+
mockOpenmrsDatePicker.mockImplementation(({ id, labelText, value, onChange }) => {
|
11
|
+
return (
|
12
|
+
<>
|
13
|
+
<label htmlFor={id}>{labelText}</label>
|
14
|
+
<input
|
15
|
+
id={id}
|
16
|
+
value={value ? dayjs(value.toString()).format('DD/MM/YYYY') : undefined}
|
17
|
+
onChange={(evt) => onChange(new Date(evt.target.value))}
|
18
|
+
/>
|
19
|
+
</>
|
20
|
+
);
|
21
|
+
});
|
22
|
+
|
23
|
+
const question: FormField = {
|
24
|
+
label: 'Visit Date',
|
25
|
+
type: 'obs',
|
26
|
+
datePickerFormat: 'calendar',
|
27
|
+
questionOptions: {
|
28
|
+
rendering: 'date',
|
29
|
+
concept: '163260AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
30
|
+
},
|
31
|
+
id: 'visit-date',
|
32
|
+
};
|
33
|
+
|
34
|
+
const encounterContext: EncounterContext = {
|
35
|
+
patient: {
|
36
|
+
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
37
|
+
},
|
38
|
+
location: {
|
39
|
+
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
|
40
|
+
},
|
41
|
+
encounter: {
|
42
|
+
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
|
43
|
+
obs: [],
|
44
|
+
},
|
45
|
+
sessionMode: 'enter',
|
46
|
+
encounterDate: new Date(2020, 11, 29),
|
47
|
+
setEncounterDate: (value) => {},
|
48
|
+
encounterProvider: '2c95f6f5-788e-4e73-9079-5626911231fa',
|
49
|
+
setEncounterProvider: jest.fn,
|
50
|
+
setEncounterLocation: jest.fn,
|
51
|
+
encounterRole: '8cb3a399-d18b-4b62-aefb-5a0f948a3809',
|
52
|
+
setEncounterRole: jest.fn,
|
53
|
+
};
|
54
|
+
|
55
|
+
const renderForm = (initialValues) => {
|
56
|
+
render(<></>);
|
57
|
+
};
|
58
|
+
|
59
|
+
describe.skip('Unspecified', () => {
|
60
|
+
it('Should toggle the "Unspecified" checkbox on click', async () => {
|
61
|
+
// setup
|
62
|
+
renderForm({});
|
63
|
+
const unspecifiedCheckbox = screen.getByRole('checkbox', { name: /Unspecified/ });
|
64
|
+
|
65
|
+
// assert initial state
|
66
|
+
expect(unspecifiedCheckbox).not.toBeChecked();
|
67
|
+
|
68
|
+
// assert checked
|
69
|
+
fireEvent.click(unspecifiedCheckbox);
|
70
|
+
expect(unspecifiedCheckbox).toBeChecked();
|
71
|
+
|
72
|
+
// assert unchecked
|
73
|
+
fireEvent.click(unspecifiedCheckbox);
|
74
|
+
expect(unspecifiedCheckbox).not.toBeChecked();
|
75
|
+
});
|
76
|
+
|
77
|
+
it('Should clear field value when the "Unspecified" checkbox is clicked', async () => {
|
78
|
+
//setup
|
79
|
+
renderForm({});
|
80
|
+
const unspecifiedCheckbox = screen.getByRole('checkbox', { name: /Unspecified/ });
|
81
|
+
const visitDateField = await findTextOrDateInput(screen, 'Visit Date');
|
82
|
+
|
83
|
+
// assert initial state
|
84
|
+
expect(unspecifiedCheckbox).not.toBeChecked();
|
85
|
+
expect(visitDateField.value).toBe('');
|
86
|
+
|
87
|
+
fireEvent.change(visitDateField, { target: { value: '2023-09-09T00:00:00.000Z' } });
|
88
|
+
|
89
|
+
// assert checked
|
90
|
+
fireEvent.click(unspecifiedCheckbox);
|
91
|
+
expect(unspecifiedCheckbox).toBeChecked();
|
92
|
+
//TODO : Fix this test case - - https://openmrs.atlassian.net/browse/O3-3479s
|
93
|
+
// expect(visitDateField.value).toBe('');
|
94
|
+
});
|
95
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useTranslation } from 'react-i18next';
|
3
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
4
|
+
import { useLaunchWorkspaceRequiringVisit } from '@openmrs/esm-patient-common-lib';
|
5
|
+
import { Button } from '@carbon/react';
|
6
|
+
import { type FormFieldInputProps } from '../../../types';
|
7
|
+
import styles from './workspace-launcher.scss';
|
8
|
+
|
9
|
+
const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
|
10
|
+
const { t } = useTranslation();
|
11
|
+
const launchWorkspace = useLaunchWorkspaceRequiringVisit(field.questionOptions?.workspaceName);
|
12
|
+
|
13
|
+
const handleLaunchWorkspace = () => {
|
14
|
+
if (!launchWorkspace) {
|
15
|
+
showSnackbar({
|
16
|
+
title: t('invalidWorkspaceName', 'Invalid workspace name.'),
|
17
|
+
subtitle: t('invalidWorkspaceNameSubtitle', 'Please provide a valid workspace name.'),
|
18
|
+
kind: 'error',
|
19
|
+
isLowContrast: true,
|
20
|
+
});
|
21
|
+
}
|
22
|
+
launchWorkspace();
|
23
|
+
};
|
24
|
+
|
25
|
+
return (
|
26
|
+
<div>
|
27
|
+
<div className={styles.label}>{t(field.label)}</div>
|
28
|
+
<div className={styles.workspaceButton}>
|
29
|
+
<Button onClick={handleLaunchWorkspace}>{field.questionOptions?.buttonLabel ?? t('launchWorkspace')}</Button>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
);
|
33
|
+
};
|
34
|
+
|
35
|
+
export default WorkspaceLauncher;
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { DefinitionTooltip } from '@carbon/react';
|
3
|
+
import styles from './label.scss';
|
4
|
+
|
5
|
+
type LabelProps = {
|
6
|
+
value: string;
|
7
|
+
tooltipText?: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
const LabelField: React.FC<LabelProps> = ({ value, tooltipText }) => {
|
11
|
+
return (
|
12
|
+
<div className={styles.label}>
|
13
|
+
<DefinitionTooltip direction="bottom" tabIndex={0} tooltipText={tooltipText}>
|
14
|
+
<span className="cds--label">{`${value}:`}</span>
|
15
|
+
</DefinitionTooltip>
|
16
|
+
</div>
|
17
|
+
);
|
18
|
+
};
|
19
|
+
|
20
|
+
export default LabelField;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useTranslation } from 'react-i18next';
|
3
|
+
import { InlineLoading } from '@carbon/react';
|
4
|
+
import styles from './loader.scss';
|
5
|
+
|
6
|
+
const Loader: React.FC = () => {
|
7
|
+
const { t } = useTranslation();
|
8
|
+
|
9
|
+
return (
|
10
|
+
<div className={styles.loaderContainer}>
|
11
|
+
<InlineLoading className={styles.loader} description={`${t('loading', 'Loading')} ...`} />
|
12
|
+
</div>
|
13
|
+
);
|
14
|
+
};
|
15
|
+
|
16
|
+
export default Loader;
|