mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
Merge branch 'integration/sprint-28' into feature/CTLE-503-optimization-weather-api-data
This commit is contained in:
@ -22,8 +22,6 @@
|
||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import CheckBoxIcon from "@mui/icons-material/CheckBox";
|
||||
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
|
||||
import {Checkbox, Chip, CircularProgress, FilterOptionsState, Icon} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
|
@ -26,8 +26,9 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
|
||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Alert, Box} from "@mui/material";
|
||||
import {Alert} from "@mui/material";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
@ -54,7 +55,7 @@ interface Props
|
||||
closeModalHandler?: (event: object, reason: string) => void;
|
||||
defaultValues: { [key: string]: string };
|
||||
disabledFields: { [key: string]: boolean } | string[];
|
||||
isDuplicate?: boolean;
|
||||
isCopy?: boolean;
|
||||
}
|
||||
|
||||
EntityForm.defaultProps = {
|
||||
@ -64,7 +65,7 @@ EntityForm.defaultProps = {
|
||||
closeModalHandler: null,
|
||||
defaultValues: {},
|
||||
disabledFields: {},
|
||||
isDuplicate: false
|
||||
isCopy: false
|
||||
};
|
||||
|
||||
function EntityForm(props: Props): JSX.Element
|
||||
@ -175,9 +176,9 @@ function EntityForm(props: Props): JSX.Element
|
||||
fieldArray.push(fieldMetaData);
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if doing an edit or duplicate, fetch the record and pre-populate the form values from it //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
let record: QRecord = null;
|
||||
let defaultDisplayValues = new Map<string, string>();
|
||||
if (props.id !== null)
|
||||
@ -185,7 +186,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
record = await qController.get(tableName, props.id);
|
||||
setRecord(record);
|
||||
|
||||
const titleVerb = props.isDuplicate ? "Duplicate" : "Edit";
|
||||
const titleVerb = props.isCopy ? "Copy" : "Edit";
|
||||
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||
|
||||
if (!props.isModal)
|
||||
@ -195,20 +196,26 @@ function EntityForm(props: Props): JSX.Element
|
||||
|
||||
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
||||
{
|
||||
if (props.isDuplicate && fieldMetaData.name == tableMetaData.primaryKeyField)
|
||||
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
|
||||
{
|
||||
return;
|
||||
}
|
||||
initialValues[key] = record.values.get(key);
|
||||
});
|
||||
|
||||
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(! props.isCopy)
|
||||
{
|
||||
setNotAllowedError("Records may not be edited in this table");
|
||||
}
|
||||
else if (!tableMetaData.editPermission)
|
||||
{
|
||||
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
||||
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
||||
{
|
||||
setNotAllowedError("Records may not be edited in this table");
|
||||
}
|
||||
else if (!tableMetaData.editPermission)
|
||||
{
|
||||
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -256,7 +263,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
//////////////////////////////////////
|
||||
// check capabilities & permissions //
|
||||
//////////////////////////////////////
|
||||
if (props.isDuplicate || !props.id)
|
||||
if (props.isCopy || !props.id)
|
||||
{
|
||||
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
|
||||
{
|
||||
@ -341,11 +348,11 @@ function EntityForm(props: Props): JSX.Element
|
||||
const fieldName = section.fieldNames[j];
|
||||
const field = tableMetaData.fields.get(fieldName);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if id !== null (and we're not duplicating) - means we're on the edit screen -- show all fields on the edit screen. //
|
||||
// || (or) we're on the insert screen in which case, only show editable fields. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if ((props.id !== null && !props.isDuplicate) || field.isEditable)
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
|
||||
// || (or) we're on the insert screen in which case, only show editable fields. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if ((props.id !== null && !props.isCopy) || field.isEditable)
|
||||
{
|
||||
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
||||
}
|
||||
@ -393,9 +400,9 @@ function EntityForm(props: Props): JSX.Element
|
||||
// but if the user used the anchors on the page, this doesn't effectively cancel... //
|
||||
// what we have here pushed a new history entry (I think?), so could be better //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if (props.id !== null && props.isDuplicate)
|
||||
if (props.id !== null && props.isCopy)
|
||||
{
|
||||
const path = `${location.pathname.replace(/\/duplicate$/, "")}`;
|
||||
const path = `${location.pathname.replace(/\/copy$/, "")}`;
|
||||
navigate(path, {replace: true});
|
||||
}
|
||||
else if (props.id !== null)
|
||||
@ -458,7 +465,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
if (props.id !== null && !props.isDuplicate)
|
||||
if (props.id !== null && !props.isCopy)
|
||||
{
|
||||
// todo - audit that it's a dupe
|
||||
await qController
|
||||
@ -504,8 +511,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
else
|
||||
{
|
||||
const path = props.isDuplicate ?
|
||||
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||
const path = props.isCopy ?
|
||||
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
||||
navigate(path, {state: {createSuccess: true}});
|
||||
}
|
||||
@ -514,8 +521,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
if(error.message.toLowerCase().startsWith("warning"))
|
||||
{
|
||||
const path = props.isDuplicate ?
|
||||
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||
const path = props.isCopy ?
|
||||
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
||||
navigate(path, {state: {createSuccess: true, warning: error.message}});
|
||||
}
|
||||
|
@ -93,7 +93,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
buildHistoryEntries();
|
||||
|
||||
const history = HistoryUtils.get();
|
||||
setHistory([ {label: "The Godfather", id: 1}, {label: "Pulp Fiction", id: 2}]);
|
||||
const options = [] as any;
|
||||
history.entries.reverse().forEach((entry, index) =>
|
||||
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||
@ -119,6 +118,11 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
setHistory(options);
|
||||
}
|
||||
|
||||
function handleHistoryOnOpen()
|
||||
{
|
||||
buildHistoryEntries();
|
||||
}
|
||||
|
||||
const handleOpenMenu = (event: any) => setOpenMenu(event.currentTarget);
|
||||
const handleCloseMenu = () => setOpenMenu(false);
|
||||
|
||||
@ -152,7 +156,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
style={{width: "200px"}}
|
||||
onOpen={buildHistoryEntries}
|
||||
onOpen={handleHistoryOnOpen}
|
||||
onChange={handleAutocompleteOnChange}
|
||||
PopperComponent={CustomPopper}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
|
@ -62,6 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
height="100%"
|
||||
style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
@ -23,7 +23,7 @@ import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QControl
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
|
||||
import {IconButton, SelectChangeEvent, ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
@ -31,9 +31,14 @@ import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import FormControl from "@mui/material/FormControl/FormControl";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select from "@mui/material/Select/Select";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import FormData from "form-data";
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
@ -56,22 +61,82 @@ export interface ScriptEditorProps
|
||||
tableName: string;
|
||||
fieldName: string;
|
||||
recordId: any;
|
||||
scriptDefinition: any;
|
||||
scriptTypeRecord: QRecord;
|
||||
scriptTypeFileSchemaList: QRecord[];
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptDefinition, scriptTypeRecord}: ScriptEditorProps): JSX.Element
|
||||
function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||
{
|
||||
const rs: {[name: string]: string} = {};
|
||||
|
||||
if(!scriptTypeFileSchemaList)
|
||||
{
|
||||
console.log("Missing scriptTypeFileSchemaList");
|
||||
}
|
||||
else
|
||||
{
|
||||
let files = scriptRevisionRecord?.associatedRecords?.get("files")
|
||||
|
||||
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||
{
|
||||
let scriptTypeFileSchema = scriptTypeFileSchemaList[i];
|
||||
let name = scriptTypeFileSchema.values.get("name");
|
||||
let contents = "";
|
||||
|
||||
for (let j = 0; j < files?.length; j++)
|
||||
{
|
||||
let file = files[j];
|
||||
if(file.values.get("fileName") == name)
|
||||
{
|
||||
contents = file.values.get("contents");
|
||||
}
|
||||
}
|
||||
|
||||
rs[name] = contents;
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||
{
|
||||
const rs: {[name: string]: string} = {};
|
||||
|
||||
if(!scriptTypeFileSchemaList)
|
||||
{
|
||||
console.log("Missing scriptTypeFileSchemaList");
|
||||
}
|
||||
else
|
||||
{
|
||||
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||
{
|
||||
let name = scriptTypeFileSchemaList[i].values.get("name");
|
||||
rs[name] = scriptTypeFileSchemaList[i].values.get("fileType");
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptTypeRecord, scriptTypeFileSchemaList}: ScriptEditorProps): JSX.Element
|
||||
{
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const [updatedCode, setUpdatedCode] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("contents") : "");
|
||||
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
|
||||
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
|
||||
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
|
||||
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
|
||||
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
|
||||
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]])
|
||||
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList))
|
||||
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList))
|
||||
console.log(`file types: ${JSON.stringify(fileTypes)}`);
|
||||
|
||||
const [commitMessage, setCommitMessage] = useState("")
|
||||
const [openTool, setOpenTool] = useState(null);
|
||||
const [errorAlert, setErrorAlert] = useState("")
|
||||
@ -200,7 +265,6 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append("scriptId", scriptId);
|
||||
formData.append("contents", updatedCode);
|
||||
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
||||
|
||||
if(apiName)
|
||||
@ -213,6 +277,15 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
formData.append("apiVersion", apiVersion);
|
||||
}
|
||||
|
||||
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||
formData.append("fileNames", fileNamesFromSchema.join(","));
|
||||
|
||||
for (let fileName in fileContents)
|
||||
{
|
||||
formData.append("fileContents:" + fileName, fileContents[fileName]);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// we don't want this job to go async, so, pass a large timeout //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
@ -249,10 +322,9 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
closeCallback(null, "cancelled");
|
||||
}
|
||||
|
||||
const updateCode = (value: string, event: any) =>
|
||||
const updateCode = (value: string, event: any, index: number) =>
|
||||
{
|
||||
console.log("Updating code")
|
||||
setUpdatedCode(value);
|
||||
fileContents[openEditorFileNames[index]] = value;
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
@ -300,8 +372,34 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
|
||||
{
|
||||
openEditorFileNames[index] = event.target.value
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
const splitEditorClicked = () =>
|
||||
{
|
||||
openEditorFileNames.push(availableFileNames[0])
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
const closeEditorClicked = (index: number) =>
|
||||
{
|
||||
openEditorFileNames.splice(index, 1)
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
const computeEditorWidth = (): string =>
|
||||
{
|
||||
return (100 / openEditorFileNames.length) + "%"
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
||||
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
||||
<Card sx={{height: "100%", p: 3}}>
|
||||
|
||||
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
|
||||
@ -348,29 +446,67 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme="github"
|
||||
name="editor"
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{
|
||||
useWorker: false,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
}}
|
||||
onChange={updateCode}
|
||||
width="100%"
|
||||
height="calc(100% - 58px)"
|
||||
value={updatedCode}
|
||||
style={{border: "1px solid gray"}}
|
||||
/>
|
||||
<Box display="flex" sx={{height: "100%"}}>
|
||||
{openEditorFileNames.map((fileName, index) =>
|
||||
{
|
||||
return (
|
||||
<Box key={`${fileName}-${index}`} sx={{height: "100%", width: computeEditorWidth()}}>
|
||||
<Box sx={{borderBottom: 1, borderColor: "divider"}} display="flex" justifyContent="space-between" alignItems="flex-end">
|
||||
<FormControl className="selectedFileTab" variant="standard" sx={{verticalAlign: "bottom"}}>
|
||||
<Select value={openEditorFileNames[index]} onChange={(event) => handleSelectingFile(event, index)}>
|
||||
{
|
||||
availableFileNames.map((name) => (
|
||||
<MenuItem key={name} value={name}>{name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box>
|
||||
{
|
||||
openEditorFileNames.length > 1 &&
|
||||
<Tooltip title="Close this editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
index == openEditorFileNames.length - 1 &&
|
||||
<Tooltip title="Open a new editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={splitEditorClicked}>
|
||||
<Icon>vertical_split</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<AceEditor
|
||||
mode={fileTypes[openEditorFileNames[index]] ?? "javascript"}
|
||||
theme="github"
|
||||
name="editor"
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{
|
||||
useWorker: false,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
}}
|
||||
onChange={(value, event) => updateCode(value, event, index)}
|
||||
width="100%"
|
||||
height="calc(100% - 88px)"
|
||||
value={fileContents[openEditorFileNames[index]]}
|
||||
style={{border: "1px solid gray"}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{
|
||||
openTool &&
|
||||
<Box sx={{height: "45%"}} pt={2}>
|
||||
{
|
||||
openTool == "test" && <ScriptTestForm scriptId={scriptId} scriptDefinition={scriptDefinition} tableName={tableName} fieldName={fieldName} recordId={recordId} code={updatedCode} apiName={apiName} apiVersion={apiVersion} />
|
||||
openTool == "test" && <ScriptTestForm scriptId={scriptId} scriptType={scriptTypeRecord} tableName={tableName} fieldName={fieldName} recordId={recordId} fileContents={fileContents} apiName={apiName} apiVersion={apiVersion} />
|
||||
}
|
||||
{
|
||||
openTool == "docs" && <ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} aceEditorHeight="100%" />
|
||||
|
@ -24,6 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Typography} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
@ -51,19 +52,21 @@ interface AssociatedScriptDefinition
|
||||
export interface ScriptTestFormProps
|
||||
{
|
||||
scriptId: number;
|
||||
scriptDefinition: AssociatedScriptDefinition;
|
||||
scriptType: QRecord;
|
||||
tableName: string;
|
||||
fieldName: string;
|
||||
recordId: any;
|
||||
code: string;
|
||||
fileContents: {[name: string]: string};
|
||||
apiName: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recordId, code, apiName, apiVersion}: ScriptTestFormProps): JSX.Element
|
||||
function ScriptTestForm({scriptId, scriptType, tableName, fieldName, recordId, fileContents, apiName, apiVersion}: ScriptTestFormProps): JSX.Element
|
||||
{
|
||||
const [testInputFields, setTestInputFields] = useState(null as QFieldMetaData[])
|
||||
const [testOutputFields, setTestOutputFields] = useState(null as QFieldMetaData[])
|
||||
const [testInputValues, setTestInputValues] = useState({} as any);
|
||||
const [testOutputValues, setTestOutputValues] = useState({} as any);
|
||||
const [logLines, setLogLines] = useState([] as any[])
|
||||
@ -77,10 +80,46 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
|
||||
if(firstRender)
|
||||
{
|
||||
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
|
||||
(async () =>
|
||||
{
|
||||
testInputValues[field.name] = field.defaultValue ?? "";
|
||||
});
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// call backend to load details about how to test this script type //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
const formData = new FormData();
|
||||
formData.append("scriptTypeId", scriptType.values.get("id"));
|
||||
const processResult = await qController.processRun("loadScriptTestDetails", formData, null, true);
|
||||
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError
|
||||
setTestException(jobError.userFacingError ?? jobError.error)
|
||||
return;
|
||||
}
|
||||
|
||||
const jobComplete = processResult as QJobComplete
|
||||
|
||||
const testInputFields = [] as QFieldMetaData[];
|
||||
for(let i = 0; i <jobComplete?.values?.testInputFields?.length; i++)
|
||||
{
|
||||
testInputFields.push(new QFieldMetaData(jobComplete.values.testInputFields[i]));
|
||||
}
|
||||
setTestInputFields(testInputFields);
|
||||
|
||||
const testOutputFields = [] as QFieldMetaData[];
|
||||
for(let i = 0; i <jobComplete?.values?.testOutputFields?.length; i++)
|
||||
{
|
||||
testOutputFields.push(new QFieldMetaData(jobComplete.values.testOutputFields[i]));
|
||||
}
|
||||
setTestOutputFields(testOutputFields);
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// set a default value in each input field //
|
||||
/////////////////////////////////////////////
|
||||
testInputFields.forEach((field: QFieldMetaData) =>
|
||||
{
|
||||
testInputValues[field.name] = field.defaultValue ?? "";
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
const buildFullExceptionMessage = (exception: any): string =>
|
||||
@ -91,9 +130,9 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
const testScript = () =>
|
||||
{
|
||||
const inputValues = new Map<string, any>();
|
||||
if (scriptDefinition.testInputFields)
|
||||
if (testInputFields)
|
||||
{
|
||||
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
|
||||
testInputFields.forEach((field: QFieldMetaData) =>
|
||||
{
|
||||
inputValues.set(field.name, testInputValues[field.name]);
|
||||
});
|
||||
@ -108,6 +147,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
try
|
||||
{
|
||||
let output;
|
||||
/*
|
||||
if(tableName && recordId && fieldName)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
@ -115,15 +155,21 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
/////////////////////////////////////////////////////////////////
|
||||
inputValues.set("apiName", apiName);
|
||||
inputValues.set("apiVersion", apiVersion);
|
||||
output = await qController.testScript(tableName, recordId, fieldName, code, inputValues);
|
||||
output = await qController.testScript(tableName, recordId, fieldName, "todo!", inputValues);
|
||||
}
|
||||
else
|
||||
*/
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append("scriptId", scriptId);
|
||||
formData.append("apiName", apiName);
|
||||
formData.append("apiVersion", apiVersion);
|
||||
formData.append("code", code);
|
||||
|
||||
formData.append("fileNames", Object.keys(fileContents).join(","))
|
||||
for (let fileName in fileContents)
|
||||
{
|
||||
formData.append("fileContents:" + fileName, fileContents[fileName]);
|
||||
}
|
||||
|
||||
for(let fieldName of inputValues.keys())
|
||||
{
|
||||
@ -195,7 +241,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
<Typography variant="h6" p={2} pb={1}>Test Input</Typography>
|
||||
<Box px={2} pb={2}>
|
||||
{
|
||||
scriptDefinition.testInputFields && testInputValues && scriptDefinition.testInputFields.map((field: QFieldMetaData) =>
|
||||
testInputFields && testInputValues && testInputFields.map((field: QFieldMetaData) =>
|
||||
{
|
||||
return (<TextField
|
||||
key={field.name}
|
||||
@ -234,16 +280,20 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
||||
</Typography>
|
||||
}
|
||||
{
|
||||
scriptDefinition.testOutputFields && scriptDefinition.testOutputFields.map((f: any) =>
|
||||
testOutputFields && testOutputFields.map((f: any) =>
|
||||
{
|
||||
const field = new QFieldMetaData(f);
|
||||
return (
|
||||
<Box key={field.name} flexDirection="row" pr={2}>
|
||||
<Typography variant="button" fontWeight="bold" pr={1}>
|
||||
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1}>
|
||||
{field.label}:
|
||||
</Typography>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
{ValueUtils.getValueForDisplay(field, testOutputValues[field.name], testOutputValues[field.name], "view")}
|
||||
{
|
||||
testOutputValues.values ?
|
||||
ValueUtils.getValueForDisplay(field, testOutputValues.values[field.name], testOutputValues.displayValues[field.name], "view") :
|
||||
ValueUtils.getValueForDisplay(field, testOutputValues[field.name], testOutputValues[field.name], "view")
|
||||
}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
);
|
||||
|
@ -27,18 +27,21 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Chip, SelectChangeEvent} from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import FormControl from "@mui/material/FormControl/FormControl";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Select from "@mui/material/Select/Select";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
@ -59,6 +62,7 @@ import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
import "ace-builds/src-noconflict/mode-java";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-velocity";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
@ -94,7 +98,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
|
||||
const [scriptLogs, setScriptLogs] = useState({} as any);
|
||||
const [scriptTypeRecord, setScriptTypeRecord] = useState(null as QRecord)
|
||||
const [testScriptDefinitionObject, setTestScriptDefinitionObject] = useState({} as any)
|
||||
const [scriptTypeFileSchemaList, setScriptTypeFileSchemaList] = useState(null as QRecord[])
|
||||
const [availableFileNames, setAvailableFileNames] = useState([] as string[]);
|
||||
const [selectedFileName, setSelectedFileName] = useState("");
|
||||
const [currentVersionId , setCurrentVersionId] = useState(null as number);
|
||||
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
@ -118,17 +124,32 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
const scriptRecord = await qController.get("script", scriptId);
|
||||
setScriptRecord(scriptRecord);
|
||||
|
||||
setScriptTypeRecord(await qController.get("scriptType", scriptRecord.values.get("scriptTypeId")));
|
||||
const scriptTypeRecord = await qController.get("scriptType", scriptRecord.values.get("scriptTypeId"));
|
||||
setScriptTypeRecord(scriptTypeRecord);
|
||||
|
||||
if(testInputFields !== null || testOutputFields !== null)
|
||||
let fileMode = scriptTypeRecord.values.get("fileMode");
|
||||
let scriptTypeFileSchemaList: QRecord[] = null;
|
||||
if(fileMode == 1) // SINGLE
|
||||
{
|
||||
setTestScriptDefinitionObject({testInputFields: testInputFields, testOutputFields: testOutputFields});
|
||||
scriptTypeFileSchemaList = [new QRecord({values: {name: "Script.js", fileType: "javascript"}})];
|
||||
}
|
||||
else
|
||||
else if(fileMode == 2) // MULTI_PRE_DEFINED
|
||||
{
|
||||
setTestScriptDefinitionObject({testInputFields: [
|
||||
new QFieldMetaData({name: "recordPrimaryKeyList", label: "Record Primary Key List"})
|
||||
], testOutputFields: []})
|
||||
const filter = new QQueryFilter([new QFilterCriteria("scriptTypeId", QCriteriaOperator.EQUALS, [scriptRecord.values.get("scriptTypeId")])], [new QFilterOrderBy("id")])
|
||||
scriptTypeFileSchemaList = await qController.query("scriptTypeFileSchema", filter);
|
||||
}
|
||||
else // MULTI AD_HOC
|
||||
{
|
||||
// todo - not yet supported
|
||||
console.log(`Script Type File Mode of ${fileMode} is not yet supported.`);
|
||||
}
|
||||
|
||||
setScriptTypeFileSchemaList(scriptTypeFileSchemaList);
|
||||
if(scriptTypeFileSchemaList)
|
||||
{
|
||||
const availableFileNames = scriptTypeFileSchemaList.map((fileSchemaRecord) => fileSchemaRecord.values.get("name"))
|
||||
setAvailableFileNames(availableFileNames);
|
||||
setSelectedFileName(availableFileNames[0])
|
||||
}
|
||||
|
||||
const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])];
|
||||
@ -141,13 +162,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
|
||||
if(versions && versions.length > 0)
|
||||
{
|
||||
setCurrentVersionId(versions[0].values.get("id"));
|
||||
const latestVersion = await qController.get("scriptRevision", versions[0].values.get("id"));
|
||||
console.log("Fetched latestVersion:");
|
||||
console.log(latestVersion);
|
||||
setSelectedVersionRecord(latestVersion);
|
||||
loadingSelectedVersion.setNotLoading();
|
||||
forceUpdate();
|
||||
selectVersion(versions[0]);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
@ -174,8 +189,8 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
editorProps.tableName = associatedScriptTableName;
|
||||
editorProps.fieldName = associatedScriptFieldName;
|
||||
editorProps.recordId = associatedScriptRecordId;
|
||||
editorProps.scriptDefinition = testScriptDefinitionObject;
|
||||
editorProps.scriptTypeRecord = scriptTypeRecord;
|
||||
editorProps.scriptTypeFileSchemaList = scriptTypeFileSchemaList;
|
||||
setEditorProps(editorProps);
|
||||
};
|
||||
|
||||
@ -223,8 +238,10 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
setCurrentVersionId(version.values.get("id"));
|
||||
loadingSelectedVersion.setLoading();
|
||||
|
||||
// fetch the full version
|
||||
const selectedVersion = await qController.get("scriptRevision", version.values.get("id"));
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// fetch the full version - including its associated scriptRevisionFile sub-records //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
const selectedVersion = await qController.get("scriptRevision", version.values.get("id"), null, true);
|
||||
console.log("Fetched selectedVersion:");
|
||||
console.log(selectedVersion);
|
||||
setSelectedVersionRecord(selectedVersion);
|
||||
@ -233,6 +250,44 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
})();
|
||||
};
|
||||
|
||||
const handleSelectFile = (event: SelectChangeEvent) =>
|
||||
{
|
||||
setSelectedFileName(event.target.value);
|
||||
}
|
||||
|
||||
const getSelectedFileCode = (): string =>
|
||||
{
|
||||
return (getSelectedVersionCode()[selectedFileName] ?? "");
|
||||
}
|
||||
|
||||
const getSelectedFileType = (): string =>
|
||||
{
|
||||
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||
{
|
||||
let name = scriptTypeFileSchemaList[i].values.get("name");
|
||||
if(name == selectedFileName)
|
||||
{
|
||||
return (scriptTypeFileSchemaList[i].values.get("fileType"));
|
||||
}
|
||||
}
|
||||
|
||||
return ("javascript"); // have some default...
|
||||
}
|
||||
|
||||
const getSelectedVersionCode = (): {[name: string]: string} =>
|
||||
{
|
||||
let rs: {[name: string]: string} = {}
|
||||
let files = selectedVersionRecord?.associatedRecords?.get("files")
|
||||
|
||||
for (let j = 0; j < files?.length; j++)
|
||||
{
|
||||
let file = files[j];
|
||||
rs[file.values.get("fileName")] = file.values.get("contents");
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
|
||||
{
|
||||
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
|
||||
@ -344,7 +399,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
*/
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid container className="scriptViewer">
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
{
|
||||
@ -420,10 +475,22 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
</CustomWidthTooltip>
|
||||
</Box>
|
||||
{
|
||||
loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("contents") ? (
|
||||
loadingSelectedVersion.isNotLoading() && selectedVersionRecord ? (
|
||||
<>
|
||||
{
|
||||
availableFileNames && availableFileNames.length > 1 &&
|
||||
<FormControl className="selectedFileTab" variant="standard" sx={{verticalAlign: "bottom", pl: "4px"}}>
|
||||
<Select value={selectedFileName} onChange={(event) => handleSelectFile(event)}>
|
||||
{
|
||||
availableFileNames.map((name) => (
|
||||
<MenuItem key={name} value={name}>{name}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
}
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
mode={getSelectedFileType()}
|
||||
theme="github"
|
||||
name={"viewData"}
|
||||
readOnly
|
||||
@ -431,8 +498,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{useWorker: false}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
value={selectedVersionRecord?.values?.get("contents")}
|
||||
height="368px"
|
||||
value={getSelectedFileCode()}
|
||||
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
@ -473,11 +541,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
<TabPanel index={2} value={selectedTab}>
|
||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||
<ScriptTestForm scriptId={scriptId}
|
||||
scriptDefinition={testScriptDefinitionObject}
|
||||
scriptType={scriptTypeRecord}
|
||||
tableName={associatedScriptTableName}
|
||||
fieldName={associatedScriptFieldName}
|
||||
recordId={associatedScriptRecordId}
|
||||
code={selectedVersionRecord?.values.get("contents")}
|
||||
fileContents={getSelectedVersionCode()}
|
||||
apiName={selectedVersionRecord?.values.get("apiName")}
|
||||
apiVersion={selectedVersionRecord?.values.get("apiVersion")} />
|
||||
</Box>
|
||||
|
Reference in New Issue
Block a user