diff --git a/src/api/index.ts b/src/api/index.ts index 8124c4df5..6776311f0 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,32 +20,40 @@ export function saveEncounter(abortController: AbortController, payload, encount export function saveAttachment(patientUuid, field, conceptUuid, date, encounterUUID, abortController) { const url = `${restBaseUrl}/attachment`; + //enable saving multiple attachments + const files = Array.isArray(field.meta.submission?.newValue?.value) + ? field.meta.submission.newValue.value + : [field.meta.submission?.newValue?.value]; + + const uploadPromises = files.map(content => { + const formData = new FormData(); + const fileCaption = field.id; + const cameraUploadType = typeof content === 'string' && content?.split(';')[0].split(':')[1].split('/')[1]; + + formData.append('fileCaption', fileCaption); + formData.append('patient', patientUuid); + + if (typeof content === 'object') { + formData.append('file', content); + } else { + formData.append('file', new File([''], `camera-upload.${cameraUploadType}`), `camera-upload.${cameraUploadType}`); + formData.append('base64Content', content); + } - const content = field.meta.submission?.newValue?.value; - const cameraUploadType = typeof content === 'string' && content?.split(';')[0].split(':')[1].split('/')[1]; - - const formData = new FormData(); - const fileCaption = field.id; - - formData.append('fileCaption', fileCaption); - formData.append('patient', patientUuid); - - if (typeof content === 'object') { - formData.append('file', content); - } else { - formData.append('file', new File([''], `camera-upload.${cameraUploadType}`), `camera-upload.${cameraUploadType}`); - formData.append('base64Content', content); - } - formData.append('encounter', encounterUUID); - formData.append('obsDatetime', date); + formData.append('encounter', encounterUUID); + formData.append('obsDatetime', date); - return openmrsFetch(url, { - method: 'POST', - signal: abortController.signal, - body: formData, + return openmrsFetch(url, { + method: 'POST', + signal: abortController.signal, + body: formData, + }); }); + + return Promise.all(uploadPromises); } + export function getAttachmentByUuid(patientUuid: string, encounterUuid: string, abortController: AbortController) { const attachmentUrl = `${restBaseUrl}/attachment`; return openmrsFetch(`${attachmentUrl}?patient=${patientUuid}&encounter=${encounterUuid}`, { diff --git a/src/components/inputs/file/file.component.tsx b/src/components/inputs/file/file.component.tsx index c4abe4a4d..d66d1ea63 100644 --- a/src/components/inputs/file/file.component.tsx +++ b/src/components/inputs/file/file.component.tsx @@ -1,44 +1,48 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { FileUploader, Button } from '@carbon/react'; +import React, { useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { isTrue } from '../../../utils/boolean-utils'; -import Camera from './camera/camera.component'; -import { Close, DocumentPdf } from '@carbon/react/icons'; +import { Layer, FileUploader, Button } from '@carbon/react'; +import { DocumentPdf, Camera, Close } from '@carbon/react/icons'; import styles from './file.scss'; -import { type FormFieldInputProps } from '../../../types'; import { useFormProviderContext } from '../../../provider/form-provider'; -import { isViewMode } from '../../../utils/common-utils'; +import { type FormFieldInputProps } from '../../../types'; +import { isTrue } from '../../../utils/boolean-utils'; +import { shouldUseInlineLayout } from '../../../utils/form-helper'; import FieldValueView from '../../value/view/field-value-view.component'; import FieldLabel from '../../field-label/field-label.component'; +import CameraComponent from './camera/camera.component'; type DataSourceType = 'filePicker' | 'camera' | null; -const File: React.FC = ({ field, value, setFieldValue }) => { +const File: React.FC = ({ field, value, errors, setFieldValue }) => { const { t } = useTranslation(); + const [dataSource, setDataSource] = useState(null); const [cameraWidgetVisible, setCameraWidgetVisible] = useState(false); const [imagePreview, setImagePreview] = useState(null); - const [dataSource, setDataSource] = useState(null); - const { sessionMode } = useFormProviderContext(); + const { layoutType, sessionMode, workspaceLayout } = useFormProviderContext(); + + const isInline = useMemo(() => { + if (['view', 'embedded-view'].includes(sessionMode) || isTrue(field.readonly)) { + return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode); + } + return false; + }, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]); const labelDescription = useMemo(() => { return field.questionOptions.allowedFileTypes ? t( 'fileUploadDescription', - `Upload one of the following file types: ${field.questionOptions.allowedFileTypes.map( - (eachItem) => ` ${eachItem}`, - )}`, + `Upload one of the following file types: ${field.questionOptions.allowedFileTypes.join(', ')}` ) : t('fileUploadDescriptionAny', 'Upload any file type'); }, [field.questionOptions.allowedFileTypes, t]); const handleFilePickerChange = useCallback( (event) => { - // TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682 - const [selectedFile]: File[] = Array.from(event.target.files); - setImagePreview(null); - setFieldValue(selectedFile); + const selectedFiles: File[] = Array.from(event.target.files); + setImagePreview(null); + setFieldValue((prevValue) => [...(prevValue || []), ...selectedFiles]); }, - [setFieldValue], + [setFieldValue] ); const handleCameraImageChange = useCallback( @@ -47,113 +51,138 @@ const File: React.FC = ({ field, value, setFieldValue }) => setCameraWidgetVisible(false); setFieldValue(newImage); }, - [setFieldValue], + [setFieldValue] ); - if (isViewMode(sessionMode) && !value) { - return ( - - ); - } - - return isViewMode(sessionMode) ? ( -
-
{t(field.label)}
-
-
- {value.bytesContentFamily === 'PDF' ? ( -
- -
- ) : ( - {t('preview', - )} -
-
-
- ) : ( -
-
- -
-
-
- -
-
- -
-
- {!dataSource && value && ( -
-
- {value.bytesContentFamily === 'PDF' ? ( + const renderFilePreview = () => ( +
+
+ {Array.isArray(value) ? ( + value.map((file, index) => ( +
+ {file.bytesContentFamily === 'PDF' ? (
) : ( - Preview + {t('preview', )}
-
- )} - {dataSource === 'filePicker' && ( -
- -
- )} - {dataSource === 'camera' && ( -
-
-

Camera

-

Capture image via camera

- -
- {cameraWidgetVisible && ( -
- -
- )} - {imagePreview && ( -
-
- {t('preview', -
-

{t('uploadedPhoto', 'Uploaded photo')}

-
{ - setImagePreview(null); - }} - className={styles.closeIcon}> - -
-
-
+ )) + ) : ( + <> + {value?.bytesContentFamily === 'PDF' ? ( +
+
+ ) : ( + {t('preview', )} -
+ )}
+
+); + + if (sessionMode === 'view' || sessionMode === 'embedded-view') { + return ( + + ); + } + + return ( + !field.isHidden && ( +
+ +
+ +
+ + +
+ + {!dataSource && value && renderFilePreview()} + + {dataSource === 'filePicker' && ( +
+ 0} + invalidText={errors[0]?.message} + /> +
+ )} + + {dataSource === 'camera' && ( +
+

Camera

+

Capture image via camera

+ + + {cameraWidgetVisible && ( +
+ +
+ )} + + {imagePreview && ( +
+
+ {t('preview', +
+
+ )} +
+ )} +
+
+
+ ) ); }; -export default File; +export default File; \ No newline at end of file diff --git a/src/components/inputs/file/file.scss b/src/components/inputs/file/file.scss index 06eacfcf5..0e798172a 100644 --- a/src/components/inputs/file/file.scss +++ b/src/components/inputs/file/file.scss @@ -1,3 +1,6 @@ +@use '@carbon/colors'; +@use '@openmrs/esm-styleguide/src/vars' as *; + .label { font-family: IBM Plex Sans; font-size: 14px; @@ -5,7 +8,12 @@ font-weight: 600; line-height: 30px; /* 128.571% */ letter-spacing: 0.16px; - color: #000000; + color: colors.$black-100; +} + +.boldedLabel label { + font-weight: 600; + color: colors.$black-100; } .saveFile { @@ -30,8 +38,30 @@ margin-bottom: 1rem; } -.selectorButton { - margin-right: 1rem; +.fileInputContainer { + display: flex; + flex-direction: column; + gap: 1rem; + + .uploadSelector { + display: flex; + gap: 1rem; + + .uploadFileButton, .cameraCaptureButton { + min-width: 150px; + @include brand-03(background-color); + color: white; + + &:hover { + @include brand-03(background-color); + } + + &:disabled { + background-color: #cccccc; + color: #666666; + } + } + } } .caption { @@ -44,13 +74,13 @@ } .fileUploader { - background-color: rgb(255, 255, 255); + background-color: colors.$white; padding: 1rem; } .cameraUploader { margin: 1rem 0; - background-color: white; + background-color: colors.$white; padding: 1rem; } @@ -58,6 +88,14 @@ margin-bottom: 1rem; } +.cameraToggle { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + +} + .cameraPreview { margin-top: 1rem; } @@ -68,7 +106,7 @@ } .titleStyles { - color: #161616; + color: colors.$gray-100; font-size: 0.875rem; font-weight: 600; letter-spacing: 0.16px; @@ -77,7 +115,7 @@ } .descriptionStyles { - color: #525252; + color: colors.$gray-70; font-size: 0.875rem; font-weight: 400; letter-spacing: 0.16px; @@ -86,13 +124,13 @@ } .editModeImage { - background-color: white; + background-color: colors.$white; padding: 1rem; } .pdfThumbnail { cursor: pointer; - background-color: gray; + background-color: colors.$cool-gray-50; display: flex; justify-content: center; align-items: center;