Skip to content

Commit

Permalink
feat: Improve the file download experience for the uploaded files in …
Browse files Browse the repository at this point in the history
…the Library Page (#1121)
  • Loading branch information
PintoGideon committed Mar 19, 2024
1 parent d7a3fec commit 205a041
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 185 deletions.
151 changes: 9 additions & 142 deletions src/api/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,148 +168,6 @@ export interface IFileBlob {
fileType: string;
}

export class FileViewerModel {
static downloadStatus: any = {};
static itemsToDownload: FeedFile[] = [];
static abortControllers: any = {};

static getFileName(item: FeedFile) {
const splitString = item.data.fname.split("/");
const filename = splitString[splitString.length - 1];
return filename;
}

static setDownloadStatus(status: number, item: FeedFile) {
this.downloadStatus = {
...this.downloadStatus,
[item.data.fname]: status,
};
}

static startDownload(
item: FeedFile,
notification: any,
callback: (status: any) => void,
) {
const findItem = this.itemsToDownload.find(
(currentItem) => currentItem.data.fname === item.data.fname,
);

const filename = this.getFileName(item);

const onDownloadProgress = (progress: any, item: FeedFile) => {
this.downloadStatus = {
...this.downloadStatus,
[item.data.fname]: progress,
};
callback(this.downloadStatus);
};

if (!findItem) {
this.itemsToDownload.push(item);
this.setDownloadStatus(0, item);
callback(this.downloadStatus);
notification.info({
message: `Preparing ${filename} for download.`,
description: `Total Jobs (${this.itemsToDownload.length})`,
duration: 5,
});

this.downloadFile(
item,
filename,
notification,
callback,
onDownloadProgress,
);
}
}

static removeJobs(
item: FeedFile,
notification: any,
callback: (status: any) => void,
status: string,
) {
const index = this.itemsToDownload.indexOf(item);
if (index > -1) {
// only splice array when item is found
this.itemsToDownload.splice(index, 1); // 2nd parameter means remove one item only
}

delete this.downloadStatus[item.data.fname];
delete this.abortControllers[item.data.fname];
const filename = this.getFileName(item);

callback(this.downloadStatus);
notification.info({
message: `${status} download for ${filename}`,
description: `Total jobs ${this.itemsToDownload.length}`,
duration: 1.5,
});
}

// Download File Blob
static async downloadFile(
item: FeedFile,
filename: string,
notification: any,
callback: (status: any) => void,
onDownloadProgressCallback: (progressEvent: number, item: FeedFile) => void,
) {
const urlString = `${item.url}${filename}`;
const client = ChrisAPIClient.getClient();
const token = client.auth.token;
const controller = new AbortController();
const { signal } = controller;

this.abortControllers = {
...this.abortControllers,
[item.data.fname]: controller,
};

const downloadPromise = axios
.get(urlString, {
responseType: "blob",
headers: {
Authorization: `Token ${token}`,
},
signal,
onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.loaded) {
const progress = Math.floor(
(progressEvent.loaded / item.data.fsize) * 100,
);

onDownloadProgressCallback(progress, item);
}
},
})
.catch((error) => {
this.removeJobs(item, notification, callback, error);
return null;
});

const response = await downloadPromise;

if (response?.data) {
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.target = "_blank";
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
setTimeout(function () {
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
}, 1000);
this.removeJobs(item, notification, callback, "Finished");
}
}
}

