all
This commit is contained in:
@ -0,0 +1,105 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { IconClose, IconSchedule, IconCheckCircle, IconError } from '@/lib'
|
||||
import {
|
||||
Upload,
|
||||
UploadDecorator,
|
||||
uploadRemoved,
|
||||
} from '@/store/entities/uploads'
|
||||
import { useAppDispatch } from '@/store/hook'
|
||||
|
||||
export type UploadItemProps = {
|
||||
upload: Upload
|
||||
}
|
||||
|
||||
const UploadItem = ({ upload: uploadProp }: UploadItemProps) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const upload = new UploadDecorator(uploadProp)
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1')}>
|
||||
<div
|
||||
className={cx(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-0.5',
|
||||
'justify-between',
|
||||
'h-2.5',
|
||||
)}
|
||||
>
|
||||
{upload.isProgressing && (
|
||||
<CircularProgress
|
||||
value={upload.progress}
|
||||
max={100}
|
||||
isIndeterminate={upload.progress === 100 && !upload.error}
|
||||
className={cx('text-black')}
|
||||
size="20px"
|
||||
/>
|
||||
)}
|
||||
{upload.isPending && (
|
||||
<IconSchedule className={cx('shrink-0', 'text-gray-500')} />
|
||||
)}
|
||||
{upload.isSucceeded && (
|
||||
<IconCheckCircle
|
||||
className={cx('shrink-0', 'text-green-500')}
|
||||
filled={true}
|
||||
/>
|
||||
)}
|
||||
{upload.isFailed && (
|
||||
<div className={cx('shrink-0', 'text-red-500')}>
|
||||
<IconError filled={true} />
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={cx(
|
||||
'grow',
|
||||
'text-ellipsis',
|
||||
'overflow-hidden',
|
||||
'whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
{upload.file.name}
|
||||
</span>
|
||||
<IconButton
|
||||
icon={<IconClose />}
|
||||
size="xs"
|
||||
variant="outline"
|
||||
colorScheme={upload.isProgressing ? 'red' : 'gray'}
|
||||
aria-label=""
|
||||
onClick={() => {
|
||||
upload.request?.abort()
|
||||
dispatch(uploadRemoved(upload.id))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{upload.isFailed && (
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem className={cx('border-none')}>
|
||||
<AccordionButton className={cx('p-0.5', 'hover:bg-red-50')}>
|
||||
<div className={cx('flex', 'flex-row', 'w-full')}>
|
||||
<span className={cx('text-red-500', 'text-left', 'grow')}>
|
||||
Upload failed. Click to expand.
|
||||
</span>
|
||||
<AccordionIcon className={cx('text-red-500')} />
|
||||
</div>
|
||||
</AccordionButton>
|
||||
<AccordionPanel className={cx('p-0.5')}>
|
||||
{upload.error}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadItem
|
@ -0,0 +1,44 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Divider } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hook'
|
||||
import { uploadsDrawerClosed } from '@/store/ui/uploads-drawer'
|
||||
import UploadItem from './upload-item'
|
||||
import { queue } from './upload-worker'
|
||||
|
||||
const UploadList = () => {
|
||||
const items = useAppSelector((state) => state.entities.uploads.items)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
for (const upload of items) {
|
||||
if (
|
||||
queue.findIndex((e) => e.id === upload.id) !== -1 ||
|
||||
upload.completed
|
||||
) {
|
||||
continue
|
||||
}
|
||||
queue.push(upload)
|
||||
}
|
||||
if (items.length === 0) {
|
||||
dispatch(uploadsDrawerClosed())
|
||||
}
|
||||
}, [items, dispatch])
|
||||
|
||||
if (items.length === 0) {
|
||||
return <span>There are no uploads.</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
{items.map((u, index) => (
|
||||
<div key={u.id} className={cx('flex', 'flex-col', 'gap-1.5')}>
|
||||
<UploadItem upload={u} />
|
||||
{index !== items.length - 1 && <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadList
|
@ -0,0 +1,49 @@
|
||||
import { FileWithPath } from 'react-dropzone'
|
||||
import FileAPI from '@/client/api/file'
|
||||
import { errorToString } from '@/client/error'
|
||||
import store from '@/store/configure-store'
|
||||
import {
|
||||
Upload,
|
||||
uploadCompleted,
|
||||
uploadUpdated,
|
||||
} from '@/store/entities/uploads'
|
||||
|
||||
export const queue: Upload[] = []
|
||||
let working = false
|
||||
|
||||
setInterval(async () => {
|
||||
if (queue.length === 0 || working) {
|
||||
return
|
||||
}
|
||||
working = true
|
||||
const upload = queue.at(0) as Upload
|
||||
try {
|
||||
const request = new XMLHttpRequest()
|
||||
store.dispatch(uploadUpdated({ id: upload.id, request }))
|
||||
await FileAPI.upload({
|
||||
workspaceId: upload.workspaceId,
|
||||
parentId: upload.parentId,
|
||||
name:
|
||||
(upload.file as FileWithPath).path ||
|
||||
upload.file.webkitRelativePath ||
|
||||
upload.file.name,
|
||||
request,
|
||||
file: upload.file,
|
||||
onProgress: (progress) => {
|
||||
store.dispatch(uploadUpdated({ id: upload.id, progress }))
|
||||
},
|
||||
})
|
||||
store.dispatch(uploadCompleted(upload.id))
|
||||
} catch (error) {
|
||||
store.dispatch(
|
||||
uploadUpdated({
|
||||
id: upload.id,
|
||||
completed: true,
|
||||
error: errorToString(error),
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
queue.shift()
|
||||
working = false
|
||||
}
|
||||
}, 1000)
|
Reference in New Issue
Block a user