From 5639551dd5b89d29dd00065c0812e2707d960209 Mon Sep 17 00:00:00 2001 From: honzapatCZ Date: Fri, 18 Oct 2024 19:19:18 +0200 Subject: [PATCH] move some misc functions --- Form/withForm.tsx | 4 +- macros/useMethod.macro.d.ts | 5 ++ macros/useMethod.macro.js | 167 ++++++++++++++++++++++++++++++++++++ macros/useQuery.macro.d.ts | 15 ++++ macros/useQuery.macro.js | 134 +++++++++++++++++++++++++++++ misc/downloadBlob.js | 31 +++++++ misc/getDate.ts | 18 ++++ misc/pdf.js | 52 +++++++++++ misc/printPage.js | 24 ++++++ 9 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 macros/useMethod.macro.d.ts create mode 100644 macros/useMethod.macro.js create mode 100644 macros/useQuery.macro.d.ts create mode 100644 macros/useQuery.macro.js create mode 100644 misc/downloadBlob.js create mode 100644 misc/getDate.ts create mode 100644 misc/pdf.js create mode 100644 misc/printPage.js diff --git a/Form/withForm.tsx b/Form/withForm.tsx index 05bdffa..fd49d0d 100644 --- a/Form/withForm.tsx +++ b/Form/withForm.tsx @@ -1,8 +1,8 @@ import { FormikForm } from "@shared/nej-react-utils/Form/FormikForm"; import { NejAccountingApiClient, CompanyResponse, CancelablePromise } from "@services/accounting-api"; import { useApiClient, useCompany } from "@utils/NejManager/NejProvider"; -import useMethod from "../../../utils/useMethod.macro"; -import useQuery from "../../../utils/useQuery.macro"; +import useMethod from "../macros/useMethod.macro"; +import useQuery from "../macros/useQuery.macro"; type FormProps = { refresh: () => void; diff --git a/macros/useMethod.macro.d.ts b/macros/useMethod.macro.d.ts new file mode 100644 index 0000000..b90828b --- /dev/null +++ b/macros/useMethod.macro.d.ts @@ -0,0 +1,5 @@ +import { CancelablePromise, Error } from "src/services/accounting-api"; + +export default function useMethod(method: (...args: Arg) => CancelablePromise, + onDone?: (data: T) => void, + onError?: (error: Error) => void = null): [(...args: Arg) => void, { data: T, loading: boolean, error: Error }]; diff --git a/macros/useMethod.macro.js b/macros/useMethod.macro.js new file mode 100644 index 0000000..c9e8c91 --- /dev/null +++ b/macros/useMethod.macro.js @@ -0,0 +1,167 @@ +const { createMacro, MacroError } = require("babel-plugin-macros") +const { addNamed } = require("@babel/helper-module-imports") +const path = require("path") + +// `createMacro` is simply a function that ensures your macro is only +// called in the context of a babel transpilation and will throw an +// error with a helpful message if someone does not have babel-plugin-macros +// configured correctly +module.exports = createMacro(useMethod) + +function useMethod({ references, state, babel }) { + // state is the second argument you're passed to a visitor in a + // normal babel plugin. `babel` is the `babel-plugin-macros` module. + // do whatever you like to the AST paths you find in `references` + // read more below... + const { default: defaultImport = [] } = references + + defaultImport.forEach(referencePath => { + if (referencePath.parentPath.type != "CallExpression") { + throw new MacroError( + "useQuery should be called with a function", + ) + } + + const ourArguments = referencePath.parentPath.get("arguments") + + if (ourArguments.length < 1) { + throw new MacroError( + "There are not enough arguments", + ) + } + else if (ourArguments.length > 3) { + throw new MacroError( + "There are too many arguments", + ) + } + + if (ourArguments[0].has("callee")) { + throw new MacroError( + "function used in useMethod can not be a function call", + ) + } + else if (ourArguments.length > 1 && !ourArguments[1].isFunction()) { + throw new MacroError( + "useMethods 2nd argument must be a callable function", + ) + } + else if (ourArguments.length > 2 && !ourArguments[2].isFunction()) { + throw new MacroError( + "useMethods 3rd argument must be a callable function", + ) + } + + let declPath = referencePath.parentPath.parentPath; + + let call = ourArguments[0].node + let nop = babel.types.arrowFunctionExpression([babel.types.identifier("e")], babel.types.blockStatement([])) + let onDone = ourArguments[1] ? ourArguments[1].node : nop; + let onError = ourArguments[2] ? ourArguments[2].node : null; + + let parsWithSet = referencePath.parentPath.parentPath.get("id").get("elements") + + if (!parsWithSet[0].isIdentifier()) + throw new MacroError( + "first result of useMethod must be a function name", + ) + let runMethod = parsWithSet[0].node + if (!parsWithSet[1].isObjectPattern()) + throw new MacroError( + "second result of useMethod must be the returned data object", + ) + let pars = parsWithSet[1].node + + let setStateIdentifier = declPath.scope.generateUidIdentifier("setStateInternalMagic") + + let variableArgsId = declPath.scope.generateUidIdentifier("variableArgsInternalMagic") + + let useStateHookImport = addNamed(declPath, "useState", "react"); + let useCallbackHookImport = addNamed(declPath, "useCallback", "react"); + + let servicePath = "./" + path.relative(state.file.opts.filename, path.join(__dirname, "../services/accounting-api")).trim(); + let apiErrorImport = addNamed(declPath, "ApiError", servicePath); + + let nejServicesPath = "./" + path.relative(path.dirname(state.file.opts.filename), path.join(__dirname, "NejManager/NejProvider")).trim(); + let emptyErrorIdentifier = declPath.scope.generateUidIdentifier("emptyErrorInternalMagic") + let onErrorIdentifier = declPath.scope.generateUidIdentifier("onErrorInternalMagic") + let onSuccessIdentifier = declPath.scope.generateUidIdentifier("onSuccessInternalMagic") + let onStartLoadingIdentifier = declPath.scope.generateUidIdentifier("onErrorInternalMagic") + let onStopLoadingIdentifier = declPath.scope.generateUidIdentifier("onErrorInternalMagic") + + let useOnErrorImport = addNamed(declPath, "useOnError", nejServicesPath); + let useOnSuccessImport = addNamed(declPath, "useOnSuccess", nejServicesPath); + let useStartLoadingImport = addNamed(declPath, "useStartLoading", nejServicesPath); + let useStopLoadingImport = addNamed(declPath, "useStopLoading", nejServicesPath); + + let useStateSrc = babel.template("[DATA, CALL_SET] = USE_STATE({data: null, error: null, loading: false})")({ + DATA: pars, + CALL_SET: setStateIdentifier, + USE_STATE: useStateHookImport + }) + + declPath.replaceWith(useStateSrc) + + let src = babel.template(` +const ON_ERROR_IDENTIFIER = USE_ERROR_IMPORT(); +const ON_SUCCESS_IDENTIFIER = USE_SUCCESS_IMPORT(); +const ON_START_LOADING_IDENTIFIER = USE_START_LOADING_IMPORT(); +const ON_STOP_LOADING_IDENTIFIER = USE_STOP_LOADING_IMPORT(); + +function EMPTY_ERROR(x){ + return null +} + +const RUN_NAME = USE_CALLBACK(async (...ARGS) => { + let notifKey = ""; + try{ + notifKey = ON_START_LOADING_IDENTIFIER(); + CALL_SET({ data: null, error: null, loading: true }); + let ret = await IMPORT_NAME(...ARGS); + CALL_SET({ data: ret, error: null, loading: false }); + ON_STOP_LOADING_IDENTIFIER(notifKey); + ON_SUCCESS_IDENTIFIER(); + DONE(ret); + return ret; + } + catch(e){ + if(e instanceof ERROR_CLASS){ + CALL_SET({data: null, error: e.body, loading: false}) + ON_STOP_LOADING_IDENTIFIER(notifKey); + ON_ERROR_IDENTIFIER(e.body); + ERROR(e.body) + return + } + CALL_SET({data: null, error: e, loading: false}) + ON_STOP_LOADING_IDENTIFIER(notifKey); + ON_ERROR_IDENTIFIER(e); + ERROR(e) + return null; + } + }, []) +`)({ + IMPORT_NAME: call, + CALL_SET: setStateIdentifier, + RUN_NAME: runMethod, + ARGS: variableArgsId, + USE_CALLBACK: useCallbackHookImport, + ERROR_CLASS: apiErrorImport, + DONE: onDone, + //if its null create an empty function to call + ERROR: onError != null ? onError : emptyErrorIdentifier, + EMPTY_ERROR: emptyErrorIdentifier, + + USE_ERROR_IMPORT: useOnErrorImport, + USE_SUCCESS_IMPORT: useOnSuccessImport, + USE_START_LOADING_IMPORT: useStartLoadingImport, + USE_STOP_LOADING_IMPORT: useStopLoadingImport, + + ON_SUCCESS_IDENTIFIER: onSuccessIdentifier, + ON_START_LOADING_IDENTIFIER: onStartLoadingIdentifier, + ON_STOP_LOADING_IDENTIFIER: onStopLoadingIdentifier, + ON_ERROR_IDENTIFIER: onErrorIdentifier, + + }) + + declPath.parentPath.insertAfter(src) + }) +} \ No newline at end of file diff --git a/macros/useQuery.macro.d.ts b/macros/useQuery.macro.d.ts new file mode 100644 index 0000000..7ebdcca --- /dev/null +++ b/macros/useQuery.macro.d.ts @@ -0,0 +1,15 @@ +import { CancelablePromise, Error } from "src/services/accounting-api"; + +//default export is a function +/* +export default function useQuery(query: CancelablePromise): [{ + data: T; + loading: boolean; + error: Error; +}, () => void]; +*/ +export default function useQuery(query: CancelablePromise): [{ + data: T; + loading: boolean; + error: Error; +}, () => void]; \ No newline at end of file diff --git a/macros/useQuery.macro.js b/macros/useQuery.macro.js new file mode 100644 index 0000000..a77b702 --- /dev/null +++ b/macros/useQuery.macro.js @@ -0,0 +1,134 @@ +const { createMacro, MacroError } = require("babel-plugin-macros") +const { addNamed } = require("@babel/helper-module-imports") +const path = require("path") +const Babel = require("@babel/core") +// `createMacro` is simply a function that ensures your macro is only +// called in the context of a babel transpilation and will throw an +// error with a helpful message if someone does not have babel-plugin-macros +// configured correctly +module.exports = createMacro(useQuery); + +//jsdoc +/** + * @param {Object} param0 + * @param {import("@babel/types").ArrayExpression} param0.references + * @param {Object} param0.state + * @param {Babel} param0.babel + */ +function useQuery({ references, state, babel }) { + // state is the second argument you're passed to a visitor in a + // normal babel plugin. `babel` is the `babel-plugin-macros` module. + // do whatever you like to the AST paths you find in `references` + // read more below... + const { default: defaultImport = [] } = references + + defaultImport.forEach(referencePath => { + if (referencePath.parentPath.type != "CallExpression") { + throw new MacroError( + "useQuery should be called with a function", + ) + } + + const ourArguments = referencePath.parentPath.get("arguments") + + if (ourArguments.length < 1) { + throw new MacroError( + "There are not enough arguments", + ) + } + else if (ourArguments.length > 1) { + throw new MacroError( + "There are too many arguments", + ) + } + //jsdoc + /** + * @type {import("@babel/types").CallExpression} + * */ + let call = ourArguments[0].node + + + let declPath = referencePath.parentPath.parentPath; + + let setStateIdentifier = declPath.scope.generateUidIdentifier("setStateInternalMagic") + + let refreshFuncName = declPath.scope.generateUidIdentifier("refreshInternalMagic") + + let queryObjName = declPath.scope.generateUidIdentifier("queryInternalMagic"); + + let assignParsPath = referencePath.parentPath.parentPath.get("id"); + let pars = assignParsPath.node + if (!assignParsPath.isObjectPattern()) { + if (!assignParsPath.isArrayPattern() || !assignParsPath.get("elements")[0].isObjectPattern()) + throw new MacroError( + "Return is either just the object or array of object and refresh method", + ) + pars = assignParsPath.get("elements")[0].node + if (assignParsPath.get("elements").length > 1) { + if (!assignParsPath.get("elements")[1].isIdentifier()) + throw new MacroError( + "There is second argument to array return, but its surprisingly not a function identifier", + ) + refreshFuncName = assignParsPath.get("elements")[1].node + } + } + + let useStateHookImport = addNamed(declPath, "useState", "react"); + let useCallbackHookImport = addNamed(declPath, "useCallback", "react"); + let useEffectHookImport = addNamed(declPath, "useEffect", "react"); + + let useRQuery = addNamed(declPath, "useQuery", "@tanstack/react-query"); + + //create a string literal for identification + //reduce the callee's MemberExpression object and property to astring with member.member.member.etc... + var reduceCallee = (callee) => { + if (callee.type == "Identifier") return callee.name + return reduceCallee(callee.object) + "." + reduceCallee(callee.property) + } + let fullCallPathString = reduceCallee(call.callee) + + let servicePath = "./" + path.relative(state.file.opts.filename, path.join(__dirname, "../services/accounting-api")).trim(); + let apiErrorImport = addNamed(declPath, "ApiError", servicePath); + + let useStateSrc = babel.template("[DATA, CALL_SET] = USE_STATE({data: null, error: null, loading: true})")({ + DATA: pars, + CALL_SET: setStateIdentifier, + USE_STATE: useStateHookImport + }) + + declPath.replaceWith(useStateSrc) + + let src = babel.template(` + + const Q_UNAME = USE_RQUERY({queryKey: ["Q_KEY", ...(new Array(DEPS))], queryFn: async () => IMPORT_NAME}) + + USE_EFFECT(() => { + + if(Q_UNAME.error instanceof ERROR_CLASS){ + CALL_SET({...Q_UNAME, error: Q_UNAME.error.body, loading: Q_UNAME.isLoading}) + return + } + CALL_SET({...Q_UNAME, loading: Q_UNAME.isLoading}) + + }, [Q_UNAME.data, Q_UNAME.error, Q_UNAME.isLoading]) + + const REFRESH = USE_CALLBACK(async () =>{ + await Q_UNAME.refetch() + }, [Q_UNAME.data, Q_UNAME.error, Q_UNAME.isLoading]) + +`)({ + IMPORT_NAME: call, + DEPS: call.arguments, + CALL_SET: setStateIdentifier, + USE_EFFECT: useEffectHookImport, + USE_CALLBACK: useCallbackHookImport, + ERROR_CLASS: apiErrorImport, + REFRESH: refreshFuncName, + Q_UNAME: queryObjName, + Q_KEY: fullCallPathString, + USE_RQUERY: useRQuery + }) + + declPath.parentPath.insertAfter(src) + }) +} diff --git a/misc/downloadBlob.js b/misc/downloadBlob.js new file mode 100644 index 0000000..d395d3b --- /dev/null +++ b/misc/downloadBlob.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +export default function downloadAsBlob(data, name) { + // Create blob link to download + const url = window.URL.createObjectURL( + new Blob([data]), + ); + console.log(url); + downloadInteral(url, name); +} + +export async function downloadUrl(url, name){ + let res = await axios.get(url, {responseType: "blob"}); + downloadAsBlob(res.data, name); +} + +function downloadInteral(url, name){ + const link = document.createElement("a"); + link.href = url; + link.download = name; + link.target = "_blank"; + + // Append to html link element page + document.body.appendChild(link); + + // Start download + link.click(); + + // Clean up and remove the link + link.parentNode.removeChild(link); +} \ No newline at end of file diff --git a/misc/getDate.ts b/misc/getDate.ts new file mode 100644 index 0000000..e410c1d --- /dev/null +++ b/misc/getDate.ts @@ -0,0 +1,18 @@ +export default function getInputDate(date: Date | string) { + + if (!date) + return date; + if (typeof (date) === "string") { + date = new Date(date); + } + try { + + return new Date(date.getTime() - new Date().getTimezoneOffset() * 60000).toISOString().substring(0, 19) + + } catch { + + return new Date(new Date().getTimezoneOffset() * 60000).toISOString().substring(0, 19) + + } + +} diff --git a/misc/pdf.js b/misc/pdf.js new file mode 100644 index 0000000..970108c --- /dev/null +++ b/misc/pdf.js @@ -0,0 +1,52 @@ +import { Document, Page } from "react-pdf"; +import "react-pdf/dist/esm/Page/AnnotationLayer.css"; +import 'react-pdf/dist/esm/Page/TextLayer.css'; + +import Pagination from "@shared/nej-react-components/Parts/Pagination"; + +import tw, { styled } from "twin.macro" +import "styled-components/macro" +import { useState } from "react"; + +import { pdfjs } from 'react-pdf'; + +pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url, +).toString(); + +export default function PDF({ data }) { + const [numPages, setNumPages] = useState(null); + const [pageNumber, setPageNumber] = useState(1); + + function onDocumentLoadSuccess({ numPages }) { + setNumPages(numPages); + } + + return ( +
+ + + + + {/* +
+ +
+
+ +
+
+ +
+
+ +
*/} + +

+ Page {pageNumber} of {numPages} +

+ setPageNumber(p)} /> +
+ ); +} \ No newline at end of file diff --git a/misc/printPage.js b/misc/printPage.js new file mode 100644 index 0000000..aa981f3 --- /dev/null +++ b/misc/printPage.js @@ -0,0 +1,24 @@ +function closePrint() { + document.body.removeChild(this.__container__); +} + +function setPrint() { + this.contentWindow.__container__ = this; + this.contentWindow.onbeforeunload = closePrint; + this.contentWindow.onafterprint = closePrint; + this.contentWindow.focus(); // Required for IE + this.contentWindow.print(); +} + +export default function printPage(sURL) { + const hideFrame = document.createElement("iframe"); + hideFrame.onload = setPrint; + hideFrame.style.position = "fixed"; + hideFrame.style.right = "0"; + hideFrame.style.bottom = "0"; + hideFrame.style.width = "0"; + hideFrame.style.height = "0"; + hideFrame.style.border = "0"; + hideFrame.src = sURL; + document.body.appendChild(hideFrame); +} \ No newline at end of file