@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.
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +6 -7
- package/src/adapters/obs-adapter.test.ts +117 -0
- package/src/adapters/obs-adapter.ts +54 -27
- package/src/api/index.ts +17 -24
- package/src/components/inputs/file/file-thumbnail.component.tsx +55 -0
- package/src/components/inputs/file/file-thumbnail.scss +42 -0
- package/src/components/inputs/file/file.component.tsx +66 -130
- package/src/components/inputs/file/file.scss +8 -88
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +10 -6
- package/src/components/renderer/field/fieldRenderUtils.test.ts +35 -5
- package/src/components/renderer/field/fieldRenderUtils.ts +10 -4
- package/src/components/renderer/field/form-field-renderer.component.tsx +26 -3
- package/src/components/sidebar/sidebar.scss +2 -3
- package/src/form-engine.component.tsx +6 -1
- package/src/form-engine.scss +1 -1
- package/src/form-engine.test.tsx +6 -3
- package/src/processors/encounter/encounter-form-processor.ts +4 -3
- package/src/processors/encounter/encounter-processor-helper.ts +17 -15
- package/src/registry/registry.ts +2 -1
- package/src/types/domain.ts +9 -9
- package/src/types/index.ts +3 -3
- package/src/components/inputs/file/camera/camera.component.tsx +0 -34
- package/src/components/inputs/file/camera/camera.scss +0 -3
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
:
|
|
35
|
-
|
|
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
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
[
|
|
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
|
|
63
|
+
return (
|
|
63
64
|
<div>
|
|
64
|
-
<div className={styles.label
|
|
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
|
-
|
|
83
|
-
<div
|
|
84
|
-
<Button
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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;
|
|
9
|
-
letter-spacing: 0.16px;
|
|
5
|
+
line-height: 30px;
|
|
10
6
|
color: #000000;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
.
|
|
14
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
18
|
+
flex-direction: column;
|
|
98
19
|
align-items: center;
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
6
|
-
*
|
|
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(
|
|
9
|
-
|
|
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
|
-
//
|
|
178
|
-
//
|
|
179
|
-
if (
|
|
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:
|
|
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:
|
|
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
|
|
package/src/form-engine.scss
CHANGED
package/src/form-engine.test.tsx
CHANGED
|
@@ -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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
|
189
|
-
|
|
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'),
|