import type { ChangeEvent, DragEvent, HTMLAttributes } from 'react' import React, { useEffect, useRef, useState } from 'react' import { Button, Card, Icon, Input } from '../..' import { useOnClickOutside } from '../../hooks' import FileUploadStatus, { FileUploadErrorType, FileUploadState, } from '../FileUploadStatus/FileUploadStatus' export interface FileUploadCardProps extends Omit, 'onDrop'> { /** * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). */ testId?: string /** * Controls whether the component is visible. */ isOpen: boolean /** * Callback function when the component should be closed. */ onDismiss?: () => void /** * Callback function when a file is selected. */ onFileSelect?: (files: File[]) => void /** * Callback function when download template is clicked. */ onDownloadTemplate?: () => void /** * Callback function when search button is clicked after file upload. */ onSearch?: (file: File) => void /** * Accepted file types. * @default '.csv' */ accept?: string /** * Allow multiple file selection. * @default false */ multiple?: boolean /** * Card title (e.g. from CMS). */ title: string /** * Aria-label for the file input (e.g. from CMS). */ fileInputAriaLabel: string /** * Aria-label for the dropzone region (e.g. from CMS). */ dropzoneAriaLabel: string /** * Dropzone title text (e.g. from CMS). */ dropzoneTitle: string /** * Label for the select file button (e.g. from CMS). */ selectFileButtonLabel: string /** * Label for the download template button (e.g. from CMS). */ downloadTemplateButtonLabel: string /** * Aria-label for the remove button in FileUploadStatus (e.g. from CMS). */ removeButtonAriaLabel: string /** * Label for the search button in FileUploadStatus (e.g. from CMS). */ searchButtonLabel: string /** * Status text when uploading in FileUploadStatus (e.g. from CMS). */ uploadingStatusText: string /** * Status text when processing/polling in FileUploadStatus (e.g. from CMS). * @default 'Importing...' */ processingStatusText?: string /** * Status text when completed in FileUploadStatus (e.g. from CMS). Receives file size in bytes. */ getCompletedStatusText: (fileSize: number) => string /** * Error messages per error type for FileUploadStatus (e.g. from CMS). */ errorMessages: Partial< Record > /** * Formatter for file size display. */ formatterFileSize?: (size: number) => string /** * Formatter for file name display. */ formatterFileName?: (name: string) => string /** * Indicates if the file is being uploaded. */ isUploading?: boolean /** * Indicates if the OES operation is polling/processing after upload. */ isProcessing?: boolean /** * Indicates if there was an error during file upload. */ hasError?: boolean /** * Type of error when hasError is true. */ errorType?: FileUploadErrorType /** * Custom error message to display when hasError is true. */ errorMessage?: string } const FileUploadCard = ({ testId = 'fs-file-upload-card', isOpen, onDismiss, onFileSelect, onDownloadTemplate, onSearch, accept = '.csv', multiple = false, title, fileInputAriaLabel, dropzoneAriaLabel, dropzoneTitle, selectFileButtonLabel, downloadTemplateButtonLabel, removeButtonAriaLabel, searchButtonLabel, uploadingStatusText, processingStatusText, getCompletedStatusText, errorMessages, formatterFileSize, formatterFileName, isUploading = false, isProcessing = false, hasError = false, errorType: errorTypeProp, errorMessage, ...otherProps }: FileUploadCardProps) => { const fileInputRef = useRef(null) const containerRef = useRef(null) const fileTypeErrorRef = useRef(undefined) const [dragActive, setDragActive] = useState(false) const [selectedFile, setSelectedFile] = useState(null) const [uploadState, setUploadState] = useState( FileUploadState.Uploading ) const [errorType, setErrorType] = useState( undefined ) useOnClickOutside(isOpen ? containerRef : undefined, () => { if (isOpen) { onDismiss?.() } }) useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { onDismiss?.() } } window.addEventListener('keydown', handleEscape) return () => window.removeEventListener('keydown', handleEscape) }, [isOpen, onDismiss]) useEffect(() => { if (!selectedFile) return if (fileTypeErrorRef.current) return if (hasError) { setUploadState(FileUploadState.Error) setErrorType(errorTypeProp ?? FileUploadErrorType.InvalidStructure) return } if (isUploading) { setUploadState(FileUploadState.Uploading) setErrorType(undefined) return } if (isProcessing) { setUploadState(FileUploadState.Processing) setErrorType(undefined) return } setUploadState(FileUploadState.Completed) setErrorType(undefined) }, [hasError, selectedFile, isUploading, isProcessing, errorTypeProp]) const isValidFileType = (file: File): boolean => { const fileName = file.name.toLowerCase() const acceptedTypes = accept .split(',') .map((value) => value.trim().toLowerCase()) .filter(Boolean) if (acceptedTypes.length === 0) { return fileName.endsWith('.csv') } return acceptedTypes.some((value) => value.startsWith('.') ? fileName.endsWith(value) : file.type.toLowerCase() === value ) } const handleFileChange = (e: ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length > 0) { const file = files[0] setSelectedFile(file) if (!isValidFileType(file)) { fileTypeErrorRef.current = FileUploadErrorType.Unsupported setUploadState(FileUploadState.Error) setErrorType(FileUploadErrorType.Unsupported) return } fileTypeErrorRef.current = undefined setErrorType(undefined) setUploadState(FileUploadState.Uploading) if (onFileSelect) { onFileSelect(files) } } } const handleDrag = (e: DragEvent) => { if (!isOpen) return e.preventDefault() e.stopPropagation() if (e.type === 'dragenter' || e.type === 'dragover') { setDragActive(true) } else if (e.type === 'dragleave') { setDragActive(false) } } const handleDrop = (e: DragEvent) => { if (!isOpen) return e.preventDefault() e.stopPropagation() setDragActive(false) if (e.dataTransfer.files && e.dataTransfer.files[0]) { const files = Array.from(e.dataTransfer.files) const file = files[0] setSelectedFile(file) if (!isValidFileType(file)) { fileTypeErrorRef.current = FileUploadErrorType.Unsupported setUploadState(FileUploadState.Error) setErrorType(FileUploadErrorType.Unsupported) return } fileTypeErrorRef.current = undefined setErrorType(undefined) setUploadState(FileUploadState.Uploading) if (onFileSelect) { onFileSelect(files) } } } const triggerFileInput = () => { if (fileInputRef.current) { fileInputRef.current.value = '' } fileInputRef.current?.click() } const handleDownloadTemplate = () => { if (onDownloadTemplate) { onDownloadTemplate() } else { const csvContent = 'SKU,Quantity\nAB001,2\nAB100,5\nAB999,49' const blob = new Blob([csvContent], { type: 'text/csv' }) const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'template.csv' a.click() window.URL.revokeObjectURL(url) } } const handleRemoveFile = () => { fileTypeErrorRef.current = undefined setSelectedFile(null) setUploadState(FileUploadState.Uploading) setErrorType(undefined) if (fileInputRef.current) { fileInputRef.current.value = '' } } const handleSearch = () => { if (selectedFile && onSearch) { onSearch(selectedFile) } } return ( {selectedFile ? ( ) : (

{dropzoneTitle}

)} ) } export default FileUploadCard