coronasafe/care_fe

View on GitHub
src/hooks/useFileUpload.tsx

Summary

Maintainability
F
4 days
Test Coverage
File `useFileUpload.tsx` has 401 lines of code (exceeds 250 allowed). Consider refactoring.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import imageCompression from "browser-image-compression";
import jsPDF from "jspdf";
import {
ChangeEvent,
DetailedHTMLProps,
InputHTMLAttributes,
useEffect,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
 
import AudioCaptureDialog from "@/components/Files/AudioCaptureDialog";
import CameraCaptureDialog from "@/components/Files/CameraCaptureDialog";
import {
CreateFileResponse,
FileCategory,
FileUploadModel,
} from "@/components/Patient/models";
 
import { DEFAULT_ALLOWED_EXTENSIONS } from "@/common/constants";
 
import routes from "@/Utils/request/api";
import mutate from "@/Utils/request/mutate";
import uploadFile from "@/Utils/request/uploadFile";
 
export type FileUploadOptions = {
multiple?: boolean;
type: string;
category?: FileCategory;
onUpload?: (file: FileUploadModel) => void;
// if allowed, will fallback to the name of the file if a seperate filename is not defined.
allowNameFallback?: boolean;
compress?: boolean;
} & (
| {
allowedExtensions?: string[];
}
| {
allowAllExtensions?: boolean;
}
);
 
export type FileInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
"id" | "title" | "type" | "accept" | "onChange"
> & {};
 
export type FileUploadReturn = {
progress: null | number;
error: null | string;
setError: (error: string | null) => void;
validateFiles: () => boolean;
handleCameraCapture: () => void;
handleAudioCapture: () => void;
handleFileUpload: (
associating_id: string,
combineToPDF?: boolean,
) => Promise<void>;
Dialogues: React.ReactNode;
Input: (_: FileInputProps) => React.ReactNode;
fileNames: string[];
files: File[];
setFileName: (names: string, index?: number) => void;
setFileNames: (names: string[]) => void;
removeFile: (index: number) => void;
clearFiles: () => void;
uploading: boolean;
previewing?: boolean;
};
 
// Array of image extensions
const ExtImage: string[] = [
"jpeg",
"jpg",
"png",
"gif",
"svg",
"bmp",
"webp",
"jfif",
];
 
Function `useFileUpload` has 323 lines of code (exceeds 25 allowed). Consider refactoring.
Function `useFileUpload` has a Cognitive Complexity of 80 (exceeds 5 allowed). Consider refactoring.
export default function useFileUpload(
options: FileUploadOptions,
): FileUploadReturn {
const {
type: fileType,
onUpload,
category = "unspecified",
multiple,
allowNameFallback = true,
} = options;
 
const { t } = useTranslation();
 
const [uploadFileNames, setUploadFileNames] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState<null | number>(null);
const [cameraModalOpen, setCameraModalOpen] = useState(false);
const [audioModalOpen, setAudioModalOpen] = useState(false);
const [uploading, setUploading] = useState(false);
const [previewing, setPreviewing] = useState(false);
 
const [files, setFiles] = useState<File[]>([]);
const queryClient = useQueryClient();
 
const generatePDF = async (files: File[]): Promise<File | null> => {
try {
toast.info(t("file_conversion_in_progress"));
const pdf = new jsPDF();
const totalFiles = files.length;
 
for (const [index, file] of files.entries()) {
const imgData = URL.createObjectURL(file);
pdf.addImage(imgData, "JPEG", 10, 10, 190, 0);
URL.revokeObjectURL(imgData);
if (index < files.length - 1) pdf.addPage();
const progress = Math.round(((index + 1) / totalFiles) * 100);
setProgress(progress);
}
const pdfBlob = pdf.output("blob");
const pdfFile = new File([pdfBlob], "combined.pdf", {
type: "application/pdf",
});
setProgress(0);
toast.success(t("file_conversion_success"));
return pdfFile;
} catch (error) {
toast.error(t("file_error__generate_pdf"));
setError(t("file_error__generate_pdf", { error: String(error) }));
setProgress(0);
return null;
}
};
const onFileChange = (e: ChangeEvent<HTMLInputElement>): any => {
if (!e.target.files?.length) {
return;
}
const selectedFiles = Array.from(e.target.files);
setFiles((prev) => [...prev, ...selectedFiles]);
if (options.compress) {
selectedFiles.forEach((file) => {
const ext: string = file.name.split(".")[1];
if (ExtImage.includes(ext)) {
const options = {
initialQuality: 0.6,
alwaysKeepResolution: true,
};
imageCompression(file, options).then((compressedFile: File) => {
setFiles((prev) =>
prev.map((f) => (f.name === file.name ? compressedFile : f)),
);
});
}
});
}
};
 
useEffect(() => {
const blanks = Array(files.length).fill("");
setUploadFileNames((names) => [...names, ...blanks].slice(0, files.length));
}, [files]);
 
Function `validateFileUpload` has 31 lines of code (exceeds 25 allowed). Consider refactoring.
const validateFileUpload = () => {
if (files.length === 0) {
setError(t("file_error__choose_file"));
return false;
}
 
for (const file of files) {
const filenameLength = file.name.trim().length;
if (filenameLength === 0) {
setError(t("file_error__file_name"));
return false;
}
if (file.size > 10e7) {
setError(t("file_error__file_size"));
return false;
}
const extension = file.name.split(".").pop()?.toLowerCase();
if (
"allowedExtensions" in options &&
!options.allowedExtensions
?.map((extension) => extension.replace(".", "").toLowerCase())
?.includes(extension || "")
) {
setError(
t("file_error__file_type", {
extension,
allowedExtensions: options.allowedExtensions?.join(", "),
}),
);
return false;
}
}
Avoid too many `return` statements within this function.
return true;
};
const { mutateAsync: markUploadComplete, error: markUploadCompleteError } =
useMutation({
mutationFn: (body: {
data: CreateFileResponse;
associating_id: string;
}) =>
mutate(routes.markUploadCompleted, {
pathParams: {
id: body.data.id,
},
})(body),
onSuccess: (_, { data, associating_id }) => {
queryClient.invalidateQueries({
queryKey: ["files", fileType, associating_id],
});
toast.success(t("file_uploaded"));
setError(null);
onUpload?.(data);
},
});
 
Function `uploadfile` has 38 lines of code (exceeds 25 allowed). Consider refactoring.
const uploadfile = async (
data: CreateFileResponse,
file: File,
associating_id: string,
) => {
const url = data.signed_url;
const internal_name = data.internal_name;
const newFile = new File([file], `${internal_name}`);
 
return new Promise<void>((resolve, reject) => {
uploadFile(
url,
newFile,
"PUT",
{ "Content-Type": file.type },
async (xhr: XMLHttpRequest) => {
if (xhr.status >= 200 && xhr.status < 300) {
setProgress(null);
await markUploadComplete({
data,
associating_id: associating_id,
});
if (markUploadCompleteError) {
toast.error(t("file_error__mark_complete_failed"));
reject();
return;
}
resolve();
} else {
toast.error(
t("file_error__dynamic", { statusText: xhr.statusText }),
);
setProgress(null);
reject();
}
},
setProgress as any,
() => {
toast.error(t("file_error__network"));
setProgress(null);
reject();
},
);
});
};
 
const { mutateAsync: createUpload } = useMutation({
mutationFn: (body: {
original_name: string;
file_type: string;
name: string;
associating_id: string;
file_category: FileCategory;
mime_type: string;
}) =>
mutate(routes.createUpload, {
body: {
original_name: body.original_name,
file_type: body.file_type,
name: body.name,
associating_id: body.associating_id,
file_category: body.file_category,
mime_type: body.mime_type,
},
})(body),
});
 
Function `handleUpload` has 59 lines of code (exceeds 25 allowed). Consider refactoring.
const handleUpload = async (
associating_id: string,
combineToPDF?: boolean,
) => {
if (combineToPDF && "allowedExtensions" in options) {
options.allowedExtensions = ["jpg", "png", "jpeg"];
}
if (!validateFileUpload()) return;
 
setProgress(0);
const errors: File[] = [];
if (combineToPDF) {
if (!uploadFileNames.length || !uploadFileNames[0]) {
setError(t("file_error__single_file_name"));
return;
}
} else {
for (const [index, file] of files.entries()) {
const filename =
allowNameFallback && uploadFileNames[index] === "" && file
? file.name
: uploadFileNames[index];
if (!filename) {
setError(t("file_error__single_file_name"));
return;
}
}
}
 
if (combineToPDF && files.length > 1) {
const pdfFile = await generatePDF(files);
if (pdfFile) {
files.splice(0, files.length, pdfFile);
} else {
clearFiles();
setError(t("file_error__generate_pdf"));
return;
}
}
 
setUploading(true);
 
for (const [index, file] of files.entries()) {
try {
const data = await createUpload({
original_name: file.name ?? "",
file_type: fileType,
name:
allowNameFallback && uploadFileNames[index] === "" && file
? file.name
: uploadFileNames[index],
associating_id,
file_category: category,
mime_type: file.type ?? "",
});
 
if (data) {
await uploadfile(data, file, associating_id);
}
} catch {
errors.push(file);
}
}
 
setUploading(false);
setFiles(errors);
setUploadFileNames(errors?.map((f) => f.name) ?? []);
setError(t("file_error__network"));
setCameraModalOpen(false);
};
 
const clearFiles = () => {
setFiles([]);
setError(null);
setUploadFileNames([]);
};
 
const Dialogues = (
<>
<CameraCaptureDialog
open={cameraModalOpen}
onOpenChange={(open) => setCameraModalOpen(open)}
onCapture={(file) => {
setFiles((prev) => [...prev, file]);
}}
onResetCapture={clearFiles}
setPreview={setPreviewing}
/>
<AudioCaptureDialog
show={audioModalOpen}
onHide={() => setAudioModalOpen(false)}
onCapture={(file) => {
setFiles((prev) => [...prev, file]);
}}
autoRecord
/>
</>
);
 
const Input = (props: FileInputProps) => (
<input
{...props}
data-cy="upload-files-input"
id={`file_upload_${fileType}`}
title={t("change_file")}
onChange={onFileChange}
type="file"
multiple={multiple}
accept={
"allowedExtensions" in options
? options.allowedExtensions?.map((e) => "." + e).join(",")
: "allowAllExtensions" in options
? "*"
: DEFAULT_ALLOWED_EXTENSIONS.join(",")
}
hidden={props.hidden || true}
/>
);
 
return {
progress,
error,
setError,
validateFiles: validateFileUpload,
handleCameraCapture: () => setCameraModalOpen(true),
handleAudioCapture: () => setAudioModalOpen(true),
handleFileUpload: handleUpload,
Dialogues,
Input,
fileNames: uploadFileNames,
files: files,
setFileNames: setUploadFileNames,
setFileName: (name: string, index = 0) => {
setUploadFileNames((prev) =>
prev.map((n, i) => (i === index ? name : n)),
);
},
removeFile: (index = 0) => {
setFiles((prev) => prev.filter((_, i) => i !== index));
setUploadFileNames((prev) => prev.filter((_, i) => i !== index));
},
clearFiles,
uploading,
previewing,
};
}