-
-
Notifications
You must be signed in to change notification settings - Fork 266
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ability to add and delete files of existing share (#306)
* feat(share): delete file api, revert complete share api. * feat(share): share edit page. * feat(share): Modify the DropZone title of the edit sharing UI. * feat(share): i18n for edit share. (en, zh) * feat(share): allow creator get share by id. * feat(share): add edit button in account/shares. * style(share): lint. * chore: some minor adjustments. * refactor: run formatter * refactor: remove unused return --------- Co-authored-by: Elias Schneider <[email protected]>
- Loading branch information
1 parent
e377ed1
commit 98380e2
Showing
15 changed files
with
493 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
import { Button, Group } from "@mantine/core"; | ||
import { useModals } from "@mantine/modals"; | ||
import { cleanNotifications } from "@mantine/notifications"; | ||
import { AxiosError } from "axios"; | ||
import pLimit from "p-limit"; | ||
import { useEffect, useMemo, useState } from "react"; | ||
import { FormattedMessage } from "react-intl"; | ||
import Dropzone from "../../components/upload/Dropzone"; | ||
import FileList from "../../components/upload/FileList"; | ||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal"; | ||
import useConfig from "../../hooks/config.hook"; | ||
import useTranslate from "../../hooks/useTranslate.hook"; | ||
import shareService from "../../services/share.service"; | ||
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type"; | ||
import toast from "../../utils/toast.util"; | ||
import { useRouter } from "next/router"; | ||
|
||
const promiseLimit = pLimit(3); | ||
const chunkSize = 10 * 1024 * 1024; // 10MB | ||
let errorToastShown = false; | ||
|
||
const EditableUpload = ({ | ||
maxShareSize, | ||
shareId, | ||
files: savedFiles = [], | ||
}: { | ||
maxShareSize?: number; | ||
isReverseShare?: boolean; | ||
shareId: string; | ||
files?: FileMetaData[]; | ||
}) => { | ||
const t = useTranslate(); | ||
const router = useRouter(); | ||
const config = useConfig(); | ||
|
||
const [existingFiles, setExistingFiles] = | ||
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles); | ||
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]); | ||
const [isUploading, setIsUploading] = useState(false); | ||
|
||
const existingAndUploadedFiles: FileListItem[] = useMemo( | ||
() => [...uploadingFiles, ...existingFiles], | ||
[existingFiles, uploadingFiles], | ||
); | ||
const dirty = useMemo(() => { | ||
return ( | ||
existingFiles.some((file) => !!file.deleted) || !!uploadingFiles.length | ||
); | ||
}, [existingFiles, uploadingFiles]); | ||
|
||
const setFiles = (files: FileListItem[]) => { | ||
const _uploadFiles = files.filter( | ||
(file) => "uploadingProgress" in file, | ||
) as FileUpload[]; | ||
const _existingFiles = files.filter( | ||
(file) => !("uploadingProgress" in file), | ||
) as FileMetaData[]; | ||
|
||
setUploadingFiles(_uploadFiles); | ||
setExistingFiles(_existingFiles); | ||
}; | ||
|
||
maxShareSize ??= parseInt(config.get("share.maxSize")); | ||
|
||
const uploadFiles = async (files: FileUpload[]) => { | ||
const fileUploadPromises = files.map(async (file, fileIndex) => | ||
// Limit the number of concurrent uploads to 3 | ||
promiseLimit(async () => { | ||
let fileId: string; | ||
|
||
const setFileProgress = (progress: number) => { | ||
setUploadingFiles((files) => | ||
files.map((file, callbackIndex) => { | ||
if (fileIndex == callbackIndex) { | ||
file.uploadingProgress = progress; | ||
} | ||
return file; | ||
}), | ||
); | ||
}; | ||
|
||
setFileProgress(1); | ||
|
||
let chunks = Math.ceil(file.size / chunkSize); | ||
|
||
// If the file is 0 bytes, we still need to upload 1 chunk | ||
if (chunks == 0) chunks++; | ||
|
||
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) { | ||
const from = chunkIndex * chunkSize; | ||
const to = from + chunkSize; | ||
const blob = file.slice(from, to); | ||
try { | ||
await new Promise((resolve, reject) => { | ||
const reader = new FileReader(); | ||
reader.onload = async (event) => | ||
await shareService | ||
.uploadFile( | ||
shareId, | ||
event, | ||
{ | ||
id: fileId, | ||
name: file.name, | ||
}, | ||
chunkIndex, | ||
chunks, | ||
) | ||
.then((response) => { | ||
fileId = response.id; | ||
resolve(response); | ||
}) | ||
.catch(reject); | ||
|
||
reader.readAsDataURL(blob); | ||
}); | ||
|
||
setFileProgress(((chunkIndex + 1) / chunks) * 100); | ||
} catch (e) { | ||
if ( | ||
e instanceof AxiosError && | ||
e.response?.data.error == "unexpected_chunk_index" | ||
) { | ||
// Retry with the expected chunk index | ||
chunkIndex = e.response!.data!.expectedChunkIndex - 1; | ||
continue; | ||
} else { | ||
setFileProgress(-1); | ||
// Retry after 5 seconds | ||
await new Promise((resolve) => setTimeout(resolve, 5000)); | ||
chunkIndex = -1; | ||
|
||
continue; | ||
} | ||
} | ||
} | ||
}), | ||
); | ||
|
||
await Promise.all(fileUploadPromises); | ||
}; | ||
|
||
const removeFiles = async () => { | ||
const removedFiles = existingFiles.filter((file) => !!file.deleted); | ||
|
||
if (removedFiles.length > 0) { | ||
await Promise.all( | ||
removedFiles.map(async (file) => { | ||
await shareService.removeFile(shareId, file.id); | ||
}), | ||
); | ||
|
||
setExistingFiles(existingFiles.filter((file) => !file.deleted)); | ||
} | ||
}; | ||
|
||
const revertComplete = async () => { | ||
await shareService.revertComplete(shareId).then(); | ||
}; | ||
|
||
const completeShare = async () => { | ||
return await shareService.completeShare(shareId); | ||
}; | ||
|
||
const save = async () => { | ||
setIsUploading(true); | ||
|
||
try { | ||
await revertComplete(); | ||
await uploadFiles(uploadingFiles); | ||
|
||
const hasFailed = uploadingFiles.some( | ||
(file) => file.uploadingProgress == -1, | ||
); | ||
|
||
if (!hasFailed) { | ||
await removeFiles(); | ||
} | ||
|
||
await completeShare(); | ||
|
||
if (!hasFailed) { | ||
toast.success(t("share.edit.notify.save-success")); | ||
router.back(); | ||
} | ||
} catch { | ||
toast.error(t("share.edit.notify.generic-error")); | ||
} finally { | ||
setIsUploading(false); | ||
} | ||
}; | ||
|
||
const appendFiles = (appendingFiles: FileUpload[]) => { | ||
setUploadingFiles([...appendingFiles, ...uploadingFiles]); | ||
}; | ||
|
||
useEffect(() => { | ||
// Check if there are any files that failed to upload | ||
const fileErrorCount = uploadingFiles.filter( | ||
(file) => file.uploadingProgress == -1, | ||
).length; | ||
|
||
if (fileErrorCount > 0) { | ||
if (!errorToastShown) { | ||
toast.error( | ||
t("upload.notify.count-failed", { count: fileErrorCount }), | ||
{ | ||
withCloseButton: false, | ||
autoClose: false, | ||
}, | ||
); | ||
} | ||
errorToastShown = true; | ||
} else { | ||
cleanNotifications(); | ||
errorToastShown = false; | ||
} | ||
}, [uploadingFiles]); | ||
|
||
return ( | ||
<> | ||
<Group position="right" mb={20}> | ||
<Button loading={isUploading} disabled={!dirty} onClick={() => save()}> | ||
<FormattedMessage id="common.button.save" /> | ||
</Button> | ||
</Group> | ||
<Dropzone | ||
title={t("share.edit.append-upload")} | ||
maxShareSize={maxShareSize} | ||
showCreateUploadModalCallback={appendFiles} | ||
isUploading={isUploading} | ||
/> | ||
{existingAndUploadedFiles.length > 0 && ( | ||
<FileList files={existingAndUploadedFiles} setFiles={setFiles} /> | ||
)} | ||
</> | ||
); | ||
}; | ||
export default EditableUpload; |
Oops, something went wrong.