@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,37 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineNotification } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { fieldRequiredErrCode, fieldOutOfBoundErrCode } from '../../validators/form-validator';
5
+ import type { ValidationResult } from '../../types';
6
+ import styles from './error.scss';
7
+
8
+ const ErrorModal: React.FC<{ errors: ValidationResult[] }> = ({ errors }) => {
9
+ const { t } = useTranslation();
10
+
11
+ const errorMessage = useMemo(() => {
12
+ const errorMessages: { [key: string]: string } = {};
13
+
14
+ errors.forEach((error) => {
15
+ if (error?.errCode === fieldRequiredErrCode && !errorMessages[fieldRequiredErrCode]) {
16
+ errorMessages[fieldRequiredErrCode] = t('nullMandatoryField', 'Please fill the required fields');
17
+ } else if (error?.errCode === fieldOutOfBoundErrCode && !errorMessages[fieldOutOfBoundErrCode]) {
18
+ errorMessages[fieldOutOfBoundErrCode] = t('valuesOutOfBound', 'Some of the values are out of bounds');
19
+ }
20
+ });
21
+
22
+ return Object.values(errorMessages).map((error: string, index) => <span key={index}>{error}</span>);
23
+ }, [errors, t]);
24
+
25
+ return (
26
+ <InlineNotification
27
+ role="alert"
28
+ className={styles.inlineErrorMessage}
29
+ kind="error"
30
+ lowContrast={true}
31
+ title={t('fieldErrorDescriptionTitle', 'Validation Errors')}
32
+ subtitle={errorMessage}
33
+ />
34
+ );
35
+ };
36
+
37
+ export default ErrorModal;
@@ -0,0 +1,4 @@
1
+
2
+ .inlineErrorMessage {
3
+ margin: 0.75rem;
4
+ }
@@ -0,0 +1,32 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import { attach, ExtensionSlot } from '@openmrs/esm-framework';
4
+ import { type FormFieldInputProps } from '../../types';
5
+ import { useFormProviderContext } from '../../provider/form-provider';
6
+
7
+ const ExtensionParcel: React.FC<FormFieldInputProps> = ({ field }) => {
8
+ const submissionNotifier = useMemo(() => new BehaviorSubject<{ isSubmitting: boolean }>({ isSubmitting: false }), []);
9
+ const { isSubmitting, patient } = useFormProviderContext();
10
+
11
+ const state = useMemo(() => ({ patientUuid: patient.id, submissionNotifier }), [patient.id, submissionNotifier]);
12
+
13
+ useEffect(() => {
14
+ if (field.questionOptions.extensionSlotName && field.questionOptions.extensionId) {
15
+ attach(field.questionOptions.extensionSlotName, field.questionOptions.extensionId);
16
+ }
17
+ }, []);
18
+
19
+ useEffect(() => {
20
+ submissionNotifier.next({ isSubmitting: isSubmitting });
21
+ }, [isSubmitting, submissionNotifier]);
22
+
23
+ return (
24
+ <>
25
+ {field.questionOptions.extensionSlotName && (
26
+ <ExtensionSlot name={field.questionOptions.extensionSlotName} state={state} />
27
+ )}
28
+ </>
29
+ );
30
+ };
31
+
32
+ export default ExtensionParcel;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { type FormField } from '../../types';
4
+ import Tooltip from '../inputs/tooltip/tooltip.component';
5
+
6
+ import styles from './field-label.scss';
7
+
8
+ interface FieldLabelProps {
9
+ field: FormField;
10
+ /**
11
+ * Custom label text to override the default field label.
12
+ */
13
+ customLabel?: string;
14
+ }
15
+
16
+ const FieldLabel: React.FC<FieldLabelProps> = ({ field, customLabel }) => {
17
+ const { t } = useTranslation();
18
+ const labelText = customLabel || t(field.label);
19
+ return (
20
+ <div className={styles.questionLabel}>
21
+ <span>{labelText}</span>
22
+ {field.isRequired && (
23
+ <span title={t('required', 'Required')} className={styles.required}>
24
+ *
25
+ </span>
26
+ )}
27
+ {field.questionInfo && <Tooltip field={field} />}
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export default FieldLabel;
@@ -0,0 +1,11 @@
1
+ @use '@carbon/colors';
2
+
3
+ .required {
4
+ color: colors.$red-60;
5
+ margin-left: 0.25rem;
6
+ }
7
+
8
+ .questionLabel {
9
+ display: flex;
10
+ align-items: center;
11
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import { type FormFieldInputProps } from '../../types';
4
+ import styles from './obs-group.scss';
5
+ import { FormFieldRenderer } from '../renderer/field/form-field-renderer.component';
6
+ import { useFormProviderContext } from '../../provider/form-provider';
7
+
8
+ export const ObsGroup: React.FC<FormFieldInputProps> = ({ field }) => {
9
+ const { formFieldAdapters } = useFormProviderContext();
10
+
11
+ const groupContent = field.questions
12
+ ?.filter((child) => !child.isHidden)
13
+ .map((child, index) => {
14
+ const keyId = child.id + '_' + index;
15
+ if (formFieldAdapters[child.type]) {
16
+ return (
17
+ <div className={classNames(styles.flexColumn)} key={keyId}>
18
+ <div className={styles.groupContainer}>
19
+ <FormFieldRenderer field={child} valueAdapter={formFieldAdapters[child.type]} />
20
+ </div>
21
+ </div>
22
+ );
23
+ }
24
+ });
25
+
26
+ return <div className={styles.flexRow}>{groupContent}</div>;
27
+ };
28
+
29
+ export default ObsGroup;
@@ -0,0 +1,12 @@
1
+ .flexColumn {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .flexFullWidth {
7
+ flex-basis: 100%;
8
+ }
9
+
10
+ .groupContainer {
11
+ margin: 0.5rem 0;
12
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import classNames from 'classnames';
4
+ import { FormGroup, ContentSwitcher as CdsContentSwitcher, Switch } from '@carbon/react';
5
+ import { shouldUseInlineLayout } from '../../../utils/form-helper';
6
+ import { isTrue } from '../../../utils/boolean-utils';
7
+ import { type FormFieldInputProps } from '../../../types';
8
+ import FieldValueView from '../../value/view/field-value-view.component';
9
+ import styles from './content-switcher.scss';
10
+ import { useFormProviderContext } from '../../../provider/form-provider';
11
+ import FieldLabel from '../../field-label/field-label.component';
12
+
13
+ const ContentSwitcher: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings, setFieldValue }) => {
14
+ const { t } = useTranslation();
15
+ const { layoutType, sessionMode, workspaceLayout, formFieldAdapters } = useFormProviderContext();
16
+
17
+ const handleChange = useCallback(
18
+ (value) => {
19
+ setFieldValue(value.name);
20
+ },
21
+ [setFieldValue],
22
+ );
23
+
24
+ const selectedIndex = useMemo(
25
+ () => field.questionOptions.answers.findIndex((option) => option.concept == value),
26
+ [value, field.questionOptions.answers],
27
+ );
28
+
29
+ const isInline = useMemo(() => {
30
+ if (['view', 'embedded-view'].includes(sessionMode) || isTrue(field.readonly)) {
31
+ return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode);
32
+ }
33
+ return false;
34
+ }, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]);
35
+
36
+ return sessionMode == 'view' || sessionMode == 'embedded-view' || isTrue(field.readonly) ? (
37
+ <div className={styles.formField}>
38
+ <FieldValueView
39
+ label={t(field.label)}
40
+ value={value ? formFieldAdapters[field.type].getDisplayValue(field, value) : value}
41
+ conceptName={field.meta?.concept?.display}
42
+ isInline={isInline}
43
+ />
44
+ </div>
45
+ ) : (
46
+ !field.isHidden && (
47
+ <FormGroup
48
+ legendText={
49
+ <div className={styles.boldedLegend}>
50
+ <FieldLabel field={field} />
51
+ </div>
52
+ }
53
+ className={classNames({
54
+ [styles.errorLegend]: errors.length > 0,
55
+ [styles.boldedLegend]: errors.length === 0,
56
+ })}>
57
+ <CdsContentSwitcher
58
+ onChange={handleChange}
59
+ selectedIndex={selectedIndex}
60
+ className={styles.selectedOption}
61
+ size="md">
62
+ {field.questionOptions.answers.map((option, index) => (
63
+ <Switch name={option.concept || option.value} text={option.label} key={index} disabled={field.isDisabled} />
64
+ ))}
65
+ </CdsContentSwitcher>
66
+ </FormGroup>
67
+ )
68
+ );
69
+ };
70
+
71
+ export default ContentSwitcher;
@@ -0,0 +1,55 @@
1
+ @use '@carbon/colors';
2
+
3
+ .formField {
4
+ margin-top: 0.625rem;
5
+ }
6
+
7
+ .formField > div > div > label {
8
+ color: colors.$gray-70;
9
+ }
10
+
11
+ .textContainer {
12
+ label {
13
+ font-size: 0.875rem !important;
14
+ }
15
+ }
16
+
17
+ .errorLegend legend,
18
+ .errorLabel label {
19
+ color: colors.$red-60 !important;
20
+ }
21
+
22
+ .boldedLegend {
23
+ font-weight: 600;
24
+ color: black;
25
+ }
26
+
27
+ .selectedOption {
28
+ border-radius: 4px;
29
+ width: 15rem;
30
+
31
+ button {
32
+ background-color: colors.$gray-10;
33
+
34
+ &:before {
35
+ display: none !important;
36
+ }
37
+ }
38
+ }
39
+
40
+ .switchOverrides {
41
+ background-color: colors.$blue-10 !important;
42
+ border: 1px solid colors.$blue-60 !important;
43
+ color: colors.$blue-60 !important;
44
+ padding: 0 0 0 1rem;
45
+
46
+ button:focus {
47
+ border: none !important;
48
+ }
49
+ }
50
+
51
+ .sansSwitchOverrides {
52
+ border: solid 1px colors.$blue-30 !important;
53
+ background-color: colors.$gray-10 !important;
54
+ padding: 0 0 0 1rem;
55
+ }
@@ -0,0 +1,149 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, TimePicker } from '@carbon/react';
4
+ import classNames from 'classnames';
5
+ import { type FormFieldInputProps } from '../../../types';
6
+ import { isTrue } from '../../../utils/boolean-utils';
7
+ import { shouldUseInlineLayout } from '../../../utils/form-helper';
8
+ import { isEmpty } from '../../../validators/form-validator';
9
+ import FieldValueView from '../../value/view/field-value-view.component';
10
+ import styles from './date.scss';
11
+ import { OpenmrsDatePicker, formatDate, formatTime } from '@openmrs/esm-framework';
12
+ import { useFormProviderContext } from '../../../provider/form-provider';
13
+ import FieldLabel from '../../field-label/field-label.component';
14
+
15
+ const DateField: React.FC<FormFieldInputProps> = ({ field, value: dateValue, errors, warnings, setFieldValue }) => {
16
+ const { t } = useTranslation();
17
+ const [time, setTime] = useState('');
18
+ const { layoutType, sessionMode, workspaceLayout } = useFormProviderContext();
19
+ const isInline = useMemo(() => {
20
+ if (['view', 'embedded-view'].includes(sessionMode) || isTrue(field.readonly)) {
21
+ return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode);
22
+ }
23
+ return false;
24
+ }, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]);
25
+
26
+ const onDateChange = useCallback(
27
+ (date: Date) => {
28
+ setTimeIfPresent(date, time);
29
+ setFieldValue(date);
30
+ },
31
+ [setFieldValue, time],
32
+ );
33
+
34
+ const setTimeIfPresent = useCallback((date: Date, time: string) => {
35
+ if (!isEmpty(time)) {
36
+ const [hours, minutes] = time.split(':').map(Number);
37
+ date.setHours(hours ?? 0, minutes ?? 0);
38
+ }
39
+ }, []);
40
+
41
+ const onTimeChange = useCallback(
42
+ (event) => {
43
+ const time = event.target.value;
44
+ setTime(time);
45
+ // TODO: Confirm if a new date should be instantiated when the date picker format is 'timer'
46
+ // If the underlying concept's datatype is 'Time', then the backend expects a time string
47
+ const date = field.datePickerFormat === 'timer' ? new Date() : new Date(dateValue);
48
+ setTimeIfPresent(date, time);
49
+ setFieldValue(date);
50
+ },
51
+ [setFieldValue, setTimeIfPresent, dateValue],
52
+ );
53
+
54
+ useEffect(() => {
55
+ if (dateValue) {
56
+ if (dateValue instanceof Date) {
57
+ const hours = dateValue.getHours() < 10 ? `0${dateValue.getHours()}` : `${dateValue.getHours()}`;
58
+ const minutes = dateValue.getMinutes() < 10 ? `0${dateValue.getMinutes()}` : `${dateValue.getMinutes()}`;
59
+ setTime([hours, minutes].join(':'));
60
+ }
61
+ }
62
+ }, [dateValue]);
63
+
64
+ const timePickerLabel = useMemo(
65
+ () =>
66
+ field.datePickerFormat === 'timer' ? (
67
+ <FieldLabel field={field} />
68
+ ) : (
69
+ <FieldLabel field={field} customLabel={t('time', 'Time')} />
70
+ ),
71
+ [field.datePickerFormat, field.label, t],
72
+ );
73
+
74
+ return sessionMode == 'view' || sessionMode == 'embedded-view' ? (
75
+ <FieldValueView
76
+ label={t(field.label)}
77
+ value={dateValue instanceof Date ? getDisplay(dateValue, field.datePickerFormat) : dateValue}
78
+ conceptName={field.meta?.concept?.display}
79
+ isInline={isInline}
80
+ />
81
+ ) : (
82
+ !field.isHidden && (
83
+ <>
84
+ <div className={styles.datetime}>
85
+ {(field.datePickerFormat === 'calendar' || field.datePickerFormat === 'both') && (
86
+ <div className={styles.datePickerSpacing}>
87
+ <Layer>
88
+ <OpenmrsDatePicker
89
+ id={field.id}
90
+ onChange={onDateChange}
91
+ labelText={
92
+ <span className={styles.datePickerLabel}>
93
+ <FieldLabel field={field} />
94
+ </span>
95
+ }
96
+ isDisabled={field.isDisabled}
97
+ isReadOnly={isTrue(field.readonly)}
98
+ isRequired={field.isRequired ?? false}
99
+ isInvalid={errors.length > 0}
100
+ invalidText={errors[0]?.message}
101
+ value={dateValue}
102
+ />
103
+ </Layer>
104
+ {warnings.length > 0 ? <div className={styles.datePickerWarn}>{warnings[0]?.message}</div> : null}
105
+ </div>
106
+ )}
107
+
108
+ {field.datePickerFormat === 'both' || field.datePickerFormat === 'timer' ? (
109
+ <div>
110
+ <Layer>
111
+ <TimePicker
112
+ className={classNames(styles.boldedLabel, styles.timeInput)}
113
+ id={field.id}
114
+ labelText={timePickerLabel}
115
+ placeholder="HH:MM"
116
+ pattern="(1[012]|[1-9]):[0-5][0-9])$"
117
+ type="time"
118
+ disabled={field.datePickerFormat === 'timer' ? field.isDisabled : !dateValue ? true : false}
119
+ invalid={errors.length > 0}
120
+ invalidText={errors[0]?.message}
121
+ warning={warnings.length > 0}
122
+ warningText={warnings[0]?.message}
123
+ value={
124
+ time
125
+ ? time
126
+ : dateValue instanceof Date
127
+ ? dateValue.toLocaleDateString(window.navigator.language)
128
+ : dateValue
129
+ }
130
+ onChange={onTimeChange}
131
+ />
132
+ </Layer>
133
+ </div>
134
+ ) : null}
135
+ </div>
136
+ </>
137
+ )
138
+ );
139
+ };
140
+
141
+ function getDisplay(date: Date, rendering: string) {
142
+ const dateString = formatDate(date);
143
+ if (rendering == 'both') {
144
+ return `${dateString} ${formatTime(date)}`;
145
+ }
146
+ return dateString;
147
+ }
148
+
149
+ export default DateField;
@@ -0,0 +1,36 @@
1
+ @use '@carbon/colors';
2
+
3
+ .datePickerWarn {
4
+ font-size: 0.75rem;
5
+ color: colors.$black-100;
6
+ }
7
+
8
+ .datePickerLabel {
9
+ font-weight: 600;
10
+ color: colors.$black-100;
11
+ }
12
+
13
+ .boldedLabel label {
14
+ font-weight: 600;
15
+ color: colors.$black-100;
16
+ }
17
+
18
+ .datetime {
19
+ display: flex;
20
+ flex-direction: row;
21
+ flex-wrap: wrap;
22
+
23
+ > .datePickerSpacing {
24
+ margin-right: 1rem;
25
+ }
26
+ }
27
+
28
+ .timeInput {
29
+ width: auto;
30
+ :hover {
31
+ cursor: text;
32
+ }
33
+ :focus {
34
+ cursor: text;
35
+ }
36
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import Webcam from 'react-webcam';
3
+ import { Button } from '@carbon/react';
4
+ import { Camera as CameraIcon } from '@carbon/react/icons';
5
+
6
+ import styles from './camera.scss';
7
+
8
+ interface CameraProps {
9
+ handleImages: (state: any) => void;
10
+ }
11
+
12
+ const Camera: React.FC<CameraProps> = ({ handleImages }) => {
13
+ const webcamRef = React.useRef(null);
14
+
15
+ const capture = React.useCallback(() => {
16
+ const imageSrc = webcamRef.current.getScreenshot();
17
+ handleImages(imageSrc);
18
+ }, [webcamRef]);
19
+
20
+ const videoConstraints = {
21
+ facingMode: 'user',
22
+ };
23
+
24
+ return (
25
+ <div>
26
+ <Webcam audio={false} ref={webcamRef} screenshotFormat="image/png" videoConstraints={videoConstraints} />
27
+ <div className={styles.captureButton}>
28
+ <Button onClick={capture} type="button" hasIconOnly renderIcon={() => <CameraIcon size={24} />}></Button>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default Camera;
@@ -0,0 +1,3 @@
1
+ .captureButton {
2
+ margin: 0.5rem 0;
3
+ }
@@ -0,0 +1,159 @@
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import { FileUploader, Button } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { isTrue } from '../../../utils/boolean-utils';
5
+ import Camera from './camera/camera.component';
6
+ import { Close, DocumentPdf } from '@carbon/react/icons';
7
+ import styles from './file.scss';
8
+ import { type FormFieldInputProps } from '../../../types';
9
+ import { useFormProviderContext } from '../../../provider/form-provider';
10
+ import { isViewMode } from '../../../utils/common-utils';
11
+ import FieldValueView from '../../value/view/field-value-view.component';
12
+ import FieldLabel from '../../field-label/field-label.component';
13
+
14
+ type DataSourceType = 'filePicker' | 'camera' | null;
15
+
16
+ const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) => {
17
+ const { t } = useTranslation();
18
+ const [cameraWidgetVisible, setCameraWidgetVisible] = useState(false);
19
+ const [imagePreview, setImagePreview] = useState(null);
20
+ const [dataSource, setDataSource] = useState<DataSourceType>(null);
21
+ const { sessionMode } = useFormProviderContext();
22
+
23
+ const labelDescription = useMemo(() => {
24
+ return field.questionOptions.allowedFileTypes
25
+ ? t(
26
+ 'fileUploadDescription',
27
+ `Upload one of the following file types: ${field.questionOptions.allowedFileTypes.map(
28
+ (eachItem) => ` ${eachItem}`,
29
+ )}`,
30
+ )
31
+ : t('fileUploadDescriptionAny', 'Upload any file type');
32
+ }, [field.questionOptions.allowedFileTypes, t]);
33
+
34
+ const handleFilePickerChange = useCallback(
35
+ (event) => {
36
+ // TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682
37
+ const [selectedFile]: File[] = Array.from(event.target.files);
38
+ setImagePreview(null);
39
+ setFieldValue(selectedFile);
40
+ },
41
+ [setFieldValue],
42
+ );
43
+
44
+ const handleCameraImageChange = useCallback(
45
+ (newImage) => {
46
+ setImagePreview(newImage);
47
+ setCameraWidgetVisible(false);
48
+ setFieldValue(newImage);
49
+ },
50
+ [setFieldValue],
51
+ );
52
+
53
+ if (isViewMode(sessionMode) && !value) {
54
+ return (
55
+ <FieldValueView label={t(field.label)} value={null} conceptName={field.meta.concept?.display} isInline={false} />
56
+ );
57
+ }
58
+
59
+ return isViewMode(sessionMode) ? (
60
+ <div>
61
+ <div className={styles.label}>{t(field.label)}</div>
62
+ <div className={styles.editModeImage}>
63
+ <div className={styles.imageContent}>
64
+ {value.bytesContentFamily === 'PDF' ? (
65
+ <div className={styles.pdfThumbnail} role="button" tabIndex={0}>
66
+ <DocumentPdf size={24} />
67
+ </div>
68
+ ) : (
69
+ <img src={value.src} alt={t('preview', 'Preview')} width="200px" />
70
+ )}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ ) : (
75
+ <div>
76
+ <div className={styles.label}>
77
+ <FieldLabel field={field} />
78
+ </div>
79
+ <div className={styles.uploadSelector}>
80
+ <div className={styles.selectorButton}>
81
+ <Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('filePicker')}>
82
+ {t('uploadImage', 'Upload image')}
83
+ </Button>
84
+ </div>
85
+ <div className={styles.selectorButton}>
86
+ <Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('camera')}>
87
+ {t('cameraCapture', 'Camera capture')}
88
+ </Button>
89
+ </div>
90
+ </div>
91
+ {!dataSource && value && (
92
+ <div className={styles.editModeImage}>
93
+ <div className={styles.imageContent}>
94
+ {value.bytesContentFamily === 'PDF' ? (
95
+ <div className={styles.pdfThumbnail} role="button" tabIndex={0}>
96
+ <DocumentPdf size={24} />
97
+ </div>
98
+ ) : (
99
+ <img src={value.src} alt="Preview" width="200px" />
100
+ )}
101
+ </div>
102
+ </div>
103
+ )}
104
+ {dataSource === 'filePicker' && (
105
+ <div className={styles.fileUploader}>
106
+ <FileUploader
107
+ accept={field.questionOptions.allowedFileTypes ?? []}
108
+ buttonKind="primary"
109
+ buttonLabel={t('addFile', 'Add files')}
110
+ filenameStatus="edit"
111
+ iconDescription={t('clearFile', 'Clear file')}
112
+ labelDescription={labelDescription}
113
+ labelTitle={t('upload', 'Upload')}
114
+ // TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682
115
+ // multiple={field.questionOptions.allowMultiple}
116
+ onChange={handleFilePickerChange}
117
+ />
118
+ </div>
119
+ )}
120
+ {dataSource === 'camera' && (
121
+ <div className={styles.cameraUploader}>
122
+ <div className={styles.camButton}>
123
+ <p className={styles.titleStyles}>Camera</p>
124
+ <p className={styles.descriptionStyles}>Capture image via camera</p>
125
+ <Button onClick={() => setCameraWidgetVisible((prevState) => !prevState)} size="md">
126
+ {cameraWidgetVisible ? t('closeCamera', 'Close camera') : t('addCameraImage', 'Add camera image')}
127
+ </Button>
128
+ </div>
129
+ {cameraWidgetVisible && (
130
+ <div className={styles.cameraPreview}>
131
+ <Camera handleImages={handleCameraImageChange} />
132
+ </div>
133
+ )}
134
+ {imagePreview && (
135
+ <div className={styles.capturedImage}>
136
+ <div className={styles.imageContent}>
137
+ <img src={imagePreview} alt={t('preview', 'Preview')} width="200px" />
138
+ <div className={styles.caption}>
139
+ <p>{t('uploadedPhoto', 'Uploaded photo')}</p>
140
+ <div
141
+ tabIndex={0}
142
+ role="button"
143
+ onClick={() => {
144
+ setImagePreview(null);
145
+ }}
146
+ className={styles.closeIcon}>
147
+ <Close />
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )}
153
+ </div>
154
+ )}
155
+ </div>
156
+ );
157
+ };
158
+
159
+ export default File;