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

View file

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