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:
parent
26ed6dc2f7
commit
2f9a7858e2
2 changed files with 85 additions and 93 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue