mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
Adding test mode to assocaited record screens; code editor as an adornment type for forms
This commit is contained in:
@ -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",
|
||||
|
@ -158,6 +158,7 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
||||
formFieldObject={field}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>`);
|
||||
|
||||
///////////////////////////////////////////
|
||||
|
@ -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,23 +105,56 @@ 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}}>
|
||||
<Typography variant="h5" pb={1}>
|
||||
{`${titlePrefix}: ${recordLabel} - ${scriptName}`}
|
||||
</Typography>
|
||||
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme="github"
|
||||
name="editor"
|
||||
editorProps={{$blockScrolling: true}}
|
||||
onChange={updateCode}
|
||||
width="100%"
|
||||
height="100%"
|
||||
value={updatedCode}
|
||||
style={{border: "1px solid gray"}}
|
||||
/>
|
||||
<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"
|
||||
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}>
|
||||
<Grid container alignItems="flex-end">
|
||||
|
@ -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>
|
||||
<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>
|
||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||
<ScriptTestForm scriptDefinition={object} tableName={tableName} fieldName={fieldName} recordId={id} code={code} />
|
||||
</Box>
|
||||
</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>
|
||||
<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's
|
||||
<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's
|
||||
<b>Batch Name</b> in Deposco.</p>
|
||||
|
||||
<p><b>Example</b></p>
|
||||
<code style={{whiteSpace: "pre-wrap"}}>
|
||||
if(today.weekday == 1)
|
||||
(
|
||||
return "TUE-Line-Haul"
|
||||
)
|
||||
</code>
|
||||
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||
<ScriptDocsForm helpText={object.scriptType.values.helpText} exampleCode={object.scriptType.values.sampleCode} />
|
||||
</Box>
|
||||
</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}
|
||||
|
82
src/qqq/pages/entity-view/ScriptDocsForm.tsx
Normal file
82
src/qqq/pages/entity-view/ScriptDocsForm.tsx
Normal 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;
|
||||
|
96
src/qqq/pages/entity-view/ScriptLogsView.tsx
Normal file
96
src/qqq/pages/entity-view/ScriptLogsView.tsx
Normal 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;
|
||||
|
||||
|
189
src/qqq/pages/entity-view/ScriptTestForm.tsx
Normal file
189
src/qqq/pages/entity-view/ScriptTestForm.tsx
Normal 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;
|
@ -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)
|
||||
|
Reference in New Issue
Block a user