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 {Auth0Provider} from "@auth0/auth0-react";
|
||||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import App from "App";
|
import App from "App";
|
||||||
@ -33,6 +34,12 @@ import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
|||||||
import {MaterialUIControllerProvider} from "qqq/context";
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
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();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
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 PieChart from "qqq/components/widgets/charts/piechart/PieChart";
|
||||||
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||||
|
import CustomComponentWidget from "qqq/components/widgets/misc/CustomComponentWidget";
|
||||||
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
||||||
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
||||||
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
||||||
@ -781,8 +782,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
{
|
{
|
||||||
widgetMetaData.type === "filterAndColumnsSetup" && (
|
widgetMetaData.type === "filterAndColumnsSetup" && (
|
||||||
widgetData && widgetData[i] &&
|
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)} />
|
<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>
|
</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"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
{
|
{
|
||||||
needLabelBox &&
|
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" flexDirection="column">
|
||||||
<Box display="flex" alignItems="baseline">
|
<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
|
** 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 [modalOpen, setModalOpen] = useState(false);
|
||||||
const [hideColumns] = useState(widgetData?.hideColumns);
|
const [hideColumns] = useState(widgetData?.hideColumns);
|
||||||
const [hidePreview] = useState(widgetData?.hidePreview);
|
const [hidePreview] = useState(widgetData?.hidePreview);
|
||||||
|
const [hideSortBy] = useState(widgetData?.hideSortBy);
|
||||||
|
const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp)
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
|
|
||||||
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
|
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
|
||||||
@ -203,7 +205,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recordValues["tableName"])
|
if (widgetData?.tableName || recordValues["tableName"])
|
||||||
{
|
{
|
||||||
setAlertContent(null);
|
setAlertContent(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@ -364,7 +366,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
<Box pt="0.5rem">
|
<Box pt="0.5rem">
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<h5>{label ?? "Query Filter"}</h5>
|
<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>
|
</Box>
|
||||||
{
|
{
|
||||||
mayShowQuery() &&
|
mayShowQuery() &&
|
||||||
|
@ -71,6 +71,7 @@ import ColumnStats from "qqq/pages/records/query/ColumnStats";
|
|||||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
|
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||||
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||||
import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils";
|
import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
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)}`);
|
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -1723,7 +1724,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
{
|
{
|
||||||
if (selectedSavedViewId != null)
|
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 //
|
// fetch, then activate the selected filter //
|
||||||
@ -1740,7 +1741,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
/////////////////////////////////
|
/////////////////////////////////
|
||||||
// this is 'new view' - right? //
|
// 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 //
|
// wipe away the saved view //
|
||||||
@ -1768,7 +1769,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
if (processResult instanceof QJobError)
|
if (processResult instanceof QJobError)
|
||||||
{
|
{
|
||||||
const jobError = processResult as 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.");
|
setAlertContent("There was an error loading the selected view.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -2438,7 +2439,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
setTableLabel(tableMetaData.label);
|
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
|
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)
|
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>);
|
</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 //
|
// 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("/*api", getRequestHandler());
|
app.use("/*api", getRequestHandler());
|
||||||
app.use("/qqq/*", getRequestHandler());
|
app.use("/qqq/*", getRequestHandler());
|
||||||
|
app.use("/dynamic-qfmd-components/*", getRequestHandler());
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user