.
This commit is contained in:
parent
474adf99c9
commit
51d1c3bbc8
@ -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"
|
||||
},
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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";
|
||||
|
||||
2
client/src/lib/hooks/useDownloader/index.ts
Normal file
2
client/src/lib/hooks/useDownloader/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import useDownloader from "./useDownloader";
|
||||
export { useDownloader };
|
||||
81
client/src/lib/hooks/useDownloader/types.ts
Normal file
81
client/src/lib/hooks/useDownloader/types.ts
Normal 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;
|
||||
}
|
||||
276
client/src/lib/hooks/useDownloader/useDownloader.tsx
Normal file
276
client/src/lib/hooks/useDownloader/useDownloader.tsx
Normal 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]
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user