.
This commit is contained in:
parent
474adf99c9
commit
51d1c3bbc8
@ -67,7 +67,6 @@
|
|||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"react-secure-storage": "^1.3.2",
|
"react-secure-storage": "^1.3.2",
|
||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"react-use-downloader": "^1.2.8",
|
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"recharts": "^2.12.7"
|
"recharts": "^2.12.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useDownloader } from "@/lib/hooks";
|
||||||
import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource";
|
import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource";
|
||||||
import {
|
import {
|
||||||
IFilterItemDataProviderParam,
|
IFilterItemDataProviderParam,
|
||||||
@ -18,8 +19,7 @@ import {
|
|||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import useDownloader from "react-use-downloader";
|
|
||||||
|
|
||||||
export type UseQuotesListParams = Omit<IGetListDataProviderParams, "filters" | "resource"> & {
|
export type UseQuotesListParams = Omit<IGetListDataProviderParams, "filters" | "resource"> & {
|
||||||
status?: string;
|
status?: string;
|
||||||
@ -142,7 +142,7 @@ export const useQuotes = () => {
|
|||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
useReport: (id?: string, params?: UseQuotesReportParamsType) => {
|
useReport2: (id?: string, params?: UseQuotesReportParamsType) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => keys().data().resource("quotes").action("report").id(id).params().get(),
|
() => keys().data().resource("quotes").action("report").id(id).params().get(),
|
||||||
@ -221,6 +221,37 @@ export const useQuotes = () => {
|
|||||||
return { download, error, isFetching, isError };
|
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: () => {
|
useDownloader: () => {
|
||||||
const auth = dataSource.getApiAuthorization();
|
const auth = dataSource.getApiAuthorization();
|
||||||
const downloader = useDownloader({
|
const downloader = useDownloader({
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export * from "./useAuth";
|
|||||||
export * from "./useCustomDialog";
|
export * from "./useCustomDialog";
|
||||||
export * from "./useDataSource";
|
export * from "./useDataSource";
|
||||||
export * from "./useDataTable";
|
export * from "./useDataTable";
|
||||||
|
export * from "./useDownloader";
|
||||||
export * from "./useLocalization";
|
export * from "./useLocalization";
|
||||||
export * from "./useMediaQuery";
|
export * from "./useMediaQuery";
|
||||||
export * from "./usePagination";
|
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