import React, { cloneElement, ComponentPropsWithRef, ReactElement, ReactNode, useCallback, useId, useRef, } from 'react'; import clsx from 'clsx'; import {mergeProps} from '@react-aria/utils'; import {toast} from '@common/ui/toast/toast'; import {Field} from '@common/ui/forms/input-field/field'; import { getInputFieldClassNames, InputFieldStyle, } from '@common/ui/forms/input-field/get-input-field-class-names'; import {FileEntry} from '@common/uploads/file-entry'; import {useAutoFocus} from '@common/ui/focus/use-auto-focus'; import {UploadStrategyConfig} from '@common/uploads/uploader/strategy/upload-strategy'; import {useActiveUpload} from '@common/uploads/uploader/use-active-upload'; import {Disk} from '@common/uploads/types/backend-metadata'; import {Button} from '@common/ui/buttons/button'; import {Trans} from '@common/i18n/trans'; import {ProgressBar} from '@common/ui/progress/progress-bar'; import {Input} from '@common/ui/forms/input-field/input'; import {useController} from 'react-hook-form'; import {useFileEntryModel} from '@common/uploads/requests/use-file-entry-model'; import {Skeleton} from '@common/ui/skeleton/skeleton'; import {AnimatePresence, m} from 'framer-motion'; import {opacityAnimation} from '@common/ui/animation/opacity-animation'; import {validateUpload} from '@common/uploads/uploader/validate-upload'; import {UploadedFile} from '@common/uploads/uploaded-file'; interface Props { className?: string; label?: ReactNode; description?: ReactNode; invalid?: boolean; errorMessage?: ReactNode; required?: boolean; disabled?: boolean; value?: string; onChange?: (newValue: string) => void; allowedFileTypes?: string[]; maxFileSize?: number; diskPrefix: string; disk?: Disk; showRemoveButton?: boolean; autoFocus?: boolean; } export function FileEntryField({ className, label, description, value, onChange, diskPrefix, disk = Disk.uploads, showRemoveButton, invalid, errorMessage, required, autoFocus, disabled, allowedFileTypes, maxFileSize, }: Props) { const { uploadFile, entry, uploadStatus, deleteEntry, isDeletingEntry, percentage, } = useActiveUpload(); const inputRef = useRef(null); useAutoFocus({autoFocus}, inputRef); const {data} = useFileEntryModel(value, {enabled: !entry && !!value}); const fieldId = useId(); const labelId = label ? `${fieldId}-label` : undefined; const descriptionId = description ? `${fieldId}-description` : undefined; const currentValue = value || entry?.url; const currentEntry = entry || data?.fileEntry; const uploadOptions: UploadStrategyConfig = { showToastOnRestrictionFail: true, restrictions: { allowedFileTypes, maxFileSize, }, metadata: { diskPrefix, disk, }, onSuccess: (entry: FileEntry) => onChange?.(entry.url), onError: message => { if (message) { toast.danger(message); } }, }; const inputFieldClassNames = getInputFieldClassNames({ description, descriptionPosition: 'top', invalid, disabled: disabled || uploadStatus === 'inProgress', }); const removeButton = showRemoveButton ? ( ) : null; const handleUpload = useCallback(() => { inputRef.current?.click(); }, []); return (
{label && (
{label}
{removeButton}
)} {description && (
{description}
)}
{ if (e.target.files?.length) { // "uploadFile" will validate, but need to validate here as well // because there's no easy way to listen for errors using "uploadFile" const errorMessage = validateUpload( new UploadedFile(e.target.files[0]), uploadOptions.restrictions ); if (errorMessage && inputRef.current) { inputRef.current.value = ''; toast.danger(errorMessage); } else { uploadFile(e.target.files[0], uploadOptions); } } }} /> {uploadStatus === 'inProgress' && ( )}
); } interface FileInputFieldProps { children: ReactElement>; inputFieldClassNames: InputFieldStyle; currentValue?: string; currentEntry?: FileEntry; handleUpload: () => void; } function FileInputField({ children, inputFieldClassNames, currentValue, currentEntry, handleUpload, }: FileInputFieldProps) { const buttonRef = useRef(null); if (currentValue) { return ( { buttonRef.current?.focus(); buttonRef.current?.click(); }, }} fieldClassNames={inputFieldClassNames} >
{currentEntry ? ( {currentEntry.name} ) : ( )}
{children}
); } return cloneElement(children, { className: clsx( inputFieldClassNames.input, 'py-8', 'file:bg-primary file:text-on-primary file:border-none file:rounded file:text-sm file:font-semibold file:px-10 file:h-24 file:mr-10' ), }); } interface FormFileEntryFieldProps extends Props { name: string; } export function FormFileEntryField(props: FormFileEntryFieldProps) { const { field: {onChange, value = null}, fieldState: {error}, } = useController({ name: props.name, }); const formProps: Partial = { onChange, value, invalid: error != null, errorMessage: error ? : null, }; return ; }