@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.
Files changed (272) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +58 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +4 -0
  7. package/LICENSE.txt +401 -0
  8. package/README.md +136 -0
  9. package/__mocks__/concepts.mock.json +140 -0
  10. package/__mocks__/forms/afe-forms/component_art.json +38 -0
  11. package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
  12. package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
  13. package/__mocks__/forms/afe-forms/form-component.json +38 -0
  14. package/__mocks__/forms/afe-forms/mini-form.json +31 -0
  15. package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
  16. package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
  17. package/__mocks__/forms/afe-forms/test-orders.json +72 -0
  18. package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
  19. package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
  20. package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
  21. package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
  22. package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
  23. package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
  24. package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
  25. package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
  26. package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
  27. package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
  28. package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
  29. package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
  30. package/__mocks__/forms/rfe-forms/form-component.json +43 -0
  31. package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
  32. package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
  33. package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
  34. package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
  35. package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
  36. package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
  37. package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
  38. package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
  39. package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
  40. package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
  41. package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
  42. package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
  43. package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
  44. package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
  45. package/__mocks__/forms/rfe-forms/required-form.json +50 -0
  46. package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
  47. package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
  48. package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
  49. package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
  50. package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
  51. package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
  52. package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
  53. package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
  54. package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
  55. package/__mocks__/packages/test-forms-registry.ts +12 -0
  56. package/__mocks__/patient.mock.ts +173 -0
  57. package/__mocks__/react-i18next.js +49 -0
  58. package/__mocks__/react-markdown.tsx +5 -0
  59. package/__mocks__/session.mock.ts +117 -0
  60. package/__mocks__/single-spa-react.js +11 -0
  61. package/__mocks__/use-initial-values/encounter.mock.json +963 -0
  62. package/__mocks__/use-initial-values/patient.mock.json +73 -0
  63. package/__mocks__/visit.mock.ts +19 -0
  64. package/dist/openmrs-esm-form-engine-lib.js +1 -0
  65. package/jest.config.js +30 -0
  66. package/package.json +104 -0
  67. package/prettier.config.js +8 -0
  68. package/readme/form-engine.jpeg +0 -0
  69. package/src/adapters/control-adapter.ts +29 -0
  70. package/src/adapters/encounter-datetime-adapter.ts +38 -0
  71. package/src/adapters/encounter-location-adapter.ts +39 -0
  72. package/src/adapters/encounter-provider-adapter.ts +48 -0
  73. package/src/adapters/encounter-role-adapter.ts +54 -0
  74. package/src/adapters/inline-date-adapter.ts +58 -0
  75. package/src/adapters/obs-adapter.ts +280 -0
  76. package/src/adapters/obs-comment-adapter.ts +60 -0
  77. package/src/adapters/orders-adapter.ts +75 -0
  78. package/src/adapters/patient-identifier-adapter.ts +40 -0
  79. package/src/adapters/program-state-adapter.ts +52 -0
  80. package/src/api/index.ts +178 -0
  81. package/src/components/error/error-modal.component.tsx +37 -0
  82. package/src/components/error/error.scss +4 -0
  83. package/src/components/extension/extension-parcel.component.tsx +32 -0
  84. package/src/components/field-label/field-label.component.tsx +32 -0
  85. package/src/components/field-label/field-label.scss +11 -0
  86. package/src/components/group/obs-group.component.tsx +29 -0
  87. package/src/components/group/obs-group.scss +12 -0
  88. package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
  89. package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
  90. package/src/components/inputs/date/date.component.tsx +149 -0
  91. package/src/components/inputs/date/date.scss +36 -0
  92. package/src/components/inputs/file/camera/camera.component.tsx +34 -0
  93. package/src/components/inputs/file/camera/camera.scss +3 -0
  94. package/src/components/inputs/file/file.component.tsx +159 -0
  95. package/src/components/inputs/file/file.scss +101 -0
  96. package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
  97. package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
  98. package/src/components/inputs/markdown/markdown.component.tsx +8 -0
  99. package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
  100. package/src/components/inputs/multi-select/multi-select.scss +25 -0
  101. package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
  102. package/src/components/inputs/number/number.component.tsx +69 -0
  103. package/src/components/inputs/number/number.scss +15 -0
  104. package/src/components/inputs/radio/radio.component.tsx +79 -0
  105. package/src/components/inputs/radio/radio.scss +36 -0
  106. package/src/components/inputs/select/dropdown.component.tsx +73 -0
  107. package/src/components/inputs/select/dropdown.scss +11 -0
  108. package/src/components/inputs/select/dropdown.test.tsx +120 -0
  109. package/src/components/inputs/text/text.component.tsx +65 -0
  110. package/src/components/inputs/text/text.scss +15 -0
  111. package/src/components/inputs/text/text.test.tsx +104 -0
  112. package/src/components/inputs/text-area/text-area.component.tsx +63 -0
  113. package/src/components/inputs/text-area/text-area.scss +11 -0
  114. package/src/components/inputs/toggle/toggle.component.tsx +66 -0
  115. package/src/components/inputs/toggle/toggle.scss +12 -0
  116. package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
  117. package/src/components/inputs/tooltip/tooltip.scss +8 -0
  118. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
  119. package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
  120. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
  121. package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
  122. package/src/components/inputs/unspecified/unspecified.scss +7 -0
  123. package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
  124. package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
  125. package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
  126. package/src/components/label/label.component.tsx +20 -0
  127. package/src/components/label/label.scss +11 -0
  128. package/src/components/loaders/loader.component.tsx +16 -0
  129. package/src/components/loaders/loader.scss +20 -0
  130. package/src/components/patient-banner/patient-banner.component.tsx +20 -0
  131. package/src/components/patient-banner/patient-banner.scss +12 -0
  132. package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
  133. package/src/components/previous-value-review/previous-value-review.scss +36 -0
  134. package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
  135. package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
  136. package/src/components/renderer/field/fieldLogic.ts +214 -0
  137. package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
  138. package/src/components/renderer/field/form-field-renderer.scss +5 -0
  139. package/src/components/renderer/form/form-renderer.component.tsx +89 -0
  140. package/src/components/renderer/form/state.ts +54 -0
  141. package/src/components/renderer/page/page.renderer.component.tsx +50 -0
  142. package/src/components/renderer/page/page.renderer.scss +36 -0
  143. package/src/components/renderer/section/section-renderer.component.tsx +21 -0
  144. package/src/components/renderer/section/section-renderer.scss +19 -0
  145. package/src/components/repeat/helpers.test.ts +29 -0
  146. package/src/components/repeat/helpers.ts +68 -0
  147. package/src/components/repeat/repeat-controls.component.tsx +38 -0
  148. package/src/components/repeat/repeat-controls.scss +7 -0
  149. package/src/components/repeat/repeat.component.tsx +201 -0
  150. package/src/components/repeat/repeat.scss +30 -0
  151. package/src/components/repeat/repeat.test.ts +29 -0
  152. package/src/components/sidebar/sidebar.component.tsx +134 -0
  153. package/src/components/sidebar/sidebar.scss +121 -0
  154. package/src/components/value/value.component.tsx +27 -0
  155. package/src/components/value/value.scss +17 -0
  156. package/src/components/value/view/field-value-view.component.tsx +33 -0
  157. package/src/components/value/view/field-value-view.scss +31 -0
  158. package/src/constants.ts +12 -0
  159. package/src/datasources/concept-data-source.ts +42 -0
  160. package/src/datasources/data-source.ts +23 -0
  161. package/src/datasources/encounter-role-datasource.ts +15 -0
  162. package/src/datasources/historical-data-source.ts +11 -0
  163. package/src/datasources/location-data-source.ts +27 -0
  164. package/src/datasources/provider-datasource.ts +15 -0
  165. package/src/datasources/select-concept-answers-datasource.ts +15 -0
  166. package/src/declarations.d.ts +4 -0
  167. package/src/external-function-context.tsx +8 -0
  168. package/src/form-context.tsx +42 -0
  169. package/src/form-engine.component.tsx +178 -0
  170. package/src/form-engine.scss +140 -0
  171. package/src/form-engine.test.tsx +817 -0
  172. package/src/globals.ts +1 -0
  173. package/src/hooks/useClobData.tsx +21 -0
  174. package/src/hooks/useConcepts.tsx +55 -0
  175. package/src/hooks/useDatasourceDependentValue.ts +16 -0
  176. package/src/hooks/useEncounter.tsx +32 -0
  177. package/src/hooks/useEncounterRole.tsx +15 -0
  178. package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
  179. package/src/hooks/useFieldValidationResults.ts +18 -0
  180. package/src/hooks/useFormCollapse.tsx +36 -0
  181. package/src/hooks/useFormFieldValidators.ts +22 -0
  182. package/src/hooks/useFormFieldValueAdapters.ts +24 -0
  183. package/src/hooks/useFormFields.ts +37 -0
  184. package/src/hooks/useFormFieldsMeta.ts +48 -0
  185. package/src/hooks/useFormJson.test.tsx +173 -0
  186. package/src/hooks/useFormJson.tsx +237 -0
  187. package/src/hooks/useFormStateHelpers.ts +50 -0
  188. package/src/hooks/useFormsConfig.tsx +27 -0
  189. package/src/hooks/useInitialValues.ts +38 -0
  190. package/src/hooks/usePatientData.tsx +32 -0
  191. package/src/hooks/usePatientPrograms.ts +32 -0
  192. package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
  193. package/src/hooks/usePostSubmissionActions.ts +31 -0
  194. package/src/hooks/useProcessorDependencies.ts +30 -0
  195. package/src/hooks/useRestMaxResultsCount.ts +5 -0
  196. package/src/hooks/useSystemSetting.ts +36 -0
  197. package/src/hooks/useWorkspaceLayout.ts +29 -0
  198. package/src/index.ts +12 -0
  199. package/src/lifecycle.ts +33 -0
  200. package/src/post-submission-actions/program-enrollment-action.ts +138 -0
  201. package/src/processors/encounter/encounter-form-processor.ts +337 -0
  202. package/src/processors/encounter/encounter-processor-helper.ts +320 -0
  203. package/src/processors/form-processor.ts +41 -0
  204. package/src/provider/form-factory-helper.ts +100 -0
  205. package/src/provider/form-factory-provider.tsx +169 -0
  206. package/src/provider/form-provider.tsx +37 -0
  207. package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
  208. package/src/registry/inbuilt-components/control-templates.ts +57 -0
  209. package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
  210. package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
  211. package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
  212. package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
  213. package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
  214. package/src/registry/inbuilt-components/template-component-map.ts +28 -0
  215. package/src/registry/registry.test.ts +20 -0
  216. package/src/registry/registry.ts +261 -0
  217. package/src/routes.json +1 -0
  218. package/src/setupI18n.ts +16 -0
  219. package/src/setupTests.ts +5 -0
  220. package/src/transformers/default-schema-transformer.test.ts +155 -0
  221. package/src/transformers/default-schema-transformer.ts +239 -0
  222. package/src/types/domain.ts +129 -0
  223. package/src/types/index.ts +130 -0
  224. package/src/types/schema.ts +238 -0
  225. package/src/typings.d.ts +9 -0
  226. package/src/utils/boolean-utils.ts +25 -0
  227. package/src/utils/common-expression-helpers.ts +503 -0
  228. package/src/utils/common-utils.test.ts +136 -0
  229. package/src/utils/common-utils.ts +55 -0
  230. package/src/utils/error-utils.ts +37 -0
  231. package/src/utils/expression-parser.test.ts +308 -0
  232. package/src/utils/expression-parser.ts +158 -0
  233. package/src/utils/expression-runner.test.ts +387 -0
  234. package/src/utils/expression-runner.ts +219 -0
  235. package/src/utils/form-helper.test.ts +482 -0
  236. package/src/utils/form-helper.ts +210 -0
  237. package/src/utils/form-page-utils.ts +13 -0
  238. package/src/utils/forms-loader.test.ts +323 -0
  239. package/src/utils/forms-loader.ts +306 -0
  240. package/src/utils/post-submission-action-helper.ts +71 -0
  241. package/src/utils/test-utils.ts +54 -0
  242. package/src/utils/zscore-service.ts +59 -0
  243. package/src/validators/conditional-answered-validator.test.ts +61 -0
  244. package/src/validators/conditional-answered-validator.ts +17 -0
  245. package/src/validators/date-validator.test.ts +46 -0
  246. package/src/validators/date-validator.ts +19 -0
  247. package/src/validators/default-value-validator.test.ts +90 -0
  248. package/src/validators/default-value-validator.ts +36 -0
  249. package/src/validators/form-validator.test.ts +188 -0
  250. package/src/validators/form-validator.ts +95 -0
  251. package/src/validators/js-expression-validator.test.ts +118 -0
  252. package/src/validators/js-expression-validator.ts +44 -0
  253. package/src/validators/schema.ts +34 -0
  254. package/src/zscore/bfa_boys_5_above.json +2522 -0
  255. package/src/zscore/bfa_girls_5_above.json +2522 -0
  256. package/src/zscore/hfa_boys_5_above.json +2186 -0
  257. package/src/zscore/hfa_boys_below5.json +22286 -0
  258. package/src/zscore/hfa_girls_5_above.json +2186 -0
  259. package/src/zscore/hfa_girls_below5.json +22286 -0
  260. package/src/zscore/wfl_boys_below5.json +7814 -0
  261. package/src/zscore/wfl_girls_below5.json +7814 -0
  262. package/src/zscore-tests/bmi-age.test.tsx +88 -0
  263. package/src/zscore-tests/height-age.test.tsx +96 -0
  264. package/src/zscore-tests/weight-height.test.tsx +87 -0
  265. package/tools/i18next-parser.config.js +93 -0
  266. package/translations/en.json +47 -0
  267. package/translations/es.json +38 -0
  268. package/translations/fr.json +38 -0
  269. package/translations/km.json +38 -0
  270. package/tsconfig.json +19 -0
  271. package/turbo.json +15 -0
  272. 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,15 @@
