diff --git a/package.json b/package.json
index 0285de9..4864fa1 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
- "@kingsrook/qqq-frontend-core": "1.0.122",
+ "@kingsrook/qqq-frontend-core": "1.0.123",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
diff --git a/pom.xml b/pom.xml
index 5b2eac0..72a02ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,7 +66,13 @@
com.kingsrook.qqq
qqq-backend-core
- 0.25.0-integration-sprint-62-20250307-205536
+ 0.26.0-integration-20250529-234230
+
+
+ com.kingsrook.qqq
+ qqq-middleware-javalin
+ true
+ 0.26.0-integration-20250529-234230
org.slf4j
diff --git a/src/App.tsx b/src/App.tsx
index ad58422..7eb9507 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -377,23 +377,57 @@ export default function App({authenticationMetaData}: Props)
});
});
- const runRecordScriptProcess = metaData.processes.get("runRecordScript");
- if (runRecordScriptProcess)
+ const materialDashboardInstanceMetaData = metaData.supplementalInstanceMetaData?.get("materialDashboard");
+ if (materialDashboardInstanceMetaData)
{
- const process = runRecordScriptProcess;
- routeList.push({
- name: process.label,
- key: process.name,
- route: `${path}/${process.name}`,
- component: ,
- });
+ const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
+ if (processNamesToAddToAllQueryAndViewScreens)
+ {
+ for (let processName of processNamesToAddToAllQueryAndViewScreens)
+ {
+ const process = metaData.processes.get(processName);
+ if (process)
+ {
+ routeList.push({
+ name: process.label,
+ key: process.name,
+ route: `${path}/${process.name}`,
+ component: ,
+ });
- routeList.push({
- name: process.label,
- key: `${app.name}/${process.name}`,
- route: `${path}/:id/${process.name}`,
- component: ,
- });
+ routeList.push({
+ name: process.label,
+ key: `${app.name}/${process.name}`,
+ route: `${path}/:id/${process.name}`,
+ component: ,
+ });
+ }
+ }
+ }
+ }
+ else
+ {
+ ////////////////
+ // deprecated //
+ ////////////////
+ const runRecordScriptProcess = metaData.processes.get("runRecordScript");
+ if (runRecordScriptProcess)
+ {
+ const process = runRecordScriptProcess;
+ routeList.push({
+ name: process.label,
+ key: process.name,
+ route: `${path}/${process.name}`,
+ component: ,
+ });
+
+ routeList.push({
+ name: process.label,
+ key: `${app.name}/${process.name}`,
+ route: `${path}/:id/${process.name}`,
+ component: ,
+ });
+ }
}
const reportsForTable = ProcessUtils.getReportsForTable(metaData, table.name, true);
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInput.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInput.java
new file mode 100644
index 0000000..9c0f600
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInput.java
@@ -0,0 +1,164 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
+
+
+import java.io.Serializable;
+import java.util.Map;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class FormAdjusterInput
+{
+ private String event;
+ private String fieldName;
+
+ private Serializable newValue;
+ private Map allValues;
+
+
+
+ /*******************************************************************************
+ ** Getter for event
+ *******************************************************************************/
+ public String getEvent()
+ {
+ return (this.event);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for event
+ *******************************************************************************/
+ public void setEvent(String event)
+ {
+ this.event = event;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for event
+ *******************************************************************************/
+ public FormAdjusterInput withEvent(String event)
+ {
+ this.event = event;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return (this.fieldName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ *******************************************************************************/
+ public FormAdjusterInput withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for newValue
+ *******************************************************************************/
+ public Serializable getNewValue()
+ {
+ return (this.newValue);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for newValue
+ *******************************************************************************/
+ public void setNewValue(Serializable newValue)
+ {
+ this.newValue = newValue;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for newValue
+ *******************************************************************************/
+ public FormAdjusterInput withNewValue(Serializable newValue)
+ {
+ this.newValue = newValue;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for allValues
+ *******************************************************************************/
+ public Map getAllValues()
+ {
+ return (this.allValues);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for allValues
+ *******************************************************************************/
+ public void setAllValues(Map allValues)
+ {
+ this.allValues = allValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for allValues
+ *******************************************************************************/
+ public FormAdjusterInput withAllValues(Map allValues)
+ {
+ this.allValues = allValues;
+ return (this);
+ }
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInterface.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInterface.java
new file mode 100644
index 0000000..f3210bb
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterInterface.java
@@ -0,0 +1,39 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+
+
+/*******************************************************************************
+ ** interface to be implemented by application-specific form-adjusters
+ *******************************************************************************/
+public interface FormAdjusterInterface
+{
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ FormAdjusterOutput execute(FormAdjusterInput input) throws QException;
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterOutput.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterOutput.java
new file mode 100644
index 0000000..30fbe48
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterOutput.java
@@ -0,0 +1,165 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
+
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class FormAdjusterOutput
+{
+ private Map updatedFieldMetaData = null;
+ private Map updatedFieldValues = null;
+ private Map updatedFieldDisplayValues = null;
+ private Set fieldsToClear = null;
+
+
+
+ /*******************************************************************************
+ ** Getter for updatedFieldValues
+ *******************************************************************************/
+ public Map getUpdatedFieldValues()
+ {
+ return (this.updatedFieldValues);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFieldValues
+ *******************************************************************************/
+ public void setUpdatedFieldValues(Map updatedFieldValues)
+ {
+ this.updatedFieldValues = updatedFieldValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFieldValues
+ *******************************************************************************/
+ public FormAdjusterOutput withUpdatedFieldValues(Map updatedFieldValues)
+ {
+ this.updatedFieldValues = updatedFieldValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldsToClear
+ *******************************************************************************/
+ public Set getFieldsToClear()
+ {
+ return (this.fieldsToClear);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldsToClear
+ *******************************************************************************/
+ public void setFieldsToClear(Set fieldsToClear)
+ {
+ this.fieldsToClear = fieldsToClear;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldsToClear
+ *******************************************************************************/
+ public FormAdjusterOutput withFieldsToClear(Set fieldsToClear)
+ {
+ this.fieldsToClear = fieldsToClear;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for updatedFieldMetaData
+ *******************************************************************************/
+ public Map getUpdatedFieldMetaData()
+ {
+ return (this.updatedFieldMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFieldMetaData
+ *******************************************************************************/
+ public void setUpdatedFieldMetaData(Map updatedFieldMetaData)
+ {
+ this.updatedFieldMetaData = updatedFieldMetaData;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFieldMetaData
+ *******************************************************************************/
+ public FormAdjusterOutput withUpdatedFieldMetaData(Map updatedFieldMetaData)
+ {
+ this.updatedFieldMetaData = updatedFieldMetaData;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for updatedFieldDisplayValues
+ *******************************************************************************/
+ public Map getUpdatedFieldDisplayValues()
+ {
+ return (this.updatedFieldDisplayValues);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for updatedFieldDisplayValues
+ *******************************************************************************/
+ public void setUpdatedFieldDisplayValues(Map updatedFieldDisplayValues)
+ {
+ this.updatedFieldDisplayValues = updatedFieldDisplayValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for updatedFieldDisplayValues
+ *******************************************************************************/
+ public FormAdjusterOutput withUpdatedFieldDisplayValues(Map updatedFieldDisplayValues)
+ {
+ this.updatedFieldDisplayValues = updatedFieldDisplayValues;
+ return (this);
+ }
+
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterRegistry.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterRegistry.java
new file mode 100644
index 0000000..7d60430
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/FormAdjusterRegistry.java
@@ -0,0 +1,149 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
+
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
+import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.MaterialDashboardFieldMetaData;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+
+
+/*******************************************************************************
+ ** Class that stores code-references for the application's defined fromAdjusters
+ ** This class also, when registering its first formAdjuster, adds the route to
+ ** the javalin instance to service form-adjuster calls from the frontend.
+ *******************************************************************************/
+public class FormAdjusterRegistry
+{
+ private static final QLogger LOG = QLogger.getLogger(FormAdjusterRegistry.class);
+
+ private static boolean didRegisterRouteProvider = false;
+ private static QInstance lastRegisteredQInstance = null;
+
+ private static Map onChangeAdjusters = new HashMap<>();
+ private static Map onLoadAdjusters = new HashMap<>();
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static void registerFormAdjusters(QInstance qInstance, MaterialDashboardFieldMetaData materialDashboardFieldMetaData) throws QException
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // support hot-swaps, by checking if the input qInstance is different from one we previously registered for //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(didRegisterRouteProvider && lastRegisteredQInstance != qInstance)
+ {
+ didRegisterRouteProvider = false;
+ onChangeAdjusters.clear();
+ onLoadAdjusters.clear();
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // if we need to register the javalin router, do so (only once per qInstance) //
+ // note, javalin is optional dep, so make sure it's available before try to use it //
+ /////////////////////////////////////////////////////////////////////////////////////
+ if(!didRegisterRouteProvider)
+ {
+ if(ClassPathUtils.isClassAvailable(QJavalinMetaData.class.getName()))
+ {
+ QJavalinMetaData javalinMetaData = QJavalinMetaData.ofOrWithNew(qInstance);
+ javalinMetaData.withRouteProvider(new JavalinRouteProviderMetaData()
+ .withHostedPath("/material-dashboard-backend/form-adjuster/{identifier}/{event}")
+ .withMethods(List.of("POST"))
+ .withProcessName(RunFormAdjusterProcess.NAME)
+ );
+
+ qInstance.add(new RunFormAdjusterProcess().produce(qInstance));
+ }
+
+ didRegisterRouteProvider = true;
+ lastRegisteredQInstance = qInstance;
+ }
+
+ ////////////////////////////////////////////////////////////////
+ // add the code-references to the map of registered adjusters //
+ ////////////////////////////////////////////////////////////////
+ String identifier = materialDashboardFieldMetaData.getFormAdjusterIdentifier();
+
+ QCodeReference onChangeCode = materialDashboardFieldMetaData.getOnChangeFormAdjuster();
+ if(onChangeCode != null)
+ {
+ if(onChangeAdjusters.containsKey(identifier))
+ {
+ LOG.warn("Attempt to register more than one onChangeFormAdjuster with identifier: " + identifier);
+ }
+ onChangeAdjusters.put(identifier, onChangeCode);
+ }
+
+ QCodeReference onLoadCode = materialDashboardFieldMetaData.getOnLoadFormAdjuster();
+ if(onLoadCode != null)
+ {
+ if(onLoadAdjusters.containsKey(identifier))
+ {
+ LOG.warn("Attempt to register more than one onLoadFormAdjuster with identifier: " + identifier);
+ }
+ onLoadAdjusters.put(identifier, onLoadCode);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static FormAdjusterInterface getOnChangeAdjuster(String identifier)
+ {
+ QCodeReference codeReference = onChangeAdjusters.get(identifier);
+ if(codeReference != null)
+ {
+ return QCodeLoader.getAdHoc(FormAdjusterInterface.class, codeReference);
+ }
+ return (null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static FormAdjusterInterface getOnLoadAdjuster(String identifier)
+ {
+ QCodeReference codeReference = onLoadAdjusters.get(identifier);
+ if(codeReference != null)
+ {
+ return QCodeLoader.getAdHoc(FormAdjusterInterface.class, codeReference);
+ }
+ return (null);
+ }
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/RunFormAdjusterProcess.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/RunFormAdjusterProcess.java
new file mode 100644
index 0000000..3f2f83d
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/actions/formadjuster/RunFormAdjusterProcess.java
@@ -0,0 +1,120 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
+
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Map;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** process that looks up a form adjuster from the registry, and then runs it
+ *******************************************************************************/
+public class RunFormAdjusterProcess implements BackendStep, MetaDataProducerInterface
+{
+ public static final String NAME = "MaterialDashboardRunFormAdjusterProcess";
+
+ private static final QLogger LOG = QLogger.getLogger(RunFormAdjusterProcess.class);
+
+ public static final String EVENT_ON_LOAD = "onLoad";
+ public static final String EVENT_ON_CHANGE = "onChange";
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ return new QProcessMetaData()
+ .withName(NAME)
+ .withStep(new QBackendStepMetaData()
+ .withName("execute")
+ .withCode(new QCodeReference(getClass())));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ ProcessBasedRouterPayload payload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class);
+
+ String identifier = payload.getPathParams().get("identifier");
+ String event = payload.getPathParams().get("event");
+
+ try
+ {
+ FormAdjusterInterface formAdjuster = switch(event)
+ {
+ case EVENT_ON_CHANGE -> FormAdjusterRegistry.getOnChangeAdjuster(identifier);
+ case EVENT_ON_LOAD -> FormAdjusterRegistry.getOnLoadAdjuster(identifier);
+ default -> throw new QException("Unknown event type: " + event);
+ };
+
+ if(formAdjuster == null)
+ {
+ throw new QException("No form adjuster found for identifier: " + identifier + " and event: " + event);
+ }
+
+ FormAdjusterInput input = new FormAdjusterInput();
+ input.setEvent(event);
+ input.setFieldName(payload.getFormParam("fieldName"));
+ input.setNewValue(payload.getFormParam("newValue"));
+
+ String allValuesJson = payload.getFormParam("allValues");
+ Map allValues = StringUtils.hasContent(allValuesJson) ? JsonUtils.toObject(allValuesJson, new TypeReference<>() {}) : Collections.emptyMap();
+ input.setAllValues(allValues);
+
+ FormAdjusterOutput output = formAdjuster.execute(input);
+
+ payload.setResponseString(JsonUtils.toJson(output));
+ runBackendStepOutput.setProcessPayload(payload);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error running form adjuster process", e, logPair("identifier", identifier), logPair("event", event));
+ throw new QException("Error running form adjuster process: " + e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardFieldMetaData.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardFieldMetaData.java
new file mode 100644
index 0000000..09f19ac
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardFieldMetaData.java
@@ -0,0 +1,244 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
+
+
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterInterface;
+import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterRegistry;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MaterialDashboardFieldMetaData extends QSupplementalFieldMetaData
+{
+ public static final String TYPE = "materialDashboard";
+
+ private static final QLogger LOG = QLogger.getLogger(MaterialDashboardFieldMetaData.class);
+
+ private String formAdjusterIdentifier = null;
+ private QCodeReference onChangeFormAdjuster = null;
+ private QCodeReference onLoadFormAdjuster = null;
+ private Set fieldsToDisableWhileRunningAdjusters = null;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean includeInFrontendMetaData()
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getType()
+ {
+ return TYPE;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for onChangeFormAdjuster
+ *******************************************************************************/
+ public QCodeReference getOnChangeFormAdjuster()
+ {
+ return (this.onChangeFormAdjuster);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for onChangeFormAdjuster
+ *******************************************************************************/
+ public void setOnChangeFormAdjuster(QCodeReference onChangeFormAdjuster)
+ {
+ this.onChangeFormAdjuster = onChangeFormAdjuster;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for onChangeFormAdjuster
+ *******************************************************************************/
+ public MaterialDashboardFieldMetaData withOnChangeFormAdjuster(QCodeReference onChangeFormAdjuster)
+ {
+ this.onChangeFormAdjuster = onChangeFormAdjuster;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for onLoadFormAdjuster
+ *******************************************************************************/
+ public QCodeReference getOnLoadFormAdjuster()
+ {
+ return (this.onLoadFormAdjuster);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for onLoadFormAdjuster
+ *******************************************************************************/
+ public void setOnLoadFormAdjuster(QCodeReference onLoadFormAdjuster)
+ {
+ this.onLoadFormAdjuster = onLoadFormAdjuster;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for onLoadFormAdjuster
+ *******************************************************************************/
+ public MaterialDashboardFieldMetaData withOnLoadFormAdjuster(QCodeReference onLoadFormAdjuster)
+ {
+ this.onLoadFormAdjuster = onLoadFormAdjuster;
+ return (this);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void enrich(QInstance qInstance, QFieldMetaData fieldMetaData)
+ {
+ try
+ {
+ FormAdjusterRegistry.registerFormAdjusters(qInstance, this);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error enriching MaterialDashboardFieldMetaData", e);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void validate(QInstance qInstance, QFieldMetaData fieldMetaData, QInstanceValidator qInstanceValidator)
+ {
+ String prefix = "MaterialDashboardFieldMetaData for field [" + fieldMetaData.getName() + "]";
+
+ boolean needsFormAdjusterIdentifer = false;
+ if(onChangeFormAdjuster != null)
+ {
+ needsFormAdjusterIdentifer = true;
+ qInstanceValidator.validateSimpleCodeReference(prefix + ", onChangeFormAdjuster", onChangeFormAdjuster, FormAdjusterInterface.class);
+ }
+
+ if(onLoadFormAdjuster != null)
+ {
+ needsFormAdjusterIdentifer = true;
+ qInstanceValidator.validateSimpleCodeReference(prefix + ", onLoadFormAdjuster", onLoadFormAdjuster, FormAdjusterInterface.class);
+ }
+
+ if(needsFormAdjusterIdentifer)
+ {
+ qInstanceValidator.assertCondition(StringUtils.hasContent(formAdjusterIdentifier), prefix + ", formAdjusterIdentifier is required if using any FormAdjusters");
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for formAdjusterIdentifier
+ *******************************************************************************/
+ public String getFormAdjusterIdentifier()
+ {
+ return (this.formAdjusterIdentifier);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for formAdjusterIdentifier
+ *******************************************************************************/
+ public void setFormAdjusterIdentifier(String formAdjusterIdentifier)
+ {
+ this.formAdjusterIdentifier = formAdjusterIdentifier;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for formAdjusterIdentifier
+ *******************************************************************************/
+ public MaterialDashboardFieldMetaData withFormAdjusterIdentifier(String formAdjusterIdentifier)
+ {
+ this.formAdjusterIdentifier = formAdjusterIdentifier;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for fieldsToDisableWhileRunningAdjusters
+ *******************************************************************************/
+ public Set getFieldsToDisableWhileRunningAdjusters()
+ {
+ return (this.fieldsToDisableWhileRunningAdjusters);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldsToDisableWhileRunningAdjusters
+ *******************************************************************************/
+ public void setFieldsToDisableWhileRunningAdjusters(Set fieldsToDisableWhileRunningAdjusters)
+ {
+ this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldsToDisableWhileRunningAdjusters
+ *******************************************************************************/
+ public MaterialDashboardFieldMetaData withFieldsToDisableWhileRunningAdjusters(Set fieldsToDisableWhileRunningAdjusters)
+ {
+ this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
+ return (this);
+ }
+
+
+}
diff --git a/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardInstanceMetaData.java b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardInstanceMetaData.java
new file mode 100644
index 0000000..8c4906d
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/frontend/materialdashboard/model/metadata/MaterialDashboardInstanceMetaData.java
@@ -0,0 +1,113 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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 .
+ */
+
+package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
+
+
+/*******************************************************************************
+ ** table-level meta-data for this module (handled as QSupplementalTableMetaData)
+ *******************************************************************************/
+public class MaterialDashboardInstanceMetaData implements QSupplementalInstanceMetaData
+{
+ public static final String TYPE = "materialDashboard";
+
+ private List processNamesToAddToAllQueryAndViewScreens = new ArrayList<>();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getName()
+ {
+ return (TYPE);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static MaterialDashboardInstanceMetaData ofOrWithNew(QInstance qInstance)
+ {
+ MaterialDashboardInstanceMetaData supplementalMetaData = (MaterialDashboardInstanceMetaData) qInstance.getSupplementalMetaData(TYPE);
+ if(supplementalMetaData == null)
+ {
+ supplementalMetaData = new MaterialDashboardInstanceMetaData();
+ qInstance.withSupplementalMetaData(supplementalMetaData);
+ }
+
+ return (supplementalMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processNamesToAddToAllQueryAndViewScreens
+ *******************************************************************************/
+ public List getProcessNamesToAddToAllQueryAndViewScreens()
+ {
+ return (this.processNamesToAddToAllQueryAndViewScreens);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addProcessNameToAddToAllQueryAndViewScreens(String processNamesToAddToAllQueryAndViewScreens)
+ {
+ if(this.processNamesToAddToAllQueryAndViewScreens == null)
+ {
+ this.processNamesToAddToAllQueryAndViewScreens = new ArrayList<>();
+ }
+ this.processNamesToAddToAllQueryAndViewScreens.add(processNamesToAddToAllQueryAndViewScreens);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processNamesToAddToAllQueryAndViewScreens
+ *******************************************************************************/
+ public void setProcessNamesToAddToAllQueryAndViewScreens(List processNamesToAddToAllQueryAndViewScreens)
+ {
+ this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processNamesToAddToAllQueryAndViewScreens
+ *******************************************************************************/
+ public MaterialDashboardInstanceMetaData withProcessNamesToAddToAllQueryAndViewScreens(List processNamesToAddToAllQueryAndViewScreens)
+ {
+ this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
+ return (this);
+ }
+
+}
diff --git a/src/qqq/authorization/anonymous/useAnonymousAuthenticationModule.tsx b/src/qqq/authorization/anonymous/useAnonymousAuthenticationModule.tsx
index 9511ece..61f2f50 100644
--- a/src/qqq/authorization/anonymous/useAnonymousAuthenticationModule.tsx
+++ b/src/qqq/authorization/anonymous/useAnonymousAuthenticationModule.tsx
@@ -48,7 +48,7 @@ export default function useAnonymousAuthenticationModule({setIsFullyAuthenticate
{
console.log("Generating random token...");
setIsFullyAuthenticated(true);
- qController.setGotAuthentication();
+ Client.setGotAuthenticationInAllControllers();
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete.");
};
diff --git a/src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx b/src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
index aa1c256..ac2f5af 100644
--- a/src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
+++ b/src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
@@ -30,6 +30,7 @@ import {useCookies} from "react-cookie";
import {useNavigate, useSearchParams} from "react-router-dom";
const qController = Client.getInstance();
+const qControllerV1 = Client.getInstanceV1();
interface Props
{
@@ -131,7 +132,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
}
setIsFullyAuthenticated(true);
- qController.setGotAuthentication();
+ Client.setGotAuthenticationInAllControllers();
setLoggedInUser(auth0User);
console.log("Token load complete.");
diff --git a/src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx b/src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
index 0bd58c7..c6e15e6 100644
--- a/src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
+++ b/src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
@@ -80,7 +80,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
console.log(`we have new session UUID: ${newSessionUuid}`);
setIsFullyAuthenticated(true);
- qController.setGotAuthentication();
+ Client.setGotAuthenticationInAllControllers();
setLoggedInUser(values?.user);
console.log("Token load complete.");
@@ -109,7 +109,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
const {values} = await qController.manageSession(null, sessionUuid, null);
setIsFullyAuthenticated(true);
- qController.setGotAuthentication();
+ Client.setGotAuthenticationInAllControllers();
setLoggedInUser(values?.user);
console.log("Token load complete.");
diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx
index 7657abf..33a2724 100644
--- a/src/qqq/components/forms/DynamicForm.tsx
+++ b/src/qqq/components/forms/DynamicForm.tsx
@@ -20,15 +20,21 @@
*/
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
+import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
+import {useFormikContext} from "formik";
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
+import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import FileInputField from "qqq/components/forms/FileInputField";
import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
-import React from "react";
+import Client from "qqq/utils/qqq/Client";
+import React, {useEffect, useState} from "react";
+
+const qController = Client.getInstance();
interface Props
{
@@ -43,7 +49,12 @@ interface Props
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
{
- const {formFields, values, errors, touched} = formData;
+ const {formFields: origFormFields, errors, touched} = formData;
+ const {setFieldValue, values} = useFormikContext>();
+
+ const [formAdjustmentCounter, setFormAdjustmentCounter] = useState(0)
+
+ const [formFields, setFormFields] = useState(origFormFields as {[key: string]: any});
const bulkEditSwitchChanged = (name: string, value: boolean) =>
{
@@ -51,6 +62,204 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
};
+ /////////////////////////////////////////
+ // run on-load handlers if we have any //
+ /////////////////////////////////////////
+ useEffect(() =>
+ {
+ for (let fieldName in formFields)
+ {
+ const field = formFields[fieldName];
+
+ const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
+ if(materialDashboardFieldMetaData?.onLoadFormAdjuster)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // todo consider cases with multiple - do they need to list a sequenceNo? do they need to run serially? //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ considerRunningFormAdjuster("onLoad", fieldName, values[fieldName]);
+ }
+ }
+ }, []);
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ const handleFieldChange = async (fieldName: string, newValue: any) =>
+ {
+ const field = formFields[fieldName];
+ if (!field)
+ {
+ return;
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // map possible-value objects to ids - also capture their labels... //
+ //////////////////////////////////////////////////////////////////////
+ let actualNewValue = newValue;
+ let possibleValueLabel: string = null;
+ if (field.possibleValueProps)
+ {
+ actualNewValue = newValue ? newValue.id : null;
+ possibleValueLabel = newValue ? newValue.label : null;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure formik has the value - and that we capture the possible-value label if needed //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ setFieldValue(fieldName, actualNewValue);
+ if (field.possibleValueProps)
+ {
+ field.possibleValueProps.initialDisplayValue = possibleValueLabel;
+ }
+
+ ///////////////////////////////////////////
+ // run onChange adjuster if there is one //
+ ///////////////////////////////////////////
+ considerRunningFormAdjuster("onChange", fieldName, actualNewValue);
+ }
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ const considerRunningFormAdjuster = async (event: "onChange" | "onLoad", fieldName: string, newValue: any) =>
+ {
+ const field = formFields[fieldName];
+ if (!field)
+ {
+ return;
+ }
+
+ const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
+ const adjuster = event == "onChange" ? materialDashboardFieldMetaData?.onChangeFormAdjuster : materialDashboardFieldMetaData?.onLoadFormAdjuster;
+ if (!adjuster)
+ {
+ return;
+ }
+
+ console.log(`Running form adjuster for field ${fieldName} ${event} (value is: ${newValue})`);
+
+ //////////////////////////////////////////////////////////////////
+ // disable fields temporarily while waiting on backend response //
+ //////////////////////////////////////////////////////////////////
+ const fieldNamesToTempDisable: string[] = materialDashboardFieldMetaData?.fieldsToDisableWhileRunningAdjusters ?? []
+ const previousIsEditableValues: {[key: string]: boolean} = {};
+ if(fieldNamesToTempDisable.length > 0)
+ {
+ for (let oldFieldName in formFields)
+ {
+ if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
+ {
+ previousIsEditableValues[oldFieldName] = formFields[oldFieldName].isEditable;
+ formFields[oldFieldName].isEditable = false;
+ }
+ }
+
+ setFormAdjustmentCounter(formAdjustmentCounter + 1);
+ setFormFields({...formFields});
+ }
+
+ ////////////////////////////////////////////////////
+ // build request to backend for field adjustments //
+ ////////////////////////////////////////////////////
+ const postBody = new FormData();
+ postBody.append("event", event);
+ postBody.append("fieldName", fieldName);
+ postBody.append("newValue", newValue);
+ postBody.append("allValues", JSON.stringify(values));
+ const response = await qController.axiosRequest(
+ {
+ method: "post",
+ url: `/material-dashboard-backend/form-adjuster/${encodeURIComponent(materialDashboardFieldMetaData.formAdjusterIdentifier)}/${event}`,
+ data: postBody,
+ headers: qController.defaultMultipartFormDataHeaders()
+ });
+ console.log("Form adjuster response: " + JSON.stringify(response));
+
+ ////////////////////////////////////////////////////
+ // un-disable any temp disabled fields from above //
+ ////////////////////////////////////////////////////
+ if(fieldNamesToTempDisable.length > 0)
+ {
+ for (let oldFieldName in formFields)
+ {
+ if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
+ {
+ formFields[oldFieldName].isEditable = previousIsEditableValues[oldFieldName];
+ }
+ }
+ setFormFields({...formFields});
+ }
+
+ ///////////////////////////////////////////////////
+ // replace field definitions, if we have updates //
+ ///////////////////////////////////////////////////
+ const updatedFields: { [fieldName: string]: QFieldMetaData } = response.updatedFieldMetaData;
+ if(updatedFields)
+ {
+ for (let updatedFieldName in updatedFields)
+ {
+ const updatedField = new QFieldMetaData(updatedFields[updatedFieldName]);
+ const dynamicField = DynamicFormUtils.getDynamicField(updatedField); // todo dynamicallyDisabledFields? second param...
+
+ const dynamicFieldInObject: any = {};
+ dynamicFieldInObject[updatedFieldName] = dynamicField;
+ let tableName = null;
+ let processName = null;
+ let displayValues = new Map();
+
+ DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [updatedFields[updatedFieldName]], tableName, processName, displayValues);
+ for (let oldFieldName in formFields)
+ {
+ if (oldFieldName == updatedFieldName)
+ {
+ formFields[updatedFieldName] = dynamicField;
+ }
+ }
+ }
+
+ setFormAdjustmentCounter(formAdjustmentCounter + 2);
+ setFormFields({...formFields});
+ }
+
+ /////////////////////////
+ // update field values //
+ /////////////////////////
+ const updatedFieldValues: {[fieldName: string]: any} = response?.updatedFieldValues ?? {};
+ for (let fieldNameToUpdate in updatedFieldValues)
+ {
+ setFieldValue(fieldNameToUpdate, updatedFieldValues[fieldNameToUpdate]);
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // todo - track if a pvs field gets a value, but not a display value, and fetch it?? //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ }
+
+ /////////////////////////////////////////////////
+ // set display values in PVS's if we have them //
+ /////////////////////////////////////////////////
+ const updatedFieldDisplayValues: {[fieldName: string]: any} = response?.updatedFieldDisplayValues ?? {};
+ for (let fieldNameToUpdate in updatedFieldDisplayValues)
+ {
+ const fieldToUpdate = formFields[fieldNameToUpdate];
+ if(fieldToUpdate?.possibleValueProps)
+ {
+ fieldToUpdate.possibleValueProps.initialDisplayValue = updatedFieldDisplayValues[fieldNameToUpdate];
+ }
+ }
+
+ ////////////////////////////////////////
+ // clear field values if we have them //
+ ////////////////////////////////////////
+ const fieldsToClear: string[] = response?.fieldsToClear ?? [];
+ for (let fieldToClear of fieldsToClear)
+ {
+ setFieldValue(fieldToClear, "");
+ }
+ };
+
+
return (
@@ -68,6 +277,8 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
return null;
}
+ const display = field.fieldMetaData?.isHidden ? "none" : "initial";
+
if (values[fieldName] === undefined)
{
values[fieldName] = "";
@@ -100,7 +311,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
}
return (
-
+
{labelElement}
@@ -119,7 +330,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
});
return (
-
+
{labelElement}
handleFieldChange(fieldName, newValue)}
/>
{formattedHelpContent}
@@ -140,7 +352,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
// everything else!! //
///////////////////////
return (
-
+
{labelElement}
handleFieldChange(fieldName, newValue)}
/>
{formattedHelpContent}
diff --git a/src/qqq/components/query/CustomPaginationComponent.tsx b/src/qqq/components/query/CustomPaginationComponent.tsx
index c6eccfc..1bf7e37 100644
--- a/src/qqq/components/query/CustomPaginationComponent.tsx
+++ b/src/qqq/components/query/CustomPaginationComponent.tsx
@@ -26,9 +26,9 @@ import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import {GridRowsProp} from "@mui/x-data-grid-pro";
-import React from "react";
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
+import React from "react";
interface CustomPaginationProps
{
@@ -56,7 +56,7 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
The number of rows shown on this screen may be greater than the number of {tableMetaData?.label} records
that match your query, because you have included fields from other tables which may have
more than one record associated with each {tableMetaData?.label}.
- >
+ >;
let distinctPart = isJoinMany ? (
({ValueUtils.safeToLocaleString(distinctRecords)} distinct
info_outlined
@@ -66,13 +66,23 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
+ if (loading)
+ {
+ return "Counting...";
+ }
+
+ if (!rows || rows.length == 0)
+ {
+ return "No rows";
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, //
// we'll do this... not quite good enough, but better than the original //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (rows.length > 0 && rows.length < to - from)
{
- to = from + rows.length;
+ to = from + (rows.length - 1);
}
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
}
@@ -102,14 +112,55 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
}
};
+ ///////////////////////////////////////////////////////////////////////////////
+ // the `count` param that we pass to below is very //
+ // important - it drives which of the < and > (prev & next) buttons are //
+ // enabled - and, it's a little tricky for tables where we don't do a count. //
+ ///////////////////////////////////////////////////////////////////////////////
+ let countForTablePagination: number;
+ if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
+ {
+ ////////////////////////////////////////////
+ // handle tables where count is disabled. //
+ ////////////////////////////////////////////
+ if(!rows || rows.length == 0)
+ {
+ /////////////////////////////////////////////
+ // if we have no rows, assume a count of 0 //
+ /////////////////////////////////////////////
+ countForTablePagination = 0;
+ }
+ if(rows.length < rowsPerPage)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the # of rows we have is less than the rowsPerPage, assume we're at the end of the query //
+ // so, setting count to pageNo*rowsPer + rows.length - leaves prev. enabled, but disables next. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ countForTablePagination = (pageNumber * rowsPerPage) + rows.length;
+ }
+ else
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, we don't know how many more pages there could be - so, just assume it's at least 1 more //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ countForTablePagination = ((pageNumber + 1) * rowsPerPage) + 1;
+ }
+ }
+ else
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // cases where count is enabled - they work much more like we'd expect: //
+ // if we don't know totalRecords (probably same as loading?) - use a -1, //
+ // which lets us see < and > both active; else, use totalRecords when known. //
+ ////////////////////////////////////////////////////////////////////////////////
+ countForTablePagination = totalRecords === null || totalRecords === undefined ? -1 : totalRecords;
+ }
return (
void;
}
-QueryScreenActionMenu.defaultProps = {
-};
+QueryScreenActionMenu.defaultProps = {};
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
{
- const [anchorElement, setAnchorElement] = useState(null)
+ const [anchorElement, setAnchorElement] = useState(null);
const navigate = useNavigate();
const openActionsMenu = (event: any) =>
{
setAnchorElement(event.currentTarget);
- }
+ };
const closeActionsMenu = () =>
{
setAnchorElement(null);
- }
-
- const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
- {
- if (menuItems.length > 0)
- {
- menuItems.push();
- }
};
const runSomething = (handler: () => void) =>
{
closeActionsMenu();
handler();
- }
+ };
const menuItems: JSX.Element[] = [];
+
+ //////////////////////////////////////////////////////
+ // start with bulk actions, if user has permissions //
+ //////////////////////////////////////////////////////
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
{
menuItems.push();
@@ -91,19 +86,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
menuItems.push();
}
- const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
- if (runRecordScriptProcess)
- {
- const process = runRecordScriptProcess;
- menuItems.push();
- }
-
- menuItems.push();
-
- if (tableProcesses && tableProcesses.length)
- {
- pushDividerIfNeeded(menuItems);
- }
+ menuItems.push();
tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
tableProcesses.map((process) =>
@@ -111,11 +94,62 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
menuItems.push();
});
+ menuItems.push();
+
+ ////////////////////////////////////////////
+ // add processes that apply to all tables //
+ ////////////////////////////////////////////
+ const materialDashboardInstanceMetaData = metaData.supplementalInstanceMetaData?.get("materialDashboard");
+ if (materialDashboardInstanceMetaData)
+ {
+ const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
+ if (processNamesToAddToAllQueryAndViewScreens)
+ {
+ for (let processName of processNamesToAddToAllQueryAndViewScreens)
+ {
+ const process = metaData?.processes.get(processName);
+ if (process)
+ {
+ menuItems.push();
+ }
+ }
+ }
+ }
+ else
+ {
+ //////////////////////////////////////
+ // deprecated in favor of the above //
+ //////////////////////////////////////
+ const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
+ if (runRecordScriptProcess)
+ {
+ const process = runRecordScriptProcess;
+ menuItems.push();
+ }
+ }
+
+ ////////////////////////////////////////
+ // todo - any conditions around this? //
+ ////////////////////////////////////////
+ menuItems.push();
+
if (menuItems.length === 0)
{
menuItems.push();
}
+ ////////////////////////////////////////////////////////////////////////////////
+ // remove any duplicated dividers, and any dividers in the first or last slot //
+ ////////////////////////////////////////////////////////////////////////////////
+ for (let i = 0; i < menuItems.length; i++)
+ {
+ if (menuItems[i].type == Divider && (i == 0 || (i > 0 && menuItems[i - 1].type == Divider) || i == menuItems.length - 1))
+ {
+ menuItems.splice(i, 1);
+ i--;
+ }
+ }
+
return (
<>
@@ -130,5 +164,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
{menuItems}
>
- )
+ );
}
diff --git a/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx b/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx
index 590eb58..4a22fe9 100644
--- a/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx
+++ b/src/qqq/components/widgets/misc/FilterAndColumnsSetupWidget.tsx
@@ -20,6 +20,7 @@
*/
+import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
@@ -42,6 +43,7 @@ import QQueryColumns, {Column} from "qqq/models/query/QQueryColumns";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
+import TableUtils from "qqq/utils/qqq/TableUtils";
import React, {useContext, useEffect, useRef, useState} from "react";
interface FilterAndColumnsSetupWidgetProps
@@ -80,6 +82,7 @@ unborderedButtonSX.opacity = "0.7";
const qController = Client.getInstance();
+const qControllerV1 = Client.getInstanceV1();
/*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns
@@ -90,13 +93,18 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
const [hideColumns] = useState(widgetData?.hideColumns);
const [hidePreview] = useState(widgetData?.hidePreview);
const [hideSortBy] = useState(widgetData?.hideSortBy);
- const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp)
+ const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
+ const [isApiVersioned] = useState(widgetData?.isApiVersioned);
+ const [apiVersion, setApiVersion] = useState(null as ApiVersion | null);
+
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson");
const [alertContent, setAlertContent] = useState(null as string);
+ const [warning, setWarning] = useState(null as string);
+ const [widgetFailureAlertContent, setWidgetFailureAlertContent] = useState(null as string);
//////////////////////////////////////////////////////////////////////////////////////////////////
// we'll actually keep 2 copies of the query filter around here - //
@@ -114,7 +122,9 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
/////////////////////////////
let columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false;
- let queryFilter = recordValues[filterFieldName] && JSON.parse(recordValues[filterFieldName]) as QQueryFilter;
+ const rawFilterValueFromRecord = recordValues[filterFieldName];
+ let queryFilter = rawFilterValueFromRecord &&
+ ((typeof rawFilterValueFromRecord == "string" ? JSON.parse(rawFilterValueFromRecord) : rawFilterValueFromRecord) as QQueryFilter);
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
if (!queryFilter)
{
@@ -167,16 +177,73 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
tableName = recordValues["tableName"];
}
+ let version: ApiVersion | null = null;
+ if (isApiVersioned)
+ {
+ let apiName = widgetData?.apiName;
+ let apiPath = widgetData?.apiPath;
+ let apiVersion = widgetData?.apiVersion;
+
+ if (!apiName && recordValues["apiName"])
+ {
+ apiName = recordValues["apiName"];
+ }
+
+ if (!apiPath && recordValues["apiPath"])
+ {
+ apiPath = recordValues["apiPath"];
+ }
+
+ if (!apiVersion && recordValues["apiVersion"])
+ {
+ apiVersion = recordValues["apiVersion"];
+ }
+
+ if (!apiName || !apiPath || !apiVersion)
+ {
+ console.log("API Name/Path/Version not set, but widget isApiVersioned, so cannot load table meta data...");
+ return;
+ }
+
+ version = {name: apiName, path: apiPath, version: apiVersion};
+ setApiVersion(version);
+ }
+
if (tableName)
{
(async () =>
{
- const tableMetaData = await qController.loadTableMetaData(tableName);
- setTableMetaData(tableMetaData);
+ try
+ {
+ const tableMetaData = await qControllerV1.loadTableMetaData(tableName, version);
+ setTableMetaData(tableMetaData);
- const queryFilterForFrontend = Object.assign({}, queryFilter);
- await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
- setFrontendQueryFilter(queryFilterForFrontend);
+ const queryFilterForFrontend = Object.assign({}, queryFilter);
+
+ let warnings: string[] = [];
+ for (let i = 0; i < queryFilterForFrontend?.criteria?.length; i++)
+ {
+ const criteria = queryFilter.criteria[i];
+ let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
+ if(!field)
+ {
+ warnings.push("Removing non-existing filter field: " + criteria.fieldName);
+ queryFilterForFrontend.criteria.splice(i, 1);
+ i--;
+ }
+ }
+
+ await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
+ setFrontendQueryFilter(queryFilterForFrontend);
+
+ setWarning(warnings.join("; "));
+ }
+ catch (e)
+ {
+ console.log(e);
+ //@ts-ignore e.message
+ setWidgetFailureAlertContent("Error preparing filter widget: " + (e.message ?? "Details not available."));
+ }
})();
}
}, [JSON.stringify(recordValues)]);
@@ -337,7 +404,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
/////////////////////////////////////////////////
// add link to widget header for opening modal //
/////////////////////////////////////////////////
- const selectTableFirstTooltipTitle = tableMetaData ? null : "You must select a table before you can set up your report filters and columns";
+ const selectTableFirstTooltipTitle = tableMetaData ? null : `You must select a table${isApiVersioned ? " and API details" : ""} before you can set up your filters${hideColumns ? "" : " and columns"}`;
const labelAdditionalElementsRight: JSX.Element[] = [];
if (isEditable)
{
@@ -351,6 +418,12 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
}
}
+ if (widgetFailureAlertContent)
+ {
+ return (
+ {widgetFailureAlertContent}
+ );
+ }
return (
@@ -363,6 +436,9 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
setAlertContent(null)}>{alertContent}
+
+ setWarning(null)}>{warning}
+
{label ?? "Query Filter"}
@@ -424,6 +500,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
isModal={true}
initialQueryFilter={frontendQueryFilter}
initialColumns={columns}
+ apiVersion={apiVersion}
/>
)}
@@ -449,6 +526,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
isModal={true}
initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter}
initialColumns={columns}
+ apiVersion={apiVersion}
/>
}
diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
index 5f9e8a6..0443afc 100644
--- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx
+++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx
@@ -49,6 +49,7 @@ export interface ChildRecordListData extends WidgetData
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
defaultValuesForNewChildRecordsFromParentFields?: { [fieldName: string]: string };
+ omitFieldNames?: string[];
}
interface Props
@@ -119,6 +120,19 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection");
+ if (data.omitFieldNames)
+ {
+ for (let i = 0; i < columns.length; i++)
+ {
+ const column = columns[i];
+ if (data.omitFieldNames.indexOf(column.field) > -1)
+ {
+ columns.splice(i, 1);
+ i--;
+ }
+ }
+ }
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/qqq/models/query/QQueryColumns.ts b/src/qqq/models/query/QQueryColumns.ts
index 8b5dccb..a824c70 100644
--- a/src/qqq/models/query/QQueryColumns.ts
+++ b/src/qqq/models/query/QQueryColumns.ts
@@ -117,6 +117,11 @@ export default class QQueryColumns
{
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName)
+ if(!field)
+ {
+ console.warn(`Couldn't find field ${fieldName} in tableMetaData - so not adding a column for it`);
+ }
+
let column: Column;
if(tableForField.name == table.name)
{
diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx
index baf1185..80baab2 100644
--- a/src/qqq/pages/processes/ProcessRun.tsx
+++ b/src/qqq/pages/processes/ProcessRun.tsx
@@ -72,6 +72,7 @@ import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"
import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
+import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
@@ -114,9 +115,14 @@ let formikSetTouched = ({}: any, touched: boolean): void =>
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
-export interface SubFormPreSubmitCallbackResultType {maySubmit: boolean; values: {[name: string]: any}}
+export interface SubFormPreSubmitCallbackResultType
+{
+ maySubmit: boolean;
+ values: { [name: string]: any };
+}
+
type SubFormPreSubmitCallback = () => SubFormPreSubmitCallbackResultType;
-type SubFormPreSubmitCallbackWithName = {name: string, callback: SubFormPreSubmitCallback}
+type SubFormPreSubmitCallbackWithName = { name: string, callback: SubFormPreSubmitCallback }
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{
@@ -161,7 +167,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
- const [controlCallbacks, setControlCallbacks] = useState({} as {[name: string]: () => void});
+ const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
const [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]);
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@@ -237,7 +243,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const bulkLoadFileMappingFormRef = useRef();
const bulkLoadValueMappingFormRef = useRef();
const bulkLoadProfileFormRef = useRef();
- const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[])
+ const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[]);
const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean =>
{
@@ -699,10 +705,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
{
- if(bulkLoadFileMappingFormRef?.current)
+ if (bulkLoadFileMappingFormRef?.current)
{
// @ts-ignore ...
- addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit)
+ addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit);
}
}
@@ -711,10 +717,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
{
- if(bulkLoadValueMappingFormRef?.current)
+ if (bulkLoadValueMappingFormRef?.current)
{
// @ts-ignore ...
- addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit)
+ addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit);
}
}
@@ -723,10 +729,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
///////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_PROFILE_FORM))
{
- if(bulkLoadProfileFormRef?.current)
+ if (bulkLoadProfileFormRef?.current)
{
// @ts-ignore ...
- addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit)
+ addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit);
}
}
@@ -1298,7 +1304,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////////
// Help make this component's fields work with our formik form //
/////////////////////////////////////////////////////////////////
- if(activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
+ if (activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
{
const fileValues = processValues.fileValues ?? [];
const valueMapping = processValues.valueMapping ?? {};
@@ -1314,22 +1320,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
for (let i = 0; i < fileValues.length; i++)
{
const dynamicField = DynamicFormUtils.getDynamicField(qFieldMetaData);
- const wrappedField: any = {};
+ const wrappedField: any = {};
wrappedField[field.name] = dynamicField;
DynamicFormUtils.addPossibleValueProps(wrappedField, [field], fieldTableName, null, null);
const initialValue = valueMapping[fileValues[i]];
- if(dynamicField.possibleValueProps)
+ if (dynamicField.possibleValueProps)
{
- dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue]
+ dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue];
}
- addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null)
+ addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null);
fieldsForComponent.push(dynamicField);
}
- setBulkLoadValueMappingFormFields(fieldsForComponent)
+ setBulkLoadValueMappingFormFields(fieldsForComponent);
}
if (Object.keys(dynamicFormFields).length > 0)
@@ -1522,15 +1528,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
***************************************************************************/
function addSubFormPreSubmitCallbacks(name: string, callback: SubFormPreSubmitCallback)
{
- if(subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
+ if (subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
{
- const newCallbacks: SubFormPreSubmitCallbackWithName[] = []
- for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
+ const newCallbacks: SubFormPreSubmitCallbackWithName[] = [];
+ for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
{
newCallbacks[i] = subFormPreSubmitCallbacks[i];
}
- newCallbacks.push({name, callback})
- setSubFormPreSubmitCallbacks(newCallbacks)
+ newCallbacks.push({name, callback});
+ setSubFormPreSubmitCallbacks(newCallbacks);
}
}
@@ -1620,7 +1626,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setRenderedWidgets({});
setSubFormPreSubmitCallbacks([]);
setQJobRunning(null);
- setBackStepName(qJobComplete.backStep)
+ setBackStepName(qJobComplete.backStep);
if (formikSetFieldValueFunction)
{
@@ -1815,8 +1821,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
- recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
- recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
+ doRecordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
+ doRecordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
if (processMetaData.tableName && !tableMetaData)
{
@@ -1838,17 +1844,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return;
}
- if(urlSearchParams.get("defaultProcessValues"))
+ if (urlSearchParams.get("defaultProcessValues"))
{
- if(!defaultProcessValues)
+ if (!defaultProcessValues)
{
- defaultProcessValues = {}
+ defaultProcessValues = {};
}
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
for (let key in values)
{
- defaultProcessValues[key] = values[key]
+ defaultProcessValues[key] = values[key];
}
}
@@ -1894,7 +1900,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setTimeout(async () =>
{
- recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
+ doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await qController.processStep(
processName,
@@ -1914,7 +1920,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{
setTimeout(async () =>
{
- recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
+ doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
@@ -1938,20 +1944,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
///////////////////////////////////////////////////////////////
// run any sub-form pre-submit callbacks that are registered //
///////////////////////////////////////////////////////////////
- for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
+ for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
{
const {maySubmit, values: moreValues} = subFormPreSubmitCallbacks[i].callback();
- if(!maySubmit)
+ if (!maySubmit)
{
console.log(`May not submit form, per callback: ${subFormPreSubmitCallbacks[i].name}`);
return;
}
- if(moreValues)
+ if (moreValues)
{
for (let key in moreValues)
{
- values[key] = moreValues[key]
+ values[key] = moreValues[key];
}
}
}
@@ -2026,7 +2032,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////////////////////////////////////////////////////////////////
setLoadingRecords(true);
- }
+ };
/*******************************************************************************
@@ -2055,6 +2061,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
};
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ function doRecordAnalytics(model: AnalyticsModel)
+ {
+ try
+ {
+ recordAnalytics(model);
+ }
+ catch (e)
+ {
+ console.log(`Error recording analytics: ${e}`);
+ }
+ }
+
const formStyles: any = {};
if (isWidget)
{
diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx
index 3a2a594..fc4e606 100644
--- a/src/qqq/pages/records/query/RecordQuery.tsx
+++ b/src/qqq/pages/records/query/RecordQuery.tsx
@@ -20,6 +20,7 @@
*/
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
+import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
@@ -69,9 +70,9 @@ import RecordQueryView from "qqq/models/query/RecordQueryView";
import ProcessRun from "qqq/pages/processes/ProcessRun";
import ColumnStats from "qqq/pages/records/query/ColumnStats";
import DataGridUtils from "qqq/utils/DataGridUtils";
+import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
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";
@@ -89,6 +90,7 @@ export type QueryScreenUsage = "queryScreen" | "reportSetup"
interface Props
{
table?: QTableMetaData;
+ apiVersion?: ApiVersion;
launchProcess?: QProcessMetaData;
usage?: QueryScreenUsage;
isModal?: boolean;
@@ -101,9 +103,10 @@ interface Props
///////////////////////////////////////////////////////
// define possible values for our pageState variable //
///////////////////////////////////////////////////////
-type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready";
+type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready" | "error";
const qController = Client.getInstance();
+const qControllerV1 = Client.getInstanceV1();
/*******************************************************************************
** function to produce standard version of the screen while we're "loading"
@@ -127,7 +130,7 @@ const getLoadingScreen = (isModal: boolean) =>
**
** Yuge component. The best. Lots of very smart people are saying so.
*******************************************************************************/
-const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
+const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
{
const tableName = table.name;
const [searchParams] = useSearchParams();
@@ -979,7 +982,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
}
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
- qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
+ // qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
+ qControllerV1.count(tableName, apiVersion, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
{
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
countResults[thisQueryId] = [];
@@ -998,7 +1002,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
setLastFetchedQFilterJSON(JSON.stringify(queryFilter));
setLastFetchedVariant(tableVariant);
- qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
+ // qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
+ qControllerV1.query(tableName, apiVersion, filterForBackend, queryJoins, tableVariant).then((results) =>
{
console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results;
@@ -1141,6 +1146,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handlePageNumberChange = (page: number) =>
{
setPageNumber(page);
+ setLoading(true);
};
/*******************************************************************************
@@ -1149,6 +1155,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handleRowsPerPageChange = (size: number) =>
{
setRowsPerPage(size);
+ setLoading(true);
view.rowsPerPage = size;
doSetView(view);
@@ -1672,8 +1679,9 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
{
if (savedViewRecord == null)
{
- console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView instead.");
+ console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView, and activating tableDefaultView instead.");
doClearCurrentSavedView();
+ activateView(buildTableDefaultView(tableMetaData));
return;
}
@@ -2435,23 +2443,33 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const metaData = await qController.loadMetaData();
setMetaData(metaData);
- const tableMetaData = await qController.loadTableMetaData(tableName);
- setTableMetaData(tableMetaData);
- setTableLabel(tableMetaData.label);
+ try
+ {
+ // const tableMetaData = await qController.loadTableMetaData(tableName);
+ const tableMetaData = await qControllerV1.loadTableMetaData(tableName, apiVersion);
+ setTableMetaData(tableMetaData);
+ setTableLabel(tableMetaData.label);
- doRecordAnalytics({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)
+ 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)
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
- // but also used when user selects new-view from the view menu //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- const newDefaultView = buildTableDefaultView(tableMetaData);
- setTableDefaultView(newDefaultView);
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
+ // but also used when user selects new-view from the view menu //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ const newDefaultView = buildTableDefaultView(tableMetaData);
+ setTableDefaultView(newDefaultView);
- setPageState("loadedMetaData");
+ setPageState("loadedMetaData");
+ }
+ catch (e)
+ {
+ setPageState("error");
+ //@ts-ignore e.message
+ setAlertContent("Error loading table: " + e?.message ?? "Details not available.");
+ }
})();
}
@@ -2719,6 +2737,16 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
);
}
+ //////////////////////////////////////////////
+ // render an error screen (alert) if needed //
+ //////////////////////////////////////////////
+ if (pageState == "error")
+ {
+ console.log(`page state is ${pageState}... rendering an alert...`);
+ const errorBody = {alertContent};
+ return isModal ? errorBody : {errorBody};
+ }
+
///////////////////////////////////////////////////////////
// render a loading screen if the page state isn't ready //
///////////////////////////////////////////////////////////
@@ -3069,6 +3097,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
RecordQuery.defaultProps = {
table: null,
+ apiVersion: null,
usage: "queryScreen",
launchProcess: null,
isModal: false,
diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx
index 3aeec37..f52e9de 100644
--- a/src/qqq/pages/records/view/RecordView.tsx
+++ b/src/qqq/pages/records/view/RecordView.tsx
@@ -440,6 +440,34 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
};
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ function getGenericProcesses(metaData: QInstance)
+ {
+ const genericProcesses: QProcessMetaData[] = [];
+ const materialDashboardInstanceMetaData = metaData?.supplementalInstanceMetaData?.get("materialDashboard");
+ if (materialDashboardInstanceMetaData)
+ {
+ const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
+ if (processNamesToAddToAllQueryAndViewScreens)
+ {
+ for (let processName of processNamesToAddToAllQueryAndViewScreens)
+ {
+ genericProcesses.push(metaData?.processes?.get(processName));
+ }
+ }
+ }
+ else
+ {
+ ////////////////
+ // deprecated //
+ ////////////////
+ genericProcesses.push(metaData?.processes.get("runRecordScript"));
+ }
+ return genericProcesses;
+ }
+
if (!asyncLoadInited)
{
setAsyncLoadInited(true);
@@ -472,11 +500,16 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// load processes that the routing needs to respect //
//////////////////////////////////////////////////////
const allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true); // these include hidden ones (e.g., to find the bulks)
- const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
- if (runRecordScriptProcess)
+ const genericProcesses = getGenericProcesses(metaData);
+
+ for (let genericProcess of genericProcesses)
{
- allTableProcesses.unshift(runRecordScriptProcess);
+ if (genericProcess)
+ {
+ allTableProcesses.unshift(genericProcess);
+ }
}
+
setAllTableProcesses(allTableProcesses);
if (launchingProcess)
@@ -726,7 +759,6 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
- const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
const renderActionsMenu = (