From b7bf18f1ec3577167966c951759bb200533eab5b Mon Sep 17 00:00:00 2001 From: Jose Manuel Serrano Amaut <a20122128@pucp.pe> Date: Sun, 21 Jan 2024 15:59:41 -0500 Subject: [PATCH] [FEAT]: enhance upload to allow different urls for each file to be uploaded --- src/Dropzone/components/dropzone/Dropzone.tsx | 271 ++++++++++-------- .../components/dropzone/DropzoneProps.ts | 2 - src/FileCard/FileCardProps.ts | 3 +- src/FileInputButton/FileInputButton.tsx | 28 +- .../components/file-mosaic/FileMosaicProps.ts | 62 +--- src/utils/url.utils.ts | 9 + 6 files changed, 181 insertions(+), 194 deletions(-) create mode 100644 src/utils/url.utils.ts diff --git a/src/Dropzone/components/dropzone/Dropzone.tsx b/src/Dropzone/components/dropzone/Dropzone.tsx index ccff70c..a130948 100644 --- a/src/Dropzone/components/dropzone/Dropzone.tsx +++ b/src/Dropzone/components/dropzone/Dropzone.tsx @@ -28,8 +28,10 @@ import { unexpectedErrorUploadResult, getRandomInt, addClassName, - Localization,completeAsureColor, FileIdGenerator, -} from "@files-ui/core" + Localization, + completeAsureColor, + FileIdGenerator, +} from "@files-ui/core"; import { mergeProps } from "../../../overridable"; import InputHidden from "../../../InputHidden/InputHidden"; import { @@ -59,7 +61,12 @@ import { } from "../../../utils"; import { FilesUiContext } from "../../../FilesUiProvider/FilesUiContext"; import DropLayer from "../../../DropLayer/components/DropLayer"; -import { useDropzoneFileListUpdater, useDropLayerClassName, useDropzoneClassName } from "../../../hooks"; +import { + useDropzoneFileListUpdater, + useDropLayerClassName, + useDropzoneClassName, +} from "../../../hooks"; +import { isThereValidUrl } from "../../../utils/url.utils"; //import { print_manager } from "../../../../../utils"; @@ -146,6 +153,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { cleanOnUpload = true, preparingTime = 1500, autoUpload = false, + urlFromExtFile, } = uploadConfig as UploadConfig; const { @@ -197,8 +205,11 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { const [localMessage, setLocalMessage] = React.useState<string>(""); //Id for uploding through FuiFileManager //const dropzoneId: string | number = useDropzoneFileListID(); - // const dropzoneId: string | number = React.useId(); - const dropzoneId: string = React.useMemo(() => FileIdGenerator.getNextId() + "",[]); + // const dropzoneId: string | number = React.useId(); + const dropzoneId: string = React.useMemo( + () => FileIdGenerator.getNextId() + "", + [] + ); //React.useId(); //Flag that determines whether to validate or not const validateFilesFlag: boolean = isValidateActive( @@ -224,6 +235,10 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { localization, validateFilesFlag ); + /** + * Flag that determines if component should perform upload given url + */ + const shouldUpload: boolean = isThereValidUrl(url, urlFromExtFile, localFiles); /** * Uploads each file in the array of ExtFiles * First, sets all the files in preparing status and awaits `preparingTime` miliseconds. @@ -247,7 +262,6 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { * @returns nothing */ const uploadfiles = async (localFiles: ExtFile[]): Promise<void> => { - //set uploading flag to true setIsUploading(true); @@ -271,12 +285,11 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { let arrOfExtFilesInstances: ExtFileInstance[] = []; const totalNumber: number = localFiles.length; - + const missingUpload: number = localFiles.filter((extFile: ExtFile) => isUploadAbleExtFile(extFile, validateFilesFlag) ).length; - let totalRejected: number = 0; let currentCountUpload: number = 0; @@ -285,7 +298,6 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { //no missing to upload if (!(missingUpload > 0)) { - setTimeout(() => { if (noMissingFilesLabel) setLocalMessage(DropzoneLocalizer.noFilesMessage as string); @@ -300,7 +312,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { setLocalMessage(uploadingMessenger(`${missingUpload}/${totalNumber}`)); // setIsUploading(true); //PREPARING stage - + onUploadStart?.(localFiles); arrOfExtFilesInstances = @@ -315,138 +327,154 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { x.toExtFile() ); - //CHANGE (o alejo el isUploading o lo alejo para que tenga m,as tiempo antes de la respuyesta) // setIsUploading(true); handleFilesChange(newExtFileLocal, true); - //AWAIT when preparing time is given //general sleep for all files await sleepPreparing(preparingTime); //return; let serverResponses: Array<ExtFile> = []; - - if(groupUpload) { - const unifiedUpload = (method, url, arrOfFiles) : Promise<{ success : boolean, message : string, payload: object }> => { - arrOfExtFilesInstances.forEach((el) => el.uploadStatus = "uploading"); - handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); - const formData = new FormData(); - for (let i=0; i < arrOfFiles.length; i++) { - formData.append('files', arrOfFiles[i].file ) - } - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.upload.onprogress = (e) => {arrOfExtFilesInstances.forEach((el) => { el.progress = (e.loaded / e.total) * 100 });handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true)}; - xhr.responseType = 'json' - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - console.log(xhr.response); - console.log(typeof xhr.response); - resolve(xhr.response); - } else { - reject(xhr.response); - } - }; - xhr.onerror = (err) => { - reject(err); - }; - xhr.open(method, url); - xhr.send(formData) - }); - }; - try { - let respo:{ success : boolean, message : string, payload: object} = await unifiedUpload("POST", url, arrOfExtFilesInstances); - arrOfExtFilesInstances.forEach( el => el.uploadStatus = "success"); - arrOfExtFilesInstances.forEach( el => el.uploadMessage = respo.message); - } catch (err) { - arrOfExtFilesInstances.forEach( el => el.uploadStatus = "error"); - arrOfExtFilesInstances.forEach( el => el.uploadMessage = err.message); - console.log(err) + + if (groupUpload) { + const unifiedUpload = ( + method, + url, + arrOfFiles + ): Promise<{ success: boolean; message: string; payload: object }> => { + arrOfExtFilesInstances.forEach((el) => (el.uploadStatus = "uploading")); + handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + const formData = new FormData(); + for (let i = 0; i < arrOfFiles.length; i++) { + formData.append("files", arrOfFiles[i].file); + } + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.upload.onprogress = (e) => { + arrOfExtFilesInstances.forEach((el) => { + el.progress = (e.loaded / e.total) * 100; + }); + handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + }; + xhr.responseType = "json"; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + console.log(xhr.response); + console.log(typeof xhr.response); + resolve(xhr.response); + } else { + reject(xhr.response); + } + }; + xhr.onerror = (err) => { + reject(err); + }; + xhr.open(method, url); + xhr.send(formData); + }); + }; + try { + let respo: { success: boolean; message: string; payload: object } = + await unifiedUpload("POST", url, arrOfExtFilesInstances); + arrOfExtFilesInstances.forEach((el) => (el.uploadStatus = "success")); + arrOfExtFilesInstances.forEach( + (el) => (el.uploadMessage = respo.message) + ); + } catch (err) { + arrOfExtFilesInstances.forEach((el) => (el.uploadStatus = "error")); + arrOfExtFilesInstances.forEach( + (el) => (el.uploadMessage = err.message) + ); + console.log(err); } handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); } else { - //Uplad files one by one - for (let i = 0; i < arrOfExtFilesInstances.length; i++) { - const currentExtFileInstance: ExtFileInstance = arrOfExtFilesInstances[i]; + //Uplad files one by one + for (let i = 0; i < arrOfExtFilesInstances.length; i++) { + const currentExtFileInstance: ExtFileInstance = + arrOfExtFilesInstances[i]; + + if ( + currentExtFileInstance.uploadStatus === "preparing" && + !currentExtFileInstance.extraData?.deleted + ) { + //set stage to "uploading" in one file and notify change + // PREPARING => UPLOADING + await sleepTransition(); - - if ( - currentExtFileInstance.uploadStatus === "preparing" && - !currentExtFileInstance.extraData?.deleted - ) { - //set stage to "uploading" in one file and notify change - // PREPARING => UPLOADING - await sleepTransition(); + instantPreparingToUploadOne(currentExtFileInstance); - instantPreparingToUploadOne(currentExtFileInstance); + //messge in footer + if (uploadProgressMessage) + setLocalMessage( + uploadingMessenger(`${++currentCountUpload}/${missingUpload}`) + ); - //messge in footer - if (uploadProgressMessage) - setLocalMessage( - uploadingMessenger(`${++currentCountUpload}/${missingUpload}`) - ); + //CHANGE FILES + handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); - //CHANGE FILES - handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + //UPLOADING => UPLOAD() + //upload one file and notify about change + let uploadResponse: ExtFile; - //UPLOADING => UPLOAD() - //upload one file and notify about change - let uploadResponse: ExtFile; - - if (fakeUpload) { - uploadResponse = await fakeFuiUpload( - currentExtFileInstance, - DropzoneLocalizer - ); - - let fakeProgress = 0; - while (fakeProgress < 100) { - fakeProgress += getRandomInt(21, 35); - currentExtFileInstance.progress = - fakeProgress > 100 ? 100 : fakeProgress; - await sleepTransition(1000); - handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); - } - } else { - try { - uploadResponse = await uploadExtFile( + if (fakeUpload) { + uploadResponse = await fakeFuiUpload( currentExtFileInstance, - url, - method, - headers, - uploadLabel - ); - } catch (error) { - uploadResponse = unexpectedErrorUploadResult( - currentExtFileInstance.toExtFile() + DropzoneLocalizer ); + + let fakeProgress = 0; + while (fakeProgress < 100) { + fakeProgress += getRandomInt(21, 35); + currentExtFileInstance.progress = + fakeProgress > 100 ? 100 : fakeProgress; + await sleepTransition(1000); + handleFilesChange( + sanitizeArrExtFile(arrOfExtFilesInstances), + true + ); + } + } else { + try { + uploadResponse = await uploadExtFile( + currentExtFileInstance, + url, + urlFromExtFile, + method, + headers, + uploadLabel + ); + } catch (error) { + uploadResponse = unexpectedErrorUploadResult( + currentExtFileInstance.toExtFile() + ); + } } - } - const uploadedFile = uploadResponse; + const uploadedFile = uploadResponse; - //update instances - currentExtFileInstance.uploadStatus = uploadedFile.uploadStatus; - currentExtFileInstance.uploadMessage = uploadedFile.uploadMessage; + //update instances + currentExtFileInstance.uploadStatus = uploadedFile.uploadStatus; + currentExtFileInstance.uploadMessage = uploadedFile.uploadMessage; - //CHANGE - if (!(currentExtFileInstance.uploadStatus === "aborted")) - await sleepTransition(); + //CHANGE + if (!(currentExtFileInstance.uploadStatus === "aborted")) + await sleepTransition(); - handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); - if (uploadedFile.uploadStatus === "error") { - totalRejected++; - } + if (uploadedFile.uploadStatus === "error") { + totalRejected++; + } - serverResponses.push(uploadResponse); - } else { - handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + serverResponses.push(uploadResponse); + } else { + handleFilesChange(sanitizeArrExtFile(arrOfExtFilesInstances), true); + } } } - } setLocalFiles(sanitizeArrExtFile(arrOfExtFilesInstances)); // upload group finished :D @@ -466,7 +494,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { const handleAbortUpload = () => { const listExtFileLocal: ExtFileInstance[] | undefined = ExtFileManager.getExtFileInstanceList(dropzoneId); - + if (!listExtFileLocal) return; listExtFileLocal.forEach((extFileInstance: ExtFileInstance) => { if ( @@ -540,7 +568,6 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { extFileList: ExtFile[], isUploading?: boolean ): void => { - let finalExtFileList: ExtFile[] = behaviour === "add" && !isUploading ? [...localFiles, ...extFileList] @@ -551,7 +578,6 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { setLocalFiles(finalExtFileList); } if (autoUpload && !isUploading) { - uploadfiles(finalExtFileList); } }; @@ -575,7 +601,8 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { } } //init xhr on each ext file - if (url) extFileListOutput = toUploadableExtFileList(extFileListOutput); + if (shouldUpload) + extFileListOutput = toUploadableExtFileList(extFileListOutput); // Clean input element to trigger onChange event on input cleanInput(inputRef.current); @@ -595,8 +622,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { */ const outerFuiValidation = (fuiFileListToValidate: ExtFile[]): ExtFile[] => { const localValidator: FileValidatorProps = { maxFileSize, accept }; - - + let finalNumberOfValids: number = numberOfValidFiles; if (behaviour === "replace") { //re-start number of valids @@ -611,7 +637,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { maxFiles, localization ); - + return validatedFuiFileList; }; @@ -693,7 +719,8 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { } //init xhr on each ext file - if (url) extFileListOutput = toUploadableExtFileList(extFileListOutput); + if (shouldUpload) + extFileListOutput = toUploadableExtFileList(extFileListOutput); handleFilesChange(extFileListOutput); }; @@ -801,7 +828,7 @@ const Dropzone: React.FC<DropzoneProps> = (props: DropzoneProps) => { maxFiles && validFilesCountHeader ? maxFiles : undefined } localization={localization} - urlPresent={url !== undefined && uploadFilesHeader} + urlPresent={shouldUpload && uploadFilesHeader} onUploadStart={ !autoUpload && !uploadButton ? () => uploadfiles(localFiles) diff --git a/src/Dropzone/components/dropzone/DropzoneProps.ts b/src/Dropzone/components/dropzone/DropzoneProps.ts index a3fc75a..3ff9187 100644 --- a/src/Dropzone/components/dropzone/DropzoneProps.ts +++ b/src/Dropzone/components/dropzone/DropzoneProps.ts @@ -153,8 +153,6 @@ export interface DropzoneFullProps extends OverridableComponentProps { * @default false */ disableRipple?: boolean; - - /** * Method for performing specific tasks on drag enter operations */ diff --git a/src/FileCard/FileCardProps.ts b/src/FileCard/FileCardProps.ts index d0ced8f..f37e660 100644 --- a/src/FileCard/FileCardProps.ts +++ b/src/FileCard/FileCardProps.ts @@ -1,3 +1,4 @@ +import { ExtFile } from "@files-ui/core"; import { FileMosaicPropsMap } from "../FileMosaic/components/file-mosaic/FileMosaicProps"; @@ -7,7 +8,7 @@ export interface FileCardPropsMap extends FileMosaicPropsMap { } -export type FileCardProps = { +export type FileCardProps = ExtFile & { [F in keyof FileCardPropsMap]: FileCardPropsMap[F] } diff --git a/src/FileInputButton/FileInputButton.tsx b/src/FileInputButton/FileInputButton.tsx index dd90b1a..07ddc27 100644 --- a/src/FileInputButton/FileInputButton.tsx +++ b/src/FileInputButton/FileInputButton.tsx @@ -23,7 +23,7 @@ import { toUploadableExtFileList, cleanInput, FileIdGenerator, -} from "@files-ui/core" +} from "@files-ui/core"; import { DropzoneActions } from "../Dropzone/components/dropzone/DropzoneProps"; import DropzoneButtons from "../Dropzone/components/DropzoneButtons/DropzoneButtons"; import { FilesUiContext } from "../FilesUiProvider/FilesUiContext"; @@ -37,6 +37,7 @@ import { defaultFileInputButtonProps, FileInputButtonProps, } from "./InputButtonProps"; +import { isThereValidUrl } from "../utils/url.utils"; const FileInputButton: React.FC<FileInputButtonProps> = ( props: FileInputButtonProps @@ -114,6 +115,7 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( cleanOnUpload = true, preparingTime = 1500, autoUpload = false, + urlFromExtFile, } = uploadConfig as UploadConfig; const { @@ -138,7 +140,10 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( //Id for uploding through FuiFileManager //const inputButtonId: string | number = React.useId(); - const inputButtonId: string = React.useMemo(() => FileIdGenerator.getNextId() + "",[]); + const inputButtonId: string = React.useMemo( + () => FileIdGenerator.getNextId() + "", + [] + ); //Flag that determines whether to validate or not const validateFilesFlag: boolean = isValidateActive( accept, @@ -164,6 +169,14 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( localization, validateFilesFlag ); + /** + * Flag that determines if component should perform upload given url + */ + const shouldUpload: boolean = isThereValidUrl( + url, + urlFromExtFile, + localFiles + ); /** * Uploads each file in the array of ExtFiles * First, sets all the files in preparing status and awaits `preparingTime` miliseconds. @@ -187,7 +200,6 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( * @returns nothing */ const uploadfiles = async (localFiles: ExtFile[]): Promise<void> => { - //set uploading flag to true setIsUploading(true); @@ -241,7 +253,6 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( x.toExtFile() ); - //CHANGE (o alejo el isUploading o lo alejo para que tenga m,as tiempo antes de la respuyesta) // setIsUploading(true); handleFilesChange(newExtFileLocal, true); @@ -259,8 +270,6 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( for (let i = 0; i < arrOfExtFilesInstances.length; i++) { const currentExtFileInstance: ExtFileInstance = arrOfExtFilesInstances[i]; - - if ( currentExtFileInstance.uploadStatus === "preparing" && !currentExtFileInstance.extraData?.deleted @@ -297,6 +306,7 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( uploadResponse = await uploadExtFile( currentExtFileInstance, url, + urlFromExtFile, method, headers, uploadLabel @@ -383,7 +393,6 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( extFileList: ExtFile[], isUploading?: boolean ): void => { - let finalExtFileList: ExtFile[] = behaviour === "add" && !isUploading ? [...localFiles, ...extFileList] @@ -417,8 +426,9 @@ const FileInputButton: React.FC<FileInputButtonProps> = ( extFileListOutput = extFileListOutput.filter((f) => f.valid); } } - //init xhr on each dui file - if (url) extFileListOutput = toUploadableExtFileList(extFileListOutput); + //init xhr on each ext file + if (shouldUpload) + extFileListOutput = toUploadableExtFileList(extFileListOutput); // Clean input element to trigger onChange event on input cleanInput(inputRef.current); diff --git a/src/FileMosaic/components/file-mosaic/FileMosaicProps.ts b/src/FileMosaic/components/file-mosaic/FileMosaicProps.ts index b5b9948..64817cd 100644 --- a/src/FileMosaic/components/file-mosaic/FileMosaicProps.ts +++ b/src/FileMosaic/components/file-mosaic/FileMosaicProps.ts @@ -1,46 +1,7 @@ -import { Localization, UPLOADSTATUS } from "@files-ui/core" +import { ExtFile, Localization, UPLOADSTATUS } from "@files-ui/core" import { OverridableComponentProps } from "../../../overridable"; export interface FileMosaicPropsMap extends OverridableComponentProps { - /** - * The identifier for the file - */ - id?: string | number; - /** - * The file object obtained from client drop or selection - */ - file?: File; - /** - * The name of the file - */ - name?: string; - /** - * The file mime type - */ - type?: string; - /** - * the size of the file in bytes - */ - size?: number; - /** - * whether to show a valid or rejected message ("ok", "rejected") - * by def. valid is false (if not present, it's false too) - * This value wil affect preview behaviour, - * If not valid, the preview will not be shown, nor the view button - */ - valid?: boolean | null; - /** - * The list of errors according to the validation criteria or custom validation function given. - */ - errors?: string[]; - /** - * The message from server - */ - uploadMessage?: string; - /** - * The current upload status of the file - */ - uploadStatus?: UPLOADSTATUS; /** * if true, and if the file is an image, * makes visible the "view" button that will get the image url @@ -52,19 +13,6 @@ export interface FileMosaicPropsMap extends OverridableComponentProps { * @default false */ info?: boolean; - /** - * A string representation or web url of the image - * that will be set to the "src" prop of an <img/> If - * given, the component will use this image source instead of - * reading the image file. - */ - imageUrl?: string; - /** - * A string representation or web url of the video - * that will be set to the "src" prop of an <video/> tag - * <video src={`${videoUrl}`} /> - */ - videoUrl?: string; /** * If true, a background blur image will be shown */ @@ -127,7 +75,6 @@ export interface FileMosaicPropsMap extends OverridableComponentProps { * Callback fired when the component is right clicked if set. */ onRightClick?: (evt: React.MouseEvent) => void; - /** * Flag that determines whether actions are visible always, or only on hover event */ @@ -136,12 +83,6 @@ export interface FileMosaicPropsMap extends OverridableComponentProps { * If present a tooltip that contains the upload message will be diplayed on hover */ resultOnTooltip?: boolean; - /** - * The url to be used to perform a GET request in order to download the file. - * This action is triggered when download button is clicked or pressed. - * In case onDownload prop is given - */ - downloadUrl?: string; /** * If not present, image width will be set to 100%. * @@ -164,6 +105,7 @@ export type FileMosaicProps = /* { [D in keyof React.HTMLProps<HTMLDivElement>]: React.HTMLProps<HTMLDivElement>[D] } & */ + ExtFile & { [F in keyof FileMosaicPropsMap]: FileMosaicPropsMap[F] } diff --git a/src/utils/url.utils.ts b/src/utils/url.utils.ts new file mode 100644 index 0000000..17877c2 --- /dev/null +++ b/src/utils/url.utils.ts @@ -0,0 +1,9 @@ +import { ExtFile, ExtFileInstance } from "@files-ui/core"; + +export const isThereValidUrl = ( + url?: string, + urlFunction?: Function, + extFileList?: ExtFile[] +): boolean => { + return ExtFileInstance.someValidUrl(extFileList) && url && url.length && urlFunction != undefined; +} \ No newline at end of file -- GitLab