Merged feature/workflows-support into integration

This commit is contained in:
2025-06-02 09:06:43 -05:00
55 changed files with 3162 additions and 157 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@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/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -66,7 +66,13 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.25.0-integration-sprint-62-20250307-205536</version> <version>0.26.0-integration-20250529-234230</version>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-middleware-javalin</artifactId>
<optional>true</optional>
<version>0.26.0-integration-20250529-234230</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -377,23 +377,57 @@ export default function App({authenticationMetaData}: Props)
}); });
}); });
const runRecordScriptProcess = metaData.processes.get("runRecordScript"); const materialDashboardInstanceMetaData = metaData.supplementalInstanceMetaData?.get("materialDashboard");
if (runRecordScriptProcess) if (materialDashboardInstanceMetaData)
{ {
const process = runRecordScriptProcess; const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
routeList.push({ if (processNamesToAddToAllQueryAndViewScreens)
name: process.label, {
key: process.name, for (let processName of processNamesToAddToAllQueryAndViewScreens)
route: `${path}/${process.name}`, {
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />, const process = metaData.processes.get(processName);
}); if (process)
{
routeList.push({
name: process.label,
key: process.name,
route: `${path}/${process.name}`,
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
});
routeList.push({ routeList.push({
name: process.label, name: process.label,
key: `${app.name}/${process.name}`, key: `${app.name}/${process.name}`,
route: `${path}/:id/${process.name}`, route: `${path}/:id/${process.name}`,
component: <RecordView table={table} launchProcess={process} />, component: <RecordView table={table} launchProcess={process} />,
}); });
}
}
}
}
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: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
});
routeList.push({
name: process.label,
key: `${app.name}/${process.name}`,
route: `${path}/:id/${process.name}`,
component: <RecordView table={table} launchProcess={process} />,
});
}
} }
const reportsForTable = ProcessUtils.getReportsForTable(metaData, table.name, true); const reportsForTable = ProcessUtils.getReportsForTable(metaData, table.name, true);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Serializable> 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<String, Serializable> getAllValues()
{
return (this.allValues);
}
/*******************************************************************************
** Setter for allValues
*******************************************************************************/
public void setAllValues(Map<String, Serializable> allValues)
{
this.allValues = allValues;
}
/*******************************************************************************
** Fluent setter for allValues
*******************************************************************************/
public FormAdjusterInput withAllValues(Map<String, Serializable> allValues)
{
this.allValues = allValues;
return (this);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, QFrontendFieldMetaData> updatedFieldMetaData = null;
private Map<String, Serializable> updatedFieldValues = null;
private Map<String, String> updatedFieldDisplayValues = null;
private Set<String> fieldsToClear = null;
/*******************************************************************************
** Getter for updatedFieldValues
*******************************************************************************/
public Map<String, Serializable> getUpdatedFieldValues()
{
return (this.updatedFieldValues);
}
/*******************************************************************************
** Setter for updatedFieldValues
*******************************************************************************/
public void setUpdatedFieldValues(Map<String, Serializable> updatedFieldValues)
{
this.updatedFieldValues = updatedFieldValues;
}
/*******************************************************************************
** Fluent setter for updatedFieldValues
*******************************************************************************/
public FormAdjusterOutput withUpdatedFieldValues(Map<String, Serializable> updatedFieldValues)
{
this.updatedFieldValues = updatedFieldValues;
return (this);
}
/*******************************************************************************
** Getter for fieldsToClear
*******************************************************************************/
public Set<String> getFieldsToClear()
{
return (this.fieldsToClear);
}
/*******************************************************************************
** Setter for fieldsToClear
*******************************************************************************/
public void setFieldsToClear(Set<String> fieldsToClear)
{
this.fieldsToClear = fieldsToClear;
}
/*******************************************************************************
** Fluent setter for fieldsToClear
*******************************************************************************/
public FormAdjusterOutput withFieldsToClear(Set<String> fieldsToClear)
{
this.fieldsToClear = fieldsToClear;
return (this);
}
/*******************************************************************************
** Getter for updatedFieldMetaData
*******************************************************************************/
public Map<String, QFrontendFieldMetaData> getUpdatedFieldMetaData()
{
return (this.updatedFieldMetaData);
}
/*******************************************************************************
** Setter for updatedFieldMetaData
*******************************************************************************/
public void setUpdatedFieldMetaData(Map<String, QFrontendFieldMetaData> updatedFieldMetaData)
{
this.updatedFieldMetaData = updatedFieldMetaData;
}
/*******************************************************************************
** Fluent setter for updatedFieldMetaData
*******************************************************************************/
public FormAdjusterOutput withUpdatedFieldMetaData(Map<String, QFrontendFieldMetaData> updatedFieldMetaData)
{
this.updatedFieldMetaData = updatedFieldMetaData;
return (this);
}
/*******************************************************************************
** Getter for updatedFieldDisplayValues
*******************************************************************************/
public Map<String, String> getUpdatedFieldDisplayValues()
{
return (this.updatedFieldDisplayValues);
}
/*******************************************************************************
** Setter for updatedFieldDisplayValues
*******************************************************************************/
public void setUpdatedFieldDisplayValues(Map<String, String> updatedFieldDisplayValues)
{
this.updatedFieldDisplayValues = updatedFieldDisplayValues;
}
/*******************************************************************************
** Fluent setter for updatedFieldDisplayValues
*******************************************************************************/
public FormAdjusterOutput withUpdatedFieldDisplayValues(Map<String, String> updatedFieldDisplayValues)
{
this.updatedFieldDisplayValues = updatedFieldDisplayValues;
return (this);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, QCodeReference> onChangeAdjusters = new HashMap<>();
private static Map<String, QCodeReference> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QProcessMetaData>
{
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<String, Serializable> 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);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String> getFieldsToDisableWhileRunningAdjusters()
{
return (this.fieldsToDisableWhileRunningAdjusters);
}
/*******************************************************************************
** Setter for fieldsToDisableWhileRunningAdjusters
*******************************************************************************/
public void setFieldsToDisableWhileRunningAdjusters(Set<String> fieldsToDisableWhileRunningAdjusters)
{
this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
}
/*******************************************************************************
** Fluent setter for fieldsToDisableWhileRunningAdjusters
*******************************************************************************/
public MaterialDashboardFieldMetaData withFieldsToDisableWhileRunningAdjusters(Set<String> fieldsToDisableWhileRunningAdjusters)
{
this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
return (this);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String> 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<String> processNamesToAddToAllQueryAndViewScreens)
{
this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
}
/*******************************************************************************
** Fluent setter for processNamesToAddToAllQueryAndViewScreens
*******************************************************************************/
public MaterialDashboardInstanceMetaData withProcessNamesToAddToAllQueryAndViewScreens(List<String> processNamesToAddToAllQueryAndViewScreens)
{
this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
return (this);
}
}

View File

@ -48,7 +48,7 @@ export default function useAnonymousAuthenticationModule({setIsFullyAuthenticate
{ {
console.log("Generating random token..."); console.log("Generating random token...");
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
qController.setGotAuthentication(); Client.setGotAuthenticationInAllControllers();
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete."); console.log("Token generation complete.");
}; };

View File

@ -30,6 +30,7 @@ import {useCookies} from "react-cookie";
import {useNavigate, useSearchParams} from "react-router-dom"; import {useNavigate, useSearchParams} from "react-router-dom";
const qController = Client.getInstance(); const qController = Client.getInstance();
const qControllerV1 = Client.getInstanceV1();
interface Props interface Props
{ {
@ -131,7 +132,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
} }
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
qController.setGotAuthentication(); Client.setGotAuthenticationInAllControllers();
setLoggedInUser(auth0User); setLoggedInUser(auth0User);
console.log("Token load complete."); console.log("Token load complete.");

View File

@ -80,7 +80,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
console.log(`we have new session UUID: ${newSessionUuid}`); console.log(`we have new session UUID: ${newSessionUuid}`);
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
qController.setGotAuthentication(); Client.setGotAuthenticationInAllControllers();
setLoggedInUser(values?.user); setLoggedInUser(values?.user);
console.log("Token load complete."); console.log("Token load complete.");
@ -109,7 +109,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
const {values} = await qController.manageSession(null, sessionUuid, null); const {values} = await qController.manageSession(null, sessionUuid, null);
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
qController.setGotAuthentication(); Client.setGotAuthenticationInAllControllers();
setLoggedInUser(values?.user); setLoggedInUser(values?.user);
console.log("Token load complete."); console.log("Token load complete.");

View File

@ -20,15 +20,21 @@
*/ */
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; 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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import {useFormikContext} from "formik";
import QDynamicFormField from "qqq/components/forms/DynamicFormField"; import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect"; import DynamicSelect from "qqq/components/forms/DynamicSelect";
import FileInputField from "qqq/components/forms/FileInputField"; import FileInputField from "qqq/components/forms/FileInputField";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent"; 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 interface Props
{ {
@ -43,7 +49,12 @@ interface Props
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element 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<Record<string, any>>();
const [formAdjustmentCounter, setFormAdjustmentCounter] = useState(0)
const [formFields, setFormFields] = useState(origFormFields as {[key: string]: any});
const bulkEditSwitchChanged = (name: string, value: boolean) => 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 ( return (
<Box> <Box>
<Box lineHeight={0}> <Box lineHeight={0}>
@ -68,6 +277,8 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
return null; return null;
} }
const display = field.fieldMetaData?.isHidden ? "none" : "initial";
if (values[fieldName] === undefined) if (values[fieldName] === undefined)
{ {
values[fieldName] = ""; values[fieldName] = "";
@ -100,7 +311,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
} }
return ( return (
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} flexDirection="column" key={fieldName}> <Grid item lg={itemLG} xs={itemXS} sm={itemSM} flexDirection="column" key={fieldName + "-" + formAdjustmentCounter}>
{labelElement} {labelElement}
<FileInputField field={field} record={record} errorMessage={errors[fieldName]} /> <FileInputField field={field} record={record} errorMessage={errors[fieldName]} />
</Grid> </Grid>
@ -119,7 +330,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
}); });
return ( return (
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName}> <Grid item display={display} lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName + "-" + formAdjustmentCounter}>
{labelElement} {labelElement}
<DynamicSelect <DynamicSelect
fieldPossibleValueProps={field.possibleValueProps} fieldPossibleValueProps={field.possibleValueProps}
@ -130,6 +341,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
bulkEditSwitchChangeHandler={bulkEditSwitchChanged} bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
otherValues={otherValuesMap} otherValues={otherValuesMap}
useCase="form" useCase="form"
onChange={(newValue: any) => handleFieldChange(fieldName, newValue)}
/> />
{formattedHelpContent} {formattedHelpContent}
</Grid> </Grid>
@ -140,7 +352,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
// everything else!! // // everything else!! //
/////////////////////// ///////////////////////
return ( return (
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName}> <Grid item display={display} lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName + "-" + formAdjustmentCounter}>
{labelElement} {labelElement}
<QDynamicFormField <QDynamicFormField
id={field.name} id={field.name}
@ -155,6 +367,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
bulkEditSwitchChangeHandler={bulkEditSwitchChanged} bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]} success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
formFieldObject={field} formFieldObject={field}
onChangeCallback={(newValue) => handleFieldChange(fieldName, newValue)}
/> />
{formattedHelpContent} {formattedHelpContent}
</Grid> </Grid>

View File

@ -26,9 +26,9 @@ import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import {GridRowsProp} from "@mui/x-data-grid-pro"; import {GridRowsProp} from "@mui/x-data-grid-pro";
import React from "react";
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React from "react";
interface CustomPaginationProps 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 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 that match your query, because you have included fields from other tables which may have
more than one record associated with each {tableMetaData?.label}. more than one record associated with each {tableMetaData?.label}.
</> </>;
let distinctPart = isJoinMany ? (<Box display="inline" component="span" textAlign="right"> let distinctPart = isJoinMany ? (<Box display="inline" component="span" textAlign="right">
&nbsp;({ValueUtils.safeToLocaleString(distinctRecords)} distinct<CustomWidthTooltip title={tooltipHTML}> &nbsp;({ValueUtils.safeToLocaleString(distinctRecords)} distinct<CustomWidthTooltip title={tooltipHTML}>
<IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton> <IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
@ -66,13 +66,23 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT)) 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, // // 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 // // we'll do this... not quite good enough, but better than the original //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (rows.length > 0 && rows.length < to - from) if (rows.length > 0 && rows.length < to - from)
{ {
to = from + rows.length; to = from + (rows.length - 1);
} }
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`); 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 <TablePagination> 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 ( return (
<TablePagination <TablePagination
component="div" component="div"
sx={{minWidth: "450px"}} sx={{minWidth: "450px", "& .MuiTablePagination-displayedRows": {minWidth: "110px"}}}
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null, count={countForTablePagination}
// so pass a sentinel value of -1...
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
page={pageNumber} page={pageNumber}
rowsPerPageOptions={[10, 25, 50, 100, 250]} rowsPerPageOptions={[10, 25, 50, 100, 250]}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}

View File

@ -29,9 +29,9 @@ import Icon from "@mui/material/Icon";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
import React, {useState} from "react"; import React, {useState} from "react";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
interface QueryScreenActionMenuProps interface QueryScreenActionMenuProps
{ {
@ -44,40 +44,35 @@ interface QueryScreenActionMenuProps
processClicked: (process: QProcessMetaData) => void; processClicked: (process: QProcessMetaData) => void;
} }
QueryScreenActionMenu.defaultProps = { QueryScreenActionMenu.defaultProps = {};
};
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element 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 navigate = useNavigate();
const openActionsMenu = (event: any) => const openActionsMenu = (event: any) =>
{ {
setAnchorElement(event.currentTarget); setAnchorElement(event.currentTarget);
} };
const closeActionsMenu = () => const closeActionsMenu = () =>
{ {
setAnchorElement(null); setAnchorElement(null);
}
const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
{
if (menuItems.length > 0)
{
menuItems.push(<Divider key="divider" />);
}
}; };
const runSomething = (handler: () => void) => const runSomething = (handler: () => void) =>
{ {
closeActionsMenu(); closeActionsMenu();
handler(); handler();
} };
const menuItems: JSX.Element[] = []; const menuItems: JSX.Element[] = [];
//////////////////////////////////////////////////////
// start with bulk actions, if user has permissions //
//////////////////////////////////////////////////////
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission) if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
{ {
menuItems.push(<MenuItem key="bulkLoad" onClick={() => runSomething(bulkLoadClicked)}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>); menuItems.push(<MenuItem key="bulkLoad" onClick={() => runSomething(bulkLoadClicked)}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>);
@ -91,19 +86,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
menuItems.push(<MenuItem key="bulkDelete" onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>); menuItems.push(<MenuItem key="bulkDelete" onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
} }
const runRecordScriptProcess = metaData?.processes.get("runRecordScript"); menuItems.push(<Divider key="divider1" />);
if (runRecordScriptProcess)
{
const process = runRecordScriptProcess;
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
}
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
if (tableProcesses && tableProcesses.length)
{
pushDividerIfNeeded(menuItems);
}
tableProcesses.sort((a, b) => a.label.localeCompare(b.label)); tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
tableProcesses.map((process) => tableProcesses.map((process) =>
@ -111,11 +94,62 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>); menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
}); });
menuItems.push(<Divider key="divider2" />);
////////////////////////////////////////////
// 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(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
}
}
}
}
else
{
//////////////////////////////////////
// deprecated in favor of the above //
//////////////////////////////////////
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
if (runRecordScriptProcess)
{
const process = runRecordScriptProcess;
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
}
}
////////////////////////////////////////
// todo - any conditions around this? //
////////////////////////////////////////
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
if (menuItems.length === 0) if (menuItems.length === 0)
{ {
menuItems.push(<MenuItem key="notAvaialableNow" disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>); menuItems.push(<MenuItem key="notAvaialableNow" disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
} }
////////////////////////////////////////////////////////////////////////////////
// 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 ( return (
<> <>
<QActionsMenuButton isOpen={anchorElement} onClickHandler={openActionsMenu} /> <QActionsMenuButton isOpen={anchorElement} onClickHandler={openActionsMenu} />
@ -130,5 +164,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
{menuItems} {menuItems}
</Menu> </Menu>
</> </>
) );
} }

View File

@ -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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; 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 RecordQuery from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils"; import FilterUtils from "qqq/utils/qqq/FilterUtils";
import TableUtils from "qqq/utils/qqq/TableUtils";
import React, {useContext, useEffect, useRef, useState} from "react"; import React, {useContext, useEffect, useRef, useState} from "react";
interface FilterAndColumnsSetupWidgetProps interface FilterAndColumnsSetupWidgetProps
@ -80,6 +82,7 @@ unborderedButtonSX.opacity = "0.7";
const qController = Client.getInstance(); const qController = Client.getInstance();
const qControllerV1 = Client.getInstanceV1();
/******************************************************************************* /*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns ** Component for editing the main setup of a report - that is: filter & columns
@ -90,13 +93,18 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
const [hideColumns] = useState(widgetData?.hideColumns); const [hideColumns] = useState(widgetData?.hideColumns);
const [hidePreview] = useState(widgetData?.hidePreview); const [hidePreview] = useState(widgetData?.hidePreview);
const [hideSortBy] = useState(widgetData?.hideSortBy); const [hideSortBy] = useState(widgetData?.hideSortBy);
const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp) const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); 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 [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson"); const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson");
const [alertContent, setAlertContent] = useState(null as string); 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 - // // 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 columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false; 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; const defaultFilterFields = widgetData?.filterDefaultFieldNames;
if (!queryFilter) if (!queryFilter)
{ {
@ -167,16 +177,73 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
tableName = recordValues["tableName"]; 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) if (tableName)
{ {
(async () => (async () =>
{ {
const tableMetaData = await qController.loadTableMetaData(tableName); try
setTableMetaData(tableMetaData); {
const tableMetaData = await qControllerV1.loadTableMetaData(tableName, version);
setTableMetaData(tableMetaData);
const queryFilterForFrontend = Object.assign({}, queryFilter); const queryFilterForFrontend = Object.assign({}, queryFilter);
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
setFrontendQueryFilter(queryFilterForFrontend); 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)]); }, [JSON.stringify(recordValues)]);
@ -337,7 +404,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
///////////////////////////////////////////////// /////////////////////////////////////////////////
// add link to widget header for opening modal // // 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[] = []; const labelAdditionalElementsRight: JSX.Element[] = [];
if (isEditable) if (isEditable)
{ {
@ -351,6 +418,12 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
} }
} }
if (widgetFailureAlertContent)
{
return (<Widget widgetMetaData={widgetMetaData}>
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}}>{widgetFailureAlertContent}</Alert>
</Widget>);
}
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}> return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}>
<React.Fragment> <React.Fragment>
@ -363,6 +436,9 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
<Collapse in={Boolean(alertContent)}> <Collapse in={Boolean(alertContent)}>
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert> <Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
</Collapse> </Collapse>
<Collapse in={Boolean(warning)}>
<Alert severity="warning" sx={{mt: 1.5, mb: 0.5}} onClose={() => setWarning(null)}>{warning}</Alert>
</Collapse>
<Box pt="0.5rem"> <Box pt="0.5rem">
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<h5>{label ?? "Query Filter"}</h5> <h5>{label ?? "Query Filter"}</h5>
@ -424,6 +500,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
isModal={true} isModal={true}
initialQueryFilter={frontendQueryFilter} initialQueryFilter={frontendQueryFilter}
initialColumns={columns} initialColumns={columns}
apiVersion={apiVersion}
/> />
</Box> </Box>
)} )}
@ -449,6 +526,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
isModal={true} isModal={true}
initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter} initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter}
initialColumns={columns} initialColumns={columns}
apiVersion={apiVersion}
/> />
} }

View File

@ -49,6 +49,7 @@ export interface ChildRecordListData extends WidgetData
defaultValuesForNewChildRecords?: { [fieldName: string]: any }; defaultValuesForNewChildRecords?: { [fieldName: string]: any };
disabledFieldsForNewChildRecords?: { [fieldName: string]: any }; disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
defaultValuesForNewChildRecordsFromParentFields?: { [fieldName: string]: string }; defaultValuesForNewChildRecordsFromParentFields?: { [fieldName: string]: string };
omitFieldNames?: string[];
} }
interface Props interface Props
@ -119,6 +120,19 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath; const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection"); 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) // // capture all-columns to use for the export (before we might splice some away from the on-screen display) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -117,6 +117,11 @@ export default class QQueryColumns
{ {
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName) 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; let column: Column;
if(tableForField.name == table.name) if(tableForField.name == table.name)
{ {

View File

@ -72,6 +72,7 @@ import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery"; 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 Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; 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 } } = {}; 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 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 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<string, QFieldMetaData>); const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } }); 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 [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]);
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext); const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -237,7 +243,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const bulkLoadFileMappingFormRef = useRef(); const bulkLoadFileMappingFormRef = useRef();
const bulkLoadValueMappingFormRef = useRef(); const bulkLoadValueMappingFormRef = useRef();
const bulkLoadProfileFormRef = useRef(); const bulkLoadProfileFormRef = useRef();
const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[]) const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[]);
const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean => 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 (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
{ {
if(bulkLoadFileMappingFormRef?.current) if (bulkLoadFileMappingFormRef?.current)
{ {
// @ts-ignore ... // @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 (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
{ {
if(bulkLoadValueMappingFormRef?.current) if (bulkLoadValueMappingFormRef?.current)
{ {
// @ts-ignore ... // @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 (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_PROFILE_FORM))
{ {
if(bulkLoadProfileFormRef?.current) if (bulkLoadProfileFormRef?.current)
{ {
// @ts-ignore ... // @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 // // 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 fileValues = processValues.fileValues ?? [];
const valueMapping = processValues.valueMapping ?? {}; const valueMapping = processValues.valueMapping ?? {};
@ -1314,22 +1320,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
for (let i = 0; i < fileValues.length; i++) for (let i = 0; i < fileValues.length; i++)
{ {
const dynamicField = DynamicFormUtils.getDynamicField(qFieldMetaData); const dynamicField = DynamicFormUtils.getDynamicField(qFieldMetaData);
const wrappedField: any = {}; const wrappedField: any = {};
wrappedField[field.name] = dynamicField; wrappedField[field.name] = dynamicField;
DynamicFormUtils.addPossibleValueProps(wrappedField, [field], fieldTableName, null, null); DynamicFormUtils.addPossibleValueProps(wrappedField, [field], fieldTableName, null, null);
const initialValue = valueMapping[fileValues[i]]; 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); fieldsForComponent.push(dynamicField);
} }
setBulkLoadValueMappingFormFields(fieldsForComponent) setBulkLoadValueMappingFormFields(fieldsForComponent);
} }
if (Object.keys(dynamicFormFields).length > 0) if (Object.keys(dynamicFormFields).length > 0)
@ -1522,15 +1528,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
***************************************************************************/ ***************************************************************************/
function addSubFormPreSubmitCallbacks(name: string, callback: SubFormPreSubmitCallback) 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[] = [] const newCallbacks: SubFormPreSubmitCallbackWithName[] = [];
for(let i = 0; i < subFormPreSubmitCallbacks.length; i++) for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
{ {
newCallbacks[i] = subFormPreSubmitCallbacks[i]; newCallbacks[i] = subFormPreSubmitCallbacks[i];
} }
newCallbacks.push({name, callback}) newCallbacks.push({name, callback});
setSubFormPreSubmitCallbacks(newCallbacks) setSubFormPreSubmitCallbacks(newCallbacks);
} }
} }
@ -1620,7 +1626,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setRenderedWidgets({}); setRenderedWidgets({});
setSubFormPreSubmitCallbacks([]); setSubFormPreSubmitCallbacks([]);
setQJobRunning(null); setQJobRunning(null);
setBackStepName(qJobComplete.backStep) setBackStepName(qJobComplete.backStep);
if (formikSetFieldValueFunction) if (formikSetFieldValueFunction)
{ {
@ -1815,8 +1821,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessMetaData(processMetaData); setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps); setSteps(processMetaData.frontendSteps);
recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label}); doRecordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label}); doRecordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
if (processMetaData.tableName && !tableMetaData) if (processMetaData.tableName && !tableMetaData)
{ {
@ -1838,17 +1844,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return; return;
} }
if(urlSearchParams.get("defaultProcessValues")) if (urlSearchParams.get("defaultProcessValues"))
{ {
if(!defaultProcessValues) if (!defaultProcessValues)
{ {
defaultProcessValues = {} defaultProcessValues = {};
} }
const values = JSON.parse(urlSearchParams.get("defaultProcessValues")); const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
for (let key in values) 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 () => setTimeout(async () =>
{ {
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label}); doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await qController.processStep( const processResponse = await qController.processStep(
processName, processName,
@ -1914,7 +1920,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
setTimeout(async () => setTimeout(async () =>
{ {
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label}); doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep( const processResponse = await Client.getInstance().processStep(
processName, processName,
@ -1938,20 +1944,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
// run any sub-form pre-submit callbacks that are registered // // 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(); const {maySubmit, values: moreValues} = subFormPreSubmitCallbacks[i].callback();
if(!maySubmit) if (!maySubmit)
{ {
console.log(`May not submit form, per callback: ${subFormPreSubmitCallbacks[i].name}`); console.log(`May not submit form, per callback: ${subFormPreSubmitCallbacks[i].name}`);
return; return;
} }
if(moreValues) if (moreValues)
{ {
for (let key in 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); 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 = {}; const formStyles: any = {};
if (isWidget) if (isWidget)
{ {

View File

@ -20,6 +20,7 @@
*/ */
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; 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 {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; 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 ProcessRun from "qqq/pages/processes/ProcessRun";
import ColumnStats from "qqq/pages/records/query/ColumnStats"; import ColumnStats from "qqq/pages/records/query/ColumnStats";
import DataGridUtils from "qqq/utils/DataGridUtils"; import DataGridUtils from "qqq/utils/DataGridUtils";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils"; import FilterUtils from "qqq/utils/qqq/FilterUtils";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils"; import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
@ -89,6 +90,7 @@ export type QueryScreenUsage = "queryScreen" | "reportSetup"
interface Props interface Props
{ {
table?: QTableMetaData; table?: QTableMetaData;
apiVersion?: ApiVersion;
launchProcess?: QProcessMetaData; launchProcess?: QProcessMetaData;
usage?: QueryScreenUsage; usage?: QueryScreenUsage;
isModal?: boolean; isModal?: boolean;
@ -101,9 +103,10 @@ interface Props
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
// define possible values for our pageState variable // // 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 qController = Client.getInstance();
const qControllerV1 = Client.getInstanceV1();
/******************************************************************************* /*******************************************************************************
** function to produce standard version of the screen while we're "loading" ** 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. ** 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 tableName = table.name;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -979,7 +982,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
} }
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables()); 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}`); console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
countResults[thisQueryId] = []; countResults[thisQueryId] = [];
@ -998,7 +1002,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
setLastFetchedQFilterJSON(JSON.stringify(queryFilter)); setLastFetchedQFilterJSON(JSON.stringify(queryFilter));
setLastFetchedVariant(tableVariant); 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}`); console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results; queryResults[thisQueryId] = results;
@ -1141,6 +1146,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handlePageNumberChange = (page: number) => const handlePageNumberChange = (page: number) =>
{ {
setPageNumber(page); setPageNumber(page);
setLoading(true);
}; };
/******************************************************************************* /*******************************************************************************
@ -1149,6 +1155,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handleRowsPerPageChange = (size: number) => const handleRowsPerPageChange = (size: number) =>
{ {
setRowsPerPage(size); setRowsPerPage(size);
setLoading(true);
view.rowsPerPage = size; view.rowsPerPage = size;
doSetView(view); doSetView(view);
@ -1672,8 +1679,9 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
{ {
if (savedViewRecord == null) 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(); doClearCurrentSavedView();
activateView(buildTableDefaultView(tableMetaData));
return; return;
} }
@ -2435,23 +2443,33 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
setMetaData(metaData); setMetaData(metaData);
const tableMetaData = await qController.loadTableMetaData(tableName); try
setTableMetaData(tableMetaData); {
setTableLabel(tableMetaData.label); // 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 setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks) setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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. // // 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 // // but also used when user selects new-view from the view menu //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const newDefaultView = buildTableDefaultView(tableMetaData); const newDefaultView = buildTableDefaultView(tableMetaData);
setTableDefaultView(newDefaultView); 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 = <Box py={3}><Alert severity="error">{alertContent}</Alert></Box>;
return isModal ? errorBody : <BaseLayout>{errorBody}</BaseLayout>;
}
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
// render a loading screen if the page state isn't ready // // 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 = { RecordQuery.defaultProps = {
table: null, table: null,
apiVersion: null,
usage: "queryScreen", usage: "queryScreen",
launchProcess: null, launchProcess: null,
isModal: false, isModal: false,

View File

@ -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) if (!asyncLoadInited)
{ {
setAsyncLoadInited(true); setAsyncLoadInited(true);
@ -472,11 +500,16 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// load processes that the routing needs to respect // // 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 allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true); // these include hidden ones (e.g., to find the bulks)
const runRecordScriptProcess = metaData?.processes.get("runRecordScript"); const genericProcesses = getGenericProcesses(metaData);
if (runRecordScriptProcess)
for (let genericProcess of genericProcesses)
{ {
allTableProcesses.unshift(runRecordScriptProcess); if (genericProcess)
{
allTableProcesses.unshift(genericProcess);
}
} }
setAllTableProcesses(allTableProcesses); setAllTableProcesses(allTableProcesses);
if (launchingProcess) 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); 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 = ( const renderActionsMenu = (
<Menu <Menu
@ -785,11 +817,14 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
))} ))}
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />} {(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
{ {
runRecordScriptProcess && getGenericProcesses(metaData).map((process) =>
<MenuItem key={runRecordScriptProcess.name} onClick={() => processClicked(runRecordScriptProcess)}> (
<ListItemIcon><Icon>{runRecordScriptProcess.iconName ?? "arrow_forward"}</Icon></ListItemIcon> process &&
{runRecordScriptProcess.label} <MenuItem key={process.name} onClick={() => processClicked(process)}>
</MenuItem> <ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label}
</MenuItem>
))
} }
<MenuItem onClick={() => navigate("dev")}> <MenuItem onClick={() => navigate("dev")}>
<ListItemIcon><Icon>code</Icon></ListItemIcon> <ListItemIcon><Icon>code</Icon></ListItemIcon>
@ -969,7 +1004,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
{ {
notFoundMessage notFoundMessage
? ?
<Alert color="error" sx={{mb: 3}}>{notFoundMessage}</Alert> <Alert color="error" sx={{mb: 3}} icon={<Icon>warning</Icon>}>{notFoundMessage}</Alert>
: :
<Box pb={3}> <Box pb={3}>
{ {

View File

@ -303,10 +303,15 @@ input[type="search"]::-webkit-search-results-decoration
.MuiTablePagination-root .MuiSvgIcon-root .MuiTablePagination-root .MuiSvgIcon-root
{ {
display: inline; display: inline;
color: gray; color: rgba(0, 0, 0, 0.54);
right: 0.125rem; right: 0.125rem;
} }
.MuiTablePagination-root .Mui-disabled .MuiSvgIcon-root
{
color: rgba(0, 0, 0, 0.16);
}
.devDocumentation ul > li .devDocumentation ul > li
{ {
margin-left: 30px; margin-left: 30px;

View File

@ -20,6 +20,7 @@
*/ */
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import {QControllerV1} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
/******************************************************************************* /*******************************************************************************
@ -29,6 +30,7 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
class Client class Client
{ {
private static qController: QController; private static qController: QController;
private static qControllerV1: QControllerV1;
private static unauthorizedCallback: () => void; private static unauthorizedCallback: () => void;
private static handleException(exception: QException) private static handleException(exception: QException)
@ -54,6 +56,22 @@ class Client
return this.qController; return this.qController;
} }
public static getInstanceV1(path: string = "/qqq/v1")
{
if (this.qControllerV1 == null)
{
this.qControllerV1 = new QControllerV1(path, this.handleException);
}
return this.qControllerV1;
}
public static setGotAuthenticationInAllControllers()
{
Client.getInstance().setGotAuthentication();
Client.getInstanceV1().setGotAuthentication();
}
static setUnauthorizedCallback(unauthorizedCallback: () => void) static setUnauthorizedCallback(unauthorizedCallback: () => void)
{ {
Client.unauthorizedCallback = unauthorizedCallback; Client.unauthorizedCallback = unauthorizedCallback;

View File

@ -108,6 +108,12 @@ class FilterUtils
const criteria = queryFilter.criteria[i]; const criteria = queryFilter.criteria[i];
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName); let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
if(!field)
{
console.warn(`Field ${criteria.fieldName} not found in tableMetaData - unable to clean up values for it..`);
return;
}
let values = criteria.values; let values = criteria.values;
let hasFilterVariable = false; let hasFilterVariable = false;
@ -401,21 +407,21 @@ class FilterUtils
{ {
const expression = new ThisOrLastPeriodExpression(value); const expression = new ThisOrLastPeriodExpression(value);
let startOfPrefix = ""; let startOfPrefix = "";
if (fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS") if (fieldMetaData?.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
{ {
startOfPrefix = "start of "; startOfPrefix = "start of ";
} }
labels.push(`${startOfPrefix}${expression.toString()}`); labels.push(`${startOfPrefix}${expression.toString()}`);
} }
else if (fieldMetaData.type == QFieldType.BOOLEAN) else if (fieldMetaData?.type == QFieldType.BOOLEAN)
{ {
labels.push(value == true ? "yes" : "no"); labels.push(value == true ? "yes" : "no");
} }
else if (fieldMetaData.type == QFieldType.DATE_TIME) else if (fieldMetaData?.type == QFieldType.DATE_TIME)
{ {
labels.push(ValueUtils.formatDateTime(value)); labels.push(ValueUtils.formatDateTime(value));
} }
else if (fieldMetaData.type == QFieldType.DATE) else if (fieldMetaData?.type == QFieldType.DATE)
{ {
labels.push(ValueUtils.formatDate(value)); labels.push(ValueUtils.formatDate(value));
} }

View File

@ -88,7 +88,7 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
const value = record.values.get(field.name); const value = record.values.get(field.name);
if (field.possibleValueSourceName && value) if (field.possibleValueSourceName && value)
{ {
const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], null, record.values, "form"); const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], [], record.values, "form");
if (possibleValues && possibleValues.length > 0) if (possibleValues && possibleValues.length > 0)
{ {
record.displayValues.set(field.name, possibleValues[0].label); record.displayValues.set(field.name, possibleValues[0].label);
@ -102,7 +102,7 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
if (!loaded) if (!loaded)
{ {
return (<div>Loading...</div>); return (<Box py={"1rem"}>Loading...</Box>);
} }
const { const {
@ -111,6 +111,18 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
} = DynamicFormUtils.getFormData(fields); } = DynamicFormUtils.getFormData(fields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, record ? record.displayValues : new Map()); DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, record ? record.displayValues : new Map());
const otherValuesMap = new Map<string, any>();
record.values.forEach((value, key) => otherValuesMap.set(key, value));
for (let fieldName in dynamicFormFields)
{
const dynamicFormField = dynamicFormFields[fieldName];
if (dynamicFormField.possibleValueProps)
{
dynamicFormField.possibleValueProps.otherValues = otherValuesMap;
}
}
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
// re-introduce these two context providers, in case the child calls this // // re-introduce these two context providers, in case the child calls this //
// method under a different root... maybe this should be optional per a param? // // method under a different root... maybe this should be optional per a param? //
@ -190,8 +202,14 @@ function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, acti
const qController = Client.getInstance(); const qController = Client.getInstance();
const qInstance = await qController.loadMetaData(); const qInstance = await qController.loadMetaData();
const queryStringParts: string[] = [];
for (let key of record?.values?.keys())
{
queryStringParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(record.values.get(key))}`);
}
setWidgetMetaData(qInstance.widgets.get(widgetName)); setWidgetMetaData(qInstance.widgets.get(widgetName));
setWidgetData(await qController.widget(widgetName, null)); // todo queryParams... ? setWidgetData(await qController.widget(widgetName, queryStringParts.join("&")));
setReady(true); setReady(true);
})(); })();
@ -199,7 +217,7 @@ function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, acti
if (!ready) if (!ready)
{ {
return (<div>Loading...</div>); return (<Box py={"1rem"}>Loading...</Box>);
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -59,4 +59,5 @@ module.exports = function (app)
app.use("/*api", getRequestHandler()); app.use("/*api", getRequestHandler());
app.use("/qqq/*", getRequestHandler()); app.use("/qqq/*", getRequestHandler());
app.use("/dynamic-qfmd-components/*", getRequestHandler()); app.use("/dynamic-qfmd-components/*", getRequestHandler());
app.use("/material-dashboard-backend/*", getRequestHandler());
}; };

