This commit is contained in:
David Arranz 2024-08-21 13:49:52 +02:00
parent 474adf99c9
commit 51d1c3bbc8
6 changed files with 394 additions and 4 deletions

View File

@ -67,7 +67,6 @@
"react-router-dom": "^6.26.0",
"react-secure-storage": "^1.3.2",
"react-toastify": "^10.0.5",
"react-use-downloader": "^1.2.8",
"react-wrap-balancer": "^1.1.1",
"recharts": "^2.12.7"
},

View File

@ -1,3 +1,4 @@
import { useDownloader } from "@/lib/hooks";
import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource";
import {
IFilterItemDataProviderParam,
@ -18,8 +19,7 @@ import {
UniqueID,
} from "@shared/contexts";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import useDownloader from "react-use-downloader";
import { useCallback, useMemo, useState } from "react";
export type UseQuotesListParams = Omit<IGetListDataProviderParams, "filters" | "resource"> & {
status?: string;
@ -142,7 +142,7 @@ export const useQuotes = () => {
...params,
}),
useReport: (id?: string, params?: UseQuotesReportParamsType) => {
useReport2: (id?: string, params?: UseQuotesReportParamsType) => {
const queryClient = useQueryClient();
const queryKey = useMemo(
() => keys().data().resource("quotes").action("report").id(id).params().get(),
@ -221,6 +221,37 @@ export const useQuotes = () => {
return { download, error, isFetching, isError };
},*/
useReport: () => {
const auth = dataSource.getApiAuthorization();
const [reportBlob, setReportBlob] = useState<Blob>();
const downloader = useDownloader({
headers: {
Authorization: auth,
},
customHandleDownload: (data: Blob) => {
const blobData = [data];
const blob = new Blob(blobData, {
type: "application/pdf",
});
setReportBlob(blob);
return true;
},
});
const download = (id: string) => {
const url = actions.getQuotePDFDownloadURL(id);
downloader.download(url, "");
return reportBlob;
};
return {
...downloader,
download,
};
},
useDownloader: () => {
const auth = dataSource.getApiAuthorization();
const downloader = useDownloader({

View File

@ -15,6 +15,7 @@ export * from "./useAuth";
export * from "./useCustomDialog";
export * from "./useDataSource";
export * from "./useDataTable";
export * from "./useDownloader";
export * from "./useLocalization";
export * from "./useMediaQuery";
export * from "./usePagination";

View File

@ -0,0 +1,2 @@
import useDownloader from "./useDownloader";
export { useDownloader };

View File

@ -0,0 +1,81 @@
import { SetStateAction } from "react";
export type ErrorMessage = {
errorMessage: string;
} | null;
/** useDownloader options for fetch call
* See fetch RequestInit for more details
*/
export type UseDownloaderOptions = RequestInit & {
customHandleDownload?: (data: Blob, filename: string, mime?: string) => boolean | NodeJS.Timeout;
};
/**
* Initiate the download of the specified asset from the specified url. Optionally supply timeout and overrideOptions.
* @example await download('https://example.com/file.zip', 'file.zip')
* @example await download('https://example.com/file.zip', 'file.zip', 500) timeouts after 500ms
* @example await download('https://example.com/file.zip', 'file.zip', undefined, { method: 'GET' }) skips optional timeout but supplies overrideOptions
*/
export type DownloadFunction = (
/** Download url
* @example https://upload.wikimedia.org/wikipedia/commons/4/4d/%D0%93%D0%BE%D0%B2%D0%B5%D1%80%D0%BB%D0%B0_%D1%96_%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D1%81_%D0%B2_%D0%BF%D1%80%D0%BE%D0%BC%D1%96%D0%BD%D1%8F%D1%85_%D0%B2%D1%80%D0%B0%D0%BD%D1%96%D1%88%D0%BD%D1%8C%D0%BE%D0%B3%D0%BE_%D1%81%D0%BE%D0%BD%D1%86%D1%8F.jpg
*/
downloadUrl: string,
/** File name
* @example carpathia.jpeg
*/
filename: string,
/** Optional timeout to download items */
timeout?: number,
/** Optional options to supplement and/or override UseDownloader options */
overrideOptions?: UseDownloaderOptions
) => Promise<void | null>;
/**
* Provides access to Downloader functionality and settings.
*
* @interface EditDialogField
* @field {number} size in bytes.
* @field {number} elapsed time in seconds.
* @field {number} percentage in string
* @field {DownloadFunction} download function handler
* @field {void} cancel function handler
* @field {ErrorMessage} error object from the request
* @field {boolean} isInProgress boolean flag denoting download status
*/
export interface UseDownloader {
/** Size in bytes */
size: number;
/** Elapsed time in seconds */
elapsed: number;
/** Percentage in string */
percentage: number;
/**
* Download function handler
* @example await download('https://example.com/file.zip', 'file.zip')
* @example await download('https://example.com/file.zip', 'file.zip', 500) timeouts after 500ms
* */
download: DownloadFunction;
/** Cancel function handler */
cancel: () => void;
/** Error object from the request */
error: ErrorMessage;
/** Boolean denoting download status */
isInProgress: boolean;
}
export interface ResolverProps {
setSize: (value: SetStateAction<number>) => void;
setControllerCallback: (controller: ReadableStreamController<Uint8Array>) => void;
setPercentageCallback: ({ loaded, total }: { loaded: number; total: number }) => void;
setErrorCallback: (err: Error) => void;
}
interface CustomNavigator extends Navigator {
msSaveBlob: (blob?: Blob, filename?: string) => boolean | NodeJS.Timeout;
}
export interface WindowDownloaderEmbedded extends Window {
navigator: CustomNavigator;
}

View File

@ -0,0 +1,276 @@
// https://github.com/the-bugging/react-use-downloader
import { useCallback, useMemo, useRef, useState } from "react";
import {
DownloadFunction,
ErrorMessage,
ResolverProps,
UseDownloader,
UseDownloaderOptions,
WindowDownloaderEmbedded,
} from "./types";
/**
* Resolver function to handle the download progress.
* @param {ResolverProps} props
* @returns {Response}
*/
export const resolver =
({ setSize, setControllerCallback, setPercentageCallback, setErrorCallback }: ResolverProps) =>
(response: Response): Response => {
if (!response.ok) {
throw Error(`${response.status} ${response.type} ${response.statusText}`);
}
if (!response.body) {
throw Error("ReadableStream not yet supported in this browser.");
}
const responseBody = response.body;
const contentEncoding = response.headers.get("content-encoding");
const contentLength = response.headers.get(contentEncoding ? "x-file-size" : "content-length");
const total = parseInt(contentLength || "0", 10);
setSize(() => total);
let loaded = 0;
const stream = new ReadableStream<Uint8Array>({
start(controller) {
setControllerCallback(controller);
const reader = responseBody.getReader();
async function read(): Promise<void> {
return reader
.read()
.then(({ done, value }) => {
if (done) {
return controller.close();
}
loaded += value?.byteLength || 0;
if (value) {
controller.enqueue(value);
}
setPercentageCallback({ loaded, total });
return read();
})
.catch((error: Error) => {
setErrorCallback(error);
reader.cancel("Cancelled");
return controller.error(error);
});
}
return read();
},
});
return new Response(stream);
};
/**
* jsDownload function to handle the download process.
* @param {Blob} data
* @param {string} filename
* @param {string} mime
* @returns {boolean | NodeJS.Timeout}
*/
export const jsDownload = (
data: Blob,
filename: string,
mime?: string
): boolean | NodeJS.Timeout => {
const blobData = [data];
const blob = new Blob(blobData, {
type: mime || "application/octet-stream",
});
if (typeof (window as unknown as WindowDownloaderEmbedded).navigator.msSaveBlob !== "undefined") {
return (window as unknown as WindowDownloaderEmbedded).navigator.msSaveBlob(blob, filename);
}
const blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob);
const tempLink = document.createElement("a");
tempLink.style.display = "none";
tempLink.href = blobURL;
tempLink.setAttribute("download", filename);
if (typeof tempLink.download === "undefined") {
tempLink.setAttribute("target", "_blank");
}
document.body.appendChild(tempLink);
tempLink.click();
return setTimeout(() => {
document.body.removeChild(tempLink);
window.URL.revokeObjectURL(blobURL);
}, 200);
};
/**
* useDownloader hook to handle the download process.
* @param {UseDownloaderOptions} options
* @returns {UseDownloader}
*/
export default function useDownloader({
customHandleDownload,
...options
}: UseDownloaderOptions = {}): UseDownloader {
let debugMode = false;
try {
debugMode = process ? !!process?.env?.REACT_APP_DEBUG_MODE : false;
} catch {
debugMode = false;
}
const [elapsed, setElapsed] = useState(0);
const [percentage, setPercentage] = useState(0);
const [size, setSize] = useState(0);
const [error, setError] = useState<ErrorMessage>(null);
const [isInProgress, setIsInProgress] = useState(false);
const controllerRef = useRef<null | ReadableStreamController<Uint8Array>>(null);
const setPercentageCallback = useCallback(
({ loaded, total }: { loaded: number; total: number }) => {
const pct = Math.round((loaded / total) * 100);
setPercentage(() => pct);
},
[]
);
const setErrorCallback = useCallback((err: Error) => {
const errorMap: { [index: string]: string } = {
"Failed to execute 'enqueue' on 'ReadableStreamDefaultController': Cannot enqueue a chunk into an errored readable stream":
"Download canceled",
"The user aborted a request.": "Download timed out",
};
setError(() => {
const resolvedError: string = errorMap[err.message] ? errorMap[err.message] : err.message;
return { errorMessage: resolvedError };
});
}, []);
const setControllerCallback = useCallback(
(controller: ReadableStreamController<Uint8Array> | null) => {
controllerRef.current = controller;
},
[]
);
const closeControllerCallback = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.error();
}
}, []);
const clearAllStateCallback = useCallback(() => {
setControllerCallback(null);
setElapsed(() => 0);
setPercentage(() => 0);
setSize(() => 0);
setIsInProgress(() => false);
}, [setControllerCallback]);
const handleDownload: DownloadFunction = useCallback(
async (downloadUrl, filename, timeout = 0, overrideOptions = {}) => {
if (isInProgress) return null;
clearAllStateCallback();
setError(() => null);
setIsInProgress(() => true);
const intervalId = setInterval(
() => setElapsed((prevValue) => prevValue + 1),
debugMode ? 1 : 1000
);
const resolverWithProgress = resolver({
setSize,
setControllerCallback,
setPercentageCallback,
setErrorCallback,
});
const fetchController = new AbortController();
const timeoutId = setTimeout(() => {
if (timeout > 0) fetchController.abort();
}, timeout);
// Use the custom handle download function if available
const _customHandleDownload = customHandleDownload || jsDownload;
return fetch(downloadUrl, {
method: "GET",
...options,
...overrideOptions,
signal: fetchController.signal,
})
.then(resolverWithProgress)
.then((data) => {
return data.blob();
})
.then((response) => _customHandleDownload(response, filename))
.then(() => {
clearAllStateCallback();
return clearInterval(intervalId);
})
.catch((err) => {
clearAllStateCallback();
setError((prevValue) => {
const { message } = err;
if (message !== "Failed to fetch") {
return {
errorMessage: err.message,
};
}
return prevValue;
});
clearTimeout(timeoutId);
return clearInterval(intervalId);
});
},
[
isInProgress,
clearAllStateCallback,
debugMode,
setControllerCallback,
setPercentageCallback,
setErrorCallback,
options,
customHandleDownload,
]
);
return useMemo(
() => ({
elapsed,
percentage,
size,
download: handleDownload,
cancel: closeControllerCallback,
error,
isInProgress,
jsDownload,
}),
[elapsed, percentage, size, handleDownload, closeControllerCallback, error, isInProgress]
);
}