@openmrs/esm-form-engine-lib 3.2.0 → 3.3.1-pre.2102

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.
@@ -1,56 +1,57 @@
1
- import React, { useState, useMemo, useCallback } from 'react';
2
- import { FileUploader, Button } from '@carbon/react';
1
+ import React, { useCallback } from 'react';
2
+ import { Button } from '@carbon/react';
3
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';
4
+ import { Add } from '@carbon/react/icons';
7
5
  import styles from './file.scss';
8
- import { type FormFieldInputProps } from '../../../types';
6
+ import { type Attachment, type FormFieldInputProps } from '../../../types';
9
7
  import { useFormProviderContext } from '../../../provider/form-provider';
10
8
  import { isViewMode } from '../../../utils/common-utils';
11
9
  import FieldValueView from '../../value/view/field-value-view.component';
12
10
  import FieldLabel from '../../field-label/field-label.component';
11
+ import { showModal, type UploadedFile, useLayoutType } from '@openmrs/esm-framework';
12
+ import { FileThumbnail } from './file-thumbnail.component';
13
+ import classNames from 'classnames';
13
14
 
14
- type DataSourceType = 'filePicker' | 'camera' | null;
15
-
16
- const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) => {
15
+ const File: React.FC<FormFieldInputProps<Array<Attachment>>> = ({ field, value, setFieldValue }) => {
17
16
  const { t } = useTranslation();
18
- const [cameraWidgetVisible, setCameraWidgetVisible] = useState(false);
19
- const [imagePreview, setImagePreview] = useState(null);
20
- const [dataSource, setDataSource] = useState<DataSourceType>(null);
21
17
  const { sessionMode } = useFormProviderContext();
18
+ const isTablet = useLayoutType() === 'tablet';
22
19
 
23
- const labelDescription = useMemo(() => {
24
- return field.questionOptions.allowedFileTypes
25
- ? t(
26
- 'fileUploadDescription',
27
- 'Upload one of the following file types: {{fileTypes}}',
28
- {
29
- fileTypes: field.questionOptions.allowedFileTypes.map(
30
- (eachItem) => ` ${eachItem}`,
31
- )
32
- }
33
- )
34
- : t('fileUploadDescriptionAny', 'Upload any file type');
35
- }, [field.questionOptions.allowedFileTypes, t]);
20
+ const showImageCaptureModal = useCallback(() => {
21
+ const close = showModal('capture-photo-modal', {
22
+ saveFile: (file: UploadedFile) => {
23
+ if (file.capturedFromWebcam && !file.fileName.includes('.')) {
24
+ file.fileName = `${file.fileName}.png`;
25
+ }
26
+ const currentFiles = value ? value : [];
27
+ setFieldValue([...currentFiles, file]);
28
+ close();
29
+ return Promise.resolve();
30
+ },
31
+ closeModal: () => {
32
+ close();
33
+ },
34
+ allowedExtensions: field.questionOptions.allowedFileTypes,
35
+ multipleFiles: field.questionOptions.allowMultiple,
36
+ collectDescription: true,
37
+ });
38
+ }, [value, field]);
36
39
 
37
- const handleFilePickerChange = useCallback(
38
- (event) => {
39
- // TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682
40
- const [selectedFile]: File[] = Array.from(event.target.files);
41
- setImagePreview(null);
42
- setFieldValue(selectedFile);
40
+ const handleRemoveFile = useCallback(
41
+ (index: number) => {
42
+ const buffer = [...value];
43
+ const attachment = buffer[index];
44
+ if (attachment.uuid) {
45
+ buffer[index] = {
46
+ ...attachment,
47
+ voided: true,
48
+ };
49
+ } else {
50
+ buffer.splice(index, 1);
51
+ }
52
+ setFieldValue(buffer);
43
53
  },
44
- [setFieldValue],
45
- );
46
-
47
- const handleCameraImageChange = useCallback(
48
- (newImage) => {
49
- setImagePreview(newImage);
50
- setCameraWidgetVisible(false);
51
- setFieldValue(newImage);
52
- },
53
- [setFieldValue],
54
+ [value],
54
55
  );
55
56
 
56
57
  if (isViewMode(sessionMode) && !value) {
@@ -59,102 +60,37 @@ const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) =>
59
60
  );
60
61
  }
61
62
 
62
- return isViewMode(sessionMode) ? (
63
+ return (
63
64
  <div>
64
- <div className={styles.label}>{t(field.label)}</div>
65
- <div className={styles.editModeImage}>
66
- <div className={styles.imageContent}>
67
- {value.bytesContentFamily === 'PDF' ? (
68
- <div className={styles.pdfThumbnail} role="button" tabIndex={0}>
69
- <DocumentPdf size={24} />
70
- </div>
71
- ) : (
72
- <img src={value.src} alt={t('preview', 'Preview')} width="200px" />
73
- )}
74
- </div>
75
- </div>
76
- </div>
77
- ) : (
78
- <div>
79
- <div className={styles.label}>
65
+ <div className={classNames(styles.label, 'cds--label')}>
80
66
  <FieldLabel field={field} />
81
67
  </div>
82
- <div className={styles.uploadSelector}>
83
- <div className={styles.selectorButton}>
84
- <Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('filePicker')}>
85
- {t('uploadImage', 'Upload image')}
86
- </Button>
87
- </div>
88
- <div className={styles.selectorButton}>
89
- <Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('camera')}>
90
- {t('cameraCapture', 'Camera capture')}
68
+ {!isViewMode(sessionMode) && (
69
+ <div>
70
+ <Button
71
+ className={styles.uploadButton}
72
+ kind={isTablet ? 'ghost' : 'tertiary'}
73
+ onClick={showImageCaptureModal}
74
+ renderIcon={(props) => <Add size={16} {...props} />}>
75
+ {field.questionOptions.buttonLabel ? t(field.questionOptions.buttonLabel) : t('addFile', 'Add file')}
91
76
  </Button>
92
77
  </div>
93
- </div>
94
- {!dataSource && value && (
95
- <div className={styles.editModeImage}>
96
- <div className={styles.imageContent}>
97
- {value.bytesContentFamily === 'PDF' ? (
98
- <div className={styles.pdfThumbnail} role="button" tabIndex={0}>
99
- <DocumentPdf size={24} />
100
- </div>
101
- ) : (
102
- <img src={value.src} alt="Preview" width="200px" />
103
- )}
104
- </div>
105
- </div>
106
78
  )}
107
- {dataSource === 'filePicker' && (
108
- <div className={styles.fileUploader}>
109
- <FileUploader
110
- accept={field.questionOptions.allowedFileTypes ?? []}
111
- buttonKind="primary"
112
- buttonLabel={t('addFile', 'Add files')}
113
- filenameStatus="edit"
114
- iconDescription={t('clearFile', 'Clear file')}
115
- labelDescription={labelDescription}
116
- labelTitle={t('upload', 'Upload')}
117
- // TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682
118
- // multiple={field.questionOptions.allowMultiple}
119
- onChange={handleFilePickerChange}
120
- />
121
- </div>
122
- )}
123
- {dataSource === 'camera' && (
124
- <div className={styles.cameraUploader}>
125
- <div className={styles.camButton}>
126
- <p className={styles.titleStyles}>Camera</p>
127
- <p className={styles.descriptionStyles}>Capture image via camera</p>
128
- <Button onClick={() => setCameraWidgetVisible((prevState) => !prevState)} size="md">
129
- {cameraWidgetVisible ? t('closeCamera', 'Close camera') : t('addCameraImage', 'Add camera image')}
130
- </Button>
131
- </div>
132
- {cameraWidgetVisible && (
133
- <div className={styles.cameraPreview}>
134
- <Camera handleImages={handleCameraImageChange} />
135
- </div>
136
- )}
137
- {imagePreview && (
138
- <div className={styles.capturedImage}>
139
- <div className={styles.imageContent}>
140
- <img src={imagePreview} alt={t('preview', 'Preview')} width="200px" />
141
- <div className={styles.caption}>
142
- <p>{t('uploadedPhoto', 'Uploaded photo')}</p>
143
- <div
144
- tabIndex={0}
145
- role="button"
146
- onClick={() => {
147
- setImagePreview(null);
148
- }}
149
- className={styles.closeIcon}>
150
- <Close />
151
- </div>
152
- </div>
79
+ <div className={styles.thumbnailGrid}>
80
+ {value &&
81
+ value
82
+ .filter((file) => !file.voided)
83
+ .map((file, index) => (
84
+ <div className={styles.thumbnailContainer}>
85
+ <FileThumbnail
86
+ title={file.fileName}
87
+ src={file.base64Content}
88
+ bytesContentFamily={file.fileType}
89
+ removeFileCb={() => handleRemoveFile(index)}
90
+ />
153
91
  </div>
154
- </div>
155
- )}
156
- </div>
157
- )}
92
+ ))}
93
+ </div>
158
94
  </div>
159
95
  );
160
96
  };
@@ -1,101 +1,21 @@
1
1
  @use '@carbon/layout';
2
2
 
3
3
  .label {
4
- font-family: IBM Plex Sans;
5
- font-size: 14px;
6
- font-style: normal;
7
4
  font-weight: 600;
8
- line-height: 30px; /* 128.571% */
9
- letter-spacing: 0.16px;
5
+ line-height: 30px;
10
6
  color: #000000;
11
7
  }
12
8
 
13
- .saveFile {
14
- margin: 0;
15
- }
16
-
17
- .capturedImage {
18
- width: 100%;
19
- }
20
-
21
- .caption {
22
- display: flex;
23
- }
24
-
25
- .closeIcon {
26
- margin: 0 1rem;
27
- }
28
-
29
- .uploadSelector {
30
- display: flex;
31
- align-items: center;
32
- margin-bottom: 1rem;
33
- flex-wrap: wrap;
9
+ .thumbnailGrid {
10
+ display: grid;
11
+ grid-template-columns: repeat(auto-fill, 7rem);
34
12
  gap: layout.$spacing-05;
13
+ margin-top: layout.$spacing-05;
35
14
  }
36
15
 
37
- .caption {
38
- display: flex;
39
- margin-top: 5px;
40
- }
41
-
42
- .imageDescription {
43
- margin-top: 5px;
44
- }
45
-
46
- .fileUploader {
47
- background-color: rgb(255, 255, 255);
48
- padding: 1rem;
49
- }
50
-
51
- .cameraUploader {
52
- margin: 1rem 0;
53
- background-color: white;
54
- padding: 1rem;
55
- }
56
-
57
- .camButton {
58
- margin-bottom: 1rem;
59
- }
60
-
61
- .cameraPreview {
62
- margin-top: 1rem;
63
- }
64
-
65
- .imageContent {
66
- padding: 0;
67
- margin: 0;
68
- }
69
-
70
- .titleStyles {
71
- color: #161616;
72
- font-size: 0.875rem;
73
- font-weight: 600;
74
- letter-spacing: 0.16px;
75
- line-height: 1.2857;
76
- margin-bottom: 0.5rem;
77
- }
78
-
79
- .descriptionStyles {
80
- color: #525252;
81
- font-size: 0.875rem;
82
- font-weight: 400;
83
- letter-spacing: 0.16px;
84
- line-height: 1.28572;
85
- margin-bottom: 1rem;
86
- }
87
-
88
- .editModeImage {
89
- background-color: white;
90
- padding: 1rem;
91
- }
92
-
93
- .pdfThumbnail {
94
- cursor: pointer;
95
- background-color: gray;
16
+ .thumbnailContainer {
96
17
  display: flex;
97
- justify-content: center;
18
+ flex-direction: column;
98
19
  align-items: center;
99
- width: 5rem;
100
- height: 5rem;
20
+ position: relative;
101
21
  }
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { showSnackbar } from '@openmrs/esm-framework';
4
- import { useLaunchWorkspaceRequiringVisit } from '@openmrs/esm-patient-common-lib';
3
+ import { launchWorkspace, showSnackbar } from '@openmrs/esm-framework';
5
4
  import { Button } from '@carbon/react';
6
5
 
7
6
  import { useFormProviderContext } from '../../../provider/form-provider';
@@ -12,11 +11,14 @@ import styles from './workspace-launcher.scss';
12
11
 
13
12
  const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
14
13
  const { t } = useTranslation();
15
- const launchWorkspace = useLaunchWorkspaceRequiringVisit(field.questionOptions?.workspaceName);
16
14
  const { sessionMode } = useFormProviderContext();
17
15
 
18
16
  const handleLaunchWorkspace = () => {
19
- if (!launchWorkspace) {
17
+ const workspaceName = field.questionOptions?.workspaceName;
18
+ // TODO: properly check if workspace name is valid
19
+ // https://openmrs.atlassian.net/browse/O3-4976
20
+ const isWorkspaceNameValid = true;
21
+ if (!isWorkspaceNameValid) {
20
22
  showSnackbar({
21
23
  title: t('invalidWorkspaceName', 'Invalid workspace name.'),
22
24
  subtitle: t('invalidWorkspaceNameSubtitle', 'Please provide a valid workspace name.'),
@@ -24,7 +26,7 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
24
26
  isLowContrast: true,
25
27
  });
26
28
  }
27
- launchWorkspace();
29
+ launchWorkspace(workspaceName);
28
30
  };
29
31
 
30
32
  if (field.isHidden || isViewMode(sessionMode)) {
@@ -36,7 +38,9 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
36
38
  <div className={styles.label}>{t(field.label)}</div>
37
39
  <div className={styles.workspaceButton}>
38
40
  <Button disabled={isTrue(field.readonly)} onClick={handleLaunchWorkspace}>
39
- {t(field.questionOptions?.buttonLabel) ?? t('launchWorkspace', 'Launch Workspace')}
41
+ {field.questionOptions.buttonLabel
42
+ ? t(field.questionOptions.buttonLabel)
43
+ : t('launchWorkspace', 'Launch Workspace')}
40
44
  </Button>
41
45
  </div>
42
46
  </div>
@@ -5,8 +5,21 @@ describe('shouldRenderField', () => {
5
5
  const sessionMode = 'embedded-view';
6
6
  const isTransient = true;
7
7
  const isEmpty = true;
8
+ const hideUnansweredQuestionsInReadonlyForms = false;
9
+
10
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
11
+
12
+ expect(result).toBe(false);
13
+ });
14
+
15
+ it('should return false for non-transient empty fields when hideUnansweredQuestionsInReadonlyForms is true in embedded-view mode', () => {
16
+ const sessionMode = 'embedded-view';
17
+ const isTransient = false;
18
+ const isEmpty = true;
19
+ const hideUnansweredQuestionsInReadonlyForms = true;
20
+
21
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
8
22
 
9
- const result = shouldRenderField(sessionMode, isTransient, isEmpty);
10
23
  expect(result).toBe(false);
11
24
  });
12
25
 
@@ -14,17 +27,32 @@ describe('shouldRenderField', () => {
14
27
  const sessionMode = 'embedded-view';
15
28
  const isTransient = true;
16
29
  const isEmpty = false;
30
+ const hideUnansweredQuestionsInReadonlyForms = true;
31
+
32
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
17
33
 
18
- const result = shouldRenderField(sessionMode, isTransient, isEmpty);
19
34
  expect(result).toBe(true);
20
35
  });
21
36
 
22
- it('should return true for non-transient fields in embedded-view mode', () => {
37
+ it('should return true for non-transient empty fields when hideUnansweredQuestionsInReadonlyForms is false in embedded-view mode', () => {
23
38
  const sessionMode = 'embedded-view';
24
39
  const isTransient = false;
25
40
  const isEmpty = true;
41
+ const hideUnansweredQuestionsInReadonlyForms = false;
42
+
43
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
44
+
45
+ expect(result).toBe(true);
46
+ });
47
+
48
+ it('should return true for non-empty fields in embedded-view mode regardless of flags', () => {
49
+ const sessionMode = 'embedded-view';
50
+ const isTransient = false;
51
+ const isEmpty = false;
52
+ const hideUnansweredQuestionsInReadonlyForms = true;
53
+
54
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
26
55
 
27
- const result = shouldRenderField(sessionMode, isTransient, isEmpty);
28
56
  expect(result).toBe(true);
29
57
  });
30
58
 
@@ -32,8 +60,10 @@ describe('shouldRenderField', () => {
32
60
  const sessionMode = 'edit';
33
61
  const isTransient = true;
34
62
  const isEmpty = true;
63
+ const hideUnansweredQuestionsInReadonlyForms = true;
64
+
65
+ const result = shouldRenderField(sessionMode, isTransient, isEmpty, hideUnansweredQuestionsInReadonlyForms);
35
66
 
36
- const result = shouldRenderField(sessionMode, isTransient, isEmpty);
37
67
  expect(result).toBe(true);
38
68
  });
39
69
  });
@@ -2,9 +2,15 @@ import { type SessionMode } from '../../../types';
2
2
 
3
3
  /**
4
4
  * @name shouldRenderField
5
- * @description Determines if a field should be rendered based on the session mode, whether it is transient, and if it is empty.
6
- * - A field will not be rendered in 'embedded-view' mode if it is transient and has no value.
5
+ * @description Returns true if a field should be rendered.
6
+ * A field is hidden in 'embedded-view' mode when it is empty
7
+ * and either transient or `hideUnansweredQuestionsInReadonlyForms` is enabled.
7
8
  */
8
- export function shouldRenderField(sessionMode: SessionMode, isTransient: boolean, isEmpty: boolean): boolean {
9
- return !(sessionMode === 'embedded-view' && isTransient && isEmpty);
9
+ export function shouldRenderField(
10
+ sessionMode: SessionMode,
11
+ isTransient: boolean,
12
+ isEmptyValue: boolean,
13
+ hideUnansweredQuestionsInReadonlyForms: boolean,
14
+ ): boolean {
15
+ return !(sessionMode === 'embedded-view' && isEmptyValue && (isTransient || hideUnansweredQuestionsInReadonlyForms));
10
16
  }
@@ -3,6 +3,7 @@ import { ToastNotification } from '@carbon/react';
3
3
  import { Controller, useWatch } from 'react-hook-form';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { ErrorBoundary } from 'react-error-boundary';
6
+ import { useConfig } from '@openmrs/esm-framework';
6
7
  import {
7
8
  type FormField,
8
9
  type FormFieldInputProps,
@@ -39,6 +40,21 @@ export const FormFieldRenderer = ({ fieldId, valueAdapter, repeatOptions }: Form
39
40
  const [historicalValue, setHistoricalValue] = useState<ValueAndDisplay>(null);
40
41
  const context = useFormProviderContext();
41
42
 
43
+ // Try to get config from external module, fallback to default if not available
44
+ let hideUnansweredQuestionsInReadonlyForms = false;
45
+ try {
46
+ const config = useConfig({
47
+ externalModuleName: '@openmrs/esm-form-engine-app',
48
+ });
49
+ hideUnansweredQuestionsInReadonlyForms = config?.hideUnansweredQuestionsInReadonlyForms ?? false;
50
+ } catch (error) {
51
+ // If external module config is not available, use default value
52
+ console.warn(
53
+ 'Failed to load @openmrs/esm-form engine-app config - using hideUnansweredQuestionsInReadonlyForms=false (empty fields will be visible in readonly mode): ',
54
+ error,
55
+ );
56
+ }
57
+
42
58
  const {
43
59
  methods: { control, getValues, getFieldState },
44
60
  patient,
@@ -174,9 +190,16 @@ export const FormFieldRenderer = ({ fieldId, valueAdapter, repeatOptions }: Form
174
190
  );
175
191
  }
176
192
 
177
- // If the field is transient and has no value, we do not render it in embedded view mode.
178
- // This is to prevent transient fields from being displayed in the form when they are not necessarily needed.
179
- if (!shouldRenderField(sessionMode, !!field.questionOptions.isTransient, isEmpty(fieldValue))) {
193
+ // In 'embedded-view' mode, empty fields are hidden if they are transient
194
+ // or if the config flag `hideUnansweredQuestionsInReadonlyForms` is enabled.
195
+ if (
196
+ !shouldRenderField(
197
+ sessionMode,
198
+ !!field.questionOptions.isTransient,
199
+ isEmpty(fieldValue),
200
+ hideUnansweredQuestionsInReadonlyForms,
201
+ )
202
+ ) {
180
203
  return null;
181
204
  }
182
205
 
@@ -65,10 +65,9 @@
65
65
  }
66
66
 
67
67
  .sidebar {
68
- width: 12rem;
68
+ width: 11.25rem;
69
69
  min-height: 8rem;
70
70
  overscroll-behavior: contain;
71
- margin-right: 1rem;
72
71
  }
73
72
 
74
73
  .sideNavActions {
@@ -91,7 +90,7 @@
91
90
  }
92
91
 
93
92
  .button {
94
- width: 11rem;
93
+ width: 10rem;
95
94
  }
96
95
 
97
96
  .topMargin {
@@ -34,6 +34,7 @@ interface FormEngineProps {
34
34
  handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
35
35
  markFormAsDirty?: (isDirty: boolean) => void;
36
36
  hideControls?: boolean;
37
+ hidePatientBanner?: boolean;
37
38
  preFilledQuestions?: PreFilledQuestions;
38
39
  }
39
40
 
@@ -51,6 +52,7 @@ const FormEngine = ({
51
52
  handleConfirmQuestionDeletion,
52
53
  markFormAsDirty,
53
54
  hideControls = false,
55
+ hidePatientBanner = false,
54
56
  preFilledQuestions,
55
57
  }: FormEngineProps) => {
56
58
  const { t } = useTranslation();
@@ -75,8 +77,11 @@ const FormEngine = ({
75
77
  } = useFormJson(formUUID, formJson, encounterUUID, formSessionIntent, preFilledQuestions);
76
78
 
77
79
  const showPatientBanner = useMemo(() => {
80
+ if (hidePatientBanner) {
81
+ return false;
82
+ }
78
83
  return patient && workspaceSize === 'ultra-wide' && mode !== 'embedded-view';
79
- }, [patient, mode, workspaceSize]);
84
+ }, [patient, mode, workspaceSize, hidePatientBanner]);
80
85
 
81
86
  const isFormWorkspaceTooNarrow = useMemo(() => ['narrow'].includes(workspaceSize), [workspaceSize]);
82
87
 
@@ -29,9 +29,9 @@
29
29
  flex-basis: 65%;
30
30
  flex-grow: 1;
31
31
  flex-shrink: 1;
32
+ display: flex;
32
33
  position: relative;
33
34
  overflow-y: hidden;
34
- display: flex;
35
35
  flex-direction: column;
36
36
  justify-content: space-between;
37
37
  }
@@ -1076,9 +1076,12 @@ describe('Form engine component', () => {
1076
1076
  await user.click(addButton);
1077
1077
 
1078
1078
  expect(screen.getByRole('button', { name: /Remove/i })).toBeInTheDocument();
1079
- expect(screen.getAllByRole('radio', { name: /^male$/i }).length).toEqual(2);
1080
- expect(screen.getAllByRole('radio', { name: /^female$/i }).length).toEqual(2);
1081
- expect(screen.getAllByRole('textbox', { name: /date of birth/i }).length).toEqual(2);
1079
+
1080
+ await waitFor(() => {
1081
+ expect(screen.getAllByRole('radio', { name: /^male$/i })).toHaveLength(2);
1082
+ expect(screen.getAllByRole('radio', { name: /female/i })).toHaveLength(2);
1083
+ expect(screen.getAllByRole('textbox', { name: /date of birth/i })).toHaveLength(2);
1084
+ });
1082
1085
  });
1083
1086
 
1084
1087
  it('should test deletion of a group', async () => {
@@ -185,9 +185,8 @@ export class EncounterFormProcessor extends FormProcessor {
185
185
  }
186
186
  // handle attachments
187
187
  try {
188
- const attachmentsResponse = await Promise.all(
189
- saveAttachments(context.formFields, savedEncounter, abortController),
190
- );
188
+ const attachmentsResponse = await saveAttachments(context.formFields, savedEncounter, abortController);
189
+
191
190
  if (attachmentsResponse?.length) {
192
191
  showSnackbar({
193
192
  title: t('attachmentsSaved', 'Attachment(s) saved successfully'),
@@ -196,6 +195,7 @@ export class EncounterFormProcessor extends FormProcessor {
196
195
  });
197
196
  }
198
197
  } catch (error) {
198
+ console.error('Error saving attachments', error);
199
199
  const errorMessages = extractErrorMessagesFromResponse(error);
200
200
  return Promise.reject({
201
201
  title: t('errorSavingAttachments', 'Error saving attachment(s)'),
@@ -206,6 +206,7 @@ export class EncounterFormProcessor extends FormProcessor {
206
206
  }
207
207
  return savedEncounter;
208
208
  } catch (error) {
209
+ console.error('Error saving encounter', error);
209
210
  const errorMessages = extractErrorMessagesFromResponse(error);
210
211
  return Promise.reject({
211
212
  title: t('errorSavingEncounter', 'Error saving encounter'),