mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Add initial support for dynamic-components - loaded from a url - as custom widgets.
This commit is contained in:
@ -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)
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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">
|
||||
{
|
||||
|
69
src/qqq/components/widgets/misc/CustomComponentWidget.tsx
Normal file
69
src/qqq/components/widgets/misc/CustomComponentWidget.tsx
Normal 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>);
|
||||
}
|
||||
|
@ -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() &&
|
||||
|
@ -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 //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
328
src/qqq/utils/qqq/QFMDBridge.tsx
Normal file
328
src/qqq/utils/qqq/QFMDBridge.tsx
Normal 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} />);
|
||||
}
|
||||
};
|
||||
|
117
src/qqq/utils/qqq/useDynamicComponents.tsx
Normal file
117
src/qqq/utils/qqq/useDynamicComponents.tsx
Normal 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
|
||||
};
|
||||
}
|
@ -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());
|
||||
};
|
||||
|
Reference in New Issue
Block a user