Adding test mode to assocaited record screens; code editor as an adornment type for forms

This commit is contained in:
2022-11-10 11:55:02 -06:00
parent 5843a00892
commit b3a131a64f
11 changed files with 600 additions and 163 deletions

View File

@ -13,7 +13,7 @@
"@fullcalendar/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "5.10.0",
"@kingsrook/qqq-frontend-core": "1.0.33",
"@kingsrook/qqq-frontend-core": "1.0.34",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1",

View File

@ -158,6 +158,7 @@ function QDynamicForm(props: Props): JSX.Element
bulkEditMode={bulkEditMode}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
formFieldObject={field}
/>
</Grid>
);

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import * as Yup from "yup";
@ -73,6 +74,17 @@ class DynamicFormUtils
fieldType = "text";
}
let more: any = {};
if (field.hasAdornment(AdornmentType.CODE_EDITOR))
{
fieldType = "ace";
const values = field.getAdornment(AdornmentType.CODE_EDITOR).values;
if (values.has("languageMode"))
{
more.languageMode = values.get("languageMode");
}
}
let label = field.label ? field.label : field.name;
label += field.isRequired ? " *" : "";
@ -84,6 +96,7 @@ class DynamicFormUtils
type: fieldType,
displayFormat: field.displayFormat,
// todo invalidMsg: "Zipcode is not valid (e.g. 70000).",
...more
});
}

View File

