Compare commits

..

33 Commits

Author SHA1 Message Date
a588d3b1c6 Fix path to developer.json fixture 2025-07-18 14:34:18 -05:00
5c2bf9e7b4 Merge pull request #89 from Kingsrook/feature/workflows-support
Feature/workflows support
2025-07-18 12:07:40 -05:00
6a004d6cdb Merge pull request #92 from Kingsrook/feature/sticky-record-buttons
put cancel & save (on insert/edit) and delete & edit (on view) button…
2025-07-18 11:15:05 -05:00
92a71bc62f Make RunFormAdjusterProcess be NOT-PROTECTED permissions 2025-07-14 15:55:16 -05:00
2c65826a91 Fix display value (labels) for PVS fields 2025-07-14 15:54:46 -05:00
86dcc90e1d Support passing possibleValueSourceFilter through to backend, specifically for the "standalone" use-case, where the field doesn't come from a table or process 2025-07-14 15:54:25 -05:00
90fd03ae46 Add omitExposedJoins prop throughout RecordQuery and all subcomponents. Initially for the FilterAndColumnsSetupWidget to allow some joins to not be exposed. 2025-07-14 15:21:58 -05:00
f0f09a8ff1 put cancel & save (on insert/edit) and delete & edit (on view) buttons into a sticky-bottom footer. also change modal edit forms from other edit forms to say OK rather than save. 2025-06-19 14:43:17 -05:00
6f15356b51 Adjustments to qqq/v1 test fixtures 2025-06-02 08:45:30 -05:00
0bf33a01f9 Copy fixture files to qqq/v1 api paths; update routes setup for fixtures too 2025-05-30 20:46:55 -05:00
0bca8e9361 Add new argument to qController.possibleValues call 2025-05-30 20:24:38 -05:00
6b90894425 Merged feature/search-possible-values-by-label into feature/workflows-support 2025-05-30 11:09:15 -05:00
248040a99f Fix dupe call to doRecordAnalytics 2025-05-30 09:20:08 -05:00
80ac2a304a Update qqq-frontend-core version to 1.0.123 (for qControllerV1 count) 2025-05-29 19:18:13 -05:00
b82b25156e Check if javalin classes are available before using (made dep on javalin optional) 2025-05-29 19:16:06 -05:00
69b46570cb Add optional dep for qqq-middleware-javalin; update version of qqq 2025-05-29 19:15:26 -05:00
3da656c01f Remove non-existing fields with a warning (attempt to improve support for api-versioned use-case) 2025-05-29 12:29:35 -05:00
1da0f4f1de Attempting to improve handling for non-countable tables (was showing 1 past the end sometimes);
disable when can't go back or forward;
min-width for more stable UI
2025-05-29 12:28:48 -05:00
ce947bc0f7 Add proxy for /material-dashboard-backend/* (initially for field onLoad/Change form adjusters) 2025-05-29 12:25:39 -05:00
0a42b9d4f0 try to be a little more graceful with fields that don't exist (e.g., other api version use cases) 2025-05-29 12:24:21 -05:00
5ab906bcfe update disabled pagination icons to look disabled 2025-05-29 12:23:49 -05:00
c1ea7081f1 Switch to use QControllerV1 for tableMetaData, query, and count calls, in support of apiVersions; add a pageState of error; setLoading when pageNo or rowsPerPage change; adjust handling of doSetCurrentSavedView, if the saved view record is null 2025-05-29 12:23:11 -05:00
020e174110 Support omitFieldNames to be specified in the widgetData 2025-05-29 11:37:10 -05:00
68c1f897af Add otherValues to form field possibleValues and queryString based on record values in widget load 2025-05-29 11:36:40 -05:00
7d6b083ae2 Try-catch around recordAnalytics calls; reformat file 2025-05-29 11:35:12 -05:00
3d4f0ba24b Update qqq-frontend-core to 1.0.121 2025-05-29 11:33:00 -05:00
6fc11bb0ba Add support for using api-versioned query screen 2025-05-29 11:31:30 -05:00
78c788812a Add support for onLoad and onChange form adjusters, plus isHidden attribute on fields 2025-05-29 11:31:30 -05:00
cb36f59090 Add java backend for field-level form adjusters 2025-05-29 11:31:30 -05:00
96bdcf1874 Add QControllerV1 usage and setGotAuthenticationInAllControllers method to replace calling it on each controller instance 2025-05-29 08:59:09 -05:00
07d116d9ba Adding MaterialDashboardInstanceMetaData with processNamesToAddToAllQueryAndViewScreens - to remove hard-coded version of this which was scripts-menu only - opening up for run-workflows to be added to all tables. 2025-05-28 16:30:15 -05:00
5bdc3a6cd0 Merged dev into feature/workflows-support 2025-05-20 07:53:21 -05:00
bb06e2743a Add initial support for dynamic-components - loaded from a url - as custom widgets. 2025-05-05 11:34:23 -05:00
77 changed files with 4064 additions and 635 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.122",
"@kingsrook/qqq-frontend-core": "1.0.124",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
@ -35,8 +35,8 @@
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"lodash": "4.17.21",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1",
"rapidoc": "9.3.4",
"react": "18.0.0",

View File

@ -66,7 +66,13 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<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>
<groupId>org.slf4j</groupId>

View File

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

View File

@ -20,6 +20,10 @@
*/
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
import React from "react";
import ReactDOM from "react-dom";
import {createRoot} from "react-dom/client";
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
import App from "App";
import "qqq/styles/qqq-override-styles.css";
import "qqq/styles/globals.scss";
@ -29,11 +33,14 @@ import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0Authen
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
import {MaterialUIControllerProvider} from "qqq/context";
import Client from "qqq/utils/qqq/Client";
import React from "react";
import {createRoot} from "react-dom/client";
import {BrowserRouter} from "react-router-dom";
/////////////////////////////////////////////////////////////////////////////////
// Expose React and ReactDOM as globals, for use by dynamically loaded modules //
/////////////////////////////////////////////////////////////////////////////////
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
const qController = Client.getInstance();
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)

View File

@ -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,123 @@
/*
* 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.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
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)
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.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...");
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
Client.setGotAuthenticationInAllControllers();
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete.");
};

View File

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

View File

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

View File

@ -20,15 +20,21 @@
*/
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import {useFormikContext} from "formik";
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import FileInputField from "qqq/components/forms/FileInputField";
import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import React from "react";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useState} from "react";
const qController = Client.getInstance();
interface Props
{
@ -43,7 +49,12 @@ interface Props
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
{
const {formFields, values, errors, touched} = formData;
const {formFields: origFormFields, errors, touched} = formData;
const {setFieldValue, values} = useFormikContext<Record<string, any>>();
const [formAdjustmentCounter, setFormAdjustmentCounter] = useState(0)
const [formFields, setFormFields] = useState(origFormFields as {[key: string]: any});
const bulkEditSwitchChanged = (name: string, value: boolean) =>
{
@ -51,6 +62,204 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
};
/////////////////////////////////////////
// run on-load handlers if we have any //
/////////////////////////////////////////
useEffect(() =>
{
for (let fieldName in formFields)
{
const field = formFields[fieldName];
const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
if(materialDashboardFieldMetaData?.onLoadFormAdjuster)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo consider cases with multiple - do they need to list a sequenceNo? do they need to run serially? //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
considerRunningFormAdjuster("onLoad", fieldName, values[fieldName]);
}
}
}, []);
/***************************************************************************
**
***************************************************************************/
const handleFieldChange = async (fieldName: string, newValue: any) =>
{
const field = formFields[fieldName];
if (!field)
{
return;
}
//////////////////////////////////////////////////////////////////////
// map possible-value objects to ids - also capture their labels... //
//////////////////////////////////////////////////////////////////////
let actualNewValue = newValue;
let possibleValueLabel: string = null;
if (field.possibleValueProps)
{
actualNewValue = newValue ? newValue.id : null;
possibleValueLabel = newValue ? newValue.label : null;
}
/////////////////////////////////////////////////////////////////////////////////////////////
// make sure formik has the value - and that we capture the possible-value label if needed //
/////////////////////////////////////////////////////////////////////////////////////////////
setFieldValue(fieldName, actualNewValue);
if (field.possibleValueProps)
{
field.possibleValueProps.initialDisplayValue = possibleValueLabel;
}
///////////////////////////////////////////
// run onChange adjuster if there is one //
///////////////////////////////////////////
considerRunningFormAdjuster("onChange", fieldName, actualNewValue);
}
/***************************************************************************
**
***************************************************************************/
const considerRunningFormAdjuster = async (event: "onChange" | "onLoad", fieldName: string, newValue: any) =>
{
const field = formFields[fieldName];
if (!field)
{
return;
}
const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
const adjuster = event == "onChange" ? materialDashboardFieldMetaData?.onChangeFormAdjuster : materialDashboardFieldMetaData?.onLoadFormAdjuster;
if (!adjuster)
{
return;
}
console.log(`Running form adjuster for field ${fieldName} ${event} (value is: ${newValue})`);
//////////////////////////////////////////////////////////////////
// disable fields temporarily while waiting on backend response //
//////////////////////////////////////////////////////////////////
const fieldNamesToTempDisable: string[] = materialDashboardFieldMetaData?.fieldsToDisableWhileRunningAdjusters ?? []
const previousIsEditableValues: {[key: string]: boolean} = {};
if(fieldNamesToTempDisable.length > 0)
{
for (let oldFieldName in formFields)
{
if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
{
previousIsEditableValues[oldFieldName] = formFields[oldFieldName].isEditable;
formFields[oldFieldName].isEditable = false;
}
}
setFormAdjustmentCounter(formAdjustmentCounter + 1);
setFormFields({...formFields});
}
////////////////////////////////////////////////////
// build request to backend for field adjustments //
////////////////////////////////////////////////////
const postBody = new FormData();
postBody.append("event", event);
postBody.append("fieldName", fieldName);
postBody.append("newValue", newValue);
postBody.append("allValues", JSON.stringify(values));
const response = await qController.axiosRequest(
{
method: "post",
url: `/material-dashboard-backend/form-adjuster/${encodeURIComponent(materialDashboardFieldMetaData.formAdjusterIdentifier)}/${event}`,
data: postBody,
headers: qController.defaultMultipartFormDataHeaders()
});
console.log("Form adjuster response: " + JSON.stringify(response));
////////////////////////////////////////////////////
// un-disable any temp disabled fields from above //
////////////////////////////////////////////////////
if(fieldNamesToTempDisable.length > 0)
{
for (let oldFieldName in formFields)
{
if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
{
formFields[oldFieldName].isEditable = previousIsEditableValues[oldFieldName];
}
}
setFormFields({...formFields});
}
///////////////////////////////////////////////////
// replace field definitions, if we have updates //
///////////////////////////////////////////////////
const updatedFields: { [fieldName: string]: QFieldMetaData } = response.updatedFieldMetaData;
if(updatedFields)
{
for (let updatedFieldName in updatedFields)
{
const updatedField = new QFieldMetaData(updatedFields[updatedFieldName]);
const dynamicField = DynamicFormUtils.getDynamicField(updatedField); // todo dynamicallyDisabledFields? second param...
const dynamicFieldInObject: any = {};
dynamicFieldInObject[updatedFieldName] = dynamicField;
let tableName = null;
let processName = null;
let displayValues = new Map();
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [updatedFields[updatedFieldName]], tableName, processName, displayValues);
for (let oldFieldName in formFields)
{
if (oldFieldName == updatedFieldName)
{
formFields[updatedFieldName] = dynamicField;
}
}
}
setFormAdjustmentCounter(formAdjustmentCounter + 2);
setFormFields({...formFields});
}
/////////////////////////
// update field values //
/////////////////////////
const updatedFieldValues: {[fieldName: string]: any} = response?.updatedFieldValues ?? {};
for (let fieldNameToUpdate in updatedFieldValues)
{
setFieldValue(fieldNameToUpdate, updatedFieldValues[fieldNameToUpdate]);
///////////////////////////////////////////////////////////////////////////////////////
// todo - track if a pvs field gets a value, but not a display value, and fetch it?? //
///////////////////////////////////////////////////////////////////////////////////////
}
/////////////////////////////////////////////////
// set display values in PVS's if we have them //
/////////////////////////////////////////////////
const updatedFieldDisplayValues: {[fieldName: string]: any} = response?.updatedFieldDisplayValues ?? {};
for (let fieldNameToUpdate in updatedFieldDisplayValues)
{
const fieldToUpdate = formFields[fieldNameToUpdate];
if(fieldToUpdate?.possibleValueProps)
{
fieldToUpdate.possibleValueProps.initialDisplayValue = updatedFieldDisplayValues[fieldNameToUpdate];
}
}
////////////////////////////////////////
// clear field values if we have them //
////////////////////////////////////////
const fieldsToClear: string[] = response?.fieldsToClear ?? [];
for (let fieldToClear of fieldsToClear)
{
setFieldValue(fieldToClear, "");
}
};
return (
<Box>
<Box lineHeight={0}>
@ -68,6 +277,8 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
return null;
}
const display = field.fieldMetaData?.isHidden ? "none" : "initial";
if (values[fieldName] === undefined)
{
values[fieldName] = "";
@ -100,7 +311,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
}
return (
<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}
<FileInputField field={field} record={record} errorMessage={errors[fieldName]} />
</Grid>
@ -119,7 +330,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
});
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}
<DynamicSelect
fieldPossibleValueProps={field.possibleValueProps}
@ -130,6 +341,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
otherValues={otherValuesMap}
useCase="form"
onChange={(newValue: any) => handleFieldChange(fieldName, newValue)}
/>
{formattedHelpContent}
</Grid>
@ -140,7 +352,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
// everything else!! //
///////////////////////
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}
<QDynamicFormField
id={field.name}
@ -155,6 +367,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
formFieldObject={field}
onChangeCallback={(newValue) => handleFieldChange(fieldName, newValue)}
/>
{formattedHelpContent}
</Grid>

