From 3a5d2b22b99210ae6bab671e4b1abe524ad8eca1 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 28 Apr 2023 16:06:51 -0500 Subject: [PATCH 1/2] Add apiName & version to script editor; always prompt for commit message --- src/qqq/components/scripts/ScriptEditor.tsx | 228 ++++++++++++++++-- src/qqq/components/scripts/ScriptTestForm.tsx | 26 +- .../components/widgets/misc/ScriptViewer.tsx | 83 ++++--- 3 files changed, 266 insertions(+), 71 deletions(-) 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 ( + props.closeHandler(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + onKeyPress={e => keyPressHandler(e)} + > + Please Enter a Commit Message + + + + { + event.target.select(); + }} + /> + + + + props.closeHandler(false)} disabled={false} /> + props.closeHandler(true, commitMessage)} disabled={false}/> + + + ) +} + export default ScriptEditor; diff --git a/src/qqq/components/scripts/ScriptTestForm.tsx b/src/qqq/components/scripts/ScriptTestForm.tsx index 551bc65..887f38c 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); @@ -72,11 +70,6 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor const [testException, setTestException] = useState(null as string) const [firstRender, setFirstRender] = useState(true); - if(firstRender) - { - setFirstRender(false) - } - if(firstRender) { scriptDefinition.testInputFields.forEach((field: QFieldMetaData) => @@ -85,6 +78,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(); @@ -116,6 +114,8 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor { 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 +142,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 6c7fee8..d759756 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} +
+ } + + } + /> +
+ +
+ ); + }) } ; } @@ -401,7 +413,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc : <> } - @@ -459,7 +471,14 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc - + From 4d92a71848f22b6e2e45410dcf13540a86bc7851 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sun, 30 Apr 2023 19:57:47 -0500 Subject: [PATCH 2/2] Fix firstRender accidental delete; pass apiName, version to qController.testScript --- src/qqq/components/scripts/ScriptTestForm.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/qqq/components/scripts/ScriptTestForm.tsx b/src/qqq/components/scripts/ScriptTestForm.tsx index 887f38c..11b16da 100644 --- a/src/qqq/components/scripts/ScriptTestForm.tsx +++ b/src/qqq/components/scripts/ScriptTestForm.tsx @@ -70,6 +70,11 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor const [testException, setTestException] = useState(null as string) const [firstRender, setFirstRender] = useState(true); + if(firstRender) + { + setFirstRender(false); + } + if(firstRender) { scriptDefinition.testInputFields.forEach((field: QFieldMetaData) => @@ -108,6 +113,8 @@ 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