@ -22,8 +22,9 @@
import {InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik";
import React, {SyntheticEvent, useState} from "react";
import {ErrorMessage, Field, FieldProps, useFormikContext} from "formik";
import React, {useState} from "react";
import AceEditor from "react-ace";
import QBooleanFieldSwitch from "qqq/components/QDynamicFormField/QBooleanFieldSwitch";
import MDBox from "qqq/components/Temporary/MDBox";
import MDInput from "qqq/components/Temporary/MDInput";
@ -43,14 +44,15 @@ interface Props
bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any;
formFieldObject: any; // is the type returned by DynamicFormUtils.getDynamicField
}
function QDynamicFormField({
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, ...rest
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, formFieldObject, ...rest
}: Props): JSX.Element
{
const [ switchChecked, setSwitchChecked ] = useState(false);
const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode);
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const {setFieldValue} = useFormikContext();
@ -82,15 +84,50 @@ function QDynamicFormField({
}
};
let field;
let getsBulkEditHtmlLabel = true;
if (type === "checkbox")
{
getsBulkEditHtmlLabel = false;
field = (<QBooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />);
}
else if (type === "ace")
{
let mode = "text";
if(formFieldObject && formFieldObject.languageMode)
{
mode = formFieldObject.languageMode;
}
const field = () =>
(type == "checkbox" ?
<QBooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} /> :
getsBulkEditHtmlLabel = false;
field = (
<>
<InputLabel shrink={true}>{label}</InputLabel>
<AceEditor
mode={mode}
theme="github"
name="editor"
editorProps={{$blockScrolling: true}}
onChange={(value: string, event: any) =>
{
setFieldValue(name, value, false);
}}
width="100%"
height="300px"
value={value}
style={{border: "1px solid gray"}}
/>
</>
);
}
else
{
field = (
<>
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onKeyPress={(e: any) =>
{
if(e.key === "Enter")
if (e.key === "Enter")
{
e.preventDefault();
}
@ -103,10 +140,16 @@ function QDynamicFormField({
</MDBox>
</>
);
}
const bulkEditSwitchChanged = () =>
{
const newSwitchValue = !switchChecked;
setBulkEditSwitch(!switchChecked);
};
const setBulkEditSwitch = (value: boolean) =>
{
const newSwitchValue = value;
setSwitchChecked(newSwitchValue);
setIsDisabled(!newSwitchValue);
bulkEditSwitchChangeHandler(name, newSwitchValue);
@ -124,13 +167,13 @@ function QDynamicFormField({
/>
</Box>
<Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}>
{/* for checkboxes, if we put the whole thing in a label, we get bad overly aggressive toggling of the outer switch... */}
{(type == "checkbox" ?
field() :
<label htmlFor={`bulkEditSwitch-${name}`}>
{field()}
</label>
)}
{
getsBulkEditHtmlLabel
? (<label htmlFor={`bulkEditSwitch-${name}`}>
{field}
</label>)
: <div onClick={() => setBulkEditSwitch(true)}>{field}</div>
}
</Box>
</Box>
);
@ -139,7 +182,7 @@ function QDynamicFormField({
{
return (
<MDBox mb={1.5}>
{field()}
{field}
</MDBox>
);
}

View File

@ -847,7 +847,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
// construct the url for the export //
//////////////////////////////////////
const d = new Date();
const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
const dateString = `${d.getFullYear()}-${zp(d.getMonth()+1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(filterModel)))}&fields=${visibleFields.join(",")}`;
@ -870,7 +870,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
}, 1);
</script>
</head>
<body>Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + "records" : ""}...</body>
<body>Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...</body>
</html>`);
///////////////////////////////////////////

View File

@ -20,7 +20,9 @@
*/
import {Typography} from "@mui/material";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {Label} from "@mui/icons-material";
import {ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
@ -29,10 +31,20 @@ import TextField from "@mui/material/TextField";
import React, {useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/QButtons";
import ScriptDocsForm from "qqq/pages/entity-view/ScriptDocsForm";
import ScriptTestForm from "qqq/pages/entity-view/ScriptTestForm";
import QClient from "qqq/utils/QClient";
interface AssociatedScriptDefinition
{
testInputFields: QFieldMetaData[];
testOutputFields: QFieldMetaData[];
scriptType: any;
}
interface Props
{
scriptDefinition: AssociatedScriptDefinition;
tableName: string;
primaryKey: any;
fieldName: string;
@ -46,11 +58,24 @@ interface Props
const qController = QClient.getInstance();
function AssociatedScriptEditor({tableName, primaryKey, fieldName, titlePrefix, recordLabel, scriptName, code, closeCallback}: Props): JSX.Element
function AssociatedScriptEditor({scriptDefinition, tableName, primaryKey, fieldName, titlePrefix, recordLabel, scriptName, code, closeCallback}: Props): JSX.Element
{
const [closing, setClosing] = useState(false);
const [updatedCode, setUpdatedCode] = useState(code)
const [commitMessage, setCommitMessage] = useState("")
const [openTool, setOpenTool] = useState(null);
const changeOpenTool = (event: React.MouseEvent<HTMLElement>, newValue: string | null) =>
{
setOpenTool(newValue);
// need this to make Ace recognize new height.
setTimeout(() =>
{
window.dispatchEvent(new Event("resize"))
}, 100);
};
const saveClicked = () =>
{
@ -80,12 +105,32 @@ function AssociatedScriptEditor({tableName, primaryKey, fieldName, titlePrefix,
}
return (
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={12}>
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
<Card sx={{height: "100%", p: 3}}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h5" pb={1}>
{`${titlePrefix}: ${recordLabel} - ${scriptName}`}
</Typography>
<Box>
<Typography variant="body2" display="inline" pr={1}>
Tools:
</Typography>
<ToggleButtonGroup
value={openTool}
exclusive
onChange={changeOpenTool}
size="small"
sx={{pb: 1}}
>
<ToggleButton value="test">Test</ToggleButton>
<ToggleButton value="docs">Docs</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
<Box sx={{height: openTool ? "45%" : "100%"}}>
<AceEditor
mode="javascript"
theme="github"
@ -97,6 +142,19 @@ function AssociatedScriptEditor({tableName, primaryKey, fieldName, titlePrefix,
value={updatedCode}
style={{border: "1px solid gray"}}
/>
</Box>
{
openTool &&
<Box sx={{height: "45%"}} pt={2}>
{
openTool == "test" && <ScriptTestForm scriptDefinition={scriptDefinition} tableName={tableName} fieldName={fieldName} recordId={primaryKey} code={updatedCode} />
}
{
openTool == "docs" && <ScriptDocsForm helpText={scriptDefinition.scriptType.values.helpText} exampleCode={scriptDefinition.scriptType.values.sampleCode} aceEditorHeight="100%" />
}
</Box>
}
<Box pt={1}>
<Grid container alignItems="flex-end">

View File

@ -20,6 +20,7 @@
*/
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Chip, Icon, ListItem, ListItemAvatar, Typography} from "@mui/material";
@ -35,24 +36,20 @@ 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 Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Tabs from "@mui/material/Tabs";
import TextField from "@mui/material/TextField";
import React, {useContext, useReducer, useState} from "react";
import AceEditor from "react-ace";
import {useParams} from "react-router-dom";
import QContext from "QContext";
import BaseLayout from "qqq/components/BaseLayout";
import CustomWidthTooltip from "qqq/components/CustomWidthTooltip/CustomWidthTooltip";
import DataTableBodyCell from "qqq/components/Temporary/DataTable/DataTableBodyCell";
import DataTableHeadCell from "qqq/components/Temporary/DataTable/DataTableHeadCell";
import MDBox from "qqq/components/Temporary/MDBox";
import AssociatedScriptEditor from "qqq/pages/entity-view/AssociatedScriptEditor";
import ScriptLogsView from "qqq/pages/entity-view/ScriptLogsView";
import ScriptTestForm from "qqq/pages/entity-view/ScriptTestForm";
import QClient from "qqq/utils/QClient";
import QValueUtils from "qqq/utils/QValueUtils";
import ScriptDocsForm from "./ScriptDocsForm";
import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript";
@ -60,7 +57,6 @@ import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/ext-language_tools";
const qController = QClient.getInstance();
interface TabPanelProps
@ -119,6 +115,8 @@ function EntityDeveloperView({table}: Props): JSX.Element
const [selectedTabs, setSelectedTabs] = useState({} as any);
const [viewingRevisions, setViewingRevisions] = useState({} as any);
const [scriptLogs, setScriptLogs] = useState({} as any);
const [testInputValues, setTestInputValues] = useState({} as any);
const [testOutputValues, setTestOutputValues] = useState({} as any);
const [editingScript, setEditingScript] = useState(null as any);
const [alertText, setAlertText] = useState(null as string);
@ -157,6 +155,23 @@ function EntityDeveloperView({table}: Props): JSX.Element
setAssociatedScripts(developerModeData.associatedScripts);
const testInputValues = {};
const testOutputValues = {};
console.log("@dk - here");
console.log(developerModeData.associatedScripts);
developerModeData.associatedScripts.forEach((object: any) =>
{
const fieldName = object.associatedScript.fieldName;
// @ts-ignore
testInputValues[fieldName] = {};
// @ts-ignore
testOutputValues[fieldName] = {};
});
setTestInputValues(testInputValues);
setTestOutputValues(testOutputValues);
const recordJSONObject = {} as any;
for (let key of record.values.keys())
{
@ -208,15 +223,44 @@ function EntityDeveloperView({table}: Props): JSX.Element
return color;
};
const editScript = (fieldName: string, code: string) =>
const editScript = (fieldName: string, code: string, object: any) =>
{
const editingScript = {} as any;
editingScript.fieldName = fieldName;
editingScript.titlePrefix = code ? "Editing Script" : "Creating New Script";
editingScript.code = code;
editingScript.scriptDefinitionObject = object;
setEditingScript(editingScript);
};
const testScript = (object: any, fieldName: string) =>
{
const viewingRevisionArray = object.scriptRevisions?.filter((rev: any) => rev?.values?.id === viewingRevisions[fieldName]);
const code = viewingRevisionArray?.length > 0 ? viewingRevisionArray[0].values.contents : "";
const inputValues = new Map<string, any>();
if (object.testInputFields)
{
object.testInputFields.forEach((field: QFieldMetaData) =>
{
console.log(`${field.name} = ${testInputValues[fieldName][field.name]}`)
inputValues.set(field.name, testInputValues[fieldName][field.name]);
});
}
const newTestOutputValues = JSON.parse(JSON.stringify(testOutputValues));
newTestOutputValues[fieldName] = {};
setTestOutputValues(newTestOutputValues);
(async () =>
{
const output = await qController.testScript(tableName, id, fieldName, code, inputValues);
const newTestOutputValues = JSON.parse(JSON.stringify(testOutputValues));
newTestOutputValues[fieldName] = output.outputValues;
setTestOutputValues(newTestOutputValues);
})();
};
const closeEditingScript = (event: object, reason: string, alert: string = null) =>
{
if (reason === "backdropClick")
@ -256,7 +300,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
scriptLogs[revisionId] = null;
setScriptLogs(scriptLogs);
loadRevisionLogs(fieldName, revisionId)
loadRevisionLogs(fieldName, revisionId);
forceUpdate();
};
@ -276,7 +320,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
setScriptLogs(scriptLogs);
forceUpdate();
})();
}
};
function getRevisionsList(scriptRevisions: any, fieldName: any, currentScriptRevisionId: any)
{
@ -333,54 +377,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
return <Typography variant="body2" p={3}>No logs available for this version.</Typography>;
}
return (
<TableContainer sx={{boxShadow: "none"}}>
<Table>
<Box component="thead">
<TableRow key="header">
<DataTableHeadCell sorted={false}>Timestamp</DataTableHeadCell>
<DataTableHeadCell sorted={false} align="right">Run Time (ms)</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Had Error?</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Input</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Output</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Logs</DataTableHeadCell>
</TableRow>
</Box>
<TableBody>
{
logs.map((logRecord) =>
{
let logs = "";
if (logRecord.values.scriptLogLine)
{
for (let i = 0; i < logRecord.values.scriptLogLine.length; i++)
{
console.log(" += " + i);
logs += (logRecord.values.scriptLogLine[i].values.text + "\n");
}
}
return (
<TableRow key={logRecord.values.id}>
<DataTableBodyCell>{QValueUtils.formatDateTime(logRecord.values.startTimestamp)}</DataTableBodyCell>
<DataTableBodyCell align="right">{logRecord.values.runTimeMillis?.toLocaleString()}</DataTableBodyCell>
<DataTableBodyCell>
<div style={{color: logRecord.values.hadError ? "red" : "auto"}}>{QValueUtils.formatBoolean(logRecord.values.hadError)}</div>
</DataTableBodyCell>
<DataTableBodyCell>{logRecord.values.input}</DataTableBodyCell>
<DataTableBodyCell>
{logRecord.values.output}
{logRecord.values.error}
</DataTableBodyCell>
<DataTableBodyCell>{logs}</DataTableBodyCell>
</TableRow>
);
})
}
</TableBody>
</Table>
</TableContainer>
);
return (<ScriptLogsView logs={logs} />);
}
return (
@ -440,7 +437,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
console.log(`Defaulting revision for ${fieldName} to ${currentScriptRevisionId}`);
viewingRevisions[fieldName] = currentScriptRevisionId;
if(!scriptLogs[currentScriptRevisionId])
if (!scriptLogs[currentScriptRevisionId])
{
loadRevisionLogs(fieldName, currentScriptRevisionId);
}
@ -508,7 +505,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
</Typography>
}
<CustomWidthTooltip title={editButtonTooltip}>
<Button sx={{py: 0}} onClick={() => editScript(fieldName, code)}>
<Button sx={{py: 0}} onClick={() => editScript(fieldName, code, object)}>
{editButtonText}
</Button>
</CustomWidthTooltip>
@ -521,6 +518,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
theme="github"
name={`view-${fieldName}`}
readOnly
highlightActiveLine={false}
editorProps={{$blockScrolling: true}}
width="100%"
height="400px"
@ -551,71 +549,15 @@ function EntityDeveloperView({table}: Props): JSX.Element
</Grid>
</TabPanel>
<TabPanel index={2} value={selectedTabs[fieldName]}>
<Grid container height="440px" spacing={2}>
<Grid item xs={6}>
<Box gap={2} pb={1} height="40px" px={2}>
<Card sx={{width: "100%", height: "400px"}}>
<Box width="100%">
<Typography variant="h6" p={2}>Test Input</Typography>
<Box px={2} pb={2}>
<TextField id="testInput1" label="Ship To Zip" variant="standard" fullWidth sx={{mb: 2}} />
<TextField id="testInput1" label="No of Cartons" variant="standard" fullWidth sx={{mb: 2}} />
<Box sx={{height: "455px"}} px={2} pb={1}>
<ScriptTestForm scriptDefinition={object} tableName={tableName} fieldName={fieldName} recordId={id} code={code} />
</Box>
<div style={{float: "right"}}>
<Button>Submit</Button>
</div>
</Box>
</Card>
</Box>
</Grid>
<Grid item xs={6}>
<Box gap={2} pb={1} height="40px">
<Card sx={{width: "100%", height: "400px"}}>
<Typography variant="h6" pl={3}>Test Output</Typography>
</Card>
</Box>
</Grid>
</Grid>
</TabPanel>
<TabPanel index={3} value={selectedTabs[fieldName]}>
<Grid container height="440px">
<Grid item xs={12}>
<Box gap={2} pb={1} pl={3}>
<Box pb={1}>
<Typography variant="h6">Documentation</Typography>
<Box sx={{height: "455px"}} px={2} pb={1}>
<ScriptDocsForm helpText={object.scriptType.values.helpText} exampleCode={object.scriptType.values.sampleCode} />
</Box>
<Box sx={{overflow: "auto"}} className="devDocumentation">
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto"}}>
<p>A <b>Deposco Order Optimization Batch Name Script</b> is called when an order is being
optimized for shipping within Deposco. It is responsible for determining the order&apos;s&nbsp;
<b>Batch Name</b> - in other words, an indication of what day the order should be shipped,
and whether or not the order is a line haul.</p>
<p><b>Input</b></p>
<p>The input to this type of script is an object named <code>input</code>, with the following fields:</p>
<ul>
<li><code>warehouseId</code> The id of the warehouse that the order is shipping from. See the <b>Warehouse</b> table for mappings.</li>
<li><code>shipToZipCode</code> The zip code that the order is shipping to.</li>
<li><code>estimatedNoOfCartons</code> The estimated number of cartons that the order will ship in.</li>
</ul>
<p><b>Output</b></p>
<p>The script is responsible only for outputting a single value - a <code>string</code> which will be set as the order&apos;s&nbsp;
<b>Batch Name</b> in Deposco.</p>
<p><b>Example</b></p>
<code style={{whiteSpace: "pre-wrap"}}>
if(today.weekday == 1)
(
return &quot;TUE-Line-Haul&quot;
)
</code>
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</TabPanel>
</Card>
);
@ -629,6 +571,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
editingScript &&
<Modal open={editingScript as boolean} onClose={(event, reason) => closeEditingScript(event, reason)}>
<AssociatedScriptEditor
scriptDefinition={editingScript.scriptDefinitionObject}
tableName={tableName}
primaryKey={id}
fieldName={editingScript.fieldName}

View File

@ -0,0 +1,82 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import React from "react";
import AceEditor from "react-ace";
interface Props
{
helpText: string;
exampleCode: string;
aceEditorHeight: string
}
ScriptDocsForm.defaultProps = {
aceEditorHeight: "100%",
};
function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.Element
{
const oneBlock = (name: string, mode: string, heading: string, code: string): JSX.Element =>
{
return (
<Grid item xs={6} height="100%">
<Box gap={2} pb={1} pr={2} height="100%">
<Card sx={{width: "100%", height: "100%"}}>
<Typography variant="h6" p={2} pb={1}>{heading}</Typography>
<Box className="devDocumentation" height="100%">
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto", height: "100%"}}>
<AceEditor
mode={mode}
theme="github"
name={name}
editorProps={{$blockScrolling: true}}
value={code}
readOnly
highlightActiveLine={false}
width="100%"
showPrintMargin={false}
height="100%"
/>
</Typography>
</Box>
</Card>
</Box>
</Grid>
)
}
return (
<Grid container spacing={2} height="100%">
{oneBlock("helpText", "text", "Documentation", helpText)}
{oneBlock("exampleCode", "javascript", "ExampleCode", exampleCode)}
</Grid>
);
}
export default ScriptDocsForm;

View File

@ -0,0 +1,96 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import React from "react";
import DataTableBodyCell from "qqq/components/Temporary/DataTable/DataTableBodyCell";
import DataTableHeadCell from "qqq/components/Temporary/DataTable/DataTableHeadCell";
import QValueUtils from "qqq/utils/QValueUtils";
interface Props
{
logs: any;
}
ScriptLogsView.defaultProps = {
logs: null,
};
function ScriptLogsView({logs}: Props): JSX.Element
{
return (
<TableContainer sx={{boxShadow: "none"}}>
<Table>
<Box component="thead">
<TableRow key="header">
<DataTableHeadCell sorted={false}>Timestamp</DataTableHeadCell>
<DataTableHeadCell sorted={false} align="right">Run Time (ms)</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Had Error?</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Input</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Output</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Logs</DataTableHeadCell>
</TableRow>
</Box>
<TableBody>
{
logs.map((logRecord: any) =>
{
let logs = "";
if (logRecord.values.scriptLogLine)
{
for (let i = 0; i < logRecord.values.scriptLogLine.length; i++)
{
console.log(" += " + i);
logs += (logRecord.values.scriptLogLine[i].values.text + "\n");
}
}
return (
<TableRow key={logRecord.values.id}>
<DataTableBodyCell>{QValueUtils.formatDateTime(logRecord.values.startTimestamp)}</DataTableBodyCell>
<DataTableBodyCell align="right">{logRecord.values.runTimeMillis?.toLocaleString()}</DataTableBodyCell>
<DataTableBodyCell>
<div style={{color: logRecord.values.hadError ? "red" : "auto"}}>{QValueUtils.formatBoolean(logRecord.values.hadError)}</div>
</DataTableBodyCell>
<DataTableBodyCell>{logRecord.values.input}</DataTableBodyCell>
<DataTableBodyCell>
{logRecord.values.output}
{logRecord.values.error}
</DataTableBodyCell>
<DataTableBodyCell>{logs}</DataTableBodyCell>
</TableRow>
);
})
}
</TableBody>
</Table>
</TableContainer>
);
}
export default ScriptLogsView;

View File

@ -0,0 +1,189 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import React, {useEffect, useState} from "react";
import MDTypography from "components/MDTypography";
import MDBox from "qqq/components/Temporary/MDBox";
import QClient from "qqq/utils/QClient";
import QValueUtils from "qqq/utils/QValueUtils";
interface AssociatedScriptDefinition
{
testInputFields: QFieldMetaData[];
testOutputFields: QFieldMetaData[];
}
interface Props
{
scriptDefinition: AssociatedScriptDefinition;
tableName: string;
fieldName: string;
recordId: any;
code: string;
}
ScriptTestForm.defaultProps = {
// foo: null,
};
const qController = QClient.getInstance();
function ScriptTestForm({scriptDefinition, tableName, fieldName, recordId, code}: Props): JSX.Element
{
const [testInputValues, setTestInputValues] = useState({} as any);
const [testOutputValues, setTestOutputValues] = useState({} as any);
const [testException, setTestException] = useState(null as string)
const [firstRender, setFirstRender] = useState(true);
if(firstRender)
{
setFirstRender(false)
}
if(firstRender)
{
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
{
testInputValues[field.name] = "";
});
}
const testScript = () =>
{
const inputValues = new Map<string, any>();
if (scriptDefinition.testInputFields)
{
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
{
inputValues.set(field.name, testInputValues[field.name]);
});
}
setTestOutputValues({});
setTestException(null);
(async () =>
{
const output = await qController.testScript(tableName, recordId, fieldName, code, inputValues);
console.log("got output:")
console.log(output);
console.log(Object.keys(output));
setTestOutputValues(output.outputObject);
if(output.exception)
{
setTestException(output.exception.message)
console.log(`set test exception to ${output.exception.message}`);
}
})();
};
// console.log("Rendering vvv");
// console.log(`${testOutputValues}`);
// console.log("Rendering ^^^");
const handleInputChange = (fieldName: string, newValue: string) =>
{
testInputValues[fieldName] = newValue;
console.log(`Setting ${fieldName} = ${newValue}`);
setTestInputValues(JSON.parse(JSON.stringify(testInputValues)));
}
// console.log(testInputValues);
return (
<Grid container spacing={2} height="100%">
<Grid item xs={6} height="100%">
<Box gap={2} pb={1} pr={2} height="100%">
<Card sx={{width: "100%", height: "100%", overflow: "auto"}}>
<Box width="100%">
<Typography variant="h6" p={2} pb={1}>Test Input</Typography>
<Box px={2} pb={2}>
{
scriptDefinition.testInputFields && testInputValues && scriptDefinition.testInputFields.map((field: QFieldMetaData) =>
{
return (<TextField
key={field.name}
id={field.name}
label={field.label}
value={testInputValues[field.name]}
variant="standard"
onChange={(event) =>
{
handleInputChange(field.name, event.target.value);
}}
fullWidth
sx={{mb: 2}}
/>);
})
}
</Box>
<div style={{float: "right"}}>
<Button onClick={() => testScript()}>Submit</Button>
</div>
</Box>
</Card>
</Box>
</Grid>
<Grid item xs={6} height="100%">
<Box gap={2} pb={1} height="100%">
<Card sx={{width: "100%", height: "100%", overflow: "auto"}}>
<Typography variant="h6" p={2} pl={3} pb={1}>Test Output</Typography>
<Box p={3} pt={0}>
{
testException &&
<Typography variant="body2" color="red">
{testException}
</Typography>
}
{
scriptDefinition.testOutputFields && testOutputValues && scriptDefinition.testOutputFields.map((f: any) =>
{
const field = new QFieldMetaData(f);
console.log(field.name);
console.log(testOutputValues[field.name]);
return (
<MDBox key={field.name} flexDirection="row" pr={2}>
<Typography variant="button" fontWeight="bold" pr={1}>
{field.label}:
</Typography>
<MDTypography variant="button" fontWeight="regular" color="text">
{QValueUtils.getValueForDisplay(field, testOutputValues[field.name], testOutputValues[field.name], "view")}
</MDTypography>
</MDBox>
);
})
}
</Box>
</Card>
</Box>
</Grid>
</Grid>
);
}
export default ScriptTestForm;

View File

@ -144,15 +144,23 @@ class QValueUtils
if (field.hasAdornment(AdornmentType.CODE_EDITOR))
{
let mode = "text";
const adornmentValues = field.getAdornment(AdornmentType.CODE_EDITOR).values;
if (adornmentValues.has("languageMode"))
{
mode = adornmentValues.get("languageMode");
}
if(usage === "view")
{
return (<AceEditor
mode="javascript"
mode={mode}
theme="github"
name={field.name}
editorProps={{$blockScrolling: true}}
value={rawValue}
readOnly
highlightActiveLine={false}
width="100%"
showPrintMargin={false}
height="200px"
@ -191,6 +199,10 @@ class QValueUtils
{
return (displayValue);
}
else if (field.type === QFieldType.BOOLEAN && (typeof displayValue) === "boolean")
{
return displayValue ? "Yes" : "No";
}
let returnValue = displayValue;
if (displayValue === undefined && rawValue !== undefined)