Commit 93f8fe84 authored by JzoNg's avatar JzoNg

feat: file upload

parent 3f9af702
...@@ -10,8 +10,9 @@ import { ToastContext } from '@/app/components/base/toast' ...@@ -10,8 +10,9 @@ import { ToastContext } from '@/app/components/base/toast'
import { upload } from '@/service/base' import { upload } from '@/service/base'
type IFileUploaderProps = { type IFileUploaderProps = {
files: FileEntity[] fileList: FileEntity[]
onFileUpdate: (file?: FileEntity) => void prepareFileList: (files: any[]) => void
onFileUpdate: (fileItem: any, progress: number, list: any[]) => void
onPreview: (file: FileEntity) => void onPreview: (file: FileEntity) => void
} }
...@@ -27,9 +28,15 @@ const ACCEPTS = [ ...@@ -27,9 +28,15 @@ const ACCEPTS = [
'.csv', '.csv',
] ]
const MAX_SIZE = 10 * 1024 * 1024 const MAX_SIZE = 15 * 1024 * 1024
const BATCH_COUNT = 5
const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => { const FileUploader = ({
fileList,
prepareFileList,
onFileUpdate,
onPreview,
}: IFileUploaderProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [dragging, setDragging] = useState(false) const [dragging, setDragging] = useState(false)
...@@ -41,6 +48,9 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -41,6 +48,9 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [percent, setPercent] = useState(0) const [percent, setPercent] = useState(0)
// TODO
const fileListRef = useRef<any>(null)
// utils // utils
const getFileType = (currentFile: File) => { const getFileType = (currentFile: File) => {
if (!currentFile) if (!currentFile)
...@@ -72,6 +82,7 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -72,6 +82,7 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
const onProgress = useCallback((e: ProgressEvent) => { const onProgress = useCallback((e: ProgressEvent) => {
if (e.lengthComputable) { if (e.lengthComputable) {
const percent = Math.floor(e.loaded / e.total * 100) const percent = Math.floor(e.loaded / e.total * 100)
// updateFileItem
setPercent(percent) setPercent(percent)
} }
}, [setPercent]) }, [setPercent])
...@@ -79,43 +90,93 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -79,43 +90,93 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
const currentXHR = uploadPromise.current const currentXHR = uploadPromise.current
currentXHR.abort() currentXHR.abort()
} }
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()
}
// TODO // TODO
const fileUpload = async (file?: File) => { const removeFile = () => {
if (!file) if (fileUploader.current)
return fileUploader.current.value = ''
if (!isValid(file)) setCurrentFile(undefined)
return // onFileUpdate()
}
setCurrentFile(file) const fileUpload = (fileItem: any) => {
setUploading(true) const fileListCopy = fileListRef.current
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', fileItem.file)
// store for abort const onProgress = (e: ProgressEvent) => {
const currentXHR = new XMLHttpRequest() if (e.lengthComputable) {
uploadPromise.current = currentXHR const percent = Math.floor(e.loaded / e.total * 100)
try { onFileUpdate(fileItem, percent, fileListCopy)
const result = await upload({ }
xhr: currentXHR,
data: formData,
onprogress: onProgress,
}) as FileEntity
onFileUpdate(result)
setUploading(false)
} }
catch (xhr: any) { console.log('fff1', fileListCopy)
setUploading(false)
// abort handle
if (xhr.readyState === 0 && xhr.status === 0) {
if (fileUploader.current)
fileUploader.current.value = ''
setCurrentFile(undefined) return upload({
return xhr: new XMLHttpRequest(),
} data: formData,
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.failed') }) onprogress: onProgress,
})
.then((res: FileEntity) => {
const completeFile = {
fileID: fileItem.fileID,
file: res,
}
console.log('fff2', fileListCopy)
onFileUpdate(completeFile, 100, fileListCopy)
return Promise.resolve({ ...completeFile })
})
.catch((err) => {
console.log(err)
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.failed') })
onFileUpdate(fileItem, -2, fileListCopy)
return Promise.resolve({ ...fileItem })
})
.finally()
}
const uploadBatchFiles = (bFiles: any) => {
bFiles.forEach((bf: any) => (bf.progress = 0))
return Promise.all(bFiles.map((bFile: any) => fileUpload(bFile)))
}
const uploadMultipleFiles = async (files: any) => {
const length = files.length
let start = 0
let end = 0
while (start < length) {
if (start + BATCH_COUNT > length)
end = length
else
end = start + BATCH_COUNT
const bFiles = files.slice(start, end)
await uploadBatchFiles(bFiles)
start = end
} }
} }
const initialUpload = (files: any) => {
if (!files.length)
return false
const preparedFiles = files.map((file: any, index: number) => {
const fileItem = {
fileID: `file${index}-${Date.now()}`,
file,
progress: -1,
}
return fileItem
})
prepareFileList(preparedFiles)
// TODO fix filelist copy
fileListRef.current = preparedFiles
uploadMultipleFiles(preparedFiles)
}
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = [...(e.target.files ?? [])].filter(file => isValid(file))
initialUpload(files)
}
const handleDragEnter = (e: DragEvent) => { const handleDragEnter = (e: DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
...@@ -147,25 +208,6 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -147,25 +208,6 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
fileUpload(files[0]) fileUpload(files[0])
} }
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()
}
// TODO
const removeFile = () => {
if (fileUploader.current)
fileUploader.current.value = ''
setCurrentFile(undefined)
onFileUpdate()
}
// TODO
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
const currentFile = e.target.files?.[0]
onFileUpdate()
fileUpload(currentFile)
}
useEffect(() => { useEffect(() => {
dropRef.current?.addEventListener('dragenter', handleDragEnter) dropRef.current?.addEventListener('dragenter', handleDragEnter)
dropRef.current?.addEventListener('dragover', handleDragOver) dropRef.current?.addEventListener('dragover', handleDragOver)
...@@ -201,6 +243,33 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -201,6 +243,33 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
{dragging && <div ref={dragRef} className={s.draggingCover}/>} {dragging && <div ref={dragRef} className={s.draggingCover}/>}
</div> </div>
<div className={s.fileList}> <div className={s.fileList}>
{fileList.map(fileItem => (
<div
// onClick={() => onPreview(currentFile)}
className={cn(
s.file,
fileItem.progress < 100 && s.uploading,
// s.active,
)}
>
{fileItem.progress < 100 && (
<div className={s.progressbar} style={{ width: `${percent}%` }}/>
)}
<div className={s.fileInfo}>
<div className={cn(s.fileIcon, s[getFileType(fileItem.file)])}/>
<div className={s.filename}>{fileItem.file.name}</div>
<div className={s.size}>{getFileSize(fileItem.file.size)}</div>
</div>
<div className={s.actionWrapper}>
{fileItem.progress < 100 && (
<div className={s.percent}>{`${fileItem.progress}%`}</div>
)}
{fileItem.progress === 100 && (
<div className={s.remove} onClick={removeFile}/>
)}
</div>
</div>
))}
{currentFile && ( {currentFile && (
<div <div
// onClick={() => onPreview(currentFile)} // onClick={() => onPreview(currentFile)}
...@@ -230,7 +299,7 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -230,7 +299,7 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
)} )}
</div> </div>
{/* TODO */} {/* TODO */}
{!currentFile && files[0] && ( {false && !currentFile && fileList[0] && (
<div <div
// onClick={() => onPreview(currentFile)} // onClick={() => onPreview(currentFile)}
className={cn( className={cn(
...@@ -243,9 +312,9 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) => ...@@ -243,9 +312,9 @@ const FileUploader = ({ files, onFileUpdate, onPreview }: IFileUploaderProps) =>
<div className={s.progressbar} style={{ width: `${percent}%` }}/> <div className={s.progressbar} style={{ width: `${percent}%` }}/>
)} )}
<div className={s.fileInfo}> <div className={s.fileInfo}>
<div className={cn(s.fileIcon, s[getFileType(files[0])])}/> <div className={cn(s.fileIcon, s[getFileType(fileList[0])])}/>
<div className={s.filename}>{files[0].name}</div> <div className={s.filename}>{fileList[0].name}</div>
<div className={s.size}>{getFileSize(files[0].size)}</div> <div className={s.size}>{getFileSize(fileList[0].size)}</div>
</div> </div>
<div className={s.actionWrapper}> <div className={s.actionWrapper}>
{uploading && ( {uploading && (
......
...@@ -8,7 +8,7 @@ import StepOne from './step-one' ...@@ -8,7 +8,7 @@ import StepOne from './step-one'
import StepTwo from './step-two' import StepTwo from './step-two'
import StepThree from './step-three' import StepThree from './step-three'
import { DataSourceType } from '@/models/datasets' import { DataSourceType } from '@/models/datasets'
import type { DataSet, File, createDocumentResponse } from '@/models/datasets' import type { DataSet, createDocumentResponse } from '@/models/datasets'
import { fetchDataSource, fetchTenantInfo } from '@/service/common' import { fetchDataSource, fetchTenantInfo } from '@/service/common'
import { fetchDataDetail } from '@/service/datasets' import { fetchDataDetail } from '@/service/datasets'
import type { DataSourceNotionPage } from '@/models/common' import type { DataSourceNotionPage } from '@/models/common'
...@@ -30,7 +30,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { ...@@ -30,7 +30,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE) const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE)
const [step, setStep] = useState(1) const [step, setStep] = useState(1)
const [indexingTypeCache, setIndexTypeCache] = useState('') const [indexingTypeCache, setIndexTypeCache] = useState('')
const [fileList, setFiles] = useState<File[]>([]) const [fileList, setFiles] = useState<any[]>([])
const [result, setResult] = useState<createDocumentResponse | undefined>() const [result, setResult] = useState<createDocumentResponse | undefined>()
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
...@@ -40,14 +40,19 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { ...@@ -40,14 +40,19 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
} }
// TODO // TODO
const updateFile = (file?: File) => { const updateFileList = (preparedFiles: any) => {
if (file) { console.log('preparedFiles', preparedFiles)
setFiles([ setFiles(preparedFiles)
...fileList, }
file, const updateFile = (fileItem: any, progress: number, list: any[]) => {
]) const targetIndex = list.findIndex((file: any) => file.fileID === fileItem.fileID)
list[targetIndex] = {
...fileItem,
progress,
} }
setFiles(list)
} }
const updateIndexingTypeCache = (type: string) => { const updateIndexingTypeCache = (type: string) => {
setIndexTypeCache(type) setIndexTypeCache(type)
} }
...@@ -111,6 +116,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { ...@@ -111,6 +116,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
dataSourceTypeDisable={!!detail?.data_source_type} dataSourceTypeDisable={!!detail?.data_source_type}
changeType={setDataSourceType} changeType={setDataSourceType}
files={fileList} files={fileList}
updateFileList={updateFileList}
updateFile={updateFile} updateFile={updateFile}
notionPages={notionPages} notionPages={notionPages}
updateNotionPages={updateNotionPages} updateNotionPages={updateNotionPages}
......
'use client' 'use client'
import React, { useState } from 'react' import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import FilePreview from '../file-preview' import FilePreview from '../file-preview'
...@@ -19,8 +19,9 @@ type IStepOneProps = { ...@@ -19,8 +19,9 @@ type IStepOneProps = {
dataSourceTypeDisable: Boolean dataSourceTypeDisable: Boolean
hasConnection: boolean hasConnection: boolean
onSetting: () => void onSetting: () => void
files: File[] files: any[]
updateFile: (file?: File) => void updateFileList: (files: any[]) => void
updateFile: (fileItem: any, progress: number, list: any[]) => void
notionPages?: any[] notionPages?: any[]
updateNotionPages: (value: any[]) => void updateNotionPages: (value: any[]) => void
onStepChange: () => void onStepChange: () => void
...@@ -54,6 +55,7 @@ const StepOne = ({ ...@@ -54,6 +55,7 @@ const StepOne = ({
onSetting, onSetting,
onStepChange, onStepChange,
files, files,
updateFileList,
updateFile, updateFile,
notionPages = [], notionPages = [],
updateNotionPages, updateNotionPages,
...@@ -80,6 +82,17 @@ const StepOne = ({ ...@@ -80,6 +82,17 @@ const StepOne = ({
setCurrentNotionPage(undefined) setCurrentNotionPage(undefined)
} }
// TODO
const nextDisabled = useMemo(() => {
if (!files.length)
return true
console.log(files)
console.log(files.some(file => file.progress !== 100))
if (files.some(file => file.progress !== 100))
return true
return false
}, [files])
return ( return (
<div className='flex w-full h-full'> <div className='flex w-full h-full'>
<div className='grow overflow-y-auto relative'> <div className='grow overflow-y-auto relative'>
...@@ -131,9 +144,13 @@ const StepOne = ({ ...@@ -131,9 +144,13 @@ const StepOne = ({
</div> </div>
{dataSourceType === DataSourceType.FILE && ( {dataSourceType === DataSourceType.FILE && (
<> <>
<FileUploader onFileUpdate={updateFile} onPreview={updateCurrentFile} files={files} /> <FileUploader
{/* TODO */} fileList={files}
<Button disabled={!files.length} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button> prepareFileList={updateFileList}
onFileUpdate={updateFile}
onPreview={updateCurrentFile}
/>
<Button disabled={nextDisabled} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
</> </>
)} )}
{dataSourceType === DataSourceType.NOTION && ( {dataSourceType === DataSourceType.NOTION && (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment