Add initial support for dynamic-components - loaded from a url - as custom widgets.

This commit is contained in:
2025-05-05 11:34:23 -05:00
parent b279a04b43
commit bb06e2743a
9 changed files with 564 additions and 10 deletions

View File

@ -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)

View File

@ -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] &&
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={(values: { [name: string]: any }) =>
{
if(actionCallback)
{
actionCallback(values)
}
}} />
)
}
@ -800,6 +805,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
<DynamicFormWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} recordValues={convertQRecordValuesFromMapToObject(record)} />
)
}
{
widgetMetaData.type === "customComponent" && (
widgetData && widgetData[i] &&
<Widget widgetMetaData={widgetMetaData}>
<CustomComponentWidget widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} />
</Widget>
)
}
</Box>
);
};

View File

@ -728,7 +728,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
{
needLabelBox &&
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"} className="widgetLabelBox">
<Box display="flex" flexDirection="column">
<Box display="flex" alignItems="baseline">
{

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (<Box sx={widgetMetaData.defaultValues?.get("sx")}>
{hasComponentLoaded(componentName) ? renderComponent(componentName, props) : <Skeleton width="100%" height="100%" />}
</Box>);
}

View File

@ -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,
<Box pt="0.5rem">
<Box display="flex" justifyContent="space-between" alignItems="center">
<h5>{label ?? "Query Filter"}</h5>
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
{!hideSortBy && <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>}
</Box>
{
mayShowQuery() &&

View File

@ -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
</Box>);
};
/***************************************************************************
**
***************************************************************************/
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 //
//////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (<div>Loading...</div>);
}
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 (<MaterialUIControllerProvider>
<ThemeProvider theme={theme}>
<Formik initialValues={initialValues} validationSchema={Yup.object().shape(formValidations)} onSubmit={handleSubmit}>
{({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 (<QDynamicForm formData={formData} record={record} />);
}}
</Formik>
</ThemeProvider>
</MaterialUIControllerProvider>);
}
/***************************************************************************
** 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 (<div>Loading...</div>);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// internally in some widgets, useNavigate happens... so we must re-introduce the browser-router context //
// plus the contexts too, as indicated. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
return (<BrowserRouter>
<MaterialUIControllerProvider>
<ThemeProvider theme={theme}>
<QContext.Provider value={{
...qContext,
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
}}>
<div className={`bridgedWidget ${widgetMetaData.type}`}>
<DashboardWidgets tableName={tableName} widgetMetaDataList={[widgetMetaData]} initialWidgetDataList={[widgetData]} record={record} entityPrimaryKey={entityPrimaryKey} omitWrappingGridContainer={true} actionCallback={actionCallback} />
</div>
</QContext.Provider>
</ThemeProvider>
</MaterialUIControllerProvider>
</BrowserRouter>);
}
/***************************************************************************
** 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 (
<Modal open={isOpen} onClose={(event, reason) => closeModalProcess(event, reason)}>
<Box className="bridgeModal" height="calc(100vh)">
{children}
</Box>
</Modal>
);
}
/***************************************************************************
** 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 (<Alert color={color} onClose={mayManuallyClose ? onClose : null}>{children}</Alert>);
}
else
{
return (<React.Fragment />);
}
}
/***************************************************************************
** define the default qfmd bridge object
***************************************************************************/
export const qfmdBridge =
{
qController: Client.getInstance(),
makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }): JSX.Element =>
{
return (<MDButton {...extra} onClick={onClick} fullWidth>{label}</MDButton>);
},
makeAlert: (text: string, color: string, mayManuallyClose?: boolean): JSX.Element =>
{
return (<QFMDBridgeAlert color={color} mayManuallyClose={mayManuallyClose}>{text}</QFMDBridgeAlert>);
},
makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void): JSX.Element =>
{
return (<QFMDBridgeModal onClose={onClose}>{children}</QFMDBridgeModal>);
},
makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean): JSX.Element =>
{
return (<QFMDBridgeWidget widgetName={widgetName} tableName={tableName} record={record} entityPrimaryKey={entityPrimaryKey} actionCallback={actionCallback} />);
},
makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void): JSX.Element =>
{
return (<QFMDBridgeForm fields={fields} record={record} handleChange={handleChange} handleSubmit={handleSubmit} />);
}
};

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 = () => <Box>Error loading {name}</Box>;
}
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<DynamicComponentProps> = dynamicComponents[name];
return (<C qfmdBridge={qfmdBridge} props={props} />);
}
else
{
return (<Box>Loading...</Box>);
}
};
return {
loadComponent,
hasComponentLoaded,
renderComponent
};
}

View File

@ -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());
};