diff --git a/src/index.tsx b/src/index.tsx index fb7929a..5bdfc9c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,6 +22,7 @@ import {Auth0Provider} from "@auth0/auth0-react"; import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData"; import React from "react"; +import ReactDOM from "react-dom"; import {createRoot} from "react-dom/client"; import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom"; import App from "App"; @@ -33,6 +34,12 @@ import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute"; import {MaterialUIControllerProvider} from "qqq/context"; import Client from "qqq/utils/qqq/Client"; +///////////////////////////////////////////////////////////////////////////////// +// Expose React and ReactDOM as globals, for use by dynamically loaded modules // +///////////////////////////////////////////////////////////////////////////////// +(window as any).React = React; +(window as any).ReactDOM = ReactDOM; + const qController = Client.getInstance(); if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1) diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index f972904..0d03dd0 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -39,6 +39,7 @@ import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineCha import PieChart from "qqq/components/widgets/charts/piechart/PieChart"; import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart"; import CompositeWidget from "qqq/components/widgets/CompositeWidget"; +import CustomComponentWidget from "qqq/components/widgets/misc/CustomComponentWidget"; import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer"; import DividerWidget from "qqq/components/widgets/misc/Divider"; import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget"; @@ -781,8 +782,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco { widgetMetaData.type === "filterAndColumnsSetup" && ( widgetData && widgetData[i] && - + { + if(actionCallback) + { + actionCallback(values) + } }} /> ) } @@ -800,6 +805,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco ) } + { + widgetMetaData.type === "customComponent" && ( + widgetData && widgetData[i] && + + + + ) + } ); }; diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 22b3ecc..83a66ef 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -728,7 +728,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element { needLabelBox && - + { diff --git a/src/qqq/components/widgets/misc/CustomComponentWidget.tsx b/src/qqq/components/widgets/misc/CustomComponentWidget.tsx new file mode 100644 index 0000000..bf11a2b --- /dev/null +++ b/src/qqq/components/widgets/misc/CustomComponentWidget.tsx @@ -0,0 +1,69 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {Skeleton} from "@mui/material"; +import Box from "@mui/material/Box"; +import useDynamicComponents from "qqq/utils/qqq/useDynamicComponents"; +import {useEffect, useState} from "react"; + + +interface CustomComponentWidgetProps +{ + widgetMetaData: QWidgetMetaData; + widgetData: any; + record: QRecord; +} + + +CustomComponentWidget.defaultProps = { +}; + + +/******************************************************************************* + ** Component to display a custom component - one dynamically loaded. + *******************************************************************************/ +export default function CustomComponentWidget({widgetMetaData, widgetData, record}: CustomComponentWidgetProps): JSX.Element +{ + const [componentName, setComponentName] = useState(widgetMetaData.defaultValues.get("componentName")); + const [componentSourceUrl, setComponentSourceUrl] = useState(widgetMetaData.defaultValues.get("componentSourceUrl")); + + const {loadComponent, hasComponentLoaded, renderComponent} = useDynamicComponents(); + + useEffect(() => + { + loadComponent(componentName, componentSourceUrl); + }, []); + + const props: any = + { + widgetMetaData: widgetMetaData, + widgetData: widgetData, + record: record, + } + + return ( + {hasComponentLoaded(componentName) ? renderComponent(componentName, props) : } + ); +} + diff --git a/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx b/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx index b58da82..590eb58 100644 --- a/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx +++ b/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx @@ -84,11 +84,13 @@ const qController = Client.getInstance(); /******************************************************************************* ** Component for editing the main setup of a report - that is: filter & columns *******************************************************************************/ -export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element +export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element { const [modalOpen, setModalOpen] = useState(false); const [hideColumns] = useState(widgetData?.hideColumns); const [hidePreview] = useState(widgetData?.hidePreview); + const [hideSortBy] = useState(widgetData?.hideSortBy); + const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp) const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson"); @@ -203,7 +205,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, return; } - if (recordValues["tableName"]) + if (widgetData?.tableName || recordValues["tableName"]) { setAlertContent(null); setModalOpen(true); @@ -364,7 +366,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
{label ?? "Query Filter"}
- {mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)} + {!hideSortBy && {mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}}
{ mayShowQuery() && diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index b84a714..3a2a594 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -71,6 +71,7 @@ import ColumnStats from "qqq/pages/records/query/ColumnStats"; import DataGridUtils from "qqq/utils/DataGridUtils"; import Client from "qqq/utils/qqq/Client"; import FilterUtils from "qqq/utils/qqq/FilterUtils"; +import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils"; import TableUtils from "qqq/utils/qqq/TableUtils"; @@ -933,7 +934,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable } } - recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label}); + doRecordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label}); console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`); setLoading(true); @@ -1723,7 +1724,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable { if (selectedSavedViewId != null) { - recordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label}); + doRecordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label}); ////////////////////////////////////////////// // fetch, then activate the selected filter // @@ -1740,7 +1741,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable ///////////////////////////////// // this is 'new view' - right? // ///////////////////////////////// - recordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label}); + doRecordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label}); ////////////////////////////// // wipe away the saved view // @@ -1768,7 +1769,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable if (processResult instanceof QJobError) { const jobError = processResult as QJobError; - console.error("Could not retrieve saved filter: " + jobError.userFacingError); + console.error("Could not retrieve saved view: " + jobError.userFacingError); setAlertContent("There was an error loading the selected view."); } else @@ -2438,7 +2439,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable setTableMetaData(tableMetaData); setTableLabel(tableMetaData.label); - recordAnalytics({location: window.location, title: "Query: " + tableMetaData.label}); + doRecordAnalytics({location: window.location, title: "Query: " + tableMetaData.label}); setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks) @@ -2815,6 +2816,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
); }; + + /*************************************************************************** + ** + ***************************************************************************/ + function doRecordAnalytics(model: AnalyticsModel) + { + try + { + recordAnalytics(model); + } + catch (e) + { + console.log(`Error recording analytics: ${e}`); + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////// // these numbers help set the height of the grid (so page won't scroll) based on space above & below it // ////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/qqq/utils/qqq/QFMDBridge.tsx b/src/qqq/utils/qqq/QFMDBridge.tsx new file mode 100644 index 0000000..a3f2447 --- /dev/null +++ b/src/qqq/utils/qqq/QFMDBridge.tsx @@ -0,0 +1,328 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {Alert} from "@mui/material"; +import Box from "@mui/material/Box"; +import Modal from "@mui/material/Modal"; +import {ThemeProvider} from "@mui/material/styles"; +import {Formik} from "formik"; +import QContext from "QContext"; +import QDynamicForm from "qqq/components/forms/DynamicForm"; +import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; +import MDButton from "qqq/components/legacy/MDButton"; +import theme from "qqq/components/legacy/Theme"; +import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; +import {MaterialUIControllerProvider} from "qqq/context"; +import Client from "qqq/utils/qqq/Client"; +import React, {ReactElement, ReactNode, useContext, useEffect, useState} from "react"; +import {BrowserRouter} from "react-router-dom"; +import * as Yup from "yup"; + + +// todo - deploy this interface somehow out of this file +export interface QFMDBridge +{ + qController?: QController; + makeAlert: (text: string, color: string) => JSX.Element; + makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }) => JSX.Element; + makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void) => JSX.Element; + makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void) => JSX.Element; + makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean) => JSX.Element; +} + + +/*************************************************************************** + ** Component to generate a form for the QFMD Bridge + ***************************************************************************/ +interface QFMDBridgeFormProps +{ + fields: QFieldMetaData[], + record: QRecord, + handleChange: (fieldName: string, newValue: any) => void, + handleSubmit: (values: any) => void +} + +QFMDBridgeForm.defaultProps = {}; + +function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridgeFormProps): JSX.Element +{ + const initialValues: any = {}; + for (let field of fields) + { + initialValues[field.name] = record.values.get(field.name); + } + const [lastValues, setLastValues] = useState(initialValues); + const [loaded, setLoaded] = useState(false) + + useEffect(() => + { + (async () => + { + const qController = Client.getInstance(); + + for (let field of fields) + { + const value = record.values.get(field.name); + if (field.possibleValueSourceName && value) + { + const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], record.values, "form"); + if (possibleValues && possibleValues.length > 0) + { + record.displayValues.set(field.name, possibleValues[0].label); + } + } + } + + setLoaded(true); + })(); + }, []); + + if (!loaded) + { + return (
Loading...
); + } + + const { + dynamicFormFields, + formValidations, + } = DynamicFormUtils.getFormData(fields); + DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, record ? record.displayValues : new Map()); + + ///////////////////////////////////////////////////////////////////////////////// + // re-introduce these two context providers, in case the child calls this // + // method under a different root... maybe this should be optional per a param? // + ///////////////////////////////////////////////////////////////////////////////// + return ( + + + {({values, errors, touched}) => + { + const formData: any = {}; + formData.values = values; + formData.touched = touched; + formData.errors = errors; + formData.formFields = dynamicFormFields; + + try + { + let anyDiffs = false; + for (let fieldName in values) + { + const value = values[fieldName]; + if (lastValues[fieldName] != value) + { + handleChange(fieldName, value); + lastValues[fieldName] = value; + anyDiffs = true; + } + } + + if (anyDiffs) + { + setLastValues(lastValues); + } + } + catch (e) + { + console.error(e); + } + + return (); + }} + + + ); +} + + +/*************************************************************************** + ** Component to render a widget for the QFMD Bridge + ***************************************************************************/ +interface QFMDBridgeWidgetProps +{ + widgetName?: string, + tableName?: string, + record?: QRecord, + entityPrimaryKey?: string, + actionCallback?: (data: any, eventValues?: { [p: string]: any }) => boolean +} + +QFMDBridgeWidget.defaultProps = {}; + +function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, actionCallback}: QFMDBridgeWidgetProps): JSX.Element +{ + const qContext = useContext(QContext); + + const [ready, setReady] = useState(false); + + const [widgetMetaData, setWidgetMetaData] = useState(null as QWidgetMetaData); + const [widgetData, setWidgetData] = useState(null as any); + + const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); + + useEffect(() => + { + (async () => + { + const qController = Client.getInstance(); + const qInstance = await qController.loadMetaData(); + + setWidgetMetaData(qInstance.widgets.get(widgetName)); + setWidgetData(await qController.widget(widgetName, null)); // todo queryParams... ? + + setReady(true); + })(); + }, []); + + if (!ready) + { + return (
Loading...
); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // internally in some widgets, useNavigate happens... so we must re-introduce the browser-router context // + // plus the contexts too, as indicated. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + return ( + + + setTableMetaData(tableMetaData), + }}> +
+ +
+
+
+
+
); +} + + +/*************************************************************************** + ** Component to render a modal for the QFMD Bridge + ***************************************************************************/ +interface QFMDBridgeModalProps +{ + children: ReactNode; + onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void; +} + +QFMDBridgeModal.defaultProps = {}; + +function QFMDBridgeModal({children, onClose}: QFMDBridgeModalProps): JSX.Element +{ + const [isOpen, setIsOpen] = useState(true); + + function closeModalProcess(event: {}, reason: "backdropClick" | "escapeKeyDown") + { + if (onClose) + { + onClose(setIsOpen, event, reason); + } + else + { + setIsOpen(false); + } + } + + return ( + closeModalProcess(event, reason)}> + + {children} + + + ); +} + + +/*************************************************************************** + ** Component to render an alert for the QFMD Bridge + ***************************************************************************/ +interface QFMDBridgeAlertProps +{ + color: string, + children: ReactNode, + mayManuallyClose?: boolean +} + +QFMDBridgeAlert.defaultProps = {}; + +function QFMDBridgeAlert({color, children, mayManuallyClose}: QFMDBridgeAlertProps): JSX.Element +{ + const [isOpen, setIsOpen] = useState(true); + + function onClose() + { + setIsOpen(false); + } + + if (isOpen) + { + //@ts-ignore color + return ({children}); + } + else + { + return (); + } +} + + +/*************************************************************************** + ** define the default qfmd bridge object + ***************************************************************************/ +export const qfmdBridge = + { + qController: Client.getInstance(), + + makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }): JSX.Element => + { + return ({label}); + }, + + makeAlert: (text: string, color: string, mayManuallyClose?: boolean): JSX.Element => + { + return ({text}); + }, + + makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void): JSX.Element => + { + return ({children}); + }, + + makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean): JSX.Element => + { + return (); + }, + + makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void): JSX.Element => + { + return (); + } + }; + diff --git a/src/qqq/utils/qqq/useDynamicComponents.tsx b/src/qqq/utils/qqq/useDynamicComponents.tsx new file mode 100644 index 0000000..44c0a8d --- /dev/null +++ b/src/qqq/utils/qqq/useDynamicComponents.tsx @@ -0,0 +1,117 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Box from "@mui/material/Box"; +import {qfmdBridge, QFMDBridge} from "qqq/utils/qqq/QFMDBridge"; +import React, {useState} from "react"; + +// todo - deploy from here!! +interface DynamicComponentProps +{ + qfmdBridge?: QFMDBridge; + props?: any; +} + + +/******************************************************************************* + ** hook for working with Dynamically loaded components + ** + *******************************************************************************/ +export default function useDynamicComponents() +{ + const [dynamicComponents, setDynamicComponents] = useState<{ [name: string]: React.FC }>({}); + + /******************************************************************************* + ** + *******************************************************************************/ + const loadComponent = async (name: string, url: string) => + { + try + { + await new Promise((resolve, reject) => + { + //////////////////////////////////////////////////////// + // Dynamically load the bundle by adding a script tag // + //////////////////////////////////////////////////////// + const script = document.createElement("script"); + script.src = url; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + catch (e) + { + //////////////////////////////////////////////// + // if the script can't be loaded log an error // + //////////////////////////////////////////////// + console.error(`Error loading bundle from [${url}]`); + } + + /////////////////////////////////////////////////////////////////////////////// + // Assuming the bundle attaches itself to window.${name} (.${name} again...) // + // (Note: if exported as UMD, you might need to access the default export) // + /////////////////////////////////////////////////////////////////////////////// + let component = (window as any)[name]?.[name]; + if (!component) + { + console.error(`Component not found on window.${name}`); + component = () => Error loading {name}; + } + + const newDCs = Object.assign({}, dynamicComponents); + newDCs[name] = component; + setDynamicComponents(newDCs); + }; + + + /*************************************************************************** + ** + ***************************************************************************/ + const hasComponentLoaded = (name: string): boolean => + { + return (!!dynamicComponents[name]); + }; + + + /*************************************************************************** + ** + ***************************************************************************/ + const renderComponent = (name: string, props?: any): JSX.Element => + { + if (dynamicComponents[name]) + { + const C: React.FC = dynamicComponents[name]; + return (); + } + else + { + return (Loading...); + } + }; + + + return { + loadComponent, + hasComponentLoaded, + renderComponent + }; +} \ No newline at end of file diff --git a/src/setupProxy.js b/src/setupProxy.js index ac29bd1..714c072 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -58,4 +58,5 @@ module.exports = function (app) app.use("/api*", getRequestHandler()); app.use("/*api", getRequestHandler()); app.use("/qqq/*", getRequestHandler()); + app.use("/dynamic-qfmd-components/*", getRequestHandler()); };