View File

@ -181,7 +181,12 @@ public class QBaseSeleniumTest
.withRouteToFile("/metaData/table/city", "metaData/table/person.json") .withRouteToFile("/metaData/table/city", "metaData/table/person.json")
.withRouteToFile("/metaData/table/script", "metaData/table/script.json") .withRouteToFile("/metaData/table/script", "metaData/table/script.json")
.withRouteToFile("/metaData/table/scriptRevision", "metaData/table/scriptRevision.json") .withRouteToFile("/metaData/table/scriptRevision", "metaData/table/scriptRevision.json")
.withRouteToFile("/qqq/v1/metaData/table/person", "qqq/v1/metaData/table/person.json")
.withRouteToFile("/qqq/v1/metaData/table/city", "qqq/v1/metaData/table/city.json")
.withRouteToFile("/qqq/v1/metaData/table/script", "qqq/v1/metaData/table/script.json")
.withRouteToFile("/qqq/v1/metaData/table/scriptRevision", "qqq/v1/metaData/table/scriptRevision.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json"); .withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
} }

View File

@ -46,7 +46,9 @@ public class AppPageNavTest extends QBaseSeleniumTest
.withRouteToString("/widget/QuickSightChartRenderer", """ .withRouteToString("/widget/QuickSightChartRenderer", """
{"url": "http://www.google.com"}""") {"url": "http://www.google.com"}""")
.withRouteToFile("/data/person/count", "data/person/count.json") .withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/city/count", "data/city/count.json"); .withRouteToFile("/data/city/count", "data/city/count.json")
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
.withRouteToFile("/qqq/v1/table/city/count", "qqq/v1/table/city/count.json");
} }

View File

@ -63,6 +63,8 @@ public class BulkEditTest extends QBaseSeleniumTest
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json"); qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json"); qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
qSeleniumJavalin.withRouteToFile("/data/person/variants", "data/person/variants.json"); qSeleniumJavalin.withRouteToFile("/data/person/variants", "data/person/variants.json");
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json");
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json");
qSeleniumJavalin.withRouteToString("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records", "[]"); qSeleniumJavalin.withRouteToString("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records", "[]");
} }

View File

@ -51,12 +51,15 @@ public class SavedReportTest extends QBaseSeleniumTest
super.addJavalinRoutes(qSeleniumJavalin); super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin qSeleniumJavalin
.withRouteToFile("/metaData/table/savedReport", "metaData/table/savedReport.json") .withRouteToFile("/metaData/table/savedReport", "metaData/table/savedReport.json")
.withRouteToFile("/qqq/v1/metaData/table/savedReport", "qqq/v1/metaData/table/savedReport.json")
.withRouteToFile("/widget/reportSetupWidget", "widget/reportSetupWidget.json") .withRouteToFile("/widget/reportSetupWidget", "widget/reportSetupWidget.json")
.withRouteToFile("/widget/pivotTableSetupWidget", "widget/pivotTableSetupWidget.json") .withRouteToFile("/widget/pivotTableSetupWidget", "widget/pivotTableSetupWidget.json")
.withRouteToFile("/data/savedReport/possibleValues/tableName", "data/savedReport/possibleValues/tableName.json") .withRouteToFile("/data/savedReport/possibleValues/tableName", "data/savedReport/possibleValues/tableName.json")
.withRouteToFile("/data/person/count", "data/person/count.json") .withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/person/query", "data/person/index.json") .withRouteToFile("/data/person/query", "data/person/index.json")
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
; ;
} }
@ -93,8 +96,8 @@ public class SavedReportTest extends QBaseSeleniumTest
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
qSeleniumJavalin.beginCapture(); qSeleniumJavalin.beginCapture();
qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns").click(); qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns").click();
qSeleniumJavalin.waitForCapturedPath("/data/person/count"); qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
qSeleniumJavalin.waitForCapturedPath("/data/person/query"); qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib); QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);

