From 53af836ae93fc98f7bc97267a31851d4ffe1686a Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Wed, 28 Aug 2024 17:08:11 -0400 Subject: [PATCH] optimize uploader --- packages/ui/src/FormComponents/Uploader.js | 595 +++++++++++---------- 1 file changed, 314 insertions(+), 281 deletions(-) diff --git a/packages/ui/src/FormComponents/Uploader.js b/packages/ui/src/FormComponents/Uploader.js index ac35d059..8b4227bb 100644 --- a/packages/ui/src/FormComponents/Uploader.js +++ b/packages/ui/src/FormComponents/Uploader.js @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from "react"; import { Button, Callout, @@ -249,86 +255,89 @@ const Uploader = ({ const filesToClean = useRef([]); // onChange received from redux-form is not working anymore, // so we need to overwrite it for redux to works. - const onChange = val => { - flushSync(() => { - if (noRedux) { - return _onChange(val); - } - dispatch(touch(formName, name)); - dispatch(change(formName, name, val)); - }); - }; + const onChange = useCallback( + val => { + flushSync(() => { + if (noRedux) { + return _onChange(val); + } + dispatch(touch(formName, name)); + dispatch(change(formName, name, val)); + }); + }, + [_onChange, dispatch, formName, name, noRedux] + ); - const handleSecondHalfOfUpload = async ({ - acceptedFiles, - cleanedFileList - }) => { - // This onChange is not changing things, we need to check whether the error is here or later - onChange(cleanedFileList); //tnw: this line is necessary, if you want to clear the file list in the beforeUpload, call onChange([]) - // beforeUpload is called, otherwise beforeUpload will not be able to truly cancel the upload - const keepGoing = beforeUpload - ? await beforeUpload(cleanedFileList, onChange) - : true; - if (!keepGoing) return; + const handleSecondHalfOfUpload = useCallback( + async ({ acceptedFiles, cleanedFileList }) => { + // This onChange is not changing things, we need to check whether the error is here or later + onChange(cleanedFileList); //tnw: this line is necessary, if you want to clear the file list in the beforeUpload, call onChange([]) + // beforeUpload is called, otherwise beforeUpload will not be able to truly cancel the upload + const keepGoing = beforeUpload + ? await beforeUpload(cleanedFileList, onChange) + : true; + if (!keepGoing) return; - if (action) { - const responses = []; - await Promise.all( - acceptedFiles.map(async fileToUpload => { - const data = new FormData(); - data.append("file", fileToUpload); - try { - const res = await (window.api - ? window.api.post(action, data) - : fetch(action, { - method: "POST", - body: data - })); - responses.push(res.data && res.data[0]); - onFileSuccess(res.data[0]).then(() => { + if (action) { + const responses = []; + await Promise.all( + acceptedFiles.map(async fileToUpload => { + const data = new FormData(); + data.append("file", fileToUpload); + try { + const res = await (window.api + ? window.api.post(action, data) + : fetch(action, { + method: "POST", + body: data + })); + responses.push(res.data && res.data[0]); + onFileSuccess(res.data[0]).then(() => { + cleanedFileList = cleanedFileList.map(file => { + const fileToReturn = { + ...file, + ...res.data[0] + }; + if (fileToReturn.id === fileToUpload.id) { + fileToReturn.loading = false; + } + return fileToReturn; + }); + onChange(cleanedFileList); + }); + } catch (err) { + console.error("Error uploading file:", err); + responses.push({ + ...fileToUpload, + error: err && err.msg ? err.msg : err + }); cleanedFileList = cleanedFileList.map(file => { - const fileToReturn = { - ...file, - ...res.data[0] - }; + const fileToReturn = { ...file }; if (fileToReturn.id === fileToUpload.id) { fileToReturn.loading = false; + fileToReturn.error = true; } return fileToReturn; }); onChange(cleanedFileList); - }); - } catch (err) { - console.error("Error uploading file:", err); - responses.push({ - ...fileToUpload, - error: err && err.msg ? err.msg : err - }); - cleanedFileList = cleanedFileList.map(file => { - const fileToReturn = { ...file }; - if (fileToReturn.id === fileToUpload.id) { - fileToReturn.loading = false; - fileToReturn.error = true; - } - return fileToReturn; - }); - onChange(cleanedFileList); - } - }) - ); - onFieldSubmit(responses); - } else { - onChange( - cleanedFileList.map(function (file) { - return { - ...file, - loading: false - }; - }) - ); - } - setLoading(false); - }; + } + }) + ); + onFieldSubmit(responses); + } else { + onChange( + cleanedFileList.map(function (file) { + return { + ...file, + loading: false + }; + }) + ); + } + setLoading(false); + }, + [action, beforeUpload, onChange, onFieldSubmit, onFileSuccess] + ); const isAcceptPromise = useMemo( () => @@ -337,13 +346,6 @@ const Uploader = ({ [__accept] ); - let dropzoneDisabled = _disabled; - let _accept = __accept; - - if (resolvedAccept) { - _accept = resolvedAccept; - } - useEffect(() => { if (isAcceptPromise) { setAcceptLoading(true); @@ -357,46 +359,54 @@ const Uploader = ({ } }, [__accept, isAcceptPromise]); - if (isAcceptPromise && !resolvedAccept) { - _accept = []; - } + const dropzoneDisabled = _disabled || acceptLoading; - if (acceptLoading) dropzoneDisabled = true; - const accept = useMemo( - () => - !_accept - ? undefined - : isAcceptPromise && !resolvedAccept - ? [] - : isPlainObject(_accept) - ? [_accept] - : isArray(_accept) - ? _accept - : _accept.split(",").map(acc => ({ type: acc })), - [_accept, isAcceptPromise, resolvedAccept] - ); + const { accept, validateAgainstSchema } = useMemo(() => { + let _accept = __accept; + if (resolvedAccept) { + _accept = resolvedAccept; + } + if (isAcceptPromise && !resolvedAccept) { + _accept = []; + } + const newAccept = !_accept + ? undefined + : isAcceptPromise && !resolvedAccept + ? [] + : isPlainObject(_accept) + ? [_accept] + : isArray(_accept) + ? _accept + : _accept.split(",").map(acc => ({ type: acc })); - const callout = _callout || accept?.find?.(a => a?.callout)?.callout; + const validateAgainstSchema = setValidateAgainstSchema( + _validateAgainstSchema || + newAccept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema + ); - const validateAgainstSchema = useMemo( - () => - setValidateAgainstSchema( - _validateAgainstSchema || - accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema - ), - [_validateAgainstSchema, accept] - ); + if ( + (validateAgainstSchema || autoUnzip) && + newAccept && + !newAccept.some(a => a.type === "zip") + ) { + newAccept?.unshift({ + type: "zip", + description: "Any of the following types, just compressed" + }); + } + return { accept: newAccept, validateAgainstSchema }; + }, [ + __accept, + _validateAgainstSchema, + autoUnzip, + isAcceptPromise, + resolvedAccept + ]); - if ( - (validateAgainstSchema || autoUnzip) && - accept && - !accept.some(a => a.type === "zip") - ) { - accept?.unshift({ - type: "zip", - description: "Any of the following types, just compressed" - }); - } + const callout = useMemo( + () => _callout || accept?.find?.(a => a?.callout)?.callout, + [_callout, accept] + ); const { showDialogPromise: showUploadCsvWizardDialog, Comp } = useDialog({ ModalComponent: UploadCsvWizardDialog @@ -407,191 +417,214 @@ const Uploader = ({ ModalComponent: SimpleInsertDataDialog }); - function cleanupFiles() { + const cleanupFiles = useCallback(() => { filesToClean.current.forEach(file => URL.revokeObjectURL(file.preview)); - } + }, []); + useEffect(() => { return () => { cleanupFiles(); }; - }, []); + }, [cleanupFiles]); - let contentOverride = maybeContentOverride; - if (contentOverride && typeof contentOverride === "function") { - contentOverride = contentOverride({ loading }); - } - let simpleAccept; - let handleManuallyEnterData; - let advancedAccept; + const contentOverride = useMemo(() => { + let _contentOverride = maybeContentOverride; + if (_contentOverride && typeof _contentOverride === "function") { + _contentOverride = _contentOverride({ loading }); + } + return _contentOverride; + }, [loading, maybeContentOverride]); - if (Array.isArray(accept)) { - if (accept.some(acc => isPlainObject(acc))) { - //advanced accept - advancedAccept = accept; - simpleAccept = flatMap(accept, acc => { - if (acc.validateAgainstSchema) { - if (!acc.type) { - acc.type = [".csv", ".xlsx"]; - } - handleManuallyEnterData = async e => { - e.stopPropagation(); - const { newEntities, fileName } = await showSimpleInsertDataDialog( - "onSimpleInsertDialogFinish", - { - validateAgainstSchema + const fileListToUse = useMemo(() => (fileList ? fileList : []), [fileList]); + + const { simpleAccept, handleManuallyEnterData, advancedAccept } = + useMemo(() => { + let simpleAccept; + let handleManuallyEnterData; + let advancedAccept; + + if (Array.isArray(accept)) { + if (accept.some(acc => isPlainObject(acc))) { + //advanced accept + advancedAccept = accept; + simpleAccept = flatMap(accept, acc => { + if (acc.validateAgainstSchema) { + if (!acc.type) { + acc.type = [".csv", ".xlsx"]; } - ); - if (!newEntities) return; - //check existing files to make sure the new file name gets incremented if necessary - // fileList - const newFileName = getNewName(fileListToUse, fileName); - const { newFile, cleanedEntities } = getNewCsvFile( - newEntities, - newFileName - ); + handleManuallyEnterData = async e => { + e.stopPropagation(); + const { newEntities, fileName } = + await showSimpleInsertDataDialog( + "onSimpleInsertDialogFinish", + { + validateAgainstSchema + } + ); + if (!newEntities) return; + //check existing files to make sure the new file name gets incremented if necessary + // fileList + const newFileName = getNewName(fileListToUse, fileName); + const { newFile, cleanedEntities } = getNewCsvFile( + newEntities, + newFileName + ); - const file = { - ...newFile, - parsedData: cleanedEntities, - meta: { - fields: validateAgainstSchema.fields.map(({ path }) => path) - }, - name: newFileName, - originFileObj: newFile, - originalFileObj: newFile, - id: nanoid(), - hasEditClick: true - }; + const file = { + ...newFile, + parsedData: cleanedEntities, + meta: { + fields: validateAgainstSchema.fields.map(({ path }) => path) + }, + name: newFileName, + originFileObj: newFile, + originalFileObj: newFile, + id: nanoid(), + hasEditClick: true + }; - const cleanedFileList = [file, ...fileListToUse].slice( - 0, - fileLimit ? fileLimit : undefined - ); - handleSecondHalfOfUpload({ - acceptedFiles: cleanedFileList, - cleanedFileList - }); + const cleanedFileList = [file, ...fileListToUse].slice( + 0, + fileLimit ? fileLimit : undefined + ); + handleSecondHalfOfUpload({ + acceptedFiles: cleanedFileList, + cleanedFileList + }); - window.toastr.success(`File Added`); - }; + window.toastr.success(`File Added`); + }; - const nameToUse = - startCase( - removeExt( - validateAgainstSchema.fileName || validateAgainstSchema.name - ) - ) || "Example"; + const nameToUse = + startCase( + removeExt( + validateAgainstSchema.fileName || validateAgainstSchema.name + ) + ) || "Example"; - const handleDownloadXlsxFile = async () => { - const dataDictionarySchema = [ - { value: f => f.displayName || f.path, column: `Column Name` }, - // { - // value: f => f.isUnique ? "Unique" : "", - // column: `Unique?` - // }, - { - value: f => (f.isRequired ? "Required" : "Optional"), - column: `Required?` - }, - { - value: f => (f.type === "dropdown" ? "text" : f.type || "text"), - column: `Data Type` - }, - { - value: f => f.description, - column: `Notes` - }, - { - value: f => f.example || f.defaultValue || "", - column: `Example Data` - } - ]; + const handleDownloadXlsxFile = async () => { + const dataDictionarySchema = [ + { + value: f => f.displayName || f.path, + column: `Column Name` + }, + // { + // value: f => f.isUnique ? "Unique" : "", + // column: `Unique?` + // }, + { + value: f => (f.isRequired ? "Required" : "Optional"), + column: `Required?` + }, + { + value: f => + f.type === "dropdown" ? "text" : f.type || "text", + column: `Data Type` + }, + { + value: f => f.description, + column: `Notes` + }, + { + value: f => f.example || f.defaultValue || "", + column: `Example Data` + } + ]; - const mainExampleData = {}; - const fieldsToUse = [ - ...validateAgainstSchema.fields, - ...(validateAgainstSchema.exampleDownloadFields ?? []) - ]; - const mainSchema = fieldsToUse.map(f => { - mainExampleData[f.displayName || f.path] = - f.example || f.defaultValue; - return { - column: f.displayName || f.path, - value: v => { - return v[f.displayName || f.path]; - } - }; - }); - const blobFile = await writeXlsxFile( - [[mainExampleData], fieldsToUse, helperText], - { - headerStyle: { - fontWeight: "bold" - }, - schema: [mainSchema, dataDictionarySchema, helperSchema], - sheets: [nameToUse, "Column Info", "Upload Instructions"], - filePath: "file.xlsx" - } - ); - downloadjs(blobFile, `${nameToUse}.xlsx`, "xlsx"); - }; - // handleDownloadXlsxFile() - acc.exampleFiles = [ - // ...(a.exampleFile ? [a.exampleFile] : []), - { - description: "Download Example CSV File", - exampleFile: () => { - const rows = []; - const schemaToUse = [ - ...acc.validateAgainstSchema.fields, - ...(acc.validateAgainstSchema.exampleDownloadFields ?? []) + const mainExampleData = {}; + const fieldsToUse = [ + ...validateAgainstSchema.fields, + ...(validateAgainstSchema.exampleDownloadFields ?? []) ]; - rows.push( - schemaToUse.map(f => { - return `${f.displayName || f.path}`; - }) - ); - rows.push( - schemaToUse.map(f => { - return `${f.example || f.defaultValue || ""}`; - }) + const mainSchema = fieldsToUse.map(f => { + mainExampleData[f.displayName || f.path] = + f.example || f.defaultValue; + return { + column: f.displayName || f.path, + value: v => { + return v[f.displayName || f.path]; + } + }; + }); + const blobFile = await writeXlsxFile( + [[mainExampleData], fieldsToUse, helperText], + { + headerStyle: { + fontWeight: "bold" + }, + schema: [mainSchema, dataDictionarySchema, helperSchema], + sheets: [nameToUse, "Column Info", "Upload Instructions"], + filePath: "file.xlsx" + } ); - const csv = unparse(rows); + downloadjs(blobFile, `${nameToUse}.xlsx`, "xlsx"); + }; + // handleDownloadXlsxFile() + acc.exampleFiles = [ + // ...(a.exampleFile ? [a.exampleFile] : []), + { + description: "Download Example CSV File", + exampleFile: () => { + const rows = []; + const schemaToUse = [ + ...acc.validateAgainstSchema.fields, + ...(acc.validateAgainstSchema.exampleDownloadFields ?? []) + ]; + rows.push( + schemaToUse.map(f => { + return `${f.displayName || f.path}`; + }) + ); + rows.push( + schemaToUse.map(f => { + return `${f.example || f.defaultValue || ""}`; + }) + ); + const csv = unparse(rows); - const downloadFn = window.Cypress?.downloadTest || downloadjs; - downloadFn(csv, `${nameToUse}.csv`, "csv"); - } - }, - { - description: "Download Example XLSX File", - subtext: "Includes Upload Instructions and Column Info", - exampleFile: handleDownloadXlsxFile - }, - ...(noBuildCsvOption - ? [] - : [ - { - description: manualEnterMessage, - subtext: manualEnterSubMessage, - icon: "manually-entered-data", - exampleFile: handleManuallyEnterData + const downloadFn = + window.Cypress?.downloadTest || downloadjs; + downloadFn(csv, `${nameToUse}.csv`, "csv"); } - ]) - ]; - delete acc.exampleFile; + }, + { + description: "Download Example XLSX File", + subtext: "Includes Upload Instructions and Column Info", + exampleFile: handleDownloadXlsxFile + }, + ...(noBuildCsvOption + ? [] + : [ + { + description: manualEnterMessage, + subtext: manualEnterSubMessage, + icon: "manually-entered-data", + exampleFile: handleManuallyEnterData + } + ]) + ]; + delete acc.exampleFile; + } + if (acc.type) return acc.type; + return acc; + }); + simpleAccept = simpleAccept.join(", "); + } else { + simpleAccept = accept.join(", "); } - if (acc.type) return acc.type; - return acc; - }); - simpleAccept = simpleAccept.join(", "); - } else { - simpleAccept = accept.join(", "); - } - } else { - simpleAccept = accept; - } - - const fileListToUse = fileList ? fileList : []; + } else { + simpleAccept = accept; + } + return { simpleAccept, handleManuallyEnterData, advancedAccept }; + }, [ + accept, + fileLimit, + fileListToUse, + handleSecondHalfOfUpload, + noBuildCsvOption, + showSimpleInsertDataDialog, + validateAgainstSchema + ]); return ( <>