View File

@ -191,6 +191,11 @@ class DynamicFormUtils
props.possibleValueSourceName = field.possibleValueSourceName;
}
if(field.possibleValueSourceFilter)
{
props.possibleValueSourceFilter = field.possibleValueSourceFilter;
}
dynamicFormFields[field.name].possibleValueProps = props;
}
}

View File

@ -188,7 +188,16 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
}
else
{
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, null, otherValues, useCase);
return await qController.possibleValues(
{
tableName,
processName,
fieldNameOrPossibleValueSourceName: possibleValueSourceName ?? fieldName,
searchTerm: searchTerm ?? "",
values: otherValues,
useCase,
possibleValueSourceFilter: fieldPossibleValueProps.possibleValueSourceFilter
});
}
};

View File

@ -59,15 +59,17 @@ import * as Yup from "yup";
interface Props
{
id?: string;
isModal: boolean;
table?: QTableMetaData;
closeModalHandler?: (event: object, reason: string) => void;
defaultValues: { [key: string]: string };
disabledFields: { [key: string]: boolean } | string[];
isCopy?: boolean;
onSubmitCallback?: (values: any, tableName: string) => void;
overrideHeading?: string;
id?: string,
isModal: boolean,
table?: QTableMetaData,
closeModalHandler?: (event: object, reason: string) => void,
defaultValues: { [key: string]: string },
disabledFields: { [key: string]: boolean } | string[],
isCopy?: boolean,
onSubmitCallback?: (values: any, tableName: string) => void,
overrideHeading?: string,
saveButtonLabel?: string,
saveButtonIcon?: string,
}
EntityForm.defaultProps = {
@ -79,6 +81,8 @@ EntityForm.defaultProps = {
disabledFields: {},
isCopy: false,
onSubmitCallback: null,
saveButtonLabel: "Save",
saveButtonIcon: "save",
};
@ -1331,12 +1335,14 @@ function EntityForm(props: Props): JSX.Element
</Box>
)) : null}
<Box component="div" p={3}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} />
</Grid>
</Box>
{formFields &&
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
</Grid>
</Box>
}
</Form>
);
@ -1355,6 +1361,8 @@ function EntityForm(props: Props): JSX.Element
disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={props.onSubmitCallback ? props.onSubmitCallback : submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
saveButtonLabel="OK"
saveButtonIcon="check"
/>
</div>
</Modal>

View File

@ -20,6 +20,7 @@
*/
import {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
@ -27,25 +28,26 @@ import {Box} from "@mui/material";
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField";
import React, {ReactNode, useState} from "react";
import React, {ReactNode, useMemo, useState} from "react";
interface FieldAutoCompleteProps
{
id: string;
metaData: QInstance;
tableMetaData: QTableMetaData;
handleFieldChange: (event: any, newValue: any, reason: string) => void;
defaultValue?: { field: QFieldMetaData, table: QTableMetaData, fieldName: string };
autoFocus?: boolean;
forceOpen?: boolean;
hiddenFieldNames?: string[];
availableFieldNames?: string[];
variant?: "standard" | "filled" | "outlined";
label?: string;
textFieldSX?: any;
autocompleteSlotProps?: any;
hasError?: boolean;
noOptionsText?: string;
id: string,
metaData: QInstance,
tableMetaData: QTableMetaData,
handleFieldChange: (event: any, newValue: any, reason: string) => void,
defaultValue?: { field: QFieldMetaData, table: QTableMetaData, fieldName: string },
autoFocus?: boolean,
forceOpen?: boolean,
hiddenFieldNames?: string[],
availableFieldNames?: string[],
variant?: "standard" | "filled" | "outlined",
label?: string,
textFieldSX?: any,
autocompleteSlotProps?: any,
hasError?: boolean,
noOptionsText?: string,
omitExposedJoins?: string[]
}
FieldAutoComplete.defaultProps =
@ -88,7 +90,7 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a
/*******************************************************************************
** Component for rendering a list of field names from a table as an auto-complete.
*******************************************************************************/
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText}: FieldAutoCompleteProps): JSX.Element
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText, omitExposedJoins}: FieldAutoCompleteProps): JSX.Element
{
const [selectedFieldName, setSelectedFieldName] = useState(defaultValue ? defaultValue.fieldName : null);
@ -96,11 +98,25 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames, availableFieldNames, selectedFieldName);
let fieldsGroupBy = null;
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
const availableExposedJoins = useMemo(() =>
{
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
const rs: QExposedJoin[] = []
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
{
const exposedJoin = tableMetaData.exposedJoins[i];
if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
{
continue;
}
rs.push(exposedJoin);
}
return (rs);
}, [tableMetaData, omitExposedJoins]);
if (availableExposedJoins && availableExposedJoins.length > 0)
{
for (let i = 0; i < availableExposedJoins.length; i++)
{
const exposedJoin = availableExposedJoins[i];
if (metaData.tables.has(exposedJoin.joinTable.name))
{
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
@ -185,7 +201,7 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
{originalEndAdornment}
</Box>;
return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />)
return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />);
}}
// @ts-ignore
defaultValue={defaultValue}

View File

@ -59,8 +59,7 @@ interface Props
bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void,
allowSelectingProfile?: boolean,
fileDescription?: FileDescription,
bulkLoadProfileResetToSuggestedMappingCallback?: () => void,
isBulkEdit?: boolean;
bulkLoadProfileResetToSuggestedMappingCallback?: () => void
}
SavedBulkLoadProfiles.defaultProps = {
@ -73,7 +72,7 @@ const qController = Client.getInstance();
** menu-button, text elements, and modal(s) that let you work with saved
** bulk-load profiles.
***************************************************************************/
function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback, isBulkEdit}: Props): JSX.Element
function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback}: Props): JSX.Element
{
const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]);
const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]);
@ -143,7 +142,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
const formData = new FormData();
formData.append("tableName", tableMetaData.name);
formData.append("isBulkEdit", isBulkEdit.toString());
const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData);
const yourSavedBulkLoadProfiles: QRecord[] = [];
@ -214,7 +212,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
break;
case RESET_TO_SUGGESTION:
setSavePopupOpen(false);
if (bulkLoadProfileResetToSuggestedMappingCallback)
if(bulkLoadProfileResetToSuggestedMappingCallback)
{
bulkLoadProfileResetToSuggestedMappingCallback();
}
@ -267,7 +265,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
const bulkLoadProfile = currentMapping.toProfile();
const mappingJson = JSON.stringify(bulkLoadProfile.profile);
formData.append("mappingJson", mappingJson);
formData.append("isBulkEdit", isBulkEdit.toString());
if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null)
{
@ -392,7 +389,6 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
return (savedBulkLoadProfiles);
}
const bulkAction = isBulkEdit ? "Edit" : "Load";
const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile");
const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile");
const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile");
@ -432,15 +428,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}}
>
{
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk {bulkAction} Profile Actions</b></MenuItem>
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk Load Profile Actions</b></MenuItem>
}
{
!allowSelectingProfile &&
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}>
{
currentSavedBulkLoadProfileRecord ?
<span>You are using the bulk {bulkAction.toLowerCase()} profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
: <span>You are not using a saved bulk {bulkAction.toLowerCase()} profile.<br /><br />You can save your profile on this screen.</span>
<span>You are using the bulk load profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
: <span>You are not using a saved bulk load profile.<br /><br />You can save your profile on this screen.</span>
}
</MenuItem>
}
@ -460,7 +456,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
}
{
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk {bulkAction.toLowerCase()} profile."}>
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk load profile."}>
<span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
@ -471,7 +467,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
}
{
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk {bulkAction.toLowerCase()} profile, with a different name, separate from the original.">
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk load profile, with a different name, separate from the original.">
<span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
@ -482,7 +478,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
}
{
hasDeletePermission && currentSavedBulkLoadProfileRecord != null &&
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk {bulkAction.toLowerCase()} profile."}>
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk load profile."}>
<span>
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
@ -493,11 +489,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
}
{
allowSelectingProfile &&
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk {bulkAction.toLowerCase()} profile for this table, removing all mappings.">
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk load profile for this table, removing all mappings.">
<span>
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
New Bulk {bulkAction} Profile
New Bulk Load Profile
</MenuItem>
</span>
</Tooltip>
@ -508,7 +504,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
{
<Divider />
}
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk {bulkAction} Profiles</b></MenuItem>
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk Load Profiles</b></MenuItem>
{
yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? (
yourSavedBulkLoadProfiles.map((record: QRecord, index: number) =>
@ -518,11 +514,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
)
) : (
<MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any saved bulk {bulkAction.toLowerCase()} profiles for this table.</i>
<i>You do not have any saved bulk load profiles for this table.</i>
</MenuItem>
)
}
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk {bulkAction} Profiles Shared with you</b></MenuItem>
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk Load Profiles Shared with you</b></MenuItem>
{
bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? (
bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) =>
@ -532,7 +528,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
)
) : (
<MenuItem disabled sx={{opacity: "1 !important"}}>
<i>You do not have any bulk {bulkAction.toLowerCase()} profiles shared with you for this table.</i>
<i>You do not have any bulk load profiles shared with you for this table.</i>
</MenuItem>
)
}
@ -541,7 +537,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
</Menu>
);
let buttonText = `Saved Bulk ${bulkAction} Profiles`;
let buttonText = "Saved Bulk Load Profiles";
let buttonBackground = "none";
let buttonBorder = colors.grayLines.main;
let buttonColor = colors.gray.main;
@ -643,13 +639,13 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
<b>Unsaved Mapping</b>
<ul style={{padding: "0.5rem 1rem"}}>
<li>You are not using a saved bulk {bulkAction.toLowerCase()} profile.</li>
<li>You are not using a saved bulk load profile.</li>
{
/*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/
}
</ul>
</>}>
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk {bulkAction} Profile As&hellip;</Button>
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk Load Profile As&hellip;</Button>
</Tooltip>
{/* vertical rule */}
@ -720,20 +716,20 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
{
currentSavedBulkLoadProfileRecord ? (
isDeleteAction ? (
<DialogTitle id="alert-dialog-title">Delete Bulk {bulkAction} Profile</DialogTitle>
<DialogTitle id="alert-dialog-title">Delete Bulk Load Profile</DialogTitle>
) : (
isSaveAsAction ? (
<DialogTitle id="alert-dialog-title">Save Bulk {bulkAction} Profile As</DialogTitle>
<DialogTitle id="alert-dialog-title">Save Bulk Load Profile As</DialogTitle>
) : (
isRenameAction ? (
<DialogTitle id="alert-dialog-title">Rename Bulk {bulkAction} Profile</DialogTitle>
<DialogTitle id="alert-dialog-title">Rename Bulk Load Profile</DialogTitle>
) : (
<DialogTitle id="alert-dialog-title">Update Existing Bulk {bulkAction} Profile</DialogTitle>
<DialogTitle id="alert-dialog-title">Update Existing Bulk Load Profile</DialogTitle>
)
)
)
) : (
<DialogTitle id="alert-dialog-title">Save New Bulk {bulkAction} Profile</DialogTitle>
<DialogTitle id="alert-dialog-title">Save New Bulk Load Profile</DialogTitle>
)
}
<DialogContent sx={{width: "500px"}}>
@ -747,15 +743,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
<Box>
{
isSaveAsAction ? (
<Box mb={3}>Enter a name for this new saved bulk {bulkAction.toLowerCase()} profile.</Box>
<Box mb={3}>Enter a name for this new saved bulk load profile.</Box>
) : (
<Box mb={3}>Enter a new name for this saved bulk {bulkAction.toLowerCase()} profile.</Box>
<Box mb={3}>Enter a new name for this saved bulk load profile.</Box>
)
}
<TextField
autoFocus
name="custom-delimiter-value"
placeholder={`Bulk ${bulkAction} Profile Name`}
placeholder="Bulk Load Profile Name"
inputProps={{width: "100%", maxLength: 100}}
value={savedBulkLoadProfileNameInputValue}
sx={{width: "100%"}}
@ -768,9 +764,9 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
</Box>
) : (
isDeleteAction ? (
<Box>Are you sure you want to delete the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
<Box>Are you sure you want to delete the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
) : (
<Box>Are you sure you want to update the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
<Box>Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
)
)
}

View File

@ -43,7 +43,6 @@ interface BulkLoadMappingFieldProps
removeFieldCallback?: () => void,
fileDescription: FileDescription,
forceParentUpdate?: () => void,
isBulkEdit?: boolean
}
const xIconButtonSX =
@ -72,7 +71,7 @@ const qController = Client.getInstance();
/***************************************************************************
** row for a single field on the bulk load mapping screen.
***************************************************************************/
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldProps): JSX.Element
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate}: BulkLoadMappingFieldProps): JSX.Element
{
const columnNames = fileDescription.getColumnNames();
@ -228,17 +227,6 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
forceParentUpdate && forceParentUpdate();
}
/***************************************************************************
**
***************************************************************************/
function clearIfEmptyChanged(value: boolean)
{
bulkLoadField.clearIfEmpty = value;
forceParentUpdate && forceParentUpdate();
}
/***************************************************************************
**
***************************************************************************/
@ -325,11 +313,8 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
<Box ml="1rem">
{
valueType == "column" && <>
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
<Box>
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
{
isBulkEdit && !isRequired && <FormControlLabel value="clearIfEmpty" control={<Checkbox size="small" defaultChecked={bulkLoadField.clearIfEmpty} onChange={(event, checked) => clearIfEmptyChanged(checked)} />} label={"Clear if empty"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
}
</Box>
<Box fontSize={mainFontSize} mt="0.5rem">
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>

View File

@ -33,7 +33,6 @@ interface BulkLoadMappingFieldsProps
bulkLoadMapping: BulkLoadMapping,
fileDescription: FileDescription,
forceParentUpdate?: () => void,
isBulkEdit?: boolean
}
@ -44,7 +43,7 @@ const ALREADY_ADDED_FIELD_TOOLTIP = "This field has already been added to your m
/***************************************************************************
** The section of the bulk load mapping screen with all the fields.
***************************************************************************/
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldsProps): JSX.Element
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate}: BulkLoadMappingFieldsProps): JSX.Element
{
const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -255,16 +254,11 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
return (
<>
{isBulkEdit ? <h5>Key Fields</h5> : <h5>Required Fields</h5>}
<h5>Required Fields</h5>
<Box pl={"1rem"}>
{
bulkLoadMapping.requiredFields.length == 0 &&
(
isBulkEdit ?
<i style={{fontSize: "0.875rem"}}>Select table key fields to continue.</i>
:
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
)
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
}
{bulkLoadMapping.requiredFields.map((bulkLoadField) => (
<BulkLoadFileMappingField
@ -273,13 +267,12 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
bulkLoadField={bulkLoadField}
isRequired={true}
forceParentUpdate={forceParentUpdate}
isBulkEdit={isBulkEdit}
/>
))}
</Box>
<Box mt="1rem">
{isBulkEdit ? <h5>Fields To Update</h5> : <h5>Additional Fields</h5>}
<h5>Additional Fields</h5>
<Box pl={"1rem"}>
{bulkLoadMapping.additionalFields.map((bulkLoadField) => (
<BulkLoadFileMappingField
@ -289,7 +282,6 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
isRequired={false}
removeFieldCallback={() => removeField(bulkLoadField)}
forceParentUpdate={forceParentUpdate}
isBulkEdit={isBulkEdit}
/>
))}

View File

@ -36,18 +36,15 @@ import {useFormikContext} from "formik";
import colors from "qqq/assets/theme/base/colors";
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
import Client from "qqq/utils/qqq/Client";
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
import React, {forwardRef, useImperativeHandle, useReducer, useState} from "react";
import ProcessViewForm from "./ProcessViewForm";
const qController = Client.getInstance();
interface BulkLoadMappingFormProps
{
@ -76,12 +73,13 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile);
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile, processMetaData.name));
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile));
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping));
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -116,8 +114,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
values["layout"] = wrappedBulkLoadMapping.get().layout;
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
values["isBulkEdit"] = wrappedBulkLoadMapping.get().isBulkEdit;
values["keyFields"] = wrappedBulkLoadMapping.get().keyFields;
let haveLocalErrors = false;
const fieldErrors: { [fieldName: string]: string } = {};
@ -134,14 +130,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
}
setFieldErrors(fieldErrors);
if (values["isBulkEdit"] && (values["keyFields"] == null || values["keyFields"] == undefined))
{
haveLocalErrors = true;
fieldErrors["keyFields"] = "This field is required.";
}
setFieldErrors(fieldErrors);
if (wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
{
setNoMappedFieldsError("You must have at least 1 field.");
haveLocalErrors = true;
@ -152,7 +141,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
setNoMappedFieldsError(null);
}
if (haveProfileErrors)
if(haveProfileErrors)
{
setTimeout(() =>
{
@ -193,7 +182,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
***************************************************************************/
function bulkLoadProfileResetToSuggestedMappingCallback()
{
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile, processValues.name));
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile));
}
@ -212,8 +201,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
setBulkLoadMapping(newBulkLoadMapping);
wrappedBulkLoadMapping.set(newBulkLoadMapping);
setFieldValue("isBulkEdit", newBulkLoadMapping.isBulkEdit);
setFieldValue("keyFields", newBulkLoadMapping.keyFields);
setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow);
setFieldValue("layout", newBulkLoadMapping.layout);
@ -241,13 +228,10 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback}
fileDescription={fileDescription}
isBulkEdit={processValues.isBulkEdit}
/>
</Box>
<BulkLoadMappingHeader
tableMetaData={tableMetaData}
isBulkEdit={processValues.isBulkEdit}
key={rerenderHeader}
bulkLoadMapping={bulkLoadMapping}
fileDescription={fileDescription}
@ -261,7 +245,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
<Box mt="2rem">
<BulkLoadFileMappingFields
isBulkEdit={processValues.isBulkEdit}
bulkLoadMapping={bulkLoadMapping}
fileDescription={fileDescription}
forceParentUpdate={() =>
@ -284,7 +267,6 @@ export default BulkLoadFileMappingForm;
interface BulkLoadMappingHeaderProps
{
isBulkEdit?: boolean,
fileDescription: FileDescription,
fileName: string,
bulkLoadMapping?: BulkLoadMapping,
@ -293,16 +275,13 @@ interface BulkLoadMappingHeaderProps
forceParentUpdate?: () => void,
frontendStep: QFrontendStepMetaData,
processMetaData: QProcessMetaData,
tableMetaData: QTableMetaData,
}
/***************************************************************************
** private subcomponent - the header section of the bulk load file mapping screen.
***************************************************************************/
function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData, tableMetaData}: BulkLoadMappingHeaderProps): JSX.Element
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element
{
const [dynamicField, setDynamicField] = useState(null);
const viewFields = [
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}),
@ -328,36 +307,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null;
useEffect(() =>
{
(async () =>
{
if (isBulkEdit)
{
/////////////////////////////////////////////////////////////////////////
// if doing a bulk edit, the selected keyFields and set as the display //
/////////////////////////////////////////////////////////////////////////
const displayValues = new Map<string, string>;
if (bulkLoadMapping.keyFields)
{
const possibleValues = await qController.possibleValues(null, processMetaData.name, "tableKeyFields", bulkLoadMapping.keyFields, null);
console.log("Received possible values of: " + JSON.stringify(possibleValues));
displayValues.set("tableKeyFields", possibleValues[0].label);
}
const tableKeyFieldsField = processMetaData.frontendSteps.find(s => s.name == "fileMapping")?.formFields.find(f => f.name == "tableKeyFields");
const newDynamicField = DynamicFormUtils.getDynamicField(tableKeyFieldsField);
const dynamicFieldInObject: any = {};
dynamicFieldInObject[tableKeyFieldsField["name"]] = newDynamicField;
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [tableKeyFieldsField], null, processMetaData.name, displayValues);
keyFieldsChanged(bulkLoadMapping.keyFields);
setDynamicField(newDynamicField);
forceParentUpdate();
}
})();
}, [JSON.stringify(bulkLoadMapping)]);
/***************************************************************************
**
***************************************************************************/
@ -382,61 +331,6 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
forceParentUpdate();
}
/***************************************************************************
**
***************************************************************************/
async function keyFieldsChanged(newValue: any)
{
fieldErrors.keyFields = null;
if (newValue && newValue.length > 0)
{
//////////////////////////////////////////////////////////
// validate that the fields in the key have been mapped //
//////////////////////////////////////////////////////////
console.log("Received key fields of: " + newValue);
const keyFields = newValue.split("|");
const unmappedKeyFields: string[] = [];
const requiredFields: BulkLoadField[] = [];
const additionalFields: BulkLoadField[] = [];
////////////////////////////////////////////////////////////////////////////////////////////////
// iterate over all fields in the table, when there are key fields found, make them required, //
// otherwise add them to addition fields //
////////////////////////////////////////////////////////////////////////////////////////////////
for (let bulkLoadField of [...bulkLoadMapping.requiredFields, ...bulkLoadMapping.additionalFields])
{
const qualifiedName = bulkLoadField.getQualifiedName();
const keyField = keyFields.find((k: string) => k == qualifiedName);
if (keyField)
{
requiredFields.push(bulkLoadField);
var fieldsByTablePrefix = bulkLoadMapping.fieldsByTablePrefix[""][keyField];
if (!fieldsByTablePrefix || fieldsByTablePrefix.columnIndex == null)
{
unmappedKeyFields.push(tableMetaData.fields.get(keyField).label);
}
}
else
{
additionalFields.push(bulkLoadField);
}
}
bulkLoadMapping.requiredFields = requiredFields;
bulkLoadMapping.additionalFields = additionalFields;
if (unmappedKeyFields.length > 0)
{
fieldErrors.keyFields = "The following key fields are not mapped: " + unmappedKeyFields.join(", ");
}
bulkLoadMapping.handleChangeToKeyFields(newValue);
}
forceParentUpdate();
}
/***************************************************************************
**
***************************************************************************/
@ -475,50 +369,27 @@ function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadM
{getFormattedHelpContent("hasHeaderRow")}
</Grid>
<Grid item xs={12} md={6}>
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
<Autocomplete
id={"layout"}
renderInput={(params) => (<TextField {...params} label={""} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
options={layoutOptions}
multiple={false}
defaultValue={selectedLayout}
onChange={layoutChanged}
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
disableClearable
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
/>
{
!isBulkEdit ? (
<>
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
<Autocomplete
id={"layout"}
renderInput={(params) => (<TextField {...params} label={""} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
options={layoutOptions}
multiple={false}
defaultValue={selectedLayout}
onChange={layoutChanged}
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
disableClearable
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
/>
{
fieldErrors.layout &&
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
</MDTypography>
}
{getFormattedHelpContent("layout")}
</>
) : (
<>
{
dynamicField &&
<>
<DynamicFormFieldLabel name={dynamicField.name} label={`${dynamicField.label} *`} />
<QDynamicFormField name={dynamicField.name} displayFormat={""} label={""} formFieldObject={dynamicField} type={"pvs"} value={bulkLoadMapping.keyFields} onChangeCallback={keyFieldsChanged} />
{
fieldErrors.keyFields &&
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
{<div className="fieldErrorMessage">{fieldErrors.keyFields}</div>}
</MDTypography>
}
{getFormattedHelpContent("tableKeyFields")}
</>
}
</>
)
fieldErrors.layout &&
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
</MDTypography>
}
{getFormattedHelpContent("layout")}
</Grid>
</Grid>
</Box>
@ -619,25 +490,25 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
const count = fields.length;
let dupeWarning = <></>;
if (fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index])
let dupeWarning = <></>
if(fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index])
{
dupeWarning = <Tooltip title="This column header is a duplicate. Only the first occurrance of it will be used." placement="top" enterDelay={500}>
<Icon color="warning" sx={{p: "0.125rem", mr: "0.25rem"}}>warning</Icon>
</Tooltip>;
</Tooltip>
}
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
<>
{
count > 0 &&
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
<Box>
{dupeWarning}
{letter}
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
</Box>
</Tooltip>
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
<Box>
{dupeWarning}
{letter}
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
</Box>
</Tooltip>
}
{
count == 0 && <Box>{dupeWarning}{letter}</Box>
@ -657,24 +528,24 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
const count = fields.length;
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
if (fileDescription.hasHeaderRow)
if(fileDescription.hasHeaderRow)
{
tdStyle.backgroundColor = "#ebebeb";
if (count > 0)
if(count > 0)
{
return <td key={value} style={tdStyle}>
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
</td>;
</td>
}
else
{
return <td key={value} style={tdStyle}>{value}</td>;
return <td key={value} style={tdStyle}>{value}</td>
}
}
else
{
return <td key={value} style={tdStyle}>{value}</td>;
return <td key={value} style={tdStyle}>{value}</td>
}
}
)}

View File

@ -43,12 +43,12 @@ interface BulkLoadValueMappingFormProps
const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) =>
{
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue));
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue))
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
const [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile));
const [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile))
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
@ -93,7 +93,6 @@ const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}
allowSelectingProfile={false}
fileDescription={fileDescription}
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
isBulkEdit={processValues.isBulkEdit}
/>
</Box>

View File

@ -75,7 +75,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
*******************************************************************************/
function initializeCurrentBulkLoadMapping(): BulkLoadMapping
{
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile, processValues.name);
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile);
if (!bulkLoadMapping.valueMappings[fieldFullName])
{
@ -155,7 +155,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
function mappedValueChanged(fileValue: string, newValue: any)
{
valueErrors[fileValue] = null;
if (newValue == null)
if(newValue == null)
{
delete currentMapping.valueMappings[fieldFullName][fileValue];
}
@ -195,7 +195,6 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
allowSelectingProfile={false}
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
fileDescription={fileDescription}
isBulkEdit={processValues.isBulkEdit}
/>
</Box>

View File

@ -83,6 +83,8 @@ interface BasicAndAdvancedQueryControlsProps
mode: string;
setMode: (mode: string) => void;
omitExposedJoins?: string[];
}
let debounceTimeout: string | number | NodeJS.Timeout;
@ -627,6 +629,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
handleSelectedField={handleSetSort}
fieldEndAdornment={<Box whiteSpace="nowrap"><Icon>arrow_upward</Icon><Icon>arrow_downward</Icon></Box>}
handleAdornmentClick={handleSetSortArrowClick}
omitExposedJoins={props.omitExposedJoins}
/>);
const filterBuilderMouseEvents =
@ -721,6 +724,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
buttonChildren={"Add Filter"}
isModeSelectOne={true}
handleSelectedField={handleFieldListMenuSelection}
omitExposedJoins={props.omitExposedJoins}
/>
}
</>

View File

@ -43,6 +43,7 @@ declare module "@mui/x-data-grid"
metaData: QInstance;
queryFilter: QQueryFilter;
updateFilter: (newFilter: QQueryFilter) => void;
omitExposedJoins?: string[]
}
}
@ -181,6 +182,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
allowVariables={props.allowVariables}
queryScreenUsage={props.queryScreenUsage}
omitExposedJoins={props.omitExposedJoins}
/>
{/*JSON.stringify(criteria)*/}
</Box>

View File

@ -26,9 +26,9 @@ import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import {GridRowsProp} from "@mui/x-data-grid-pro";
import React from "react";
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React from "react";
interface CustomPaginationProps
{
@ -56,7 +56,7 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
The number of rows shown on this screen may be greater than the number of {tableMetaData?.label} records
that match your query, because you have included fields from other tables which may have
more than one record associated with each {tableMetaData?.label}.
</>
</>;
let distinctPart = isJoinMany ? (<Box display="inline" component="span" textAlign="right">
&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>
@ -66,13 +66,23 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
if (loading)
{
return "Counting...";
}
if (!rows || rows.length == 0)
{
return "No rows";
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, //
// we'll do this... not quite good enough, but better than the original //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (rows.length > 0 && rows.length < to - from)
{
to = from + rows.length;
to = from + (rows.length - 1);
}
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
}
@ -102,14 +112,55 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
}
};
///////////////////////////////////////////////////////////////////////////////
// the `count` param that we pass to <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 (
<TablePagination
component="div"
sx={{minWidth: "450px"}}
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
// so pass a sentinel value of -1...
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
sx={{minWidth: "450px", "& .MuiTablePagination-displayedRows": {minWidth: "110px"}}}
count={countForTablePagination}
page={pageNumber}
rowsPerPageOptions={[10, 25, 50, 100, 250]}
rowsPerPage={rowsPerPage}

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Box from "@mui/material/Box";
@ -31,28 +32,26 @@ import ListItem, {ListItemProps} from "@mui/material/ListItem/ListItem";
import Menu from "@mui/material/Menu";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
import React, {useState} from "react";
import React, {useMemo, useState} from "react";
interface FieldListMenuProps
{
idPrefix: string;
heading?: string;
placeholder?: string;
tableMetaData: QTableMetaData;
showTableHeaderEvenIfNoExposedJoins: boolean;
fieldNamesToHide?: string[];
buttonProps: any;
buttonChildren: JSX.Element | string;
isModeSelectOne?: boolean;
handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void;
isModeToggle?: boolean;
toggleStates?: {[fieldName: string]: boolean};
handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void;
fieldEndAdornment?: JSX.Element
handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void;
idPrefix: string,
heading?: string,
placeholder?: string,
tableMetaData: QTableMetaData,
showTableHeaderEvenIfNoExposedJoins: boolean,
fieldNamesToHide?: string[],
buttonProps: any,
buttonChildren: JSX.Element | string,
isModeSelectOne?: boolean,
handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void,
isModeToggle?: boolean,
toggleStates?: { [fieldName: string]: boolean },
handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void,
fieldEndAdornment?: JSX.Element,
handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void,
omitExposedJoins?: string[]
}
FieldListMenu.defaultProps = {
@ -71,38 +70,52 @@ interface TableWithFields
** Component to render a list of fields from a table (and its join tables)
** which can be interacted with...
*******************************************************************************/
export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick}: FieldListMenuProps): JSX.Element
export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick, omitExposedJoins}: FieldListMenuProps): JSX.Element
{
const [menuAnchorElement, setMenuAnchorElement] = useState(null);
const [searchText, setSearchText] = useState("");
const [focusedIndex, setFocusedIndex] = useState(null as number);
const [fieldsByTable, setFieldsByTable] = useState([] as TableWithFields[]);
const [collapsedTables, setCollapsedTables] = useState({} as {[tableName: string]: boolean});
const [collapsedTables, setCollapsedTables] = useState({} as { [tableName: string]: boolean });
const [lastMouseOverXY, setLastMouseOverXY] = useState({x: 0, y: 0});
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0)
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0);
const availableExposedJoins = useMemo(() =>
{
const rs: QExposedJoin[] = []
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
{
if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
{
continue;
}
rs.push(exposedJoin);
}
return (rs);
}, [tableMetaData, omitExposedJoins]);
//////////////////
// check usages //
//////////////////
if(isModeSelectOne)
if (isModeSelectOne)
{
if(!handleSelectedField)
if (!handleSelectedField)
{
throw("In FieldListMenu, if isModeSelectOne=true, then a callback for handleSelectedField must be provided.");
throw ("In FieldListMenu, if isModeSelectOne=true, then a callback for handleSelectedField must be provided.");
}
}
if(isModeToggle)
if (isModeToggle)
{
if(!toggleStates)
if (!toggleStates)
{
throw("In FieldListMenu, if isModeToggle=true, then a model for toggleStates must be provided.");
throw ("In FieldListMenu, if isModeToggle=true, then a model for toggleStates must be provided.");
}
if(!handleToggleField)
if (!handleToggleField)
{
throw("In FieldListMenu, if isModeToggle=true, then a callback for handleToggleField must be provided.");
throw ("In FieldListMenu, if isModeToggle=true, then a callback for handleToggleField must be provided.");
}
}
@ -113,16 +126,16 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{
collapsedTables[tableMetaData.name] = false;
if (tableMetaData.exposedJoins?.length > 0)
if (availableExposedJoins?.length > 0)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have exposed joins, put the table meta data with its fields, and then all of the join tables & fields too //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fieldsByTable.push({table: tableMetaData, fields: getTableFieldsAsAlphabeticalArray(tableMetaData)});
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++)
for (let i = 0; i < availableExposedJoins?.length; i++)
{
const joinTable = tableMetaData.exposedJoins[i].joinTable;
const joinTable = availableExposedJoins[i].joinTable;
fieldsByTable.push({table: joinTable, fields: getTableFieldsAsAlphabeticalArray(joinTable)});
collapsedTables[joinTable.name] = false;
@ -150,16 +163,16 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
table.fields.forEach(field =>
{
let fullFieldName = field.name;
if(table.name != tableMetaData.name)
if (table.name != tableMetaData.name)
{
fullFieldName = `${table.name}.${field.name}`;
}
if(fieldNamesToHide && fieldNamesToHide.indexOf(fullFieldName) > -1)
if (fieldNamesToHide && fieldNamesToHide.indexOf(fullFieldName) > -1)
{
return;
}
fields.push(field)
fields.push(field);
});
fields.sort((a, b) => a.label.localeCompare(b.label));
return (fields);
@ -181,7 +194,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
/*******************************************************************************
**
*******************************************************************************/
function getShownFieldAndTableByIndex(targetIndex: number): {field: QFieldMetaData, table: QTableMetaData}
function getShownFieldAndTableByIndex(targetIndex: number): { field: QFieldMetaData, table: QTableMetaData }
{
let index = -1;
for (let i = 0; i < fieldsByTableToShow.length; i++)
@ -191,9 +204,9 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{
index++;
if(index == targetIndex)
if (index == targetIndex)
{
return {field: tableWithField.fields[j], table: tableWithField.table}
return {field: tableWithField.fields[j], table: tableWithField.table};
}
}
}
@ -210,7 +223,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
// console.log(`Event key: ${event.key}`);
setTimeout(() => document.getElementById(`field-list-dropdown-${idPrefix}-textField`).focus());
if(isModeSelectOne && event.key == "Enter" && focusedIndex != null)
if (isModeSelectOne && event.key == "Enter" && focusedIndex != null)
{
setTimeout(() =>
{
@ -249,13 +262,13 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
/////////////////
// a down move //
/////////////////
if(startIndex == null)
if (startIndex == null)
{
startIndex = -1;
}
let goalIndex = startIndex + offset;
if(goalIndex > maxFieldIndex - 1)
if (goalIndex > maxFieldIndex - 1)
{
goalIndex = maxFieldIndex - 1;
}
@ -268,7 +281,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
// an up move //
////////////////
let goalIndex = startIndex + offset;
if(goalIndex < 0)
if (goalIndex < 0)
{
goalIndex = 0;
}
@ -335,7 +348,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
// the last x/y isn't really useful, because the mouse generally isn't left exactly where it was when the mouse-over happened (edge of the element) //
// but the keyboard last-arrow time that we capture, that's what's actually being useful in here //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(event.clientX == lastMouseOverXY.x && event.clientY == lastMouseOverXY.y)
if (event.clientX == lastMouseOverXY.x && event.clientY == lastMouseOverXY.y)
{
// console.log("mouse didn't move, so, doesn't count");
return;
@ -343,7 +356,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
const now = new Date().getTime();
// console.log(`Compare now [${now}] to last arrow [${timeOfLastArrow}] (diff: [${now - timeOfLastArrow}])`);
if(now < timeOfLastArrow + 300)
if (now < timeOfLastArrow + 300)
{
// console.log("An arrow event happened less than 300 mills ago, so doesn't count.");
return;
@ -480,7 +493,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
for (let i = 0; i < fieldsList.length; i++)
{
const field = fieldsList[i];
if(doesFieldMatchSearchText(field))
if (doesFieldMatchSearchText(field))
{
handleToggleField(field, table, event.target.checked);
}
@ -491,18 +504,18 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
/////////////////////////////////////////////////////////
// compute the table-level toggle state & count values //
/////////////////////////////////////////////////////////
const tableToggleStates: {[tableName: string]: boolean} = {};
const tableToggleCounts: {[tableName: string]: number} = {};
const tableToggleStates: { [tableName: string]: boolean } = {};
const tableToggleCounts: { [tableName: string]: number } = {};
if(isModeToggle)
if (isModeToggle)
{
const {allOn, count} = getTableToggleState(tableMetaData, true);
tableToggleStates[tableMetaData.name] = allOn;
tableToggleCounts[tableMetaData.name] = count;
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++)
for (let i = 0; i < availableExposedJoins?.length; i++)
{
const join = tableMetaData.exposedJoins[i];
const join = availableExposedJoins[i];
const {allOn, count} = getTableToggleState(join.joinTable, false);
tableToggleStates[join.joinTable.name] = allOn;
tableToggleCounts[join.joinTable.name] = count;
@ -513,7 +526,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
/*******************************************************************************
**
*******************************************************************************/
function getTableToggleState(table: QTableMetaData, isMainTable: boolean): {allOn: boolean, count: number}
function getTableToggleState(table: QTableMetaData, isMainTable: boolean): { allOn: boolean, count: number }
{
const fieldsList = [...table.fields.values()];
let allOn = true;
@ -522,7 +535,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{
const field = fieldsList[i];
const name = isMainTable ? field.name : `${table.name}.${field.name}`;
if(!toggleStates[name])
if (!toggleStates[name])
{
allOn = false;
}
@ -541,7 +554,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
*******************************************************************************/
function toggleCollapsedTable(tableName: string)
{
collapsedTables[tableName] = !collapsedTables[tableName]
collapsedTables[tableName] = !collapsedTables[tableName];
setCollapsedTables(Object.assign({}, collapsedTables));
}
@ -559,7 +572,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
let index = -1;
const textFieldId = `field-list-dropdown-${idPrefix}-textField`;
let listItemPadding = isModeToggle ? "0.125rem": "0.5rem";
let listItemPadding = isModeToggle ? "0.125rem" : "0.5rem";
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for z-indexes, we set each table header to i+1, then the fields in that table to i (so they go behind it) //
@ -607,12 +620,12 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{
let headerContents = null;
const headerTable = tableWithFields.table || tableMetaData;
if(tableWithFields.table || showTableHeaderEvenIfNoExposedJoins)
if (tableWithFields.table || showTableHeaderEvenIfNoExposedJoins)
{
headerContents = (<b>{headerTable.label} Fields</b>);
}
if(isModeToggle)
if (isModeToggle)
{
headerContents = (<FormControlLabel
sx={{display: "flex", alignItems: "flex-start", "& .MuiFormControlLabel-label": {lineHeight: "1.4", fontWeight: "500 !important"}}}
@ -622,10 +635,10 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
checked={tableToggleStates[headerTable.name]}
onChange={(event) => handleTableToggle(event, headerTable)}
/>}
label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b>&nbsp;<span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />)
label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b>&nbsp;<span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />);
}
if(isModeToggle)
if (isModeToggle)
{
headerContents = (
<>
@ -638,11 +651,11 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
</IconButton>
{headerContents}
</>
)
);
}
let marginLeft = "unset";
if(isModeToggle)
if (isModeToggle)
{
marginLeft = "-1rem";
}
@ -652,14 +665,14 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
return (
<React.Fragment key={tableWithFields.table?.name ?? "theTable"}>
<>
{headerContents && <ListItem sx={{position: "sticky", top: -1, zIndex: zIndex+1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start", backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 90%, rgba(255,255,255,0))"}}>{headerContents}</ListItem>}
{headerContents && <ListItem sx={{position: "sticky", top: -1, zIndex: zIndex + 1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start", backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 90%, rgba(255,255,255,0))"}}>{headerContents}</ListItem>}
{
tableWithFields.fields.map((field) =>
{
index++;
const key = `${tableWithFields.table?.name}-${field.name}`
const key = `${tableWithFields.table?.name}-${field.name}`;
if(collapsedTables[headerTable.name])
if (collapsedTables[headerTable.name])
{
return (<React.Fragment key={key} />);
}
@ -677,13 +690,13 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{
closeMenu();
handleSelectedField(field, tableWithFields.table ?? tableMetaData);
}
};
}
let label: JSX.Element | string = field.label;
const fullFieldName = tableWithFields.table && tableWithFields.table.name != tableMetaData.name ? `${tableWithFields.table.name}.${field.name}` : field.name;
if(fieldEndAdornment)
if (fieldEndAdornment)
{
label = <Box width="100%" display="inline-flex" justifyContent="space-between">
{label}

View File

@ -33,6 +33,7 @@ import MenuItem from "@mui/material/MenuItem";
import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import {omit} from "lodash";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
@ -190,17 +191,18 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
interface FilterCriteriaRowProps
{
id: number;
index: number;
tableMetaData: QTableMetaData;
metaData: QInstance;
criteria: QFilterCriteria;
booleanOperator: "AND" | "OR" | null;
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
removeCriteria: () => void;
updateBooleanOperator: (newValue: string) => void;
queryScreenUsage?: QueryScreenUsage;
allowVariables?: boolean;
id: number,
index: number,
tableMetaData: QTableMetaData,
metaData: QInstance,
criteria: QFilterCriteria,
booleanOperator: "AND" | "OR" | null,
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void,
removeCriteria: () => void,
updateBooleanOperator: (newValue: string) => void,
queryScreenUsage?: QueryScreenUsage,
allowVariables?: boolean,
omitExposedJoins?: string[]
}
FilterCriteriaRow.defaultProps =
@ -269,7 +271,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
return {criteriaIsValid, criteriaStatusTooltip};
}
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables, omitExposedJoins}: FilterCriteriaRowProps): JSX.Element
{
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
@ -488,7 +490,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
</Box>
<Box display="inline-block" width={250} className="fieldColumn">
<FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange}
autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
omitExposedJoins={omitExposedJoins} autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
/>
</Box>
<Box display="inline-block" width={200} className="operatorColumn">

View File

@ -40,14 +40,13 @@ interface QueryScreenActionMenuProps
tableProcesses: QProcessMetaData[];
bulkLoadClicked: () => void;
bulkEditClicked: () => void;
bulkEditWithFileClicked: () => void;
bulkDeleteClicked: () => void;
processClicked: (process: QProcessMetaData) => void;
}
QueryScreenActionMenu.defaultProps = {};
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkEditWithFileClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
{
const [anchorElement, setAnchorElement] = useState(null);
@ -63,14 +62,6 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
setAnchorElement(null);
};
const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
{
if (menuItems.length > 0)
{
menuItems.push(<Divider key="divider" />);
}
};
const runSomething = (handler: () => void) =>
{
closeActionsMenu();
@ -78,6 +69,10 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
};
const menuItems: JSX.Element[] = [];
//////////////////////////////////////////////////////
// start with bulk actions, if user has permissions //
//////////////////////////////////////////////////////
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>);
@ -85,26 +80,13 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission)
{
menuItems.push(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
menuItems.push(<MenuItem key="bulkEditWithFile" onClick={() => runSomething(bulkEditWithFileClicked)}><ListItemIcon><Icon>edit_note</Icon></ListItemIcon>Bulk Edit With File</MenuItem>);
}
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
{
menuItems.push(<MenuItem key="bulkDelete" onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
}
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>);
}
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);
}
menuItems.push(<Divider key="divider1" />);
tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
tableProcesses.map((process) =>
@ -112,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(<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)
{
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 (
<>
<QActionsMenuButton isOpen={anchorElement} onClickHandler={openActionsMenu} />

View File

@ -39,6 +39,7 @@ import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineCha
import PieChart from "qqq/components/widgets/charts/piechart/PieChart";
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
import CustomComponentWidget from "qqq/components/widgets/misc/CustomComponentWidget";
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
import DividerWidget from "qqq/components/widgets/misc/Divider";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
@ -781,8 +782,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{
widgetMetaData.type === "filterAndColumnsSetup" && (
widgetData && widgetData[i] &&
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={(values: { [name: string]: any }) =>
{
if(actionCallback)
{
actionCallback(values)
}
}} />
)
}
@ -800,6 +805,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
<DynamicFormWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} recordValues={convertQRecordValuesFromMapToObject(record)} />
)
}
{
widgetMetaData.type === "customComponent" && (
widgetData && widgetData[i] &&
<Widget widgetMetaData={widgetMetaData}>
<CustomComponentWidget widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} />
</Widget>
)
}
</Box>
);
};

View File

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

View File

@ -0,0 +1,69 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Skeleton} from "@mui/material";
import Box from "@mui/material/Box";
import useDynamicComponents from "qqq/utils/qqq/useDynamicComponents";
import {useEffect, useState} from "react";
interface CustomComponentWidgetProps
{
widgetMetaData: QWidgetMetaData;
widgetData: any;
record: QRecord;
}
CustomComponentWidget.defaultProps = {
};
/*******************************************************************************
** Component to display a custom component - one dynamically loaded.
*******************************************************************************/
export default function CustomComponentWidget({widgetMetaData, widgetData, record}: CustomComponentWidgetProps): JSX.Element
{
const [componentName, setComponentName] = useState(widgetMetaData.defaultValues.get("componentName"));
const [componentSourceUrl, setComponentSourceUrl] = useState(widgetMetaData.defaultValues.get("componentSourceUrl"));
const {loadComponent, hasComponentLoaded, renderComponent} = useDynamicComponents();
useEffect(() =>
{
loadComponent(componentName, componentSourceUrl);
}, []);
const props: any =
{
widgetMetaData: widgetMetaData,
widgetData: widgetData,
record: record,
}
return (<Box sx={widgetMetaData.defaultValues?.get("sx")}>
{hasComponentLoaded(componentName) ? renderComponent(componentName, props) : <Skeleton width="100%" height="100%" />}
</Box>);
}

View File

@ -20,6 +20,7 @@
*/
import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
@ -42,7 +43,8 @@ import QQueryColumns, {Column} from "qqq/models/query/QQueryColumns";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
import React, {useContext, useEffect, useRef, useState} from "react";
import TableUtils from "qqq/utils/qqq/TableUtils";
import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
interface FilterAndColumnsSetupWidgetProps
{
@ -80,21 +82,31 @@ unborderedButtonSX.opacity = "0.7";
const qController = Client.getInstance();
const qControllerV1 = Client.getInstanceV1();
/*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns
*******************************************************************************/
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element
export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element
{
const [modalOpen, setModalOpen] = useState(false);
const [hideColumns] = useState(widgetData?.hideColumns);
const [hidePreview] = useState(widgetData?.hidePreview);
const [hideSortBy] = useState(widgetData?.hideSortBy);
const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [isApiVersioned] = useState(widgetData?.isApiVersioned);
const [apiVersion, setApiVersion] = useState(null as ApiVersion | null);
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson");
const [alertContent, setAlertContent] = useState(null as string);
const [warning, setWarning] = useState(null as string);
const [widgetFailureAlertContent, setWidgetFailureAlertContent] = useState(null as string);
const omitExposedJoins: string[] = widgetData?.omitExposedJoins ?? [];
//////////////////////////////////////////////////////////////////////////////////////////////////
// we'll actually keep 2 copies of the query filter around here - //
@ -112,7 +124,9 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/////////////////////////////
let columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false;
let queryFilter = recordValues[filterFieldName] && JSON.parse(recordValues[filterFieldName]) as QQueryFilter;
const rawFilterValueFromRecord = recordValues[filterFieldName];
let queryFilter = rawFilterValueFromRecord &&
((typeof rawFilterValueFromRecord == "string" ? JSON.parse(rawFilterValueFromRecord) : rawFilterValueFromRecord) as QQueryFilter);
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
if (!queryFilter)
{
@ -165,16 +179,73 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
tableName = recordValues["tableName"];
}
let version: ApiVersion | null = null;
if (isApiVersioned)
{
let apiName = widgetData?.apiName;
let apiPath = widgetData?.apiPath;
let apiVersion = widgetData?.apiVersion;
if (!apiName && recordValues["apiName"])
{
apiName = recordValues["apiName"];
}
if (!apiPath && recordValues["apiPath"])
{
apiPath = recordValues["apiPath"];
}
if (!apiVersion && recordValues["apiVersion"])
{
apiVersion = recordValues["apiVersion"];
}
if (!apiName || !apiPath || !apiVersion)
{
console.log("API Name/Path/Version not set, but widget isApiVersioned, so cannot load table meta data...");
return;
}
version = {name: apiName, path: apiPath, version: apiVersion};
setApiVersion(version);
}
if (tableName)
{
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
try
{
const tableMetaData = await qControllerV1.loadTableMetaData(tableName, version);
setTableMetaData(tableMetaData);
const queryFilterForFrontend = Object.assign({}, queryFilter);
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
setFrontendQueryFilter(queryFilterForFrontend);
const queryFilterForFrontend = Object.assign({}, queryFilter);
let warnings: string[] = [];
for (let i = 0; i < queryFilterForFrontend?.criteria?.length; i++)
{
const criteria = queryFilter.criteria[i];
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
if(!field)
{
warnings.push("Removing non-existing filter field: " + criteria.fieldName);
queryFilterForFrontend.criteria.splice(i, 1);
i--;
}
}
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
setFrontendQueryFilter(queryFilterForFrontend);
setWarning(warnings.join("; "));
}
catch (e)
{
console.log(e);
//@ts-ignore e.message
setWidgetFailureAlertContent("Error preparing filter widget: " + (e.message ?? "Details not available."));
}
})();
}
}, [JSON.stringify(recordValues)]);
@ -203,7 +274,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
return;
}
if (recordValues["tableName"])
if (widgetData?.tableName || recordValues["tableName"])
{
setAlertContent(null);
setModalOpen(true);
@ -335,7 +406,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/////////////////////////////////////////////////
// add link to widget header for opening modal //
/////////////////////////////////////////////////
const selectTableFirstTooltipTitle = tableMetaData ? null : "You must select a table before you can set up your report filters and columns";
const selectTableFirstTooltipTitle = tableMetaData ? null : `You must select a table${isApiVersioned ? " and API details" : ""} before you can set up your filters${hideColumns ? "" : " and columns"}`;
const labelAdditionalElementsRight: JSX.Element[] = [];
if (isEditable)
{
@ -349,6 +420,12 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
}
}
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}>
<React.Fragment>
@ -361,10 +438,13 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<Collapse in={Boolean(alertContent)}>
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
</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 display="flex" justifyContent="space-between" alignItems="center">
<h5>{label ?? "Query Filter"}</h5>
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
<h5>{label ?? widgetData.label ?? widgetMetaData.label ?? "Query Filter"}</h5>
{!hideSortBy && <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>}
</Box>
{
mayShowQuery() &&
@ -376,7 +456,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
{
isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}>
<span><Button disabled={!recordValues["tableName"]} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span>
<span><Button disabled={tableMetaData == null} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span>
</Tooltip>
}
{
@ -422,6 +502,8 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
isModal={true}
initialQueryFilter={frontendQueryFilter}
initialColumns={columns}
apiVersion={apiVersion}
omitExposedJoins={omitExposedJoins}
/>
</Box>
)}
@ -431,7 +513,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<div>
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{m: "2rem", p: "2rem"}}>
<h3>Edit Filters and Columns</h3>
<h3>Edit Filters {hideColumns ? "" : " and Columns"}</h3>
{
showHelp("modalSubheader") &&
<Box color={colors.gray.main} pb={"0.5rem"}>
@ -447,6 +529,8 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
isModal={true}
initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter}
initialColumns={columns}
apiVersion={apiVersion}
omitExposedJoins={omitExposedJoins}
/>
}

View File

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

View File

@ -20,6 +20,7 @@
*/
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
/*******************************************************************************
** Properties attached to a (formik?) form field, to denote how it behaves as
@ -34,5 +35,6 @@ export interface FieldPossibleValueProps
tableName?: string;
processName?: string;
possibleValueSourceName?: string;
possibleValueSourceFilter?: QQueryFilter;
}

View File

@ -39,7 +39,6 @@ export class BulkLoadField
headerName?: string = null;
defaultValue?: any = null;
doValueMapping: boolean = false;
clearIfEmpty?: boolean = false;
wideLayoutIndexPath: number[] = [];
@ -52,7 +51,7 @@ export class BulkLoadField
/***************************************************************************
**
***************************************************************************/
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null, clearIfEmpty?: boolean)
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
{
this.field = field;
this.tableStructure = tableStructure;
@ -65,7 +64,6 @@ export class BulkLoadField
this.error = error;
this.warning = warning;
this.key = new Date().getTime().toString();
this.clearIfEmpty = clearIfEmpty ?? false;
}
@ -74,7 +72,7 @@ export class BulkLoadField
***************************************************************************/
public static clone(source: BulkLoadField): BulkLoadField
{
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning, source.clearIfEmpty));
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning));
}
@ -175,9 +173,6 @@ export interface BulkLoadTableStructure
associationPath: string;
fields: QFieldMetaData[];
associations: BulkLoadTableStructure[];
isBulkEdit: boolean;
possibleKeyFields: string[];
keyFields?: string;
}
@ -198,8 +193,6 @@ export class BulkLoadMapping
valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {};
isBulkEdit: boolean;
keyFields: string;
hasHeaderRow: boolean;
layout: string;
@ -218,8 +211,6 @@ export class BulkLoadMapping
}
}
this.isBulkEdit = tableStructure.isBulkEdit;
this.keyFields = tableStructure.keyFields;
this.hasHeaderRow = true;
}
@ -227,13 +218,11 @@ export class BulkLoadMapping
/***************************************************************************
**
***************************************************************************/
public processTableStructure(tableStructure: BulkLoadTableStructure)
private processTableStructure(tableStructure: BulkLoadTableStructure)
{
const prefix = tableStructure.isMain ? "" : tableStructure.associationPath;
this.fieldsByTablePrefix[prefix] = {};
this.tablesByPath[prefix] = tableStructure;
this.isBulkEdit = tableStructure.isBulkEdit;
this.keyFields = tableStructure.keyFields;
for (let field of tableStructure.fields)
{
@ -244,35 +233,13 @@ export class BulkLoadMapping
this.fields[qualifiedName] = bulkLoadField;
this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField;
if (this.isBulkEdit)
if (tableStructure.isMain && field.isRequired)
{
if (this.keyFields == null)
{
this.unusedFields.push(bulkLoadField);
}
else
{
const keyFields = this.keyFields.split("|");
if (keyFields.includes(qualifiedName))
{
this.requiredFields.push(bulkLoadField);
}
else
{
this.unusedFields.push(bulkLoadField);
}
}
this.requiredFields.push(bulkLoadField);
}
else
{
if (tableStructure.isMain && field.isRequired)
{
this.requiredFields.push(bulkLoadField);
}
else
{
this.unusedFields.push(bulkLoadField);
}
this.unusedFields.push(bulkLoadField);
}
}
}
@ -299,16 +266,14 @@ export class BulkLoadMapping
** take a saved bulk load profile - and convert it into a working bulkLoadMapping
** for the frontend to use!
***************************************************************************/
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile, processName?: string): BulkLoadMapping
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile): BulkLoadMapping
{
const bulkLoadMapping = new BulkLoadMapping(tableStructure);
if (bulkLoadProfile.version == "v1")
{
bulkLoadMapping.isBulkEdit = bulkLoadProfile.isBulkEdit;
bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow;
bulkLoadMapping.layout = bulkLoadProfile.layout;
bulkLoadMapping.keyFields = bulkLoadProfile.keyFields;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, //
@ -357,7 +322,6 @@ export class BulkLoadMapping
{
bulkLoadField.valueType = "column";
bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping;
bulkLoadField.clearIfEmpty = bulkLoadProfileField.clearIfEmpty;
bulkLoadField.headerName = bulkLoadProfileField.headerName;
bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex;
@ -380,29 +344,6 @@ export class BulkLoadMapping
}
}
if (!bulkLoadMapping.keyFields && tableStructure.possibleKeyFields?.length > 0)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// look at each of the possible key fields, compare with the fields in the bulk load profile, //
// on the first one that matches, use that as the default bulk load mapping key field //
////////////////////////////////////////////////////////////////////////////////////////////////
for (let keyField of tableStructure.possibleKeyFields)
{
const parts = keyField.split("|");
const allPartsMatch = parts.every(part =>
(bulkLoadProfile.fieldList ?? []).some((field: BulkLoadProfileField) =>
field.fieldName === part
)
);
if (allPartsMatch)
{
bulkLoadMapping.keyFields = keyField;
break; // stop after the first valid match
}
}
}
return (bulkLoadMapping);
}
else
@ -424,8 +365,6 @@ export class BulkLoadMapping
profile.version = "v1";
profile.hasHeaderRow = this.hasHeaderRow;
profile.layout = this.layout;
profile.isBulkEdit = this.isBulkEdit;
profile.keyFields = this.keyFields;
for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields])
{
@ -445,7 +384,7 @@ export class BulkLoadMapping
}
else
{
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping, clearIfEmpty: bulkLoadField.clearIfEmpty};
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping};
if (this.valueMappings[fullFieldName])
{
@ -637,16 +576,6 @@ export class BulkLoadMapping
return (rs);
}
/***************************************************************************
**
***************************************************************************/
public handleChangeToKeyFields(newKeyFields: any)
{
this.keyFields = newKeyFields;
}
/***************************************************************************
**
***************************************************************************/
@ -671,7 +600,7 @@ export class BulkLoadMapping
{
const newField = BulkLoadField.clone(field);
newField.columnIndex = null;
newField.warning = "This field was assigned to a column with a duplicated header";
newField.warning = "This field was assigned to a column with a duplicated header"
newRequiredFields.push(newField);
anyChangesToRequiredFields = true;
}
@ -687,7 +616,7 @@ export class BulkLoadMapping
{
const newField = BulkLoadField.clone(field);
newField.columnIndex = null;
newField.warning = "This field was assigned to a column with a duplicated header";
newField.warning = "This field was assigned to a column with a duplicated header"
newAdditionalFields.push(newField);
anyChangesToAdditionalFields = true;
}
@ -869,8 +798,6 @@ export class BulkLoadProfile
fieldList: BulkLoadProfileField[] = [];
hasHeaderRow: boolean;
layout: string;
isBulkEdit: boolean;
keyFields: string;
}
type BulkLoadProfileField =
@ -880,7 +807,6 @@ type BulkLoadProfileField =
headerName?: string,
defaultValue?: any,
doValueMapping?: boolean,
clearIfEmpty?: boolean,
valueMappings?: { [fileValue: string]: any }
};

View File

@ -117,6 +117,11 @@ export default class QQueryColumns
{
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName)
if(!field)
{
console.warn(`Couldn't find field ${fieldName} in tableMetaData - so not adding a column for it`);
}
let column: Column;
if(tableForField.name == table.name)
{

View File

@ -72,6 +72,7 @@ import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"
import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
@ -114,9 +115,14 @@ let formikSetTouched = ({}: any, touched: boolean): void =>
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
export interface SubFormPreSubmitCallbackResultType {maySubmit: boolean; values: {[name: string]: any}}
export interface SubFormPreSubmitCallbackResultType
{
maySubmit: boolean;
values: { [name: string]: any };
}
type SubFormPreSubmitCallback = () => SubFormPreSubmitCallbackResultType;
type SubFormPreSubmitCallbackWithName = {name: string, callback: SubFormPreSubmitCallback}
type SubFormPreSubmitCallbackWithName = { name: string, callback: SubFormPreSubmitCallback }
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{
@ -161,7 +167,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
const [controlCallbacks, setControlCallbacks] = useState({} as {[name: string]: () => void});
const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
const [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]);
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -237,7 +243,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const bulkLoadFileMappingFormRef = useRef();
const bulkLoadValueMappingFormRef = useRef();
const bulkLoadProfileFormRef = useRef();
const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[])
const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[]);
const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean =>
{
@ -699,10 +705,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
{
if(bulkLoadFileMappingFormRef?.current)
if (bulkLoadFileMappingFormRef?.current)
{
// @ts-ignore ...
addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit)
addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit);
}
}
@ -711,10 +717,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
{
if(bulkLoadValueMappingFormRef?.current)
if (bulkLoadValueMappingFormRef?.current)
{
// @ts-ignore ...
addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit)
addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit);
}
}
@ -723,10 +729,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
///////////////////////////////////////////////////////////////////////////
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_PROFILE_FORM))
{
if(bulkLoadProfileFormRef?.current)
if (bulkLoadProfileFormRef?.current)
{
// @ts-ignore ...
addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit)
addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit);
}
}
@ -1298,7 +1304,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////////
// Help make this component's fields work with our formik form //
/////////////////////////////////////////////////////////////////
if(activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
if (activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
{
const fileValues = processValues.fileValues ?? [];
const valueMapping = processValues.valueMapping ?? {};
@ -1314,22 +1320,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
for (let i = 0; i < fileValues.length; i++)
{
const dynamicField = DynamicFormUtils.getDynamicField(qFieldMetaData);
const wrappedField: any = {};
const wrappedField: any = {};
wrappedField[field.name] = dynamicField;
DynamicFormUtils.addPossibleValueProps(wrappedField, [field], fieldTableName, null, null);
const initialValue = valueMapping[fileValues[i]];
if(dynamicField.possibleValueProps)
if (dynamicField.possibleValueProps)
{
dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue]
dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue];
}
addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null)
addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null);
fieldsForComponent.push(dynamicField);
}
setBulkLoadValueMappingFormFields(fieldsForComponent)
setBulkLoadValueMappingFormFields(fieldsForComponent);
}
if (Object.keys(dynamicFormFields).length > 0)
@ -1522,15 +1528,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
***************************************************************************/
function addSubFormPreSubmitCallbacks(name: string, callback: SubFormPreSubmitCallback)
{
if(subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
if (subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
{
const newCallbacks: SubFormPreSubmitCallbackWithName[] = []
for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
const newCallbacks: SubFormPreSubmitCallbackWithName[] = [];
for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
{
newCallbacks[i] = subFormPreSubmitCallbacks[i];
}
newCallbacks.push({name, callback})
setSubFormPreSubmitCallbacks(newCallbacks)
newCallbacks.push({name, callback});
setSubFormPreSubmitCallbacks(newCallbacks);
}
}
@ -1620,7 +1626,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setRenderedWidgets({});
setSubFormPreSubmitCallbacks([]);
setQJobRunning(null);
setBackStepName(qJobComplete.backStep)
setBackStepName(qJobComplete.backStep);
if (formikSetFieldValueFunction)
{
@ -1815,8 +1821,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
doRecordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
doRecordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
if (processMetaData.tableName && !tableMetaData)
{
@ -1838,17 +1844,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return;
}
if(urlSearchParams.get("defaultProcessValues"))
if (urlSearchParams.get("defaultProcessValues"))
{
if(!defaultProcessValues)
if (!defaultProcessValues)
{
defaultProcessValues = {}
defaultProcessValues = {};
}
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
for (let key in values)
{
defaultProcessValues[key] = values[key]
defaultProcessValues[key] = values[key];
}
}
@ -1894,7 +1900,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await qController.processStep(
processName,
@ -1914,7 +1920,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
@ -1938,20 +1944,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
///////////////////////////////////////////////////////////////
// run any sub-form pre-submit callbacks that are registered //
///////////////////////////////////////////////////////////////
for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
{
const {maySubmit, values: moreValues} = subFormPreSubmitCallbacks[i].callback();
if(!maySubmit)
if (!maySubmit)
{
console.log(`May not submit form, per callback: ${subFormPreSubmitCallbacks[i].name}`);
return;
}
if(moreValues)
if (moreValues)
{
for (let key in moreValues)
{
values[key] = moreValues[key]
values[key] = moreValues[key];
}
}
}
@ -2026,7 +2032,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////////////////////////////////////////////////////////////////
setLoadingRecords(true);
}
};
/*******************************************************************************
@ -2055,6 +2061,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
};
/***************************************************************************
**
***************************************************************************/
function doRecordAnalytics(model: AnalyticsModel)
{
try
{
recordAnalytics(model);
}
catch (e)
{
console.log(`Error recording analytics: ${e}`);
}
}
const formStyles: any = {};
if (isWidget)
{

View File

@ -20,6 +20,7 @@
*/
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
@ -69,6 +70,7 @@ import RecordQueryView from "qqq/models/query/RecordQueryView";
import ProcessRun from "qqq/pages/processes/ProcessRun";
import ColumnStats from "qqq/pages/records/query/ColumnStats";
import DataGridUtils from "qqq/utils/DataGridUtils";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
@ -87,22 +89,25 @@ export type QueryScreenUsage = "queryScreen" | "reportSetup"
interface Props
{
table?: QTableMetaData;
launchProcess?: QProcessMetaData;
usage?: QueryScreenUsage;
isModal?: boolean;
isPreview?: boolean;
initialQueryFilter?: QQueryFilter;
initialColumns?: QQueryColumns;
allowVariables?: boolean;
table?: QTableMetaData,
apiVersion?: ApiVersion,
launchProcess?: QProcessMetaData,
usage?: QueryScreenUsage,
isModal?: boolean,
isPreview?: boolean,
initialQueryFilter?: QQueryFilter,
initialColumns?: QQueryColumns,
allowVariables?: boolean,
omitExposedJoins?: string[]
}
///////////////////////////////////////////////////////
// define possible values for our pageState variable //
///////////////////////////////////////////////////////
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready";
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready" | "error";
const qController = Client.getInstance();
const qControllerV1 = Client.getInstanceV1();
/*******************************************************************************
** function to produce standard version of the screen while we're "loading"
@ -126,7 +131,7 @@ const getLoadingScreen = (isModal: boolean) =>
**
** Yuge component. The best. Lots of very smart people are saying so.
*******************************************************************************/
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns, omitExposedJoins}: Props, ref) =>
{
const tableName = table.name;
const [searchParams] = useSearchParams();
@ -933,7 +938,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
}
}
recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
doRecordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
setLoading(true);
@ -978,7 +983,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
}
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
// qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
qControllerV1.count(tableName, apiVersion, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
{
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
countResults[thisQueryId] = [];
@ -997,7 +1003,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
setLastFetchedQFilterJSON(JSON.stringify(queryFilter));
setLastFetchedVariant(tableVariant);
qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
// qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
qControllerV1.query(tableName, apiVersion, filterForBackend, queryJoins, tableVariant).then((results) =>
{
console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results;
@ -1140,6 +1147,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handlePageNumberChange = (page: number) =>
{
setPageNumber(page);
setLoading(true);
};
/*******************************************************************************
@ -1148,6 +1156,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const handleRowsPerPageChange = (size: number) =>
{
setRowsPerPage(size);
setLoading(true);
view.rowsPerPage = size;
doSetView(view);
@ -1557,7 +1566,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
/*******************************************************************************
** function to open one of the bulk (insert/edit/delete) processes.
*******************************************************************************/
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete" | "EditWithFile", processLabelPart: "Load" | "Edit" | "Delete" | "Edit With File") =>
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
{
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
if (processList.length > 0)
@ -1593,15 +1602,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
};
/*******************************************************************************
** Event handler for the bulk-edit-with-file process being selected
*******************************************************************************/
const bulkEditWithFileClicked = () =>
{
openBulkProcess("EditWithFile", "Edit With File");
};
/*******************************************************************************
** Event handler for the bulk-delete process being selected
*******************************************************************************/
@ -1680,8 +1680,9 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
{
if (savedViewRecord == null)
{
console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView instead.");
console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView, and activating tableDefaultView instead.");
doClearCurrentSavedView();
activateView(buildTableDefaultView(tableMetaData));
return;
}
@ -1732,7 +1733,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
{
if (selectedSavedViewId != null)
{
recordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
doRecordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
//////////////////////////////////////////////
// fetch, then activate the selected filter //
@ -1749,7 +1750,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
/////////////////////////////////
// this is 'new view' - right? //
/////////////////////////////////
recordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
doRecordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
//////////////////////////////
// wipe away the saved view //
@ -1777,7 +1778,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
console.error("Could not retrieve saved filter: " + jobError.userFacingError);
console.error("Could not retrieve saved view: " + jobError.userFacingError);
setAlertContent("There was an error loading the selected view.");
}
else
@ -2443,23 +2444,33 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
const metaData = await qController.loadMetaData();
setMetaData(metaData);
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
setTableLabel(tableMetaData.label);
try
{
// const tableMetaData = await qController.loadTableMetaData(tableName);
const tableMetaData = await qControllerV1.loadTableMetaData(tableName, apiVersion);
setTableMetaData(tableMetaData);
setTableLabel(tableMetaData.label);
recordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
doRecordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
// but also used when user selects new-view from the view menu //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const newDefaultView = buildTableDefaultView(tableMetaData);
setTableDefaultView(newDefaultView);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
// but also used when user selects new-view from the view menu //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const newDefaultView = buildTableDefaultView(tableMetaData);
setTableDefaultView(newDefaultView);
setPageState("loadedMetaData");
setPageState("loadedMetaData");
}
catch (e)
{
setPageState("error");
//@ts-ignore e.message
setAlertContent("Error loading table: " + e?.message ?? "Details not available.");
}
})();
}
@ -2727,6 +2738,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 //
///////////////////////////////////////////////////////////
@ -2814,6 +2835,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
idPrefix="columns"
tableMetaData={tableMetaData}
showTableHeaderEvenIfNoExposedJoins={true}
omitExposedJoins={omitExposedJoins}
placeholder="Search Fields"
buttonProps={{sx: columnMenuButtonStyles}}
buttonChildren={<><Icon sx={{mr: "0.5rem"}}>view_week_outline</Icon> Columns ({view.queryColumns.getVisibleColumnCount()}) <Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon></>}
@ -2824,6 +2846,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
</Box>);
};
/***************************************************************************
**
***************************************************************************/
function doRecordAnalytics(model: AnalyticsModel)
{
try
{
recordAnalytics(model);
}
catch (e)
{
console.log(`Error recording analytics: ${e}`);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// these numbers help set the height of the grid (so page won't scroll) based on space above & below it //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -2870,7 +2908,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
tableProcesses={tableProcesses}
bulkLoadClicked={bulkLoadClicked}
bulkEditClicked={bulkEditClicked}
bulkEditWithFileClicked={bulkEditWithFileClicked}
bulkDeleteClicked={bulkDeleteClicked}
processClicked={processClicked}
/>
@ -2940,6 +2977,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
setMode={doSetMode}
savedViewsComponent={savedViewsComponent}
columnMenuComponent={buildColumnMenu()}
omitExposedJoins={omitExposedJoins}
/>
}
@ -2965,7 +3003,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
metaData: metaData,
queryFilter: queryFilter,
updateFilter: doSetQueryFilter,
allowVariables: allowVariables
allowVariables: allowVariables,
omitExposedJoins: omitExposedJoins,
}
}}
localeText={{
@ -3062,6 +3101,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
RecordQuery.defaultProps = {
table: null,
apiVersion: null,
usage: "queryScreen",
launchProcess: null,
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)
{
setAsyncLoadInited(true);
@ -472,11 +500,16 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// load processes that the routing needs to respect //
//////////////////////////////////////////////////////
const allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true); // these include hidden ones (e.g., to find the bulks)
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
if (runRecordScriptProcess)
const genericProcesses = getGenericProcesses(metaData);
for (let genericProcess of genericProcesses)
{
allTableProcesses.unshift(runRecordScriptProcess);
if (genericProcess)
{
allTableProcesses.unshift(genericProcess);
}
}
setAllTableProcesses(allTableProcesses);
if (launchingProcess)
@ -726,7 +759,6 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
const renderActionsMenu = (
<Menu
@ -785,11 +817,14 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
))}
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
{
runRecordScriptProcess &&
<MenuItem key={runRecordScriptProcess.name} onClick={() => processClicked(runRecordScriptProcess)}>
<ListItemIcon><Icon>{runRecordScriptProcess.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{runRecordScriptProcess.label}
</MenuItem>
getGenericProcesses(metaData).map((process) =>
(
process &&
<MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label}
</MenuItem>
))
}
<MenuItem onClick={() => navigate("dev")}>
<ListItemIcon><Icon>code</Icon></ListItemIcon>
@ -969,7 +1004,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
{
notFoundMessage
?
<Alert color="error" sx={{mb: 3}}>{notFoundMessage}</Alert>
<Alert color="error" sx={{mb: 3}} icon={<Icon>warning</Icon>}>{notFoundMessage}</Alert>
:
<Box pb={3}>
{
@ -1046,16 +1081,19 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
</React.Fragment>
)) : null}
</Grid>
<Box component="form" p={3}>
<Grid container justifyContent="flex-end" spacing={3}>
{
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
}
{
table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && <QEditButton />
}
</Grid>
</Box>
{
tableMetaData && record && ((table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission) || (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)) &&
<Box component="div" p={3} className={"stickyBottomButtonBar"}>
<Grid container justifyContent="flex-end" spacing={3}>
{
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
}
{
table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && <QEditButton />
}
</Grid>
</Box>
}
</Grid>
</Grid>

View File

