diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index 40dc159..9dc851c 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -11,6 +11,7 @@ module.exports = { parser: "@typescript-eslint/parser", plugins: ["react-refresh"], rules: { + "@typescript-eslint/no-require-imports": "none", "@typescript-eslint/no-explicit-any": "warn", "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "react/no-unescaped-entities": "off", diff --git a/client/package.json b/client/package.json index f552964..c7a19e0 100644 --- a/client/package.json +++ b/client/package.json @@ -68,7 +68,9 @@ "react-secure-storage": "^1.3.2", "react-toastify": "^10.0.5", "react-wrap-balancer": "^1.1.1", - "recharts": "^2.12.7" + "recharts": "^2.12.7", + "slugify": "^1.6.6", + "use-debounce": "^10.0.3" }, "devDependencies": { "@tanstack/react-query-devtools": "^5.51.23", @@ -82,6 +84,7 @@ "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", + "@welldone-software/why-did-you-render": "^8.0.3", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "eslint": "^8.57.0", diff --git a/client/src/app/quotes/components/DownloadQuoteDialog.tsx b/client/src/app/quotes/components/DownloadQuoteDialog.tsx index 41b2329..5c73688 100644 --- a/client/src/app/quotes/components/DownloadQuoteDialog.tsx +++ b/client/src/app/quotes/components/DownloadQuoteDialog.tsx @@ -11,7 +11,7 @@ import { Progress, } from "@/ui"; import { t } from "i18next"; -import { useEffect } from "react"; +import { useEffect, useId } from "react"; import { UseDownloader } from "react-use-downloader/dist/types"; type DownloadQuoteDialogProps = Omit & { @@ -20,6 +20,7 @@ type DownloadQuoteDialogProps = Omit & { export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => { const { percentage, cancel, error, isInProgress, onFinishDownload } = props; + const panelId = useId(); useEffect(() => { if (!isInProgress && !error && percentage === 100) { @@ -36,7 +37,7 @@ export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => { {t("quotes.downloading_dialog.title")} {t("quotes.downloading_dialog.description")} -
+
- - {quote?.reference} - {quote?.date.toString()} - + {false && ( + + {quote?.reference} + {quote?.date.toString()} + + )} - + - + {/**/} ); }; + +QuotePDFPreview.whyDidYouRender = true; + +export { QuotePDFPreview }; diff --git a/client/src/app/quotes/components/QuotesDataTable.tsx b/client/src/app/quotes/components/QuotesDataTable.tsx index 745d174..f545d79 100644 --- a/client/src/app/quotes/components/QuotesDataTable.tsx +++ b/client/src/app/quotes/components/QuotesDataTable.tsx @@ -28,7 +28,7 @@ import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/cont import { ColumnDef, Row } from "@tanstack/react-table"; import { t } from "i18next"; import { FilePenLineIcon, MoreVerticalIcon } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuotes } from "../hooks"; import { DownloadQuoteDialog } from "./DownloadQuoteDialog"; @@ -43,6 +43,10 @@ export const QuotesDataTable = ({ }) => { const navigate = useNavigate(); const { toast } = useToast(); + + const tableId = useId(); + const previewId = useId(); + const { pagination, globalFilter, isFiltered } = useDataTableContext(); const [activeRow, setActiveRow] = useState | undefined>(undefined); @@ -246,7 +250,12 @@ export const QuotesDataTable = ({ return ( <> - + {preview && } {preview && ( - + )} diff --git a/client/src/app/quotes/hooks/useQuotes.tsx b/client/src/app/quotes/hooks/useQuotes.tsx index da918a7..a51d05a 100644 --- a/client/src/app/quotes/hooks/useQuotes.tsx +++ b/client/src/app/quotes/hooks/useQuotes.tsx @@ -1,5 +1,5 @@ import { useDownloader } from "@/lib/hooks"; -import { UseListQueryResult, useCustom, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; +import { UseListQueryResult, useList, useOne, useSave } from "@/lib/hooks/useDataSource"; import { IFilterItemDataProviderParam, IGetListDataProviderParams, @@ -13,13 +13,12 @@ import { IGetQuote_Response_DTO, IListQuotes_Response_DTO, IListResponse_DTO, - IReportQuote_Response_DTO, IUpdateQuote_Request_DTO, IUpdateQuote_Response_DTO, UniqueID, } from "@shared/contexts"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; +import slugify from "slugify"; export type UseQuotesListParams = Omit & { status?: string; @@ -64,6 +63,23 @@ export const useQuotes = () => { const dataSource = useDataSource(); const keys = useQueryKey(); + const getQuotePDFDownloadURL = useCallback( + (id: string) => `${dataSource.getApiUrl()}/quotes/${id}/report`, + [dataSource] + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const getQuotePDFFilename = useCallback( + (quote: IListQuotes_Response_DTO | IGetQuote_Response_DTO) => + `quote-${slugify(quote.reference, { + lower: true, // Convierte a minúsculas + strict: true, // Elimina caracteres que no son letras o números + locale: "en", // Establece la localización para la conversión + trim: true, // Elimina espacios en blanco al principio y al final + })}.pdf`, + [] + ); + const actions = { useList: (params: UseQuotesListParams): UseQuotesListResponse => { const dataSource = useDataSource(); @@ -142,7 +158,7 @@ export const useQuotes = () => { ...params, }), - useReport2: (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(), @@ -172,12 +188,11 @@ export const useQuotes = () => { }), cancelQuery: () => queryClient.cancelQueries({ queryKey }), }; - }, + },*/ - getQuotePDFDownloadURL: (id: string) => `${dataSource.getApiUrl()}/quotes/${id}/report`, + getQuotePDFDownloadURL, - getQuotePDFFilename: (quote: IListQuotes_Response_DTO | IGetQuote_Response_DTO) => - `filename-quote.pdf`, + getQuotePDFFilename, /*useDownload2: (id?: string, params?: UseQuotesReportParamsType) => { const queryKey = useMemo( @@ -223,32 +238,36 @@ export const useQuotes = () => { useReport: () => { const auth = dataSource.getApiAuthorization(); - const [reportBlob, setReportBlob] = useState(); + const [report, setReport] = useState(undefined); const downloader = useDownloader({ headers: { Authorization: auth, }, - customHandleDownload: (data: Blob) => { - const blobData = [data]; - const blob = new Blob(blobData, { - type: "application/pdf", - }); - setReportBlob(blob); - - return true; - }, + customHandleDownload: useCallback( + (data: Blob) => { + const blobData = [data]; + const blob = new Blob(blobData, { + type: "application/octet-stream", + }); + setReport(blob); + return true; + }, + [setReport] + ), }); - const download = (id: string) => { - const url = actions.getQuotePDFDownloadURL(id); - downloader.download(url, ""); - return reportBlob; - }; + const download = useCallback( + (id: string) => { + return downloader.download(actions.getQuotePDFDownloadURL(id), ""); + }, + [downloader] + ); return { ...downloader, download, + report, }; }, diff --git a/client/src/components/PDFViewer/PDFViewer.tsx b/client/src/components/PDFViewer/PDFViewer.tsx index c6b8447..e2ea801 100644 --- a/client/src/components/PDFViewer/PDFViewer.tsx +++ b/client/src/components/PDFViewer/PDFViewer.tsx @@ -10,23 +10,22 @@ import { LoadingSpinner } from "../LoadingSpinner"; import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "react-pdf/dist/esm/Page/TextLayer.css"; -pdfjs.GlobalWorkerOptions.workerSrc = new URL( +/*pdfjs.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url -).toString(); +).toString();*/ -const options = { - cMapUrl: "/cmaps/", - standardFontDataUrl: "/standard_fonts/", -}; +pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; const maxWidth = 800; const resizeObserverOptions = {}; export interface PDFViewerProps { - file?: { - data: Uint8Array; - }; + file?: + | string + | { + data: Uint8Array; + }; className?: string; } @@ -67,7 +66,9 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { const goToNextPage = useCallback(() => changePage(1), [changePage]); const goToPrevPage = useCallback(() => changePage(-1), [changePage]); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const goToFirstPage = useCallback(() => setPageNumber(1), [setPageNumber]); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const goToLastPage = useCallback(() => setPageNumber(numPages), [setPageNumber, numPages]); const isLoading = useMemo( @@ -75,13 +76,20 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => { [renderedPageNumber, pageNumber] ); + const options = useMemo( + () => ({ + cMapUrl: "/cmaps/", + standardFontDataUrl: "/standard_fonts/", + }), + [] + ); + return (
} diff --git a/client/src/lib/hooks/useDownloader/useDownloader.tsx b/client/src/lib/hooks/useDownloader/useDownloader.tsx index 63dc8d8..8dbdfc5 100644 --- a/client/src/lib/hooks/useDownloader/useDownloader.tsx +++ b/client/src/lib/hooks/useDownloader/useDownloader.tsx @@ -214,39 +214,37 @@ export default function useDownloader({ // 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); + try { + const response = await fetch(downloadUrl, { + method: "GET", + ...options, + ...overrideOptions, + signal: fetchController.signal, }); + + const data = resolverWithProgress(response); + const blob = await data.blob(); + _customHandleDownload(blob, filename); + + clearAllStateCallback(); + } catch (err: unknown) { + clearAllStateCallback(); + setError((prevValue) => { + const { message } = err as Error; + + if (message !== "Failed to fetch") { + return { + errorMessage: message, + }; + } + + return prevValue; + }); + + clearTimeout(timeoutId); + } finally { + clearInterval(intervalId); + } }, [ isInProgress, diff --git a/client/src/main.tsx b/client/src/main.tsx index be3ddaa..d1aeef9 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,3 +1,6 @@ +// eslint-disable-next-line import/order +import "./wdyr"; // <--- always first import + import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; diff --git a/client/src/wdyr.ts b/client/src/wdyr.ts new file mode 100644 index 0000000..cc3420f --- /dev/null +++ b/client/src/wdyr.ts @@ -0,0 +1,13 @@ +/// +import * as React from "react"; + +if (import.meta.env.DEV && import.meta.env.VITE_ENABLE_WHY_DID_YOU_RENDER === "true") { + const { default: wdyr } = await import("@welldone-software/why-did-you-render"); + + wdyr(React, { + include: [/.*/], + exclude: [/^BrowserRouter/, /^Link/, /^Route/], + trackHooks: true, + trackAllPureComponents: true, + }); +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 7acf55b..3657786 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -8,20 +8,10 @@ const require = createRequire(import.meta.url); const pdfjsDistPath = path.dirname(require.resolve("pdfjs-dist/package.json")); const cMapsDir = normalizePath(path.join(pdfjsDistPath, "cmaps")); - -//const require = createRequire(import.meta.url); - -/*const cMapsDir = normalizePath( - path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "cmaps") -); const standardFontsDir = normalizePath( - path.join( - path.dirname(require.resolve("pdfjs-dist/package.json")), - "standard_fonts" - ) -);*/ + path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts") +); -// https://vitejs.dev/config/ export default defineConfig({ plugins: [ react(), @@ -31,6 +21,10 @@ export default defineConfig({ src: cMapsDir, dest: "", }, + { + src: standardFontsDir, + dest: "", + }, ], }), ],