import React, { forwardRef, useImperativeHandle } from 'react'; import { DropzoneOptions, useDropzone } from 'react-dropzone'; import { HiOutlineCloudUpload } from 'react-icons/hi'; import { alpha, Box, BoxProps, Button, ButtonProps, FormControl, FormControlProps, FormHelperText, FormHelperTextProps, FormLabel, FormLabelProps, Stack, StackProps } from '@mui/material'; import { downloadBlob } from '@reevolve/core'; import { isFunction } from 'lodash'; import useDefaultProps from '../../hooks/useDefaultProps'; import { useLocalization } from '../../localization'; import { fileFormat } from '../file-thumbnail'; import Spin from '../spin/Spin'; import DeleteButton, { DeleteButtonProps } from './components/DeleteButton'; import DownloadButton, { DownloadButtonProps } from './components/DownloadButton'; import MultiFilePreview, { MultiFilePreviewProps } from './components/MultiFilePreview'; import RejectionFiles, { RejectionFilesProps } from './components/RejectionFiles'; import SingleFilePreview, { SingleFilePreviewProps } from './components/SingleFilePreview'; import UploadPlaceholder, { UploadPlaceholderProps } from './components/UploadPlaceholder'; export interface UploadPlaceholderSlotProps extends Pick< UploadProps, | 'accept' | 'disabled' | 'error' | 'label' | 'required' | 'maxFiles' | 'minSize' | 'maxSize' | 'multiple' | 'noClick' | 'noDrag' | 'preventDropOnDocument' > {} export interface UploadProps extends Omit { required?: boolean; loading?: boolean; /** * single image file preview aspect ratio * @default 16 / 9 */ imagePreviewAspectRatio?: number; /** * show singe file preview in list view (multiFilePreview) * @default true */ singleFileListPreview?: boolean; /** * show download button in file preview * @default false */ enableDownload?: boolean; slotProps?: { wrap?: BoxProps; formLabel?: Omit; helperText?: FormHelperTextProps; formControl?: Omit; deleteButton?: Omit; deleteIconButton?: Omit; downloadIconButton?: Omit; actionButtonsContainer?: Omit; removeAllButton?: Omit; uploadButton?: Omit; multiFilePreview?: Omit; singleFilePreview?: Omit; placeholder?: UploadPlaceholderProps; rejectionFiles?: Omit; }; slots?: { placeholder?: | ((params: UploadPlaceholderSlotProps) => React.ReactNode) | React.ReactElement; }; sx?: BoxProps['sx']; multiple?: TMultiple; name?: string; value?: TMultiple extends true ? File[] : File; label?: React.ReactNode; error?: boolean; disabled?: boolean; helperText?: React.ReactNode; thumbnail?: boolean; onDelete?: () => void; onUpload?: () => void; onRemove?: (file: File) => void; onRemoveAll?: () => void; onBlur?: (e: React.FocusEvent) => void; } export default forwardRef(function Upload( inProps: UploadProps, ref: React.ForwardedRef ) { const { translate } = useLocalization(); const { required, slotProps, sx, value, error, disabled, onDelete, onUpload, onRemove, thumbnail, helperText, onRemoveAll, multiple = false, label, maxFiles, minSize, maxSize, accept, slots, onDrop, name, onBlur, noClick, noDrag, preventDropOnDocument, loading = false, imagePreviewAspectRatio = 16 / 9, singleFileListPreview = true, enableDownload = false, ...other } = useDefaultProps({ name: 'ReevolveUpload', props: inProps }); const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({ multiple, disabled, maxFiles, minSize, maxSize, accept, noClick, noDrag, preventDropOnDocument, onDrop(acceptedFiles, fileRejections, event) { if (multiple) { if (maxFiles && ((value as File[])?.length || 0) + acceptedFiles.length > maxFiles) { return; } if ( (value as File[])?.some((file) => acceptedFiles.some((acceptedFile) => acceptedFile.name === file.name && acceptedFile.size === file.size) ) ) { return; } } onDrop?.(acceptedFiles, fileRejections, event); }, ...other }); const rootProps = getRootProps(); const inputProps = getInputProps(); // eslint-disable-next-line react-hooks/exhaustive-deps useImperativeHandle(ref, () => (inputProps as any)?.ref?.current, []); const isArray = Array.isArray(value) && multiple; const hasFile = !isArray && !!value; const hasFiles = isArray && !!value.length; const hasError = isDragReject || !!error; const renderPlaceholder = () => { if (slots?.placeholder) { const placeHolderProps: UploadPlaceholderSlotProps = { accept, disabled, error, label, maxFiles, maxSize, minSize, multiple, noClick, noDrag, preventDropOnDocument, required }; if (isFunction(slots.placeholder)) { return slots.placeholder(placeHolderProps); } if (React.isValidElement(slots.placeholder)) { return React.cloneElement(slots.placeholder, placeHolderProps); } } return ; }; return ( {label && ( ({ pb: 0.75, ...(isFunction(slotProps?.formLabel?.sx) ? (slotProps.formLabel.sx as Function)(theme) : slotProps?.formLabel?.sx) })} {...slotProps?.formLabel} > {label} )} ({ width: 1, position: 'relative', ...(isFunction(slotProps?.wrap?.sx) ? (slotProps.wrap.sx as Function)(theme) : slotProps?.wrap?.sx) })} > ({ padding: 5, outline: 'none', borderRadius: 1, cursor: 'pointer', overflow: 'hidden', position: 'relative', border: `1px dashed ${alpha(theme.palette.grey[500], 0.75)}`, transition: theme.transitions.create(['opacity', 'padding', 'background-color']), '&:hover': { opacity: 0.72 }, ...(hasError && { color: 'error.main', borderColor: 'error.main', bgcolor: alpha(theme.palette.error.main, 0.08) }), ...(isDragActive && { borderColor: 'primary.main', bgcolor: alpha(theme.palette.primary.main, 0.08) }), ...(disabled && { opacity: 0.48, pointerEvents: 'none' }), display: 'flex', justifyContent: 'center', ...(hasFile && !singleFileListPreview && fileFormat((value as File).name) === 'image' && { paddingTop: 0, paddingBottom: `${100 / imagePreviewAspectRatio}%` }), ...(isFunction(sx) ? (sx as Function)(theme) : sx) })} > { inputProps?.onBlur?.(e); onBlur?.(e); }} /> {/* Single file */} {!singleFileListPreview && hasFile ? ( ) : ( renderPlaceholder() )} {/* Single file */} {!singleFileListPreview && hasFile && ( {enableDownload && ( { e.stopPropagation(); downloadBlob(value as File); }} /> )} {!disabled && onDelete && ( )} )} {helperText && ( {helperText} )} {hasFile && singleFileListPreview && ( ({ my: 3, ...(isFunction(slotProps?.multiFilePreview?.sx) ? (slotProps.multiFilePreview.sx as Function)(theme) : slotProps?.multiFilePreview?.sx) })} /> )} {/* Multi files */} {hasFiles && ( ({ my: 3, ...(isFunction(slotProps?.multiFilePreview?.sx) ? (slotProps.multiFilePreview.sx as Function)(theme) : slotProps?.multiFilePreview?.sx) })} /> )} {((hasFiles && onRemoveAll) || (hasFile && onDelete) || ((hasFile || hasFiles) && onUpload)) && ( ({ mt: hasFile ? 3 : undefined, ...(isFunction(slotProps?.actionButtonsContainer?.sx) ? (slotProps.actionButtonsContainer.sx as Function)(theme) : slotProps?.actionButtonsContainer?.sx) })} > {hasFiles && onRemoveAll && ( )} {hasFile && onUpload && onDelete && ( )} {onUpload && ( )} )} ); }); declare module '@mui/material' { interface Components { ReevolveUpload?: { defaultProps?: Partial; }; } }