1
+ @use '@carbon/colors';
2
+
3
+ .boldedLabel {
4
+ flex: 95%;
5
+
6
+ label {
7
+ font-weight: 600;
8
+ color: colors.$black-100;
9
+ }
10
+ }
11
+
12
+ .errorLabel label {
13
+ color: colors.$red-60;
14
+ font-weight: 600;
15
+ }
@@ -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,7 @@
1
+ @use '@carbon/colors';
2
+
3
+ .unspecified > div label {
4
+ padding-top: 0.3rem;
5
+ font-size: 12px;
6
+ color: colors.$gray-70;
7
+ }
@@ -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,15 @@
1
+ @use '@carbon/colors';
2
+
3
+ .label {
4
+ font-weight: 600;
5
+ color: colors.$black-100;
6
+ }
7
+
8
+ .errorLabel label {
9
+ color: colors.$red-60;
10
+ font-weight: 600;
11
+ }
12
+
13
+ .workspaceButton {
14
+ margin: 1rem 0;
15
+ }
@@ -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,11 @@
1
+ .label {
2
+ :global(.cds--assistive-text) {
3
+ max-width: 30rem !important;
4
+ font-size: 0.75rem;
5
+ white-space: pre-line !important;
6
+ }
7
+
8
+ :global(.cds--label) {
9
+ font-weight: bolder;
10
+ }
11
+ }
@@ -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;