@ -303,10 +303,15 @@ input[type="search"]::-webkit-search-results-decoration
.MuiTablePagination-root .MuiSvgIcon-root
{
display: inline;
color: gray;
color: rgba(0, 0, 0, 0.54);
right: 0.125rem;
}
.MuiTablePagination-root .Mui-disabled .MuiSvgIcon-root
{
color: rgba(0, 0, 0, 0.16);
}
.devDocumentation ul > li
{
margin-left: 30px;
@ -837,3 +842,26 @@ input[type="search"]::-webkit-search-results-decoration
flex-grow: 1 !important;
}
}
.stickyBottomButtonBar
{
padding-bottom: 1rem !important;
padding-right: 0 !important;
margin-bottom: -4rem !important;
margin-top: -1.5rem !important;
position: sticky;
bottom: 0;
background: linear-gradient(to bottom, transparent 0, #f0f2f5 4px);
z-index: 10; /* have needed a little here, e.g. to get above MuiDataGrid-overlay and ACE */
}
.modalBottomButtonBar
{
padding-bottom: 0 !important;
padding-right: 0 !important;
}
.stickyBottomButtonBar>.MuiGrid-container
{
padding-top: 1rem;
}

View File

@ -20,6 +20,7 @@
*/
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";
/*******************************************************************************
@ -29,6 +30,7 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
class Client
{
private static qController: QController;
private static qControllerV1: QControllerV1;
private static unauthorizedCallback: () => void;
private static handleException(exception: QException)
@ -54,6 +56,22 @@ class Client
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)
{
Client.unauthorizedCallback = unauthorizedCallback;

View File

@ -108,6 +108,12 @@ class FilterUtils
const criteria = queryFilter.criteria[i];
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 hasFilterVariable = false;
@ -401,21 +407,21 @@ class FilterUtils
{
const expression = new ThisOrLastPeriodExpression(value);
let startOfPrefix = "";
if (fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
if (fieldMetaData?.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
{
startOfPrefix = "start of ";
}
labels.push(`${startOfPrefix}${expression.toString()}`);
}
else if (fieldMetaData.type == QFieldType.BOOLEAN)
else if (fieldMetaData?.type == QFieldType.BOOLEAN)
{
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));
}
else if (fieldMetaData.type == QFieldType.DATE)
else if (fieldMetaData?.type == QFieldType.DATE)
{
labels.push(ValueUtils.formatDate(value));
}

View File

@ -0,0 +1,361 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert} from "@mui/material";
import Box from "@mui/material/Box";
import Modal from "@mui/material/Modal";
import {ThemeProvider} from "@mui/material/styles";
import {Formik} from "formik";
import QContext from "QContext";
import QDynamicForm from "qqq/components/forms/DynamicForm";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDButton from "qqq/components/legacy/MDButton";
import theme from "qqq/components/legacy/Theme";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import {MaterialUIControllerProvider} from "qqq/context";
import Client from "qqq/utils/qqq/Client";
import React, {ReactElement, ReactNode, useContext, useEffect, useState} from "react";
import {BrowserRouter} from "react-router-dom";
import * as Yup from "yup";
// todo - deploy this interface somehow out of this file
export interface QFMDBridge
{
qController?: QController;
makeAlert: (text: string, color: string) => JSX.Element;
makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }) => JSX.Element;
makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void) => JSX.Element;
makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void) => JSX.Element;
makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean) => JSX.Element;
}
/***************************************************************************
** Component to generate a form for the QFMD Bridge
***************************************************************************/
interface QFMDBridgeFormProps
{
fields: QFieldMetaData[],
record: QRecord,
handleChange: (fieldName: string, newValue: any) => void,
handleSubmit: (values: any) => void
}
QFMDBridgeForm.defaultProps = {};
function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridgeFormProps): JSX.Element
{
const initialValues: any = {};
for (let field of fields)
{
initialValues[field.name] = record.values.get(field.name);
if(initialValues[field.name] === undefined && field.defaultValue !== undefined)
{
initialValues[field.name] = field.defaultValue;
}
}
const [lastValues, setLastValues] = useState(initialValues);
const [loaded, setLoaded] = useState(false);
///////////////////////////////////////////////////////////////////////////////
// store reference to record display values in a state var - see usage below //
///////////////////////////////////////////////////////////////////////////////
const [recordDisplayValues, setRecordDisplayValues] = useState(record?.displayValues ?? new Map<string, string>())
useEffect(() =>
{
(async () =>
{
const qController = Client.getInstance();
for (let field of fields)
{
const value = record.values.get(field.name);
if (field.possibleValueSourceName && value)
{
const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], [], record.values, "form");
if (possibleValues && possibleValues.length > 0)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// originally, we put this in record.displayValues, but, sometimes that would then be empty at the usage point below... //
// this works, so, we'll go with it //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
recordDisplayValues.set(field.name, possibleValues[0].label)
setRecordDisplayValues(recordDisplayValues);
}
}
}
setLoaded(true);
})();
}, []);
if (!loaded)
{
return (<Box py={"1rem"}>Loading...</Box>);
}
const {
dynamicFormFields,
formValidations,
} = DynamicFormUtils.getFormData(fields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, recordDisplayValues);
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 //
// method under a different root... maybe this should be optional per a param? //
/////////////////////////////////////////////////////////////////////////////////
return (<MaterialUIControllerProvider>
<ThemeProvider theme={theme}>
<Formik initialValues={initialValues} validationSchema={Yup.object().shape(formValidations)} onSubmit={handleSubmit}>
{({values, errors, touched}) =>
{
const formData: any = {};
formData.values = values;
formData.touched = touched;
formData.errors = errors;
formData.formFields = dynamicFormFields;
try
{
let anyDiffs = false;
for (let fieldName in values)
{
const value = values[fieldName];
if (lastValues[fieldName] != value)
{
handleChange(fieldName, value);
lastValues[fieldName] = value;
anyDiffs = true;
}
}
if (anyDiffs)
{
setLastValues(lastValues);
}
}
catch (e)
{
console.error(e);
}
return (<QDynamicForm formData={formData} record={record} />);
}}
</Formik>
</ThemeProvider>
</MaterialUIControllerProvider>);
}
/***************************************************************************
** Component to render a widget for the QFMD Bridge
***************************************************************************/
interface QFMDBridgeWidgetProps
{
widgetName?: string,
tableName?: string,
record?: QRecord,
entityPrimaryKey?: string,
actionCallback?: (data: any, eventValues?: { [p: string]: any }) => boolean
}
QFMDBridgeWidget.defaultProps = {};
function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, actionCallback}: QFMDBridgeWidgetProps): JSX.Element
{
const qContext = useContext(QContext);
const [ready, setReady] = useState(false);
const [widgetMetaData, setWidgetMetaData] = useState(null as QWidgetMetaData);
const [widgetData, setWidgetData] = useState(null as any);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
useEffect(() =>
{
(async () =>
{
const qController = Client.getInstance();
const qInstance = await qController.loadMetaData();
const queryStringParts: string[] = [];
for (let key of record?.values?.keys())
{
queryStringParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(record.values.get(key))}`);
}
setWidgetMetaData(qInstance.widgets.get(widgetName));
setWidgetData(await qController.widget(widgetName, queryStringParts.join("&")));
setReady(true);
})();
}, []);
if (!ready)
{
return (<Box py={"1rem"}>Loading...</Box>);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// internally in some widgets, useNavigate happens... so we must re-introduce the browser-router context //
// plus the contexts too, as indicated. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
return (<BrowserRouter>
<MaterialUIControllerProvider>
<ThemeProvider theme={theme}>
<QContext.Provider value={{
...qContext,
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
}}>
<div className={`bridgedWidget ${widgetMetaData.type}`}>
<DashboardWidgets tableName={tableName} widgetMetaDataList={[widgetMetaData]} initialWidgetDataList={[widgetData]} record={record} entityPrimaryKey={entityPrimaryKey} omitWrappingGridContainer={true} actionCallback={actionCallback} />
</div>
</QContext.Provider>
</ThemeProvider>
</MaterialUIControllerProvider>
</BrowserRouter>);
}
/***************************************************************************
** Component to render a modal for the QFMD Bridge
***************************************************************************/
interface QFMDBridgeModalProps
{
children: ReactNode;
onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}
QFMDBridgeModal.defaultProps = {};
function QFMDBridgeModal({children, onClose}: QFMDBridgeModalProps): JSX.Element
{
const [isOpen, setIsOpen] = useState(true);
function closeModalProcess(event: {}, reason: "backdropClick" | "escapeKeyDown")
{
if (onClose)
{
onClose(setIsOpen, event, reason);
}
else
{
setIsOpen(false);
}
}
return (
<Modal open={isOpen} onClose={(event, reason) => closeModalProcess(event, reason)}>
<Box className="bridgeModal" height="calc(100vh)">
{children}
</Box>
</Modal>
);
}
/***************************************************************************
** Component to render an alert for the QFMD Bridge
***************************************************************************/
interface QFMDBridgeAlertProps
{
color: string,
children: ReactNode,
mayManuallyClose?: boolean
}
QFMDBridgeAlert.defaultProps = {};
function QFMDBridgeAlert({color, children, mayManuallyClose}: QFMDBridgeAlertProps): JSX.Element
{
const [isOpen, setIsOpen] = useState(true);
function onClose()
{
setIsOpen(false);
}
if (isOpen)
{
//@ts-ignore color
return (<Alert color={color} onClose={mayManuallyClose ? onClose : null}>{children}</Alert>);
}
else
{
return (<React.Fragment />);
}
}
/***************************************************************************
** define the default qfmd bridge object
***************************************************************************/
export const qfmdBridge =
{
qController: Client.getInstance(),
makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }): JSX.Element =>
{
return (<MDButton {...extra} onClick={onClick} fullWidth>{label}</MDButton>);
},
makeAlert: (text: string, color: string, mayManuallyClose?: boolean): JSX.Element =>
{
return (<QFMDBridgeAlert color={color} mayManuallyClose={mayManuallyClose}>{text}</QFMDBridgeAlert>);
},
makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void): JSX.Element =>
{
return (<QFMDBridgeModal onClose={onClose}>{children}</QFMDBridgeModal>);
},
makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean): JSX.Element =>
{
return (<QFMDBridgeWidget widgetName={widgetName} tableName={tableName} record={record} entityPrimaryKey={entityPrimaryKey} actionCallback={actionCallback} />);
},
makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void): JSX.Element =>
{
return (<QFMDBridgeForm fields={fields} record={record} handleChange={handleChange} handleSubmit={handleSubmit} />);
}
};

View File

@ -0,0 +1,117 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import {qfmdBridge, QFMDBridge} from "qqq/utils/qqq/QFMDBridge";
import React, {useState} from "react";
// todo - deploy from here!!
interface DynamicComponentProps
{
qfmdBridge?: QFMDBridge;
props?: any;
}
/*******************************************************************************
** hook for working with Dynamically loaded components
**
*******************************************************************************/
export default function useDynamicComponents()
{
const [dynamicComponents, setDynamicComponents] = useState<{ [name: string]: React.FC }>({});
/*******************************************************************************
**
*******************************************************************************/
const loadComponent = async (name: string, url: string) =>
{
try
{
await new Promise((resolve, reject) =>
{
////////////////////////////////////////////////////////
// Dynamically load the bundle by adding a script tag //
////////////////////////////////////////////////////////
const script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
catch (e)
{
////////////////////////////////////////////////
// if the script can't be loaded log an error //
////////////////////////////////////////////////
console.error(`Error loading bundle from [${url}]`);
}
///////////////////////////////////////////////////////////////////////////////
// Assuming the bundle attaches itself to window.${name} (.${name} again...) //
// (Note: if exported as UMD, you might need to access the default export) //
///////////////////////////////////////////////////////////////////////////////
let component = (window as any)[name]?.[name];
if (!component)
{
console.error(`Component not found on window.${name}`);
component = () => <Box>Error loading {name}</Box>;
}
const newDCs = Object.assign({}, dynamicComponents);
newDCs[name] = component;
setDynamicComponents(newDCs);
};
/***************************************************************************
**
***************************************************************************/
const hasComponentLoaded = (name: string): boolean =>
{
return (!!dynamicComponents[name]);
};
/***************************************************************************
**
***************************************************************************/
const renderComponent = (name: string, props?: any): JSX.Element =>
{
if (dynamicComponents[name])
{
const C: React.FC<DynamicComponentProps> = dynamicComponents[name];
return (<C qfmdBridge={qfmdBridge} props={props} />);
}
else
{
return (<Box>Loading...</Box>);
}
};
return {
loadComponent,
hasComponentLoaded,
renderComponent
};
}

View File

@ -58,4 +58,6 @@ module.exports = function (app)
app.use("/api*", getRequestHandler());
app.use("/*api", getRequestHandler());
app.use("/qqq/*", 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/script", "metaData/table/script.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");
}

View File

@ -46,7 +46,9 @@ public class AppPageNavTest extends QBaseSeleniumTest
.withRouteToString("/widget/QuickSightChartRenderer", """
{"url": "http://www.google.com"}""")
.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

@ -42,7 +42,7 @@ public class AssociatedRecordScriptTest extends QBaseSeleniumTest
{
super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin.withRouteToFile("/data/person/1", "data/person/1701.json");
qSeleniumJavalin.withRouteToFile("/data/person/1/developer", "data/person/1701.json");
qSeleniumJavalin.withRouteToFile("/data/person/1/developer", "data/person/developer.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/query", "data/person/index.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", "[]");
}

View File

@ -51,12 +51,15 @@ public class SavedReportTest extends QBaseSeleniumTest
super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin
.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/pivotTableSetupWidget", "widget/pivotTableSetupWidget.json")
.withRouteToFile("/data/savedReport/possibleValues/tableName", "data/savedReport/possibleValues/tableName.json")
.withRouteToFile("/data/person/count", "data/person/count.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();
qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns").click();
qSeleniumJavalin.waitForCapturedPath("/data/person/count");
qSeleniumJavalin.waitForCapturedPath("/data/person/query");
qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
qSeleniumJavalin.endCapture();
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);

View File

@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.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/variants", "data/person/variants.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");

View File

@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlBasicModeTest extends QBaseSeleniumTest
qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.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/variants", "data/person/variants.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");

View File

@ -51,6 +51,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin
.withRouteToFile("/data/person/count", "data/person/count.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/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
@ -82,8 +84,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
///////////////////////////////////////////////////////////////////
String idEquals1FilterSubstring = """
{"fieldName":"id","operator":"EQUALS","values":["1"]}""";
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/count", idEquals1FilterSubstring);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", idEquals1FilterSubstring);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/count", idEquals1FilterSubstring);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", idEquals1FilterSubstring);
qSeleniumJavalin.endCapture();
///////////////////////////////////////
@ -102,8 +104,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
////////////////////////////////////////////////////////////////////
// assert that query & count both no longer have the filter value //
////////////////////////////////////////////////////////////////////
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
qSeleniumJavalin.endCapture();
@ -135,9 +137,9 @@ public class QueryScreenTest extends QBaseSeleniumTest
String expectedFilterContents2 = """
"booleanOperator":"OR\"""";
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents0);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents0);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents1);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents2);
qSeleniumJavalin.endCapture();
}
@ -409,7 +411,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin.beginCapture();
queryScreenLib.setBasicBooleanFilter(fieldLabel, operatorLabel);
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture();
}
@ -423,7 +425,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
qSeleniumJavalin.beginCapture();
queryScreenLib.setBasicFilterPossibleValues(fieldLabel, operatorLabel, values);
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture();
}
@ -469,7 +471,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
queryScreenLib.addAdvancedQueryFilterInput(0, fieldLabel, operatorLabel, value, null);
qSeleniumLib.clickBackdrop();
queryScreenLib.waitForAdvancedQueryStringMatchingRegex(expectQueryStringRegex);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
qSeleniumJavalin.endCapture();
queryScreenLib.clickAdvancedFilterClearIcon();
}

View File

@ -58,6 +58,8 @@ public class SavedViewsTest extends QBaseSeleniumTest
super.addJavalinRoutes(qSeleniumJavalin);
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.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");
}
@ -135,7 +137,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
queryScreenLib.assertSavedViewNameOnScreen("Some People");
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"));
qSeleniumJavalin.endCapture();
@ -162,7 +164,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
qSeleniumLib.waitForCondition("Current URL should not have filter id", () -> !driver.getCurrentUrl().endsWith("/person/savedView/2"));
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.*"));
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": {
}
}