/* * 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 {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, 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"; import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/ext-language_tools"; export interface ScriptEditorProps { title: string; scriptId: number; scriptRevisionRecord: QRecord; closeCallback: any; tableName: string; fieldName: string; recordId: any; scriptDefinition: any; scriptTypeRecord: QRecord; } const qController = Client.getInstance(); function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptDefinition, scriptTypeRecord}: ScriptEditorProps): JSX.Element { const [closing, setClosing] = useState(false); 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(() => { // @ts-ignore // eslint-disable-next-line import/namespace const langTools = ace.require("ace/ext/language_tools"); const myCompleter = { // @ts-ignore getCompletions: function (editor, session, pos, prefix, callback) { // @ts-ignore 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 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.runProcess(", meta: "Run a process"}); // 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 callback(null, completions); } }; langTools.addCompleter(myCompleter); const preventUnload = (event: BeforeUnloadEvent) => { /////////////////////////////////////////////////////////////////////// // 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; }; window.addEventListener("beforeunload", preventUnload); return () => { 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) => { setOpenTool(newValue); // need this to make Ace recognize new height. setTimeout(() => { window.dispatchEvent(new Event("resize")) }, 100); }; 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 () => { const formData = new FormData(); formData.append("scriptId", scriptId); formData.append("contents", updatedCode); 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(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000); try { const processResult = await qController.processInit("storeScriptRevision", formData, qController.defaultMultipartFormDataHeaders()); 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); } 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 ( { if (reason === "clickaway") { return; } setErrorAlert("") }} anchorOrigin={{vertical: "top", horizontal: "center"}}> setErrorAlert("")}> {errorAlert} {title} Tools: Test Docs { openTool && { openTool == "test" && } { openTool == "docs" && } } 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;