Skip to content

Commit

Permalink
Cu 86b2qw06r feat add upload event listeners (DevinoSolutions#247)
Browse files Browse the repository at this point in the history
* feat(uploader): enhanced file preview with react-file-viewer

- Added support for previewing multiple file types (PDF, DOCX, XLSX, etc.)
- Implemented `react-file-viewer` for better file preview capabilities
- Maintained fallback to FileIcon component when preview fails

OTHER CHANGES
- Resolved React version mismatch (16.14.0 -> 18.2.0) to fix hook errors in Storybook
- Added VSCode workspace folder to `.gitignore`

BREAKING CHANGE: New dependency `react-file-viewer` is required

* feat(uploader): enhanced file preview hover interactions

- Added hover effect with background color change and shadow elevation
- Implemented cursor pointer for clickable file previews
- Added active state for click feedback
- Enhanced dark mode hover states
- Maintained consistent hover behaviour across mini and full preview modes

* fix(uploader): resolved DOCX preview conflicts and improve type safety

- Added modal-based DOCX preview to prevent interference between multiple previews
- Created FileWithId interface and shared type definitions
- Fixed file ID generation and management
- Added proper cleanup for object URLs
- Ensured consistent key generation for previews

BREAKING CHANGE: File state type is now FileWithId[] instead of File[]

* feat(preview): implemented universal portal-based file preview

- Replaced DOCX-specific preview with universal file preview component
- Added modal preview support for all file types
- Fixed file name truncation in preview cards

* chore(app): cleaned up unused files

* feat(events): implemented comprehensive upload event system

- Added file handling events (onFileClick, onFilesChange, onFileRemove)
- Added upload lifecycle events (onUpload, onCompletedUpload, onAllCompleted, onUploadFail)
- Added progress tracking events (onFileProgress, onTotalProgress)
- Added drag-and-drop events (onDragOver, onDragLeave, onDrop)
- Added integration selection event (onClick)

The event system provides granular control and visibility into:
- File selection and removal
- Upload progress per file and overall
- Upload success/failure states
- Drag-and-drop interactions
- Integration method selection
- Updates

Demonstrates all events in Storybook with status updates and console logs

* feat(app): implemented event handlers for mini uploader

* refactor(app): name refinements to reduce ambiguity and ensure consistency

per review, changed:
- `onClick` to `onIntegrationClick`
- `onUpload` to `onFileUploadStart`
- `onCompletedUpload` to `onFileUploadComplete`
- `onAllCompleted` to `onAllUploadsComplete`
- `onUploadFail` to `onFileUploadFail`
- `onTotalProgress` to `onTotalUploadProgress`
- `onDragOver` to `onFileDragOver`
- `onDragLeave` to `onFileDragLeave`
- `onDrop` to `onFileDrop`

fix failing build caused by `src/lib/uploadObject.ts`

* feat: added onFileTypeMismatch and onCancelUpload events

- Added onFileTypeMismatch event to handle file type validation failures
- Added onCancelUpload event for upload cancellation handling
- Updated useProgress hook to properly handle upload abortion
- Exposed cancelUpload method through component ref

* refactor(app): swapped `Date.now` and `Math.random` calls for more accurate values from `uuid` call

* fix(app): prettier fixes
  • Loading branch information
amjedidiah authored Nov 27, 2024
1 parent e9a36c1 commit 54c53aa
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 236 deletions.
292 changes: 191 additions & 101 deletions src/UpupUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
RefAttributes,
SetStateAction,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useState,
Expand Down Expand Up @@ -134,6 +135,79 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
*/
const client = getClient(s3Configs)

/**
* Check if file type is accepted
* @param file File to check
* @throws Error if file type is not accepted
*/
const validateFileType = (file: File) => {
if (!checkFileType(file, accept, baseConfigs?.onFileTypeMismatch)) {
const error = new Error(`File type ${file.type} not accepted`)
baseConfigs?.onFileUploadFail?.(file, error)
throw error
}
}

/**
* Check if file size is within limits
* @param files Array of files to check
* @throws Error if total file size exceeds limit
*/
const validateFileSize = (files: File[]) => {
if (maxFilesSize) {
const totalSize = files.reduce(
(acc, file) => acc + file.size,
0,
)
if (totalSize > maxFilesSize) {
const error = new Error(
`Total file size must be less than ${
maxFilesSize / 1024 / 1024
}MB`,
)
files.forEach(
file => baseConfigs?.onFileUploadFail?.(file, error),
)
throw error
}
}
}

/**
* Compress files if compression is enabled
* @param files Array of files to compress
* @returns Promise<File[]> Compressed files
*/
const compressFiles = async (files: File[]): Promise<File[]> => {
if (!toBeCompressed) return files

try {
return await Promise.all(
files.map(async file => {
const compressed = await compressFile({
element: file,
element_name: file.name,
})
return compressed
}),
)
} catch (error) {
files.forEach(
file =>
baseConfigs?.onFileUploadFail?.(file, error as Error),
)
throw error
}
}

const handleUploadCancel = useCallback(() => {
if (progress > 0 && progress < 100) {
// Cancel the ongoing upload
handler.abort()
baseConfigs?.onCancelUpload?.(files)
}
}, [progress, files, handler, baseConfigs])

/**
* Expose the handleUpload function to the parent component
*/
Expand All @@ -150,93 +224,60 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
: files
return await this.proceedUpload(filesList)
},

