From 205a04174e6807041b667204af8ddfd44b0eb3c8 Mon Sep 17 00:00:00 2001 From: Gideon Pinto Date: Tue, 19 Mar 2024 11:37:35 -0400 Subject: [PATCH] feat: Improve the file download experience for the uploaded files in the Library Page (#1121) --- src/api/model.ts | 151 ++---------------- src/components/LibraryCopy/FileCard.tsx | 114 ++++++++----- .../Preview/displays/DcmDisplay.tsx | 3 +- 3 files changed, 83 insertions(+), 185 deletions(-) diff --git a/src/api/model.ts b/src/api/model.ts index cf5f112f9..6824a2b5f 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -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 = { @@ -345,3 +203,12 @@ export function getFileExtension(filename: string) { const name = filename.substring(filename.lastIndexOf(".") + 1); return name; } + +// biome-ignore lint/complexity/noStaticOnlyClass: +export class FileViewerModel { + public getFileName(item: FeedFile) { + const splitString = item.data.fname.split("/"); + const filename = splitString[splitString.length - 1]; + return filename; + } +} diff --git a/src/components/LibraryCopy/FileCard.tsx b/src/components/LibraryCopy/FileCard.tsx index d91a0eec2..b755c4cb9 100644 --- a/src/components/LibraryCopy/FileCard.tsx +++ b/src/components/LibraryCopy/FileCard.tsx @@ -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); @@ -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 ( { @@ -52,9 +104,9 @@ const FileCard = ({ file }: { file: any }) => { isRounded > - + - + @@ -78,25 +130,13 @@ const FileCard = ({ file }: { file: any }) => {