View File

@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
qSeleniumJavalin qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.json") .withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/person/query", "data/person/index.json") .withRouteToFile("/data/person/query", "data/person/index.json")
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json") .withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
.withRouteToFile("/data/person/variants", "data/person/variants.json") .withRouteToFile("/data/person/variants", "data/person/variants.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json"); .withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");

View File

@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlBasicModeTest extends QBaseSeleniumTest
qSeleniumJavalin qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.json") .withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/person/query", "data/person/index.json") .withRouteToFile("/data/person/query", "data/person/index.json")
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json") .withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
.withRouteToFile("/data/person/variants", "data/person/variants.json") .withRouteToFile("/data/person/variants", "data/person/variants.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json"); .withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");

View File

@ -48,6 +48,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.json") .withRouteToFile("/data/person/count", "data/person/count.json")
.withRouteToFile("/data/person/query", "data/person/index.json") .withRouteToFile("/data/person/query", "data/person/index.json")
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
.withRouteToFile("/data/person/variants", "data/person/variants.json") .withRouteToFile("/data/person/variants", "data/person/variants.json")
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json") .withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json"); .withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
@ -79,8 +81,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
String idEquals1FilterSubstring = """ String idEquals1FilterSubstring = """
{"fieldName":"id","operator":"EQUALS","values":["1"]}"""; {"fieldName":"id","operator":"EQUALS","values":["1"]}""";
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/count", idEquals1FilterSubstring); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/count", idEquals1FilterSubstring);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", idEquals1FilterSubstring); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", idEquals1FilterSubstring);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
/////////////////////////////////////// ///////////////////////////////////////
@ -99,8 +101,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
// assert that query & count both no longer have the filter value // // assert that query & count both no longer have the filter value //
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count"); CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query"); CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring); assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring); assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
@ -132,9 +134,9 @@ public class QueryScreenTest extends QBaseSeleniumTest
String expectedFilterContents2 = """ String expectedFilterContents2 = """
"booleanOperator":"OR\""""; "booleanOperator":"OR\"""";
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents0); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents0);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents1);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents2);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
} }
@ -208,7 +210,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin.beginCapture(); qSeleniumJavalin.beginCapture();
queryScreenLib.setBasicBooleanFilter(fieldLabel, operatorLabel); queryScreenLib.setBasicBooleanFilter(fieldLabel, operatorLabel);
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex); queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
} }
@ -222,7 +224,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin.beginCapture(); qSeleniumJavalin.beginCapture();
queryScreenLib.setBasicFilterPossibleValues(fieldLabel, operatorLabel, values); queryScreenLib.setBasicFilterPossibleValues(fieldLabel, operatorLabel, values);
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex); queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
} }
@ -268,7 +270,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
queryScreenLib.addAdvancedQueryFilterInput(0, fieldLabel, operatorLabel, value, null); queryScreenLib.addAdvancedQueryFilterInput(0, fieldLabel, operatorLabel, value, null);
qSeleniumLib.clickBackdrop(); qSeleniumLib.clickBackdrop();
queryScreenLib.waitForAdvancedQueryStringMatchingRegex(expectQueryStringRegex); queryScreenLib.waitForAdvancedQueryStringMatchingRegex(expectQueryStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains); qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
queryScreenLib.clickAdvancedFilterClearIcon(); queryScreenLib.clickAdvancedFilterClearIcon();
} }

