refactor: Improve createFileUpload reliability and performance (#5697)

*  (create-file-upload.ts): Refactor createFileUpload function to improve readability and maintainability. Move input element creation and event listeners to separate functions for better separation of concerns. Use module-level variables for input element and promise resolver to handle cleanup and resolution more effectively.

*  (create-file-upload.ts): refactor createFileUpload function to improve code readability and maintainability. Remove unnecessary module-level variables and simplify the logic for handling file uploads.

* 📝 (create-file-upload.ts): refactor createFileUpload function to improve code readability and maintainability
🐛 (create-file-upload.ts): fix issue with resolving promise multiple times and improve file input handling

* 🔧 (create-file-upload.ts): refactor createFileUpload function to improve file upload process and cleanup event listeners for better performance and code readability

* 📝 (index.tsx): Remove unused useRef import and fileInputRef variable
♻️ (index.tsx): Refactor checkFileType function to use a for loop for better readability and performance
 (index.tsx): Introduce createFileUpload helper function to handle file selection and validation
📝 (index.tsx): Update handleButtonClick function to use createFileUpload helper function and improve file handling logic
📝 (index.tsx): Update handleButtonClick function to handle file upload and error messages more efficiently
📝 (index.tsx): Update input element to display file name and handle file selection more effectively

* add error handling in case document.body.removeChild Fails

---------

Co-authored-by: anovazzi1 <otavio2204@gmail.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-01-15 15:03:39 -03:00 committed by GitHub
commit 2f9a7858e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 93 deletions

View file

@ -1,7 +1,8 @@
import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file";
import { createFileUpload } from "@/helpers/create-file-upload";
import useFileSizeValidator from "@/shared/hooks/use-file-size-validator";
import { cn } from "@/utils/utils";
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import {
CONSOLE_ERROR_MSG,
INVALID_FILE_ALERT,
@ -23,77 +24,70 @@ export default function InputFileComponent({
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { validateFileSize } = useFileSizeValidator(setErrorData);
const fileInputRef = useRef<HTMLInputElement>(null);
// Clear component state
useEffect(() => {
if (disabled && value !== "") {
handleOnNewValue({ value: "", file_path: "" }, { skipSnapshot: true });
}
}, [disabled, handleOnNewValue, value]);
}, [disabled, handleOnNewValue]);
function checkFileType(fileName: string): boolean {
if (!fileTypes?.length) return true;
return fileTypes.some((type) =>
fileName.toLowerCase().endsWith(type.toLowerCase()),
);
if (fileTypes === undefined) return true;
for (let index = 0; index < fileTypes.length; index++) {
if (fileName.endsWith(fileTypes[index])) {
return true;
}
}
return false;
}
const { mutate, isPending } = usePostUploadFile();
const handleFileSelection = (file: File | null) => {
if (!file) {
setErrorData({
title: "Error selecting file",
list: ["No file was selected"],
});
return;
}
const handleButtonClick = (): void => {
createFileUpload({ multiple: false, accept: fileTypes?.join(",") }).then(
(files) => {
const file = files[0];
if (file) {
if (!validateFileSize(file)) {
return;
}
if (!validateFileSize(file)) {
return;
}
if (checkFileType(file.name)) {
// Upload the file
mutate(
{ file, id: currentFlowId },
{
onSuccess: (data) => {
// Get the file name from the response
const { file_path } = data;
if (!checkFileType(file.name)) {
setErrorData({
title: INVALID_FILE_ALERT,
list: [fileTypes?.join(", ") || ""],
});
return;
}
mutate(
{ file, id: currentFlowId },
{
onSuccess: (data) => {
const { file_path } = data;
handleOnNewValue({ value: file.name, file_path });
},
onError: (error) => {
console.error(CONSOLE_ERROR_MSG);
setErrorData({
title: "Error uploading file",
list: [error.response?.data?.detail || "Unknown error occurred"],
});
},
// sets the value that goes to the backend
// Update the state and on with the name of the file
// sets the value to the user
handleOnNewValue({ value: file.name, file_path });
},
onError: (error) => {
console.error(CONSOLE_ERROR_MSG);
setErrorData({
title: "Error uploading file",
list: [error.response?.data?.detail],
});
},
},
);
} else {
// Show an error if the file type is not allowed
setErrorData({
title: INVALID_FILE_ALERT,
list: [fileTypes?.join(", ") || ""],
});
}
}
},
);
};
const handleButtonClick = () => {
fileInputRef.current?.click();
};
const handleNativeInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0] || null;
handleFileSelection(file);
if (event.target) {
event.target.value = "";
}
};
const isDisabled = disabled || isPending;
return (
@ -102,29 +96,18 @@ export default function InputFileComponent({
<div className="flex items-center gap-2.5">
<div className="relative flex w-full">
<div className="w-full">
<Button
unstyled
<input
data-testid="input-file-component"
type="text"
className={cn(
"primary-input h-9 w-full justify-start rounded-r-none text-sm focus:border-border focus:outline-none focus:ring-0",
"primary-input h-9 w-full cursor-pointer rounded-r-none text-sm focus:border-border focus:outline-none focus:ring-0",
!value && "text-placeholder-foreground",
editNode && "h-6",
)}
onClick={handleButtonClick}
value={value || "Upload a file..."}
readOnly
disabled={isDisabled}
variant="outline"
>
<span className={cn(editNode && "relative -top-1.5")}>
{value || "Upload a file..."}
</span>
</Button>
<input
ref={fileInputRef}
type="file"
className="hidden"
accept={fileTypes?.join(",")}
onChange={handleNativeInputChange}
onClick={(e) => e.stopPropagation()}
onClick={handleButtonClick}
/>
</div>
<div>

View file

@ -1,60 +1,69 @@
export async function createFileUpload(props?: {
export function createFileUpload(props?: {
accept?: string;
multiple?: boolean;
}): Promise<File[]> {
return new Promise((resolve) => {
// Create input element
const input = document.createElement("input");
input.type = "file";
input.style.position = "fixed";
input.style.top = "0";
input.style.left = "0";
input.style.opacity = "0.001";
input.style.pointerEvents = "none";
input.accept = props?.accept ?? ".json";
input.multiple = props?.multiple ?? true;
input.style.display = "none";
let isResolved = false;
let isHandled = false;
const cleanup = () => {
// Check if the input element still exists in the DOM before attempting to remove it
if (input && document.body.contains(input)) {
if (document.body.contains(input)) {
try {
document.body.removeChild(input);
} catch (error) {
console.warn("Error removing input element:", error);
}
}
window.removeEventListener("focus", handleFocus);
input.removeEventListener("change", handleChange);
document.removeEventListener("focus", handleFocus);
};
const handleChange = (e: Event) => {
if (!isResolved) {
isResolved = true;
const files = Array.from((e.target as HTMLInputElement).files!);
cleanup();
resolve(files);
}
const handleChange = (event: Event) => {
if (isHandled) return;
isHandled = true;
const files = Array.from((event.target as HTMLInputElement).files || []);
cleanup();
resolve(files);
};
const handleFocus = () => {
setTimeout(() => {
if (!isResolved) {
isResolved = true;
if (!isHandled) {
isHandled = true;
cleanup();
resolve([]);
}
}, 300);
}, 100);
};
input.addEventListener("change", handleChange);
window.addEventListener("focus", handleFocus);
document.addEventListener("focus", handleFocus);
document.body.appendChild(input);
input.click();
// Fallback timeout to ensure resolution
requestAnimationFrame(() => {
if (!isHandled) {
input.click();
}
});
setTimeout(() => {
if (!isResolved) {
isResolved = true;
if (!isHandled) {
isHandled = true;
cleanup();
resolve([]);
}
}, 60000);
}, 30000);
});
}