diff --git a/src/qqq/components/scripts/ScriptEditor.tsx b/src/qqq/components/scripts/ScriptEditor.tsx
index 5954da4..d67ffa3 100644
--- a/src/qqq/components/scripts/ScriptEditor.tsx
+++ b/src/qqq/components/scripts/ScriptEditor.tsx
@@ -19,19 +19,26 @@
* along with this program. If not, see .
*/
+import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
+import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
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 Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
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, {useEffect, useReducer, useState} from "react";
+import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
+import DynamicSelect from "qqq/components/forms/DynamicSelect";
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
import Client from "qqq/utils/qqq/Client";
@@ -44,7 +51,7 @@ export interface ScriptEditorProps
{
title: string;
scriptId: number;
- contents: string;
+ scriptRevisionRecord: QRecord;
closeCallback: any;
tableName: string;
fieldName: string;
@@ -55,14 +62,22 @@ export interface ScriptEditorProps
const qController = Client.getInstance();
-function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fieldName, recordId, scriptDefinition, scriptTypeRecord}: ScriptEditorProps): JSX.Element
+function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptDefinition, scriptTypeRecord}: ScriptEditorProps): JSX.Element
{
const [closing, setClosing] = useState(false);
- const [updatedCode, setUpdatedCode] = useState(contents)
+
+ const [updatedCode, setUpdatedCode] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("contents") : "");
+ const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
+ const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
+ const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
+ const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
+
const [commitMessage, setCommitMessage] = useState("")
const [openTool, setOpenTool] = useState(null);
const [errorAlert, setErrorAlert] = useState("")
+ const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
+ const ref = useRef();
useEffect(() =>
{
@@ -78,16 +93,20 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
let completions = [];
// todo - get from backend, based on the script type
+ completions.push({value: "api.get(", meta: "Get a records in a table."});
completions.push({value: "api.query(", meta: "Search for records in a table."});
- completions.push({value: "api.insert(", meta: "Create one or more records in a table."});
- completions.push({value: "api.update(", meta: "Update one or more records in a table."});
- completions.push({value: "api.delete(", meta: "Remove one or more records from a table."});
- completions.push({value: "api.newRecord(", meta: "Create a new QRecord object."});
- completions.push({value: "api.newQueryInput(", meta: "Create a new QueryInput object."});
- completions.push({value: "api.newQueryFilter(", meta: "Create a new QueryFilter object."});
- completions.push({value: "api.newFilterCriteria(", meta: "Create a new FilterCriteria object."});
- completions.push({value: "api.newFilterOrderBy(", meta: "Create a new FilterOrderBy object."});
- completions.push({value: "getValue(", meta: "Get a value from a record"});
+ completions.push({value: "api.insert(", meta: "Create one record in a table."});
+ completions.push({value: "api.update(", meta: "Update one record in a table."});
+ completions.push({value: "api.delete(", meta: "Remove one record from a table."});
+ completions.push({value: "api.bulkInsert(", meta: "Create multiple records in a table."});
+ completions.push({value: "api.bulkUpdate(", meta: "Update multiple records in a table."});
+ completions.push({value: "api.bulkDelete(", meta: "Remove multiple records from a table."});
+ // completions.push({value: "api.newRecord(", meta: "Create a new QRecord object."});
+ // completions.push({value: "api.newQueryInput(", meta: "Create a new QueryInput object."});
+ // completions.push({value: "api.newQueryFilter(", meta: "Create a new QueryFilter object."});
+ // completions.push({value: "api.newFilterCriteria(", meta: "Create a new FilterCriteria object."});
+ // completions.push({value: "api.newFilterOrderBy(", meta: "Create a new FilterOrderBy object."});
+ // completions.push({value: "getValue(", meta: "Get a value from a record"});
completions.push({value: "logger.log(", meta: "Write a Script Log Line"});
// @ts-ignore
@@ -98,7 +117,9 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
const preventUnload = (event: BeforeUnloadEvent) =>
{
- // NOTE: This message isn't used in modern browsers, but is required
+ ///////////////////////////////////////////////////////////////////////
+ // NOTE: This message isn't used in modern browsers, but is required //
+ ///////////////////////////////////////////////////////////////////////
const message = "Are you sure you want to leave?";
event.preventDefault();
event.returnValue = message;
@@ -109,8 +130,43 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
{
window.removeEventListener("beforeunload", preventUnload);
};
+
}, []);
+ /*
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // nice idea here, but we can't figure out how to call the function in the child component :( //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ const handleCommandT = () =>
+ {
+ console.log("Command-T pressed!");
+ if(openTool != "test")
+ {
+ console.log("Setting open tool to 'test'")
+ setOpenTool("test");
+ return;
+ }
+
+ if(runTestCallback)
+ {
+ console.log("Trying to call triggerTestScript...")
+ runTestCallback();
+ }
+ // @ts-ignore
+ // ref.current?.triggerTestScript();
+ };
+
+ useEffect(() =>
+ {
+ const editor = getAceInstance().edit("editor");
+ editor.commands.removeCommand("customCommandT");
+ editor.commands.addCommand({
+ name: "customCommandT",
+ bindKey: {win: "Ctrl-T", mac: "Command-T"},
+ exec: handleCommandT,
+ });
+ }, [openTool]);
+ */
const changeOpenTool = (event: React.MouseEvent, newValue: string | null) =>
{
@@ -123,8 +179,20 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
}, 100);
};
- const saveClicked = () =>
+ const saveClicked = (overrideCommitMessage?: string) =>
{
+ if(!apiName || !apiVersion)
+ {
+ setErrorAlert("You must select a value for both API Name and API Version.")
+ return;
+ }
+
+ if(!commitMessage && !overrideCommitMessage)
+ {
+ setPromptForCommitMessageOpen(true);
+ return;
+ }
+
setClosing(true);
(async () =>
@@ -132,20 +200,26 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
const formData = new FormData();
formData.append("scriptId", scriptId);
formData.append("contents", updatedCode);
- formData.append("commitMessage", commitMessage);
+ formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
+
+ if(apiName)
+ {
+ formData.append("apiName", apiName);
+ }
+
+ if(apiVersion)
+ {
+ formData.append("apiVersion", apiVersion);
+ }
//////////////////////////////////////////////////////////////////
// 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",
- };
+ formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000);
try
{
- const processResult = await qController.processInit("storeScriptRevision", formData, formDataHeaders);
+ const processResult = await qController.processInit("storeScriptRevision", formData, qController.defaultMultipartFormDataHeaders());
console.log("process result");
console.log(processResult);
@@ -186,6 +260,45 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
setCommitMessage(event.target.value);
}
+ const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
+ {
+ setPromptForCommitMessageOpen(false);
+
+ if(wasSaveClicked)
+ {
+ setCommitMessage(message)
+ saveClicked(message);
+ }
+ else
+ {
+ setClosing(false);
+ }
+ }
+
+ const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
+ {
+ if(apiNamePossibleValue)
+ {
+ setApiName(apiNamePossibleValue.id);
+ }
+ else
+ {
+ setApiName(null);
+ }
+ }
+
+ const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
+ {
+ if(apiVersionPossibleValue)
+ {
+ setApiVersion(apiVersionPossibleValue.id);
+ }
+ else
+ {
+ setApiVersion(null);
+ }
+ }
+
return (
@@ -226,6 +339,14 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
+
+
+
+
+
+
+
+
@@ -248,7 +369,7 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
openTool &&
{
- openTool == "test" &&
+ openTool == "test" &&
}
{
openTool == "docs" &&
@@ -259,17 +380,72 @@ function ScriptEditor({title, scriptId, contents, closeCallback, tableName, fiel
-
+
-
+ saveClicked()} />
+
+
+
);
}
+function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void})
+{
+ const [commitMessage, setCommitMessage] = useState("No commit message given")
+
+ const updateCommitMessage = (event: React.ChangeEvent) =>
+ {
+ setCommitMessage(event.target.value);
+ }
+
+ const keyPressHandler = (e: React.KeyboardEvent) =>
+ {
+ if(e.key === "Enter")
+ {
+ props.closeHandler(true, commitMessage);
+ }
+ }
+
+ return (
+
+ )
+}
+
export default ScriptEditor;
diff --git a/src/qqq/components/scripts/ScriptTestForm.tsx b/src/qqq/components/scripts/ScriptTestForm.tsx
index 551bc65..11b16da 100644
--- a/src/qqq/components/scripts/ScriptTestForm.tsx
+++ b/src/qqq/components/scripts/ScriptTestForm.tsx
@@ -48,7 +48,7 @@ interface AssociatedScriptDefinition
testOutputFields: QFieldMetaData[];
}
-interface Props
+export interface ScriptTestFormProps
{
scriptId: number;
scriptDefinition: AssociatedScriptDefinition;
@@ -56,15 +56,13 @@ interface Props
fieldName: string;
recordId: any;
code: string;
+ apiName: string;
+ apiVersion: string;
}
-ScriptTestForm.defaultProps = {
- // foo: null,
-};
-
const qController = Client.getInstance();
-function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recordId, code}: Props): JSX.Element
+function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recordId, code, apiName, apiVersion}: ScriptTestFormProps): JSX.Element
{
const [testInputValues, setTestInputValues] = useState({} as any);
const [testOutputValues, setTestOutputValues] = useState({} as any);
@@ -74,7 +72,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
if(firstRender)
{
- setFirstRender(false)
+ setFirstRender(false);
}
if(firstRender)
@@ -85,6 +83,11 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
});
}
+ const buildFullExceptionMessage = (exception: any): string =>
+ {
+ return (exception.message + (exception.cause ? "\ncaused by: " + buildFullExceptionMessage(exception.cause) : ""));
+ };
+
const testScript = () =>
{
const inputValues = new Map();
@@ -110,12 +113,16 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
/////////////////////////////////////////////////////////////////
// associated record scripts - run this way (at least for now) //
/////////////////////////////////////////////////////////////////
+ inputValues.set("apiName", apiName);
+ inputValues.set("apiVersion", apiVersion);
output = await qController.testScript(tableName, recordId, fieldName, code, inputValues);
}
else
{
const formData = new FormData();
formData.append("scriptId", scriptId);
+ formData.append("apiName", apiName);
+ formData.append("apiVersion", apiVersion);
formData.append("code", code);
for(let fieldName of inputValues.keys())
@@ -142,8 +149,8 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
setTestOutputValues(output.outputObject ?? {});
if(output.exception)
{
- setTestException(output.exception.message)
- console.log(`set test exception to ${output.exception.message}`);
+ const exceptionMessage = buildFullExceptionMessage(output.exception);
+ setTestException(exceptionMessage)
}
if(output.scriptLogLines && output.scriptLogLines.length)
diff --git a/src/qqq/components/widgets/misc/ScriptViewer.tsx b/src/qqq/components/widgets/misc/ScriptViewer.tsx
index 36e45e4..2f25223 100644
--- a/src/qqq/components/widgets/misc/ScriptViewer.tsx
+++ b/src/qqq/components/widgets/misc/ScriptViewer.tsx
@@ -165,11 +165,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
})();
}
- const editData = (contents: string) =>
+ const editData = (selectedVersionRecord: QRecord) =>
{
const editorProps = {} as ScriptEditorProps;
- editorProps.title = (contents ? "Editing Code for Script: " : "Initializing Code for Script: ") + scriptRecord?.values?.get("name");
- editorProps.contents = contents;
+ editorProps.title = (selectedVersionRecord ? "Editing Code for Script: " : "Initializing Code for Script: ") + scriptRecord?.values?.get("name");
+ editorProps.scriptRevisionRecord = selectedVersionRecord;
editorProps.scriptId = scriptId;
editorProps.tableName = associatedScriptTableName;
editorProps.fieldName = associatedScriptFieldName;
@@ -244,33 +244,45 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
: <>>
}
{
- 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")}
- >
- }
- />
-
-
-
- ))
+ versionRecordList?.map((version: any) =>
+ {
+ const timeAuthorLine = `${ValueUtils.formatDateTime(version.values.get("createDate"))} by ${version.values.get("author")}`;
+ const apiLine = `API: ${version.displayValues.get("apiName") ?? "None"} version ${version.displayValues.get("apiVersion") ?? "None"}`;
+
+ return (
+
+ selectVersion(version)}>
+
+ {`${version.values.get("sequenceNo")}`}
+
+
+ {scriptRecord.values.get("currentScriptRevisionId") == version?.values?.get("id") && }
+ {version.values.get("commitMessage")}
+
+ }
+ secondary={
+ <>
+
+ {timeAuthorLine}
+
+ {
+ (version.displayValues.get("apiName") || version.displayValues.get("apiVersion")) &&
+
+ {apiLine}
+
+ }
+ >
+ }
+ />
+
+
+
+ );
+ })
}
;
}
@@ -402,7 +414,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
: <>>
}
-
@@ -460,7 +472,14 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
-
+