View File

@ -58,6 +58,8 @@ public class SavedViewsTest extends QBaseSeleniumTest
super.addJavalinRoutes(qSeleniumJavalin); super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json"); qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json"); qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json");
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json");
qSeleniumJavalin.withRouteToFile("/data/person/*", "data/person/1701.json"); qSeleniumJavalin.withRouteToFile("/data/person/*", "data/person/1701.json");
} }
@ -135,7 +137,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2")); qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
queryScreenLib.assertSavedViewNameOnScreen("Some People"); queryScreenLib.assertSavedViewNameOnScreen("Some People");
qSeleniumLib.waitForSelectorContaining("DIV", "Unsaved Changes"); qSeleniumLib.waitForSelectorContaining("DIV", "Unsaved Changes");
CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query"); CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
assertTrue(capturedContext.getBody().contains("Kelkhoff")); assertTrue(capturedContext.getBody().contains("Kelkhoff"));
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
@ -162,7 +164,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
qSeleniumLib.waitForSelectorContaining("A", "Person").click(); qSeleniumLib.waitForSelectorContaining("A", "Person").click();
qSeleniumLib.waitForCondition("Current URL should not have filter id", () -> !driver.getCurrentUrl().endsWith("/person/savedView/2")); qSeleniumLib.waitForCondition("Current URL should not have filter id", () -> !driver.getCurrentUrl().endsWith("/person/savedView/2"));
qSeleniumLib.waitForSelectorContaining("BUTTON", "Save View As"); qSeleniumLib.waitForSelectorContaining("BUTTON", "Save View As");
capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query"); capturedContext = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
assertTrue(capturedContext.getBody().matches("(?s).*id.*LESS_THAN.*10.*")); assertTrue(capturedContext.getBody().matches("(?s).*id.*LESS_THAN.*10.*"));
qSeleniumJavalin.endCapture(); qSeleniumJavalin.endCapture();
} }

View File