cancelUpload: handleUploadCancel,
async proceedUpload(filesList: File[]) {
return new Promise(async (resolve, reject) => {
/**
* Check if the total size of files is less than the maximum size
*/
const filesSize = maxFilesSize
? files.reduce((acc, file) => acc + file.size, 0)
: 0
if (maxFilesSize && filesSize > maxFilesSize) {
reject(
new Error(
'The total size of files must be less than ' +
maxFilesSize / 1024 / 1024 +
'MB',
),
)
}
/**
* Upload the file to the cloud storage
*/
let filesToUpload: File[]
let keys: string[] = []
/**
* Compress the file before uploading it to the cloud storage
*/
if (toBeCompressed)
filesToUpload = await Promise.all(
filesList.map(async file => {
/**
* Compress the file
*/
return await compressFile({
element: file,
element_name: file.name,
})
}),
)
else filesToUpload = filesList
/**
* Loop through the files array and upload the files
*/
if (filesToUpload) {
try {
filesToUpload.map(async file => {
try {
// Validate all files first
filesList.forEach(validateFileType)
validateFileSize(filesList)

// Notify upload start
filesList.forEach(file => {
baseConfigs?.onFileUploadStart?.(file)
})

// Compress files if needed
const processedFiles = await compressFiles(filesList)

const uploadPromises = processedFiles.map(
async file => {
const fileExtension = file.name.split('.').pop()
/**
* assign a unique name for the file contain timestamp and random string with extension from the original file
*/
const key = `${Date.now()}__${uuidv4()}.${fileExtension}`

/**
* Upload the file to the cloud storage
*/
await uploadObject({
client,
bucket,
key,
file,
})
.then(data => {
if (data.httpStatusCode === 200) {
keys.push(key)
} else
throw new Error(
'Something went wrong',
)
})
.catch(err => {
throw new Error(err.message)
const key = `${uuidv4()}.${fileExtension}`

try {
const result = await uploadObject({
client,
bucket,
key,
file,
})
.finally(() => {
if (
keys.length === filesToUpload.length

if (result.httpStatusCode === 200) {
baseConfigs?.onFileUploadComplete?.(
file,
key,
)
resolve(keys) // return the keys to the parent component
})
})
} catch (error) {
if (error instanceof Error) {
// ✅ TypeScript knows err is Error
reject(new Error(error.message))
} else {
reject(new Error('Something went wrong'))
}
}
} else reject(undefined)
return key
} else {
throw new Error('Upload failed')
}
} catch (error) {
baseConfigs?.onFileUploadFail?.(
file,
error as Error,
)
throw error
}
},
)

const keys = await Promise.all(uploadPromises)
baseConfigs?.onAllUploadsComplete?.(keys)
resolve(keys)
} catch (error) {
reject(error)
}
})
},
}))
Expand Down Expand Up @@ -297,30 +338,79 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
mutateFiles()
}, [files])

