coronasafe/care_fe

View on GitHub
src/hooks/useFileManager.tsx

Summary

Maintainability
F
4 days
Test Coverage
File `useFileManager.tsx` has 514 lines of code (exceeds 250 allowed). Consider refactoring.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { t } from "i18next";
import { useState } from "react";
import { Trans } from "react-i18next";
import { toast } from "sonner";
 
import { cn } from "@/lib/utils";
 
import CareIcon from "@/CAREUI/icons/CareIcon";
 
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
 
import FilePreviewDialog from "@/components/Common/FilePreviewDialog";
import { StateInterface } from "@/components/Common/FilePreviewDialog";
import { FileUploadModel } from "@/components/Patient/models";
 
import {
FILE_EXTENSIONS,
PREVIEWABLE_FILE_EXTENSIONS,
} from "@/common/constants";
 
import routes from "@/Utils/request/api";
import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import { formatDateTime } from "@/Utils/utils";
 
export interface FileManagerOptions {
type: string;
onArchive?: () => void;
onEdit?: () => void;
uploadedFiles?: FileUploadModel[];
}
export interface FileManagerResult {
viewFile: (file: FileUploadModel, associating_id: string) => void;
archiveFile: (
file: FileUploadModel,
associating_id: string,
skipPrompt?: { reason: string },
) => void;
editFile: (file: FileUploadModel, associating_id: string) => void;
Dialogues: React.ReactNode;
isPreviewable: (file: FileUploadModel) => boolean;
getFileType: (
file: FileUploadModel,
) => keyof typeof FILE_EXTENSIONS | "UNKNOWN";
downloadFile: (
file: FileUploadModel,
associating_id: string,
) => Promise<void>;
type: string;
}
 
