Files
payloadcms/packages/ui/src/elements/BulkUpload/FileSidebar/index.tsx
Tylan Davis 32cc1a5761 fix(ui): missing thumbnail for non-image files in bulk upload sidebar (#8102)
## Description

Uses the `Thumbnail` component used in other places for the bulk upload
file rows. Closes #8099

In the future, we should consider adding different thumbnail icons based
on the `mimeType` to better describe the files being uploaded.

Before:
![Screenshot 2024-09-06 at 4 51
56 PM](https://github.com/user-attachments/assets/35cd528c-5086-465e-8d3c-7bb66d7c35da)


After:
![Screenshot 2024-09-06 at 4 50
12 PM](https://github.com/user-attachments/assets/54d2b98d-ac11-481e-abe5-4be071c3c8f2)


- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-09-06 21:28:50 +00:00

191 lines
6.8 KiB
TypeScript

'use client'
import { useModal } from '@faceless-ui/modal'
import { useWindowInfo } from '@faceless-ui/window-info'
import { isImage } from 'payload/shared'
import React from 'react'
import AnimateHeightImport from 'react-animate-height'
import { ChevronIcon } from '../../../icons/Chevron/index.js'
import { XIcon } from '../../../icons/X/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { Button } from '../../Button/index.js'
import { Drawer } from '../../Drawer/index.js'
import { ErrorPill } from '../../ErrorPill/index.js'
import { Pill } from '../../Pill/index.js'
import { ShimmerEffect } from '../../ShimmerEffect/index.js'
import { Thumbnail } from '../../Thumbnail/index.js'
import { Actions } from '../ActionsBar/index.js'
import { AddFilesView } from '../AddFilesView/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import { useBulkUpload } from '../index.js'
import './index.scss'
const AnimateHeight = (AnimateHeightImport.default ||
AnimateHeightImport) as typeof AnimateHeightImport.default
const addMoreFilesDrawerSlug = 'bulk-upload-drawer--add-more-files'
const baseClass = 'file-selections'
export function FileSidebar() {
const {
activeIndex,
addFiles,
forms,
isInitializing,
removeFile,
setActiveIndex,
totalErrorCount,
} = useFormsManager()
const { initialFiles, maxFiles } = useBulkUpload()
const { i18n, t } = useTranslation()
const { closeModal, openModal } = useModal()
const [showFiles, setShowFiles] = React.useState(false)
const { breakpoints } = useWindowInfo()
const handleRemoveFile = React.useCallback(
(indexToRemove: number) => {
removeFile(indexToRemove)
},
[removeFile],
)
const handleAddFiles = React.useCallback(
(filelist: FileList) => {
void addFiles(filelist)
closeModal(addMoreFilesDrawerSlug)
},
[addFiles, closeModal],
)
const getFileSize = React.useCallback((file: File) => {
const size = file.size
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
const decimals = i > 1 ? 1 : 0
const formattedSize =
(size / Math.pow(1024, i)).toFixed(decimals) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
return formattedSize
}, [])
const totalFileCount = isInitializing ? initialFiles.length : forms.length
return (
<div
className={[baseClass, showFiles && `${baseClass}__showingFiles`].filter(Boolean).join(' ')}
>
{breakpoints.m && showFiles ? <div className={`${baseClass}__mobileBlur`} /> : null}
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__headerTopRow`}>
<div className={`${baseClass}__header__text`}>
<ErrorPill count={totalErrorCount} i18n={i18n} withMessage />
<p>
<strong
title={`${totalFileCount} ${t(totalFileCount > 1 ? 'upload:filesToUpload' : 'upload:fileToUpload')}`}
>
{totalFileCount}{' '}
{t(totalFileCount > 1 ? 'upload:filesToUpload' : 'upload:fileToUpload')}
</strong>
</p>
</div>
<div className={`${baseClass}__header__actions`}>
{typeof maxFiles === 'number' && totalFileCount < maxFiles ? (
<Pill onClick={() => openModal(addMoreFilesDrawerSlug)}>{t('upload:addFile')}</Pill>
) : null}
<Button
buttonStyle="transparent"
className={`${baseClass}__toggler`}
onClick={() => setShowFiles((prev) => !prev)}
>
<ChevronIcon direction={showFiles ? 'down' : 'up'} />
</Button>
<Drawer gutter={false} Header={null} slug={addMoreFilesDrawerSlug}>
<AddFilesView
onCancel={() => closeModal(addMoreFilesDrawerSlug)}
onDrop={handleAddFiles}
/>
</Drawer>
</div>
</div>
<div className={`${baseClass}__header__mobileDocActions`}>
<Actions />
</div>
</div>
<div className={`${baseClass}__animateWrapper`}>
<AnimateHeight duration={200} height={!breakpoints.m || showFiles ? 'auto' : 0}>
<div className={`${baseClass}__filesContainer`}>
{isInitializing && forms.length === 0 && initialFiles.length > 0
? Array.from(initialFiles).map((file, index) => (
<ShimmerEffect
animationDelay={`calc(${index} * ${60}ms)`}
height="35px"
key={index}
/>
))
: null}
{forms.map(({ errorCount, formState }, index) => {
const currentFile = formState.file.value as File
return (
<div
className={[
`${baseClass}__fileRowContainer`,
index === activeIndex && `${baseClass}__fileRowContainer--active`,
errorCount && errorCount > 0 && `${baseClass}__fileRowContainer--error`,
]
.filter(Boolean)
.join(' ')}
key={index}
>
<button
className={`${baseClass}__fileRow`}
onClick={() => setActiveIndex(index)}
type="button"
>
<Thumbnail
className={`${baseClass}__thumbnail`}
fileSrc={
isImage(currentFile.type) ? URL.createObjectURL(currentFile) : undefined
}
/>
<div className={`${baseClass}__fileDetails`}>
<p className={`${baseClass}__fileName`} title={currentFile.name}>
{currentFile.name}
</p>
</div>
<p className={`${baseClass}__fileSize`}>{getFileSize(currentFile)}</p>
<div className={`${baseClass}__remove ${baseClass}__remove--underlay`}>
<XIcon />
</div>
{errorCount ? (
<ErrorPill
className={`${baseClass}__errorCount`}
count={errorCount}
i18n={i18n}
/>
) : null}
</button>
<button
aria-label={t('general:remove')}
className={`${baseClass}__remove ${baseClass}__remove--overlay`}
onClick={() => handleRemoveFile(index)}
type="button"
>
<XIcon />
</button>
</div>
)
})}
</div>
</AnimateHeight>
</div>
</div>
)
}