@ -0,0 +1,166 @@
{
"name": "person",
"label": "Person",
"isHidden": false,
"primaryKeyField": "id",
"iconName": "person",
"deletePermission": true,
"editPermission": true,
"insertPermission": true,
"readPermission": true,
"fields": {
"firstName": {
"name": "firstName",
"label": "First Name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
"lastName": {
"name": "lastName",
"label": "Last Name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"displayFormat": "%s"
},
"annualSalary": {
"name": "annualSalary",
"label": "Annual Salary",
"type": "DECIMAL",
"isRequired": false,
"isEditable": true,
"displayFormat": "$%,.2f"
},
"modifyDate": {
"name": "modifyDate",
"label": "Modify Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"daysWorked": {
"name": "daysWorked",
"label": "Days Worked",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"displayFormat": "%,d"
},
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"birthDate": {
"name": "birthDate",
"label": "Birth Date",
"type": "DATE",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"isEmployed": {
"name": "isEmployed",
"label": "Is Employed",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"homeCityId": {
"name": "homeCityId",
"label": "Home City",
"type": "INTEGER",
"possibleValueSourceName": "city",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"email": {
"name": "email",
"label": "Email",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"createDate": {
"name": "createDate",
"label": "Create Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
}
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": [
"id",
"firstName",
"lastName"
],
"icon": {
"name": "badge"
},
"isHidden": false
},
{
"name": "basicInfo",
"label": "Basic Info",
"tier": "T2",
"fieldNames": [
"email",
"birthDate"
],
"icon": {
"name": "dataset"
},
"isHidden": false
},
{
"name": "employmentInfo",
"label": "Employment Info",
"tier": "T2",
"fieldNames": [
"isEmployed",
"annualSalary",
"daysWorked"
],
"icon": {
"name": "work"
},
"isHidden": false
},
{
"name": "dates",
"label": "Dates",
"tier": "T3",
"fieldNames": [
"createDate",
"modifyDate"
],
"icon": {
"name": "calendar_month"
},
"isHidden": false
}
],
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_DELETE",
"TABLE_INSERT",
"TABLE_UPDATE"
]
}

View File

@ -0,0 +1,216 @@
{
"name": "savedReport",
"label": "Saved Report",
"isHidden": false,
"primaryKeyField": "id",
"iconName": "article",
"fields": {
"queryFilterJson": {
"name": "queryFilterJson",
"label": "Query Filter",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"columnsJson": {
"name": "columnsJson",
"label": "Columns",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"inputFieldsJson": {
"name": "inputFieldsJson",
"label": "Input Fields",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"pivotTableJson": {
"name": "pivotTableJson",
"label": "Pivot Table",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"modifyDate": {
"name": "modifyDate",
"label": "Modify Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
},
"label": {
"name": "label",
"label": "Report Name",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
},
"userId": {
"name": "userId",
"label": "User Id",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"tableName": {
"name": "tableName",
"label": "Table",
"type": "STRING",
"isRequired": true,
"isEditable": true,
"isHeavy": false,
"possibleValueSourceName": "tables",
"displayFormat": "%s"
},
"createDate": {
"name": "createDate",
"label": "Create Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
}
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": [
"id",
"label",
"tableName"
],
"icon": {
"name": "badge"
},
"isHidden": false
},
{
"name": "filtersAndColumns",
"label": "Filters and Columns",
"tier": "T2",
"widgetName": "reportSetupWidget",
"icon": {
"name": "table_chart"
},
"isHidden": false
},
{
"name": "pivotTable",
"label": "Pivot Table",
"tier": "T2",
"widgetName": "pivotTableSetupWidget",
"icon": {
"name": "pivot_table_chart"
},
"isHidden": false
},
{
"name": "data",
"label": "Data",
"tier": "T2",
"fieldNames": [
"queryFilterJson",
"columnsJson",
"pivotTableJson"
],
"icon": {
"name": "text_snippet"
},
"isHidden": true
},
{
"name": "hidden",
"label": "Hidden",
"tier": "T2",
"fieldNames": [
"inputFieldsJson",
"userId"
],
"icon": {
"name": "text_snippet"
},
"isHidden": true
},
{
"name": "dates",
"label": "Dates",
"tier": "T3",
"fieldNames": [
"createDate",
"modifyDate"
],
"icon": {
"name": "calendar_month"
},
"isHidden": false
}
],
"exposedJoins": [],
"supplementalTableMetaData": {
"materialDashboard": {
"fieldRules": [
{
"trigger": "ON_CHANGE",
"sourceField": "tableName",
"action": "CLEAR_TARGET_FIELD",
"targetField": "queryFilterJson"
},
{
"trigger": "ON_CHANGE",
"sourceField": "tableName",
"action": "CLEAR_TARGET_FIELD",
"targetField": "columnsJson"
},
{
"trigger": "ON_CHANGE",
"sourceField": "tableName",
"action": "CLEAR_TARGET_FIELD",
"targetField": "pivotTableJson"
}
],
"type": "materialDashboard"
}
},
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"QUERY_STATS",
"TABLE_UPDATE",
"TABLE_INSERT",
"TABLE_DELETE"
],
"readPermission": true,
"insertPermission": true,
"editPermission": true,
"deletePermission": true,
"usesVariants": false
}

View File

@ -0,0 +1,137 @@
{
"name": "script",
"label": "Script",
"isHidden": false,
"primaryKeyField": "id",
"iconName": "data_object",
"fields": {
"modifyDate": {
"name": "modifyDate",
"label": "Modify Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"name": {
"name": "name",
"label": "Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"displayFormat": "%s"
},
"currentScriptRevisionId": {
"name": "currentScriptRevisionId",
"label": "Current Script Revision",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"possibleValueSourceName": "scriptRevision",
"displayFormat": "%s",
"adornments": [
{
"type": "LINK",
"values": {
"toRecordFromTable": "scriptRevision"
}
}
]
},
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"tableName": {
"name": "tableName",
"label": "Table Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"possibleValueSourceName": "tables",
"displayFormat": "%s"
},
"createDate": {
"name": "createDate",
"label": "Create Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"displayFormat": "%s"
},
"scriptTypeId": {
"name": "scriptTypeId",
"label": "Script Type",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"possibleValueSourceName": "scriptType",
"displayFormat": "%s",
"adornments": [
{
"type": "LINK",
"values": {
"toRecordFromTable": "scriptType"
}
}
]
}
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": [
"id",
"name",
"scriptTypeId",
"tableName",
"currentScriptRevisionId"
],
"icon": {
"name": "badge"
},
"isHidden": false
},
{
"name": "contents",
"label": "Contents",
"tier": "T2",
"widgetName": "scriptViewer",
"icon": {
"name": "data_object"
},
"isHidden": false
},
{
"name": "dates",
"label": "Dates",
"tier": "T3",
"fieldNames": [
"createDate",
"modifyDate"
],
"icon": {
"name": "calendar_month"
},
"isHidden": false
}
],
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_INSERT",
"TABLE_DELETE",
"TABLE_UPDATE"
],
"readPermission": true,
"insertPermission": true,
"editPermission": true,
"deletePermission": true
}

View File

@ -0,0 +1,150 @@
{
"name": "scriptRevision",
"label": "Script Revision",
"isHidden": false,
"primaryKeyField": "id",
"iconName": "history_edu",
"fields": {
"scriptId": {
"name": "scriptId",
"label": "Script",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"possibleValueSourceName": "script",
"displayFormat": "%s",
"adornments": [
{
"type": "SIZE",
"values": {
"width": "large"
}
},
{
"type": "LINK",
"values": {
"toRecordFromTable": "script"
}
}
]
},
"apiName": {
"name": "apiName",
"label": "API Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"possibleValueSourceName": "apiName",
"displayFormat": "%s"
},
"sequenceNo": {
"name": "sequenceNo",
"label": "Sequence No",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"apiVersion": {
"name": "apiVersion",
"label": "API Version",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"possibleValueSourceName": "apiVersion",
"displayFormat": "%s"
},
"commitMessage": {
"name": "commitMessage",
"label": "Commit Message",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"modifyDate": {
"name": "modifyDate",
"label": "Modify Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
},
"author": {
"name": "author",
"label": "Author",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
"id": {
"name": "id",
"label": "Id",
"type": "INTEGER",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
},
"createDate": {
"name": "createDate",
"label": "Create Date",
"type": "DATE_TIME",
"isRequired": false,
"isEditable": false,
"isHeavy": false,
"displayFormat": "%s"
}
},
"sections": [
{
"name": "identity",
"label": "Identity",
"tier": "T1",
"fieldNames": [
"id",
"scriptId",
"sequenceNo"
],
"icon": {
"name": "badge"
},
"isHidden": false
},
{
"name": "dates",
"label": "Dates",
"tier": "T3",
"fieldNames": [
"createDate",
"modifyDate"
],
"icon": {
"name": "calendar_month"
},
"isHidden": false
}
],
"exposedJoins": [],
"capabilities": [
"TABLE_COUNT",
"TABLE_GET",
"TABLE_QUERY",
"TABLE_INSERT",
"TABLE_UPDATE",
"QUERY_STATS"
],
"readPermission": true,
"insertPermission": true,
"editPermission": true,
"deletePermission": true,
"usesVariants": false
}

View File

@ -0,0 +1,3 @@
{
"records": []
}

View File

@ -0,0 +1,245 @@
{
"records": [
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 623577,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z",
"clientId": 107,
"auditDetail.id": 278660,
"auditDetail.auditId": 623577,
"auditDetail.message": "Set First Name to John",
"auditDetail.fieldName": "firstName",
"auditDetail.newValue": "John"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "623577",
"recordId": "1191682",
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 623577,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z",
"clientId": 107,
"auditDetail.id": 278661,
"auditDetail.auditId": 623577,
"auditDetail.message": "Removed Doe from Last Name",
"auditDetail.fieldName": "lastName",
"auditDetail.oldValue": "Doe"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "623577",
"recordId": "1191682",
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 623577,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z",
"clientId": 107,
"auditDetail.id": 278662,
"auditDetail.auditId": 623577,
"auditDetail.message": "Set Client to ACME",
"auditDetail.fieldName": "clientId",
"auditDetail.oldValue": "BetaMax",
"auditDetail.newValue": "ACME"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "623577",
"recordId": "1191682",
"message": "Record was Inserted",
"timestamp": "2023-02-17T14:11:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 624804,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Edited",
"timestamp": "2023-02-17T14:13:16Z",
"clientId": 107,
"auditDetail.id": 278990,
"auditDetail.auditId": 624804,
"auditDetail.message": "Set SLA Expected Service Days to 2",
"auditDetail.fieldName": "slaExpectedServiceDays",
"auditDetail.newValue": "2"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "624804",
"recordId": "1191682",
"message": "Record was Edited",
"timestamp": "2023-02-17T14:13:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 624804,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Edited",
"timestamp": "2023-02-17T14:13:16Z",
"clientId": 107,
"auditDetail.id": 278991,
"auditDetail.auditId": 624804,
"auditDetail.message": "Set SLA Status to \"Pending\"",
"auditDetail.fieldName": "slaStatusId",
"auditDetail.newValue": "Pending"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "624804",
"recordId": "1191682",
"message": "Record was Edited",
"timestamp": "2023-02-17T14:13:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 624809,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Audit message here",
"timestamp": "2023-02-17T14:13:16Z",
"clientId": 107,
"auditDetail.id": 279000,
"auditDetail.auditId": 624809,
"auditDetail.message": "This is a detail message"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "624809",
"recordId": "1191682",
"message": "Audit message here",
"timestamp": "2023-02-17T14:13:16Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 737694,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Edited",
"timestamp": "2023-02-17T17:22:08Z",
"clientId": 107,
"auditDetail.id": 299222,
"auditDetail.auditId": 737694,
"auditDetail.message": "Set Estimated Delivery Date Time to 2023-02-18 07:00:00 PM EST",
"auditDetail.fieldName": "estimatedDeliveryDateTime",
"auditDetail.newValue": "2023-02-18 07:00:00 PM EST"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "737694",
"recordId": "1191682",
"message": "Record was Edited",
"timestamp": "2023-02-17T17:22:08Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 737694,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Record was Edited",
"timestamp": "2023-02-17T17:22:08Z",
"clientId": 107,
"auditDetail.id": 299223,
"auditDetail.auditId": 737694,
"auditDetail.message": "Changed Parcel Tracking Status from \"Unknown\" to \"Pre Transit\"",
"auditDetail.fieldName": "parcelTrackingStatusId",
"auditDetail.oldValue": "Unknown",
"auditDetail.newValue": "Pre Transit"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "737694",
"recordId": "1191682",
"message": "Record was Edited",
"timestamp": "2023-02-17T17:22:08Z"
}
},
{
"tableName": "audit",
"recordLabel": "Parcel 1191682",
"values": {
"id": 737695,
"auditTableId": 4,
"auditUserId": 2,
"recordId": 1191682,
"message": "Updating Parcel based on updated tracking details",
"timestamp": "2023-02-17T17:22:09Z",
"clientId": 107,
"auditDetail.id": 299224,
"auditDetail.auditId": 737695,
"auditDetail.message": "Set Parcel Tracking Status to Pre Transit based on most recent tracking update: Shipment information sent to FedEx"
},
"displayValues": {
"auditTableId": "Parcel",
"auditUserId": "QQQ User",
"clientId": "ACME",
"id": "737695",
"recordId": "1191682",
"message": "Updating Parcel based on updated tracking details",
"timestamp": "2023-02-17T17:22:09Z"
}
}
]
}

View File

@ -0,0 +1,3 @@
{
"count": 101406
}

View File

@ -0,0 +1,16 @@
{
"tableName": "person",
"recordLabel": "John Doe",
"values": {
"name": "John Doe",
"id": 1710,
"createDate": "2022-08-30T00:31:00Z",
"modifyDate": "2022-08-30T00:31:00Z"
},
"displayValues": {
"name": "John Doe",
"id": 1710,
"createDate": "2022-08-30T00:31:00Z",
"modifyDate": "2022-08-30T00:31:00Z"
}
}

View File

@ -0,0 +1,3 @@
{
"count": 101406
}

View File

@ -0,0 +1,276 @@
{
"record": {
"tableName": "client",
"recordLabel": "John Doe",
"values": {
"name": "John Doe",
"id": 120,
"deposcoOrderOptimizationCoolingScriptId": 2,
"createDate": "2022-08-30T00:31:00Z",
"modifyDate": "2023-02-19T01:28:30Z",
"isFulfillmentCenter": false,
"infoplusLobId": 18698,
"deposcoBusinessUnitName": "TRIFECTA",
"deposcoBusinessUnitId": 77,
"optimizationConfigId": 1,
"nfCode": "Client 224"
},
"displayValues": {
"optimizationConfigId": "Client: 120",
"name": "John Doe",
"id": "120",
"deposcoOrderOptimizationCoolingScriptId": "2",
"createDate": "2022-08-30T00:31:00Z",
"modifyDate": "2023-02-19T01:28:30Z",
"isFulfillmentCenter": "No",
"infoplusLobId": "18698",
"deposcoBusinessUnitName": "TRIFECTA",
"deposcoBusinessUnitId": "77",
"nfCode": "Client 224"
}
},
"associatedScripts": [
{
"testInputFields": [
{
"name": "selectedTimeInTransitDays",
"label": "Selected Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "standardTimeInTransitDays",
"label": "Standard Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"scriptType": {
"tableName": "scriptType",
"values": {
"name": "Deposco Order Optimization Cooling",
"id": 2,
"createDate": "2022-10-31T19:06:50Z",
"modifyDate": "2022-10-31T19:06:50Z"
}
},
"scriptRevisions": [
{
"tableName": "scriptRevision",
"values": {
"id": 1,
"contents": "1;",
"createDate": "2023-02-19T01:28:30Z",
"modifyDate": "2023-02-19T01:28:30Z",
"scriptId": 2,
"sequenceNo": 1,
"commitMessage": "Initial version",
"author": "Darin Kelkhoff"
}
}
],
"testOutputFields": [
{
"name": "sku",
"label": "Sku",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "quantityPerCarton",
"label": "Quantity Per Carton",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "useClientProvidedCoolingSolution",
"label": "Use Client Provided Cooling Solution",
"type": "BOOLEAN",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "reason",
"label": "Reason",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"script": {
"tableName": "script",
"values": {
"name": "John Doe - Deposco Order Optimization Cooling",
"id": 2,
"scriptTypeId": 2,
"createDate": "2023-02-19T01:28:30Z",
"modifyDate": "2023-02-19T01:28:30Z",
"currentScriptRevisionId": 1
}
},
"associatedScript": {
"fieldName": "deposcoOrderOptimizationCoolingScriptId",
"scriptTypeId": 2,
"scriptTester": {
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationCoolingScript",
"codeType": "JAVA",
"codeUsage": "SCRIPT_TESTER"
}
}
},
{
"testInputFields": [
{
"name": "selectedTimeInTransitDays",
"label": "Selected Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "standardTimeInTransitDays",
"label": "Standard Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "runtimeWeekday",
"label": "Runtime Weekday",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"scriptType": {
"tableName": "scriptType",
"values": {
"name": "Deposco Order Optimization Batch Name",
"id": 1,
"createDate": "2022-10-31T19:06:50Z",
"modifyDate": "2022-10-31T19:06:50Z"
}
},
"testOutputFields": [
{
"name": "batchName",
"label": "Batch Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "reason",
"label": "Reason",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"associatedScript": {
"fieldName": "deposcoOrderOptimizationBatchNameScriptId",
"scriptTypeId": 1,
"scriptTester": {
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationBatchNameScript",
"codeType": "JAVA",
"codeUsage": "SCRIPT_TESTER"
}
}
},
{
"testInputFields": [
{
"name": "selectedTimeInTransitDays",
"label": "Selected Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "standardTimeInTransitDays",
"label": "Standard Time In Transit Days",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "runtimeWeekday",
"label": "Runtime Weekday",
"type": "INTEGER",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"scriptType": {
"tableName": "scriptType",
"values": {
"name": "Deposco Order Optimization Batch Name",
"id": 1,
"createDate": "2022-10-31T19:06:50Z",
"modifyDate": "2022-10-31T19:06:50Z"
}
},
"testOutputFields": [
{
"name": "batchName",
"label": "Batch Name",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
},
{
"name": "reason",
"label": "Reason",
"type": "STRING",
"isRequired": false,
"isEditable": true,
"isHeavy": false,
"displayFormat": "%s"
}
],
"associatedScript": {
"fieldName": "deposcoOrderOptimizationCartonizationScriptId",
"scriptTypeId": 1,
"scriptTester": {
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationBatchNameScript",
"codeType": "JAVA",
"codeUsage": "SCRIPT_TESTER"
}
}
}
]
}

View File

@ -0,0 +1,64 @@
{
"records": [
{
"tableName": "person",
"values": {
"id": 1,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-22T19:17:06",
"firstName": "Jonny",
"lastName": "Doe",
"birthDate": "1980-05-31",
"email": "jdoe@kingsrook.com"
}
},
{
"tableName": "person",
"values": {
"id": 2,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "James",
"lastName": "Maes",
"birthDate": "1980-05-15",
"email": "jmaes@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 3,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Tim",
"lastName": "Chamberlain",
"birthDate": "1976-05-28",
"email": "tchamberlain@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 4,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Tyler",
"lastName": "Samples",
"birthDate": "1986-05-28",
"email": "tsamples@mmltholdings.com"
}
},
{
"tableName": "person",
"values": {
"id": 5,
"createDate": "2022-07-23T00:17:00",
"modifyDate": "2022-07-23T00:17:00",
"firstName": "Garret",
"lastName": "Richardson",
"birthDate": "1981-01-01",
"email": "grichardson@mmltholdings.com"
}
}
]
}

View File

@ -0,0 +1,12 @@
{
"options": [
{
"id": 1,
"label": "St. Louis"
},
{
"id": 2,
"label": "Chesterfield"
}
]
}

View File

@ -0,0 +1,8 @@
{
"options": [
{
"id": 1,
"label": "St. Louis"
}
]
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,16 @@
{
"options": [
{
"id": "person",
"label": "Person"
},
{
"id": "city",
"label": "City"
},
{
"id": "savedReport",
"label": "Saved Report"
}
]
}

View File

@ -0,0 +1,22 @@
{
"tableName": "script",
"recordLabel": "Hello, Script",
"values": {
"name": "Hello, Script",
"id": 1,
"currentScriptRevisionId": 100,
"tableName": "client",
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z",
"scriptTypeId": 1
},
"displayValues": {
"tableName": "Client",
"scriptTypeId": "Record Script",
"name": "Hello, Script",
"currentScriptRevisionId": 100,
"id": "1",
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z"
}
}

View File

@ -0,0 +1,3 @@
{
"records": []
}

View File

@ -0,0 +1,36 @@
{
"tableName": "scriptRevision",
"recordLabel": "Hello, Script Revision",
"values": {
"id": "100",
"name": "Hello, Script Revision",
"sequenceNo": "22",
"commitMessage": "Initial checkin",
"author": "Jon Programmer",
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z"
},
"displayValues": {
"id": "1",
"name": "Hello, Script Revision",
"scriptId": "1",
"sequenceNo": "22",
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z"
},
"associatedRecords": {
"files": [
{
"tableName": "scriptRevisionFile",
"values": {
"id": 101,
"fileName": "Script.js",
"contents": "var hello;",
"scriptRevisionId": 100,
"createDate": "2023-06-23T21:59:57Z",
"modifyDate": "2023-06-23T21:59:57Z"
}
}
]
}
}

View File

@ -0,0 +1,32 @@
{
"records": [
{
"tableName": "scriptRevision",
"values": {
"contents": "var hello;",
"id": 100,
"sequenceNo": 2,
"commitMessage": "2nd commit",
"author": "Jon Programmer",
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z"
},
"displayValues": {
}
},
{
"tableName": "scriptRevision",
"values": {
"contents": "var goodBye;",
"id": 99,
"sequenceNo": 1,
"commitMessage": "Initial checkin",
"author": "Jane Programmer",
"createDate": "2023-02-17T00:47:51Z",
"modifyDate": "2023-02-17T00:47:51Z"
},
"displayValues": {
}
}
]
}

View File

@ -0,0 +1,13 @@
{
"tableName": "scriptType",
"recordLabel": "Record Script",
"values": {
"name": "Record Script",
"id": 1,
"createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z",
"fileMode": 1
},
"displayValues": {
}
}