diff --git a/src/qqq/components/scripts/ScriptEditor.tsx b/src/qqq/components/scripts/ScriptEditor.tsx
new file mode 100644
index 0000000..a12b4a3
--- /dev/null
+++ b/src/qqq/components/scripts/ScriptEditor.tsx
@@ -0,0 +1,210 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. 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 .
+ */
+
+import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
+import {ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
+import Alert from "@mui/material/Alert";
+import Box from "@mui/material/Box";
+import Card from "@mui/material/Card";
+import Grid from "@mui/material/Grid";
+import Snackbar from "@mui/material/Snackbar";
+import TextField from "@mui/material/TextField";
+import FormData from "form-data";
+import React, {useReducer, useState} from "react";
+import AceEditor from "react-ace";
+import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
+import Client from "qqq/utils/qqq/Client";
+
+export interface ScriptEditorProps
+{
+ title: string;
+ scriptId: number;
+ contents: string;
+ closeCallback: any;
+}
+
+
+const qController = Client.getInstance();
+
+function ScriptEditor({title, scriptId, contents, closeCallback}: ScriptEditorProps): JSX.Element
+{
+ const [closing, setClosing] = useState(false);
+ const [updatedCode, setUpdatedCode] = useState(contents)
+ const [commitMessage, setCommitMessage] = useState("")
+ const [openTool, setOpenTool] = useState(null);
+ const [errorAlert, setErrorAlert] = useState("")
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+
+ const changeOpenTool = (event: React.MouseEvent, newValue: string | null) =>
+ {
+ setOpenTool(newValue);
+
+ // need this to make Ace recognize new height.
+ setTimeout(() =>
+ {
+ window.dispatchEvent(new Event("resize"))
+ }, 100);
+ };
+
+ const saveClicked = () =>
+ {
+ setClosing(true);
+
+ (async () =>
+ {
+ const formData = new FormData();
+ formData.append("scriptId", scriptId);
+ formData.append("contents", updatedCode);
+ formData.append("commitMessage", commitMessage);
+
+ //////////////////////////////////////////////////////////////////
+ // we don't want this job to go async, so, pass a large timeout //
+ //////////////////////////////////////////////////////////////////
+ formData.append("_qStepTimeoutMillis", 60 * 1000);
+
+ const formDataHeaders = {
+ "content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
+ };
+
+ try
+ {
+ const processResult = await qController.processInit("storeScriptRevision", formData, formDataHeaders);
+ console.log("process result");
+ console.log(processResult);
+
+ if (processResult instanceof QJobError)
+ {
+ const jobError = processResult as QJobError
+ setErrorAlert(jobError.userFacingError ?? jobError.error)
+ setClosing(false);
+ return;
+ }
+
+ closeCallback(null, "saved", "Saved New Script Version");
+ }
+ catch(e)
+ {
+ // @ts-ignore
+ setErrorAlert(e.message ?? "Unexpected error saving script")
+ setClosing(false);
+ }
+ })();
+ }
+
+ const cancelClicked = () =>
+ {
+ setClosing(true);
+ closeCallback(null, "cancelled");
+ }
+
+ const updateCode = (value: string, event: any) =>
+ {
+ console.log("Updating code")
+ setUpdatedCode(value);
+ forceUpdate();
+ }
+
+ const updateCommitMessage = (event: React.ChangeEvent) =>
+ {
+ setCommitMessage(event.target.value);
+ }
+
+ return (
+
+
+
+
+ {
+ if (reason === "clickaway")
+ {
+ return;
+ }
+ setErrorAlert("")
+ }} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setErrorAlert("")}>
+ {errorAlert}
+
+
+
+
+
+ {title}
+
+
+
+
+ Tools:
+
+
+ Preview
+
+
+
+
+
+
+
+
+ {/*
+ openTool &&
+
+ {
+ openTool == "preview" &&
+
+
+
+ }
+
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ScriptEditor;
diff --git a/src/qqq/components/widgets/misc/ScriptViewer.tsx b/src/qqq/components/widgets/misc/ScriptViewer.tsx
new file mode 100644
index 0000000..b4a6668
--- /dev/null
+++ b/src/qqq/components/widgets/misc/ScriptViewer.tsx
@@ -0,0 +1,394 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. 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 .
+ */
+
+import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
+import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
+import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
+import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
+import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
+import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
+import Alert from "@mui/material/Alert";
+import Avatar from "@mui/material/Avatar";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Chip from "@mui/material/Chip";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import Icon from "@mui/material/Icon";
+import List from "@mui/material/List";
+import ListItem from "@mui/material/ListItem";
+import ListItemAvatar from "@mui/material/ListItemAvatar";
+import ListItemText from "@mui/material/ListItemText";
+import Modal from "@mui/material/Modal";
+import Snackbar from "@mui/material/Snackbar";
+import Tab from "@mui/material/Tab";
+import Tabs from "@mui/material/Tabs";
+import Typography from "@mui/material/Typography";
+import React, {useReducer, useState} from "react";
+import AceEditor from "react-ace";
+import TabPanel from "qqq/components/misc/TabPanel";
+import ScriptEditor, {ScriptEditorProps} from "qqq/components/scripts/ScriptEditor";
+import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
+import {LoadingState} from "qqq/models/LoadingState";
+import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
+import Client from "qqq/utils/qqq/Client";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
+
+import "ace-builds/src-noconflict/mode-java";
+import "ace-builds/src-noconflict/mode-javascript";
+import "ace-builds/src-noconflict/mode-json";
+import "ace-builds/src-noconflict/theme-github";
+import "ace-builds/src-noconflict/ext-language_tools";
+
+const qController = Client.getInstance();
+
+// Declaring props types for ViewForm
+interface Props
+{
+ scriptId: number
+}
+
+ScriptViewer.defaultProps =
+ {
+ };
+
+export default function ScriptViewer({scriptId}: Props): JSX.Element
+{
+ const [scriptRecord, setScriptRecord] = useState(null as QRecord);
+ const [asyncLoadInited, setAsyncLoadInited] = useState(false);
+ const [versionRecordList, setVersionRecordList] = useState(null as QRecord[]);
+ const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
+ const [currentVersionId , setCurrentVersionId] = useState(null as number);
+ const [notFoundMessage, setNotFoundMessage] = useState(null);
+ const [selectedTab, setSelectedTab] = useState(0);
+ const [editorProps, setEditorProps] = useState(null as ScriptEditorProps);
+ const [successText, setSuccessText] = useState(null as string);
+ const [failText, setFailText] = useState(null as string)
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+
+ const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading"));
+
+ if (!asyncLoadInited)
+ {
+ setAsyncLoadInited(true);
+
+ (async () =>
+ {
+ try
+ {
+ const scriptRecord = await qController.get("script", scriptId);
+ setScriptRecord(scriptRecord);
+
+ const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])];
+ const orderBys = [new QFilterOrderBy("sequenceNo", false)];
+ const filter = new QQueryFilter(criteria, orderBys);
+ const versions = await qController.query("scriptRevision", filter, 25, 0);
+ console.log("Fetched versions:");
+ console.log(versions);
+ setVersionRecordList(versions);
+
+ if(versions && versions.length > 0)
+ {
+ setCurrentVersionId(versions[0].values.get("id"));
+ const latestVersion = await qController.get("scriptRevision", versions[0].values.get("id"));
+ console.log("Fetched latestVersion:");
+ console.log(latestVersion);
+ setSelectedVersionRecord(latestVersion);
+ loadingSelectedVersion.setNotLoading();
+ forceUpdate();
+ }
+ }
+ catch (e)
+ {
+ if (e instanceof QException)
+ {
+ if ((e as QException).status === "404")
+ {
+ setNotFoundMessage("Script code could not be found.");
+ return;
+ }
+ }
+ setNotFoundMessage("Error loading Script code: " + e);
+ }
+ })();
+ }
+
+ const editData = (contents: string) =>
+ {
+ const editorProps = {} as ScriptEditorProps;
+ editorProps.title = (contents ? "Editing Code for Script: " : "Initializing Code for Script: ") + scriptRecord?.values?.get("name");
+ editorProps.contents = contents;
+ editorProps.scriptId = scriptId;
+ setEditorProps(editorProps);
+ };
+
+ const closeEditingScript = (event: object, reason: string, alert: string = null) =>
+ {
+ if (reason === "backdropClick" || reason === "escapeKeyDown")
+ {
+ return;
+ }
+
+ if (reason === "saved")
+ {
+ setAsyncLoadInited(false);
+ forceUpdate();
+
+ if (alert)
+ {
+ setSuccessText(alert);
+ }
+ }
+ else if (reason === "failed")
+ {
+ setAsyncLoadInited(false);
+ forceUpdate();
+
+ if (alert)
+ {
+ setFailText(alert);
+ }
+ }
+
+ setEditorProps(null);
+ };
+
+ const changeTab = (newValue: number) =>
+ {
+ setSelectedTab(newValue);
+ forceUpdate();
+ };
+
+ const selectVersion = (version: QRecord) =>
+ {
+ (async () =>
+ {
+ // fetch the full version
+ setSelectedVersionRecord(version);
+ loadingSelectedVersion.setLoading();
+
+ const selectedVersion = await qController.get("scriptVersion", version.values.get("id"));
+ console.log("Fetched selectedVersion:");
+ console.log(selectedVersion);
+ setSelectedVersionRecord(selectedVersion);
+ loadingSelectedVersion.setNotLoading();
+ forceUpdate();
+ })();
+ };
+
+ function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
+ {
+ return
+ {
+ (versionRecordList == null || versionRecordList.length == 0) ?
+
+ There are not any versions of this script.
+
+ : <>>
+ }
+ {
+ versionRecordList?.map((version: any) => (
+
+ selectVersion(version)}>
+
+ {`${version.values.get("sequenceNo")}`}
+
+
+ {currentVersionId == version?.values?.get("id") && }
+ {version.values.get("commitMessage")}
+
+ }
+ secondary={
+ <>
+ {ValueUtils.formatDateTime(version.values.get("createDate"))}
+
+ {version.values.get("author")}
+ >
+ }
+ />
+
+
+
+ ))
+ }
+
;
+ }
+
+ let editButtonTooltip = "";
+ let editButtonText = "Create New Version";
+ if (currentVersionId)
+ {
+ if (currentVersionId === selectedVersionRecord?.values?.get("id"))
+ {
+ editButtonTooltip = "If you make any changes to this script, a new version will be created when you hit Save.";
+ editButtonText = "Edit";
+ }
+ else
+ {
+ editButtonTooltip = "If you want to make this previous version active, bring up the Edit window, make any changes " +
+ "to the old version if they are needed, then click Save. A new version will be created, and set as current.";
+ editButtonText = "Edit and Activate";
+ }
+ }
+
+ return (
+
+
+
+ {
+ notFoundMessage
+ ?
+ {notFoundMessage}
+ :
+
+ {
+ successText ? (
+ setSuccessText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setSuccessText(null)}>
+ {successText}
+
+
+ ) : ("")
+ }
+ {
+ failText ? (
+ setFailText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
+ setFailText(null)}>
+ {failText}
+
+
+ ) : ("")
+ }
+
+
+
+ <>
+
+
+ changeTab(newValue)}
+ variant="standard"
+ >
+
+
+
+
+
+
+
+
+
+ Versions
+
+ {getVersionsList(versionRecordList, selectedVersionRecord)}
+
+
+
+
+ {
+ selectedVersionRecord ?
+
+ Version {selectedVersionRecord.values.get("sequenceNo")}
+ {
+ currentVersionId === selectedVersionRecord.values.get("id")
+ ? (<> (Current)>)
+ : <>>
+ }
+
+ : <>>
+ }
+
+
+
+
+ {
+ loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("contents") ? (
+ <>
+
+ >
+ ) : null
+ }
+ {
+ loadingSelectedVersion.isLoadingSlow() && Loading...
+ }
+
+
+
+ {/*
+
+
+
+
+ Versions
+
+ {getVersionsList(versionRecordList, selectedVersionRecord)}
+
+
+
+ Data Preview (Version {selectedVersionRecord?.values?.get("sequenceNo")})
+
+
+ {loadingSelectedVersion.isNotLoading() && selectedTab == 1 && selectedVersionRecord?.values?.get("data") && }
+ {loadingSelectedVersion.isLoadingSlow() && Loading...}
+
+
+
+
+ */}
+ >
+
+
+
+ {
+ editorProps &&
+ closeEditingScript(event, reason)}>
+
+
+ }
+
+
+ }
+
+
+
+ );
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/AssociatedRecordScriptTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/AssociatedRecordScriptTest.java
new file mode 100755
index 0000000..737d32a
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/AssociatedRecordScriptTest.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.materialdashboard.tests;
+
+
+import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
+import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Test for Associated Record Scripts functionality.
+ *******************************************************************************/
+public class AssociatedRecordScriptTest extends QBaseSeleniumTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
+ {
+ super.addJavalinRoutes(qSeleniumJavalin);
+ qSeleniumJavalin.withRouteToFile("/data/person/1", "data/person/1701.json");
+ qSeleniumJavalin.withRouteToFile("/data/person/1/developer", "data/person/1701.json");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNavigatingBackAndForth()
+ {
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person/1", "John Doe");
+ qSeleniumLib.waitForSelectorContaining("BUTTON", "actions").click();
+
+ qSeleniumLib.waitForSelectorContaining("LI", "Developer Mode").click();
+ assertTrue(qSeleniumLib.driver.getCurrentUrl().endsWith("/1/dev"));
+
+ qSeleniumLib.waitForever();
+ }
+
+}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ScriptTableTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ScriptTableTest.java
new file mode 100755
index 0000000..0f3f85b
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/ScriptTableTest.java
@@ -0,0 +1,66 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.materialdashboard.tests;
+
+
+import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
+import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
+import org.junit.jupiter.api.Test;
+
+
+/*******************************************************************************
+ ** Test for the scripts table
+ *******************************************************************************/
+public class ScriptTableTest extends QBaseSeleniumTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
+ {
+ super.addJavalinRoutes(qSeleniumJavalin);
+ qSeleniumJavalin
+ .withRouteToFile("/data/script/1", "data/script/1.json")
+ .withRouteToFile("/metaData/table/script", "metaData/table/script.json")
+ .withRouteToFile("/widget/scriptViewer", "widget/scriptViewer.json")
+ ;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/developer/script/1", "Hello, Script");
+
+ qSeleniumLib.waitForSelectorContaining("span", "uh, script");
+
+ qSeleniumLib.takeScreenshotToFile();
+ // qSeleniumLib.waitForever();
+ }
+
+}
diff --git a/src/test/resources/fixtures/data/person/developer.json b/src/test/resources/fixtures/data/person/developer.json
new file mode 100644
index 0000000..3efcb82
--- /dev/null
+++ b/src/test/resources/fixtures/data/person/developer.json
@@ -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.nutrifresh.one.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.nutrifresh.one.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.nutrifresh.one.processes.deposco.RunDeposcoOrderOptimizationBatchNameScript",
+ "codeType": "JAVA",
+ "codeUsage": "SCRIPT_TESTER"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/fixtures/data/script/1.json b/src/test/resources/fixtures/data/script/1.json
new file mode 100644
index 0000000..59a8d1b
--- /dev/null
+++ b/src/test/resources/fixtures/data/script/1.json
@@ -0,0 +1,20 @@
+{
+ "tableName": "script",
+ "recordLabel": "Hello, Script",
+ "values": {
+ "name": "Hello, Script",
+ "id": 1,
+ "tableName": "client",
+ "createDate": "2023-02-18T00:47:51Z",
+ "modifyDate": "2023-02-18T00:47:51Z",
+ "scriptTypeId": 1
+ },
+ "displayValues": {
+ "tableName": "Client",
+ "scriptTypeId": "Unknown",
+ "name": "Hello, Script",
+ "id": "1",
+ "createDate": "2023-02-18T00:47:51Z",
+ "modifyDate": "2023-02-18T00:47:51Z"
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/fixtures/metaData/table/script.json b/src/test/resources/fixtures/metaData/table/script.json
new file mode 100644
index 0000000..e4b664f
--- /dev/null
+++ b/src/test/resources/fixtures/metaData/table/script.json
@@ -0,0 +1,139 @@
+{
+ "table": {
+ "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
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/fixtures/widget/scriptViewer.json b/src/test/resources/fixtures/widget/scriptViewer.json
new file mode 100644
index 0000000..12f33aa
--- /dev/null
+++ b/src/test/resources/fixtures/widget/scriptViewer.json
@@ -0,0 +1,7 @@
+{
+ "type": "scriptViewer",
+ "queryParams": {
+ "id": "1",
+ "tableName": "script"
+ }
+}
\ No newline at end of file