Function `useFileManager` has 457 lines of code (exceeds 25 allowed). Consider refactoring.
Function `useFileManager` has a Cognitive Complexity of 29 (exceeds 5 allowed). Consider refactoring.
export default function useFileManager(
options: FileManagerOptions,
): FileManagerResult {
const { type: fileType, onArchive, onEdit, uploadedFiles } = options;
 
const [file_state, setFileState] = useState<StateInterface>({
open: false,
isImage: false,
name: "",
extension: "",
zoom: 4,
isZoomInDisabled: false,
isZoomOutDisabled: false,
rotation: 0,
});
const [fileUrl, setFileUrl] = useState<string>("");
const [downloadURL, setDownloadURL] = useState<string>("");
const [archiveDialogueOpen, setArchiveDialogueOpen] = useState<
(FileUploadModel & { associating_id: string }) | null
>(null);
const [archiveReason, setArchiveReason] = useState("");
const [archiveReasonError, setArchiveReasonError] = useState("");
const [archiving, setArchiving] = useState(false);
const [editing, setEditing] = useState(false);
const [editDialogueOpen, setEditDialogueOpen] =
useState<FileUploadModel | null>(null);
const [editError, setEditError] = useState("");
const [currentIndex, setCurrentIndex] = useState<number>(-1);
const queryClient = useQueryClient();
 
const getExtension = (url: string) => {
const div1 = url.split("?")[0].split(".");
const ext: string = div1[div1.length - 1].toLowerCase();
return ext;
};
 
const retrieveUpload = async (
file: FileUploadModel,
associating_id: string,
) => {
return queryClient.fetchQuery({
queryKey: ["file", fileType, associating_id, file.id],
queryFn: () =>
query(routes.retrieveUpload, {
queryParams: {
file_type: fileType,
associating_id,
},
pathParams: { id: file.id || "" },
})({} as any),
});
};
 
const viewFile = async (file: FileUploadModel, associating_id: string) => {
const index = uploadedFiles?.findIndex((f) => f.id === file.id) ?? -1;
setCurrentIndex(index);
setFileUrl("");
setFileState({ ...file_state, open: true });
 
const data = await retrieveUpload(file, associating_id);
 
if (!data) return;
 
const signedUrl = data.read_signed_url as string;
const extension = getExtension(signedUrl);
 
setFileState({
...file_state,
open: true,
name: data.name as string,
extension,
isImage: FILE_EXTENSIONS.IMAGE.includes(
extension as (typeof FILE_EXTENSIONS.IMAGE)[number],
),
});
setDownloadURL(signedUrl);
setFileUrl(signedUrl);
};
 
Similar blocks of code found in 2 locations. Consider refactoring.
const validateArchiveReason = (name: any) => {
if (name.trim() === "") {
setArchiveReasonError(t("please_enter_a_valid_reason"));
return false;
} else {
setArchiveReasonError("");
return true;
}
};
 
const { mutateAsync: archiveUpload } = useMutation({
mutationFn: (body: { id: string; archive_reason: string }) =>
query(routes.archiveUpload, {
body: { archive_reason: body.archive_reason },
pathParams: { id: body.id },
})({} as any),
onSuccess: () => {
toast.success(t("file_archived_successfully"));
queryClient.invalidateQueries({
queryKey: ["files", fileType, archiveDialogueOpen?.associating_id],
});
},
});
 
const handleFileArchive = async (archiveFile: typeof archiveDialogueOpen) => {
if (!validateArchiveReason(archiveReason)) {
setArchiving(false);
return;
}
 
await archiveUpload({
id: archiveFile?.id || "",
archive_reason: archiveReason,
});
 
setArchiveDialogueOpen(null);
setArchiving(false);
setArchiveReason("");
onArchive?.();
};
 
const archiveFile = (
file: FileUploadModel,
associating_id: string,
skipPrompt?: { reason: string },
) => {
if (skipPrompt) {
setArchiving(true);
setArchiveReason(skipPrompt.reason);
handleFileArchive({
...file,
associating_id,
});
return;
}
setArchiveDialogueOpen({ ...file, associating_id });
};
 
const handleFilePreviewClose = () => {
setDownloadURL("");
setFileState({
...file_state,
open: false,
zoom: 4,
isZoomInDisabled: false,
isZoomOutDisabled: false,
});
};
 
Similar blocks of code found in 2 locations. Consider refactoring.
const validateEditFileName = (name: string) => {
if (name.trim() === "") {
setEditError(t("please_enter_a_name"));
return false;
} else {
setEditError("");
return true;
}
};
 
const { mutateAsync: editUpload } = useMutation({
mutationFn: (body: { id: string; name: string; associating_id: string }) =>
mutate(routes.editUpload, {
body: { name: body.name },
pathParams: { id: body.id },
})(body),
onSuccess: (_, { associating_id }) => {
toast.success(t("file_name_changed_successfully"));
setEditDialogueOpen(null);
onEdit?.();
queryClient.invalidateQueries({
queryKey: ["files", fileType, associating_id],
});
},
});
 
const partialupdateFileName = async (file: FileUploadModel) => {
if (!validateEditFileName(file.name || "")) {
setEditing(false);
return;
}
 
await editUpload({
id: file.id || "",
name: file.name || "",
associating_id: file.associating_id || "",
});
 
setEditing(false);
};
 
const editFile = (file: FileUploadModel, associating_id: string) => {
setEditDialogueOpen({ ...file, associating_id });
};
 
const Dialogues = (
<>
<FilePreviewDialog
show={file_state.open}
fileUrl={fileUrl}
file_state={file_state}
setFileState={setFileState}
downloadURL={downloadURL}
uploadedFiles={uploadedFiles}
onClose={handleFilePreviewClose}
className="h-[80vh] w-full md:h-screen"
loadFile={viewFile}
currentIndex={currentIndex}
/>
<Dialog
open={
archiveDialogueOpen !== null &&
archiveDialogueOpen.archived_datetime === null
}
onOpenChange={() => setArchiveDialogueOpen(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex flex-row">
<div className="my-1 mr-3 rounded-full bg-red-100 px-5 py-4 text-center">
<CareIcon
icon="l-exclamation-triangle"
className="text-lg text-danger-500"
/>
</div>
Similar blocks of code found in 2 locations. Consider refactoring.
<div className="text-sm">
<h1 className="text-xl text-black">{t("archive_file")}</h1>
<span className="text-sm text-secondary-600">
{t("this_action_is_irreversible")}
</span>
</div>
</div>
</DialogTitle>
</DialogHeader>
 
<form
onSubmit={(event: any) => {
event.preventDefault();
handleFileArchive(archiveDialogueOpen);
}}
className="mx-2 my-4 flex w-full flex-col"
>
<div>
<Label className="text-gray-800 mb-2">
<Trans
i18nKey="state_reason_for_archiving"
values={{ name: archiveDialogueOpen?.name }}
components={{ strong: <strong /> }}
/>
</Label>
<Textarea
name="editFileName"
id="archive-file-reason"
rows={6}
required
placeholder="Type the reason..."
value={archiveReason}
onChange={(e) => setArchiveReason(e.target.value)}
className={cn(
archiveReasonError &&
"border-red-500 focus-visible:ring-red-500",
)}
data-cy="archive-reason-textarea"
/>
{archiveReasonError && (
<p className="text-sm text-red-500">{archiveReasonError}</p>
)}
</div>
<div className="mt-4 flex flex-col-reverse justify-end gap-2 md:flex-row">
<Button
type="button"
variant="outline"
onClick={() => setArchiveDialogueOpen(null)}
>
{t("cancel")}
</Button>
<Button type="submit" variant="primary" disabled={archiving}>
{t("proceed")}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog
open={
archiveDialogueOpen !== null &&
archiveDialogueOpen.archived_datetime !== null
}
onOpenChange={() => setArchiveDialogueOpen(null)}
>
<DialogContent className="md:w-[700px]">
<DialogHeader>
<DialogTitle className="text-xl text-black">
{archiveDialogueOpen?.name} {t("archived")}
</DialogTitle>
</DialogHeader>
<div className="mb-8 text-xs text-secondary-700">
<CareIcon icon="l-archive" className="mr-2" />
{t("this_file_has_been_archived")}
</div>
<div className="flex flex-col gap-4 md:grid md:grid-cols-2">
{[
{
label: "File Name",
content: archiveDialogueOpen?.name,
icon: "l-file",
},
{
label: "Uploaded By",
content: archiveDialogueOpen?.uploaded_by?.username,
icon: "l-user",
},
{
label: "Uploaded On",
content: formatDateTime(archiveDialogueOpen?.created_date),
icon: "l-clock",
},
{
label: "Archive Reason",
content: archiveDialogueOpen?.archive_reason,
icon: "l-archive",
},
{
label: "Archived By",
content: archiveDialogueOpen?.archived_by?.username,
icon: "l-user",
},
{
label: "Archived On",
content: formatDateTime(archiveDialogueOpen?.archived_datetime),
icon: "l-clock",
},
].map((item, index) => (
<div key={index} className="flex gap-2">
<div className="flex aspect-square h-10 items-center justify-center rounded-full bg-primary-100">
<CareIcon
icon={item.icon as any}
className="text-lg text-primary-500"
/>
</div>
<div>
<div className="text-xs uppercase text-secondary-700">
{item.label}
</div>
<div
className="break-words text-base"
data-archive-info={item.label}
>
{item.content}
</div>
</div>
</div>
))}
</div>
<div className="mt-10 flex justify-end">
<Button
type="button"
variant="outline"
onClick={() => setArchiveDialogueOpen(null)}
>
{t("cancel")}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog
open={editDialogueOpen !== null}
onOpenChange={() => setEditDialogueOpen(null)}
>
<DialogContent>
Similar blocks of code found in 2 locations. Consider refactoring.
<DialogHeader>
<DialogTitle>
<div className="flex flex-row">
<div className="rounded-full bg-primary-100 px-5 py-4">
<CareIcon
icon="l-edit-alt"
className="text-lg text-primary-500"
/>
</div>
<div className="m-4">
<h1 className="text-xl text-black">{t("rename_file")}</h1>
</div>
</div>
</DialogTitle>
</DialogHeader>
<form
onSubmit={(event: any) => {
event.preventDefault();
setEditing(true);
if (editDialogueOpen) partialupdateFileName(editDialogueOpen);
}}
className="flex w-full flex-col"
>
<div>
<Label>{t("enter_the_file_name")}</Label>
<Input
name="editFileName"
id="edit-file-name"
value={editDialogueOpen?.name}
onChange={(e) => {
setEditDialogueOpen({
...editDialogueOpen,
name: e.target.value,
});
}}
data-cy="edit-filename-input"
/>
{editError && <p className="text-sm text-red-500">{editError}</p>}
</div>
<div className="mt-4 flex flex-col-reverse justify-end gap-2 md:flex-row">
<Button
type="button"
variant="outline"
onClick={() => setEditDialogueOpen(null)}
>
{t("cancel")}
</Button>
<Button
type="submit"
variant="primary"
disabled={
editing === true ||
editDialogueOpen?.name === "" ||
editDialogueOpen?.name?.length === 0
}
>
{t("proceed")}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
);
 
const isPreviewable = (file: FileUploadModel) =>
!!file.extension &&
PREVIEWABLE_FILE_EXTENSIONS.includes(
file.extension.slice(1) as (typeof PREVIEWABLE_FILE_EXTENSIONS)[number],
);
 
const getFileType: (
f: FileUploadModel,
) => keyof typeof FILE_EXTENSIONS | "UNKNOWN" = (file: FileUploadModel) => {
if (!file.extension) return "UNKNOWN";
const ftype = (
Object.keys(FILE_EXTENSIONS) as (keyof typeof FILE_EXTENSIONS)[]
).find((type) =>
FILE_EXTENSIONS[type].includes((file.extension?.slice(1) || "") as never),
);
return ftype || "UNKNOWN";
};
 
const downloadFile = async (
file: FileUploadModel,
associating_id: string,
) => {
try {
if (!file.id) return;
toast.success(t("file_download_started"));
const fileData = await retrieveUpload(file, associating_id);
const response = await fetch(fileData?.read_signed_url || "");
if (!response.ok) throw new Error("Network response was not ok.");
 
const data = await response.blob();
const blobUrl = window.URL.createObjectURL(data);
 
const a = document.createElement("a");
a.href = blobUrl;
a.download = file.name || "file";
document.body.appendChild(a);
a.click();
 
// Clean up
window.URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
toast.success(t("file_download_completed"));
} catch {
toast.error(t("file_download_failed"));
}
};
 
return {
viewFile,
archiveFile,
editFile,
Dialogues,
isPreviewable,
getFileType,
downloadFile,
type: fileType,
};
}