Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix) O3-3682 - Refactor File Picker Component and Fix Multiple Selection Support #431

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`, {
Expand Down
255 changes: 142 additions & 113 deletions src/components/inputs/file/file.component.tsx
Original file line number Diff line number Diff line change
@@ -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<FormFieldInputProps> = ({ field, value, setFieldValue }) => {
const File: React.FC<FormFieldInputProps> = ({ field, value, errors, setFieldValue }) => {
const { t } = useTranslation();
const [dataSource, setDataSource] = useState<DataSourceType>(null);
const [cameraWidgetVisible, setCameraWidgetVisible] = useState(false);
const [imagePreview, setImagePreview] = useState(null);
const [dataSource, setDataSource] = useState<DataSourceType>(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(
Expand All @@ -47,113 +51,138 @@ const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) =>
setCameraWidgetVisible(false);
setFieldValue(newImage);
},
[setFieldValue],
[setFieldValue]
);

if (isViewMode(sessionMode) && !value) {
return (
<FieldValueView label={t(field.label)} value={null} conceptName={field.meta.concept?.display} isInline={false} />
);
}

return isViewMode(sessionMode) ? (
<div>
<div className={styles.label}>{t(field.label)}</div>
<div className={styles.editModeImage}>
<div className={styles.imageContent}>
{value.bytesContentFamily === 'PDF' ? (
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
<DocumentPdf size={24} />
</div>
) : (
<img src={value.src} alt={t('preview', 'Preview')} width="200px" />
)}
</div>
</div>
</div>
) : (
<div>
<div className={styles.label}>
<FieldLabel field={field} />
</div>
<div className={styles.uploadSelector}>
<div className={styles.selectorButton}>
<Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('filePicker')}>
{t('uploadImage', 'Upload image')}
</Button>
</div>
<div className={styles.selectorButton}>
<Button disabled={isTrue(field.readonly)} onClick={() => setDataSource('camera')}>
{t('cameraCapture', 'Camera capture')}
</Button>
</div>
</div>
{!dataSource && value && (
<div className={styles.editModeImage}>
<div className={styles.imageContent}>
{value.bytesContentFamily === 'PDF' ? (
const renderFilePreview = () => (
<div className={styles.editModeImage}>
<div className={styles.imageContent}>
{Array.isArray(value) ? (
value.map((file, index) => (
<div key={index} className={styles.fileThumbnail}>
{file.bytesContentFamily === 'PDF' ? (
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
<DocumentPdf size={24} />
</div>
) : (
<img src={value.src} alt="Preview" width="200px" />
<img src={file.src} alt={t('preview', 'Preview')} width="200px" />
)}
</div>
</div>
)}
{dataSource === 'filePicker' && (
<div className={styles.fileUploader}>
<FileUploader
accept={field.questionOptions.allowedFileTypes ?? []}
buttonKind="primary"
buttonLabel={t('addFile', 'Add files')}
filenameStatus="edit"
iconDescription={t('clearFile', 'Clear file')}
labelDescription={labelDescription}
labelTitle={t('upload', 'Upload')}
// TODO: Add multiple file upload support; see: https://openmrs.atlassian.net/browse/O3-3682
// multiple={field.questionOptions.allowMultiple}
onChange={handleFilePickerChange}
/>
</div>
)}
{dataSource === 'camera' && (
<div className={styles.cameraUploader}>
<div className={styles.camButton}>
<p className={styles.titleStyles}>Camera</p>
<p className={styles.descriptionStyles}>Capture image via camera</p>
<Button onClick={() => setCameraWidgetVisible((prevState) => !prevState)} size="md">
{cameraWidgetVisible ? t('closeCamera', 'Close camera') : t('addCameraImage', 'Add camera image')}
</Button>
</div>
{cameraWidgetVisible && (
<div className={styles.cameraPreview}>
<Camera handleImages={handleCameraImageChange} />
</div>
)}
{imagePreview && (
<div className={styles.capturedImage}>
<div className={styles.imageContent}>
<img src={imagePreview} alt={t('preview', 'Preview')} width="200px" />
<div className={styles.caption}>
<p>{t('uploadedPhoto', 'Uploaded photo')}</p>
<div
tabIndex={0}
role="button"
onClick={() => {
setImagePreview(null);
}}
className={styles.closeIcon}>
<Close />
</div>
</div>
</div>
))
) : (
<>
{value?.bytesContentFamily === 'PDF' ? (
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
<DocumentPdf size={24} />
</div>
) : (
<img src={value?.src} alt={t('preview', 'Preview')} width="200px" />
)}
</div>
</>
)}
</div>
</div>
);

if (sessionMode === 'view' || sessionMode === 'embedded-view') {
return (
<FieldValueView
label={t(field.label)}
value={value}
conceptName={field.meta?.concept?.display}
isInline={isInline}
/>
);
}

return (
!field.isHidden && (
<div className={styles.boldedLabel}>
<Layer>
<div className={styles.fileInputContainer}>
<FieldLabel field={field} />
<div className={styles.uploadSelector}>
<Button
disabled={isTrue(field.readonly)}
onClick={() => setDataSource('filePicker')}
kind="secondary"
size="md"
className={`${styles.uploadFileButton}`}
>
{t('uploadFile', 'Upload file')}
</Button>
<Button
disabled={isTrue(field.readonly)}
onClick={() => setDataSource('camera')}
kind="secondary"
size="md"
renderIcon={Camera}
className={`${styles.cameraCaptureButton}`}
>
{t('cameraCapture', 'Camera capture')}
</Button>
</div>

{!dataSource && value && renderFilePreview()}

{dataSource === 'filePicker' && (
<div className={styles.fileUploader}>
<FileUploader
accept={field.questionOptions.allowedFileTypes ?? []}
multiple={field.questionOptions.allowMultiple ?? true}
buttonKind="primary"
buttonLabel={t('addFile', 'Add file')}
filenameStatus="edit"
iconDescription={t('clearFile', 'Clear file')}
labelDescription={labelDescription}
labelTitle={t('upload', 'Upload')}
onChange={handleFilePickerChange}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
/>
</div>
)}

{dataSource === 'camera' && (
<div className={styles.cameraUploader}>
<p className={styles.titleStyles}>Camera</p>
<p className={styles.descriptionStyles}>Capture image via camera</p>
<Button
onClick={() => setCameraWidgetVisible((prev) => !prev)}
size="md"
className={styles.cameraToggle}
>
{cameraWidgetVisible ? t('closeCamera', 'Close camera') : t('openCamera', 'Open camera')}
</Button>

{cameraWidgetVisible && (
<div className={styles.cameraPreview}>
<CameraComponent handleImages={handleCameraImageChange} />
</div>
)}

{imagePreview && (
<div className={styles.capturedImage}>
<div className={styles.imageContent}>
<img src={imagePreview} alt={t('preview', 'Preview')} width="200px" />
<Button
hasIconOnly
renderIcon={Close}
iconDescription={t('clearImage', 'Clear image')}
onClick={() => setImagePreview(null)}
size="sm"
kind="ghost"
/>
</div>
</div>
)}
</div>
)}
</div>
</Layer>
</div>
)
);
};

export default File;
export default File;
Loading