// Description: Mapping for Viewer type by file type *Note: Should come from db
// File type: Viewer component name
export const fileViewerMap: any = {
Expand Down Expand Up @@ -345,3 +203,12 @@ export function getFileExtension(filename: string) {
const name = filename.substring(filename.lastIndexOf(".") + 1);
return name;
}

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export class FileViewerModel {
public getFileName(item: FeedFile) {
const splitString = item.data.fname.split("/");
const filename = splitString[splitString.length - 1];
return filename;
}
}
114 changes: 72 additions & 42 deletions src/components/LibraryCopy/FileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@ import {
Card,
CardBody,
CardHeader,
Split,
SplitItem,
Progress,
Modal,
ModalVariant,
Progress,
Split,
SplitItem,
} from "@patternfly/react-core";
import { useMutation } from "@tanstack/react-query";
import { notification } from "antd";
import { useContext, useState } from "react";
import FaFile from "@patternfly/react-icons/dist/esm/icons/file-icon";
import FaDownload from "@patternfly/react-icons/dist/esm/icons/download-icon";
import AiOutlineClose from "@patternfly/react-icons/dist/esm/icons/close-icon";
import useLongPress from "./utils";
import FileDetailView from "../Preview/FileDetailView";
import axios, { AxiosProgressEvent } from "axios";
import { useContext, useEffect, useState } from "react";
import ChrisAPIClient from "../../api/chrisapiclient";
import { FileViewerModel } from "../../api/model";
import { DotsIndicator } from "../Common";
import { elipses } from "./utils";
import { DownloadIcon, FileIcon } from "../Icons";
import FileDetailView from "../Preview/FileDetailView";
import { LibraryContext } from "./context";
import useLongPress, { elipses } from "./utils";

const FileCard = ({ file }: { file: any }) => {
const { state } = useContext(LibraryContext);
Expand All @@ -35,6 +34,59 @@ const FileCard = ({ file }: { file: any }) => {
const fileName = fileNameArray[fileNameArray.length - 1];
const { previewAll } = state;

const downloadFile = useMutation({
mutationFn: () => {
const url = file.collection.items[0].links[0].href;
if (!url) {
throw new Error("Count not fetch the file from this url");
}
const client = ChrisAPIClient.getClient();
const token = client.auth.token;

const downloadPromise = axios.get(url, {
headers: {
Authorization: `Token ${token}`,
},
onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent?.progress) {
setDownloadStatus(progressEvent.progress * 100);
}
},
});
return downloadPromise;
},
});

useEffect(() => {
if (downloadFile.isSuccess) {
const { data: response } = downloadFile;
const link = document.createElement("a");
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const fileViewer = new FileViewerModel();
const fileName = fileViewer.getFileName(file);
link.target = "_blank";
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
notification.info({
message: `Triggered download for ${fileName}`,
duration: 0.5,
});
downloadFile.reset();
setDownloadStatus(-1);
}

if (downloadFile.isError) {
notification.error({
message: `${downloadFile.error.message}`,
duration: 2,
});
downloadFile.reset();
}
}, [downloadFile.isSuccess, downloadFile.isError]);

return (
<Card
onClick={(e) => {
Expand All @@ -52,9 +104,9 @@ const FileCard = ({ file }: { file: any }) => {
isRounded
>
<CardHeader>
<Split style={{ overflow: "hidden" }}>
<Split style={{ overflow: "hidden", alignItems: "center" }}>
<SplitItem>
<FaFile />
<FileIcon />
</SplitItem>
<SplitItem isFilled>
<Button variant="link">{elipses(fileName, 40)}</Button>
Expand All @@ -78,25 +130,13 @@ const FileCard = ({ file }: { file: any }) => {
<Button
style={{ marginLeft: "0.5rem" }}
variant="link"
icon={
<FaDownload
style={{ cursor: "pointer" }}
onClick={async (event) => {
event.stopPropagation();
FileViewerModel.startDownload(
file,
notification,
(statusCallbackValue: any) => {
const statusValue = statusCallbackValue[file.data.fname];
setDownloadStatus(statusValue);
},
);
}}
/>
}
onClick={async (event) => {
event.stopPropagation();
downloadFile.mutate();
}}
icon={<DownloadIcon style={{ cursor: "pointer" }} />}
/>
{status === 0 && <DotsIndicator title="Processing Download..." />}
{status && status > 0 ? (
{status > 0 && (
<div
style={{
display: "flex",
Expand All @@ -110,19 +150,9 @@ const FileCard = ({ file }: { file: any }) => {
}}
size="sm"
value={status}
/>{" "}
<AiOutlineClose
style={{
color: "red",
marginLeft: "0.25rem",
}}
onClick={(event) => {
event.stopPropagation();
FileViewerModel.abortControllers[file.data.fname].abort();
}}
/>
</div>
) : null}
)}
</div>
</CardBody>
{largePreview && (
Expand Down
3 changes: 2 additions & 1 deletion src/components/Preview/displays/DcmDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const DcmDisplay: React.FC<DcmImageProps> = (props: DcmImageProps) => {
if (extension === "dcm") {
imageID = await loadDicomImage(blob);
} else {
const fileName = FileViewerModel.getFileName(file);
const fileviewer = new FileViewerModel();
const fileName = fileviewer.getFileName(file);
imageID = `web:${file.url}${fileName}`;
}
const activeViewport = await displayDicomImage(element, imageID);
Expand Down

0 comments on commit 205a041

Please sign in to comment.