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/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0", "@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "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/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",

View File

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

View File

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

View File

@ -22,8 +22,9 @@
import {InputAdornment, InputLabel} from "@mui/material"; import {InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik"; import {ErrorMessage, Field, FieldProps, useFormikContext} from "formik";
import React, {SyntheticEvent, useState} from "react"; import React, {useState} from "react";
import AceEditor from "react-ace";
import QBooleanFieldSwitch from "qqq/components/QDynamicFormField/QBooleanFieldSwitch"; import QBooleanFieldSwitch from "qqq/components/QDynamicFormField/QBooleanFieldSwitch";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDInput from "qqq/components/Temporary/MDInput"; import MDInput from "qqq/components/Temporary/MDInput";
@ -43,14 +44,15 @@ interface Props
bulkEditMode?: boolean; bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any; bulkEditSwitchChangeHandler?: any;
formFieldObject: any; // is the type returned by DynamicFormUtils.getDynamicField
} }
function QDynamicFormField({ function QDynamicFormField({
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, ...rest label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, formFieldObject, ...rest
}: Props): JSX.Element }: Props): JSX.Element
{ {
const [ switchChecked, setSwitchChecked ] = useState(false); const [switchChecked, setSwitchChecked] = useState(false);
const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode); const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const {setFieldValue} = useFormikContext(); 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 = () => getsBulkEditHtmlLabel = false;
(type == "checkbox" ? field = (
<QBooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} /> : <>
<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} <Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onKeyPress={(e: any) => onKeyPress={(e: any) =>
{ {
if(e.key === "Enter") if (e.key === "Enter")
{ {
e.preventDefault(); e.preventDefault();
} }
@ -103,10 +140,16 @@ function QDynamicFormField({
</MDBox> </MDBox>
</> </>
); );
}
const bulkEditSwitchChanged = () => const bulkEditSwitchChanged = () =>
{ {
const newSwitchValue = !switchChecked; setBulkEditSwitch(!switchChecked);
};
const setBulkEditSwitch = (value: boolean) =>
{
const newSwitchValue = value;
setSwitchChecked(newSwitchValue); setSwitchChecked(newSwitchValue);
setIsDisabled(!newSwitchValue); setIsDisabled(!newSwitchValue);
bulkEditSwitchChangeHandler(name, newSwitchValue); bulkEditSwitchChangeHandler(name, newSwitchValue);
@ -124,13 +167,13 @@ function QDynamicFormField({
/> />
</Box> </Box>
<Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}> <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" ? getsBulkEditHtmlLabel
field() : ? (<label htmlFor={`bulkEditSwitch-${name}`}>
<label htmlFor={`bulkEditSwitch-${name}`}> {field}
{field()} </label>)
</label> : <div onClick={() => setBulkEditSwitch(true)}>{field}</div>
)} }
</Box> </Box>
</Box> </Box>
); );
@ -139,7 +182,7 @@ function QDynamicFormField({
{ {
return ( return (
<MDBox mb={1.5}> <MDBox mb={1.5}>
{field()} {field}
</MDBox> </MDBox>
); );
} }

View File

@ -847,7 +847,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
// construct the url for the export // // construct the url for the export //
////////////////////////////////////// //////////////////////////////////////
const d = new Date(); 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 filename = `${tableMetaData.label} Export ${dateString}.${format}`;
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(filterModel)))}&fields=${visibleFields.join(",")}`; 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); }, 1);
</script> </script>
</head> </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>`); </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 Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -29,10 +31,20 @@ import TextField from "@mui/material/TextField";
import React, {useState} from "react"; import React, {useState} from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/QButtons"; 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"; import QClient from "qqq/utils/QClient";
interface AssociatedScriptDefinition
{
testInputFields: QFieldMetaData[];
testOutputFields: QFieldMetaData[];
scriptType: any;
}
interface Props interface Props
{ {
scriptDefinition: AssociatedScriptDefinition;
tableName: string; tableName: string;
primaryKey: any; primaryKey: any;
fieldName: string; fieldName: string;
@ -46,11 +58,24 @@ interface Props
const qController = QClient.getInstance(); 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 [closing, setClosing] = useState(false);
const [updatedCode, setUpdatedCode] = useState(code) const [updatedCode, setUpdatedCode] = useState(code)
const [commitMessage, setCommitMessage] = useState("") 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 = () => const saveClicked = () =>
{ {
@ -80,23 +105,56 @@ function AssociatedScriptEditor({tableName, primaryKey, fieldName, titlePrefix,
} }
return ( 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}}> <Card sx={{height: "100%", p: 3}}>
<Typography variant="h5" pb={1}>
{`${titlePrefix}: ${recordLabel} - ${scriptName}`}
</Typography>
<AceEditor <Box display="flex" justifyContent="space-between" alignItems="center">
mode="javascript" <Typography variant="h5" pb={1}>
theme="github" {`${titlePrefix}: ${recordLabel} - ${scriptName}`}
name="editor" </Typography>
editorProps={{$blockScrolling: true}}
onChange={updateCode} <Box>
width="100%" <Typography variant="body2" display="inline" pr={1}>
height="100%" Tools:
value={updatedCode} </Typography>
style={{border: "1px solid gray"}} <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"
name="editor"
editorProps={{$blockScrolling: true}}
onChange={updateCode}
width="100%"
height="100%"
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}> <Box pt={1}>
<Grid container alignItems="flex-end"> <Grid container alignItems="flex-end">

View File

@ -20,6 +20,7 @@
*/ */
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Chip, Icon, ListItem, ListItemAvatar, Typography} from "@mui/material"; 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 Modal from "@mui/material/Modal";
import Snackbar from "@mui/material/Snackbar"; import Snackbar from "@mui/material/Snackbar";
import Tab from "@mui/material/Tab"; 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 Tabs from "@mui/material/Tabs";
import TextField from "@mui/material/TextField";
import React, {useContext, useReducer, useState} from "react"; import React, {useContext, useReducer, useState} from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import {useParams} from "react-router-dom"; import {useParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
import CustomWidthTooltip from "qqq/components/CustomWidthTooltip/CustomWidthTooltip"; 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 MDBox from "qqq/components/Temporary/MDBox";
import AssociatedScriptEditor from "qqq/pages/entity-view/AssociatedScriptEditor"; 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 QClient from "qqq/utils/QClient";
import QValueUtils from "qqq/utils/QValueUtils"; import QValueUtils from "qqq/utils/QValueUtils";
import ScriptDocsForm from "./ScriptDocsForm";
import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript"; 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/theme-github";
import "ace-builds/src-noconflict/ext-language_tools"; import "ace-builds/src-noconflict/ext-language_tools";
const qController = QClient.getInstance(); const qController = QClient.getInstance();
interface TabPanelProps interface TabPanelProps
@ -119,6 +115,8 @@ function EntityDeveloperView({table}: Props): JSX.Element
const [selectedTabs, setSelectedTabs] = useState({} as any); const [selectedTabs, setSelectedTabs] = useState({} as any);
const [viewingRevisions, setViewingRevisions] = useState({} as any); const [viewingRevisions, setViewingRevisions] = useState({} as any);
const [scriptLogs, setScriptLogs] = 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 [editingScript, setEditingScript] = useState(null as any);
const [alertText, setAlertText] = useState(null as string); const [alertText, setAlertText] = useState(null as string);
@ -157,6 +155,23 @@ function EntityDeveloperView({table}: Props): JSX.Element
setAssociatedScripts(developerModeData.associatedScripts); 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; const recordJSONObject = {} as any;
for (let key of record.values.keys()) for (let key of record.values.keys())
{ {
@ -208,15 +223,44 @@ function EntityDeveloperView({table}: Props): JSX.Element
return color; return color;
}; };
const editScript = (fieldName: string, code: string) => const editScript = (fieldName: string, code: string, object: any) =>
{ {
const editingScript = {} as any; const editingScript = {} as any;
editingScript.fieldName = fieldName; editingScript.fieldName = fieldName;
editingScript.titlePrefix = code ? "Editing Script" : "Creating New Script"; editingScript.titlePrefix = code ? "Editing Script" : "Creating New Script";
editingScript.code = code; editingScript.code = code;
editingScript.scriptDefinitionObject = object;
setEditingScript(editingScript); 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) => const closeEditingScript = (event: object, reason: string, alert: string = null) =>
{ {
if (reason === "backdropClick") if (reason === "backdropClick")
@ -256,7 +300,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
scriptLogs[revisionId] = null; scriptLogs[revisionId] = null;
setScriptLogs(scriptLogs); setScriptLogs(scriptLogs);
loadRevisionLogs(fieldName, revisionId) loadRevisionLogs(fieldName, revisionId);
forceUpdate(); forceUpdate();
}; };
@ -276,7 +320,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
setScriptLogs(scriptLogs); setScriptLogs(scriptLogs);
forceUpdate(); forceUpdate();
})(); })();
} };
function getRevisionsList(scriptRevisions: any, fieldName: any, currentScriptRevisionId: any) 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 <Typography variant="body2" p={3}>No logs available for this version.</Typography>;
} }
return ( return (<ScriptLogsView logs={logs} />);
<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 ( return (
@ -440,7 +437,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
console.log(`Defaulting revision for ${fieldName} to ${currentScriptRevisionId}`); console.log(`Defaulting revision for ${fieldName} to ${currentScriptRevisionId}`);
viewingRevisions[fieldName] = currentScriptRevisionId; viewingRevisions[fieldName] = currentScriptRevisionId;
if(!scriptLogs[currentScriptRevisionId]) if (!scriptLogs[currentScriptRevisionId])
{ {
loadRevisionLogs(fieldName, currentScriptRevisionId); loadRevisionLogs(fieldName, currentScriptRevisionId);
} }
@ -508,7 +505,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
</Typography> </Typography>
} }
<CustomWidthTooltip title={editButtonTooltip}> <CustomWidthTooltip title={editButtonTooltip}>
<Button sx={{py: 0}} onClick={() => editScript(fieldName, code)}> <Button sx={{py: 0}} onClick={() => editScript(fieldName, code, object)}>
{editButtonText} {editButtonText}
</Button> </Button>
</CustomWidthTooltip> </CustomWidthTooltip>
@ -521,6 +518,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
theme="github" theme="github"
name={`view-${fieldName}`} name={`view-${fieldName}`}
readOnly readOnly
highlightActiveLine={false}
editorProps={{$blockScrolling: true}} editorProps={{$blockScrolling: true}}
width="100%" width="100%"
height="400px" height="400px"
@ -551,71 +549,15 @@ function EntityDeveloperView({table}: Props): JSX.Element
</Grid> </Grid>
</TabPanel> </TabPanel>
<TabPanel index={2} value={selectedTabs[fieldName]}> <TabPanel index={2} value={selectedTabs[fieldName]}>
<Grid container height="440px" spacing={2}> <Box sx={{height: "455px"}} px={2} pb={1}>
<Grid item xs={6}> <ScriptTestForm scriptDefinition={object} tableName={tableName} fieldName={fieldName} recordId={id} code={code} />
<Box gap={2} pb={1} height="40px" px={2}> </Box>
<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>
<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>
<TabPanel index={3} value={selectedTabs[fieldName]}> <TabPanel index={3} value={selectedTabs[fieldName]}>
<Grid container height="440px"> <Box sx={{height: "455px"}} px={2} pb={1}>
<Grid item xs={12}> <ScriptDocsForm helpText={object.scriptType.values.helpText} exampleCode={object.scriptType.values.sampleCode} />
<Box gap={2} pb={1} pl={3}> </Box>
<Box pb={1}>
<Typography variant="h6">Documentation</Typography>
</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> </TabPanel>
</Card> </Card>
); );
@ -629,6 +571,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
editingScript && editingScript &&
<Modal open={editingScript as boolean} onClose={(event, reason) => closeEditingScript(event, reason)}> <Modal open={editingScript as boolean} onClose={(event, reason) => closeEditingScript(event, reason)}>
<AssociatedScriptEditor <AssociatedScriptEditor
scriptDefinition={editingScript.scriptDefinitionObject}
tableName={tableName} tableName={tableName}
primaryKey={id} primaryKey={id}
fieldName={editingScript.fieldName} 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)) 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") if(usage === "view")
{ {
return (<AceEditor return (<AceEditor
mode="javascript" mode={mode}
theme="github" theme="github"
name={field.name} name={field.name}
editorProps={{$blockScrolling: true}} editorProps={{$blockScrolling: true}}
value={rawValue} value={rawValue}
readOnly readOnly
highlightActiveLine={false}
width="100%" width="100%"
showPrintMargin={false} showPrintMargin={false}
height="200px" height="200px"
@ -191,6 +199,10 @@ class QValueUtils
{ {
return (displayValue); return (displayValue);
} }
else if (field.type === QFieldType.BOOLEAN && (typeof displayValue) === "boolean")
{
return displayValue ? "Yes" : "No";
}
let returnValue = displayValue; let returnValue = displayValue;
if (displayValue === undefined && rawValue !== undefined) if (displayValue === undefined && rawValue !== undefined)