// Modify the input onChange handler
// Modify the input onChange handler to include validation
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const acceptedFiles = Array.from(e.target.files || [])
.filter(file => checkFileType(file, accept))
.map(createFileWithId)
setFiles(files =>
isAddingMore ? [...files, ...acceptedFiles] : acceptedFiles,
)
e.target.value = ''
try {
const newFiles = Array.from(e.target.files || [])
newFiles.forEach(validateFileType)
validateFileSize(newFiles)

const acceptedFiles = newFiles.map(createFileWithId)
setFiles(files =>
isAddingMore ? [...files, ...acceptedFiles] : acceptedFiles,
)
} catch (error) {
console.error(error)
// Don't set files if validation fails
} finally {
e.target.value = ''
}
}

// Modify the DropZone props
const handleDropzoneFiles = (newFiles: File[]) => {
const filesWithIds = newFiles.map(createFileWithId)
setFiles(files =>
isAddingMore ? [...files, ...filesWithIds] : [...filesWithIds],
)
// Modify the DropZone handler to include validation and maintain existing files
const handleDropzoneFiles: Dispatch<
SetStateAction<File[]>
> = filesOrUpdater => {
if (typeof filesOrUpdater === 'function') {
setFiles(prevFiles => {
try {
const updatedFiles = filesOrUpdater(prevFiles)
const newFiles = updatedFiles.slice(prevFiles.length)

// Validate only new files
newFiles.forEach(validateFileType)
validateFileSize([...prevFiles, ...newFiles])

const filesWithIds = newFiles.map(createFileWithId)
return [...prevFiles, ...filesWithIds]
} catch (error) {
console.error(error)
return prevFiles
}
})
} else {
try {
filesOrUpdater.forEach(validateFileType)
setFiles(prevFiles => {
try {
validateFileSize([...prevFiles, ...filesOrUpdater])
const filesWithIds =
filesOrUpdater.map(createFileWithId)
return [...prevFiles, ...filesWithIds]
} catch (error) {
console.error(error)
return prevFiles
}
})
} catch (error) {
console.error(error)
}
}
}

// Add file removal handler
const handleFileRemove = (file: FileWithId) => {
setFiles(prev => prev.filter(f => f !== file))
baseConfigs?.onFileRemove?.(file)
}

return mini ? (
<UpupMini
files={files}
setFiles={setFiles}
maxFileSize={maxFileSize}
handleFileRemove={handleFileRemove}
baseConfigs={baseConfigs}
/>
) : (
<div
Expand All @@ -332,14 +422,11 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
<AnimatePresence>
{isDragging && (
<DropZone
setFiles={
handleDropzoneFiles as Dispatch<
SetStateAction<File[]>
>
}
setFiles={handleDropzoneFiles}
setIsDragging={setIsDragging}
multiple={multiple}
accept={accept}
baseConfigs={baseConfigs}
/>
)}
</AnimatePresence>
Expand Down Expand Up @@ -372,6 +459,8 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
onFileClick={onFileClick}
progress={progress}
limit={limit}
handleFileRemove={handleFileRemove}
onCancelUpload={baseConfigs.onCancelUpload}
/>
<div className="h-full p-2">
<div className="grid h-full w-full grid-rows-[1fr,auto] place-items-center rounded-md border border-dashed border-[#dfdfdf] transition-all">
Expand All @@ -381,6 +470,7 @@ export const UpupUploader: FC<UpupUploaderProps & RefAttributes<any>> =
methods={METHODS.filter(method => {
return uploadAdapters.includes(method.id as any)
})}
baseConfigs={baseConfigs}
/>
<MetaVersion
customMessage={customMessage}
Expand Down
1 change: 1 addition & 0 deletions src/components/FilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const FilePreview: FC<PreviewProps> = ({ file, objectUrl }) => {
filePath={objectUrl}
onError={(e: Error) => {
console.error('Error in file preview:', e)
return <FileIcon extension={extension} />
}}
errorComponent={() => (
<FileIcon extension={extension} />
Expand Down
Loading

0 comments on commit 54c53aa

Please sign in to comment.