Merge branch 'integration/sprint-28' into feature/CTLE-214-dot-menu

This commit is contained in:
2023-06-28 15:46:50 -05:00
committed by GitHub
15 changed files with 440 additions and 82 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.69", "@kingsrook/qqq-frontend-core": "1.0.71",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

@ -200,7 +200,7 @@ function SavedFilters({qController, metaData, tableMetaData, currentSavedFilter,
else else
{ {
formData.append("tableName", tableMetaData.name); formData.append("tableName", tableMetaData.name);
formData.append("filterJson", JSON.stringify(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel))); formData.append("filterJson", JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel))));
if (isSaveFilterAs || isRenameFilter || currentSavedFilter == null) if (isSaveFilterAs || isRenameFilter || currentSavedFilter == null)
{ {

View File

@ -62,6 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
width="100%" width="100%"
showPrintMargin={false} showPrintMargin={false}
height="100%" height="100%"
style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}}
/> />
</Typography> </Typography>
</Box> </Box>

View File

@ -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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; 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 Alert from "@mui/material/Alert";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -31,9 +31,14 @@ import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import FormControl from "@mui/material/FormControl/FormControl";
import Grid from "@mui/material/Grid"; 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 Snackbar from "@mui/material/Snackbar";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import FormData from "form-data"; import FormData from "form-data";
import React, {useEffect, useReducer, useRef, useState} from "react"; import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
@ -56,22 +61,82 @@ export interface ScriptEditorProps
tableName: string; tableName: string;
fieldName: string; fieldName: string;
recordId: any; recordId: any;
scriptDefinition: any;
scriptTypeRecord: QRecord; scriptTypeRecord: QRecord;
scriptTypeFileSchemaList: QRecord[];
} }
const qController = Client.getInstance(); 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 [closing, setClosing] = useState(false);
const [updatedCode, setUpdatedCode] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("contents") : "");
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null) const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null) const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null) const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.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 [commitMessage, setCommitMessage] = useState("")
const [openTool, setOpenTool] = useState(null); const [openTool, setOpenTool] = useState(null);
const [errorAlert, setErrorAlert] = useState("") const [errorAlert, setErrorAlert] = useState("")
@ -200,7 +265,6 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
const formData = new FormData(); const formData = new FormData();
formData.append("scriptId", scriptId); formData.append("scriptId", scriptId);
formData.append("contents", updatedCode);
formData.append("commitMessage", overrideCommitMessage ?? commitMessage); formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
if(apiName) if(apiName)
@ -213,6 +277,15 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
formData.append("apiVersion", apiVersion); 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 // // 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"); closeCallback(null, "cancelled");
} }
const updateCode = (value: string, event: any) => const updateCode = (value: string, event: any, index: number) =>
{ {
console.log("Updating code") fileContents[openEditorFileNames[index]] = value;
setUpdatedCode(value);
forceUpdate(); 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 ( 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}}> <Card sx={{height: "100%", p: 3}}>
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) => <Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
@ -348,8 +446,42 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} /> <DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
</Box> </Box>
</Grid> </Grid>
<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 <AceEditor
mode="javascript" mode={fileTypes[openEditorFileNames[index]] ?? "javascript"}
theme="github" theme="github"
name="editor" name="editor"
editorProps={{$blockScrolling: true}} editorProps={{$blockScrolling: true}}
@ -358,19 +490,23 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
enableBasicAutocompletion: true, enableBasicAutocompletion: true,
enableLiveAutocompletion: true, enableLiveAutocompletion: true,
}} }}
onChange={updateCode} onChange={(value, event) => updateCode(value, event, index)}
width="100%" width="100%"
height="calc(100% - 58px)" height="calc(100% - 88px)"
value={updatedCode} value={fileContents[openEditorFileNames[index]]}
style={{border: "1px solid gray"}} style={{border: "1px solid gray"}}
/> />
</Box> </Box>
);
})}
</Box>
</Box>
{ {
openTool && openTool &&
<Box sx={{height: "45%"}} pt={2}> <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%" /> openTool == "docs" && <ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} aceEditorHeight="100%" />

View File

@ -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 {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; 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 {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";
@ -51,19 +52,21 @@ interface AssociatedScriptDefinition
export interface ScriptTestFormProps export interface ScriptTestFormProps
{ {
scriptId: number; scriptId: number;
scriptDefinition: AssociatedScriptDefinition; scriptType: QRecord;
tableName: string; tableName: string;
fieldName: string; fieldName: string;
recordId: any; recordId: any;
code: string; fileContents: {[name: string]: string};
apiName: string; apiName: string;
apiVersion: string; apiVersion: string;
} }
const qController = Client.getInstance(); 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 [testInputValues, setTestInputValues] = useState({} as any);
const [testOutputValues, setTestOutputValues] = useState({} as any); const [testOutputValues, setTestOutputValues] = useState({} as any);
const [logLines, setLogLines] = useState([] as any[]) const [logLines, setLogLines] = useState([] as any[])
@ -77,10 +80,46 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
if(firstRender) if(firstRender)
{ {
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) => (async () =>
{
/////////////////////////////////////////////////////////////////////
// 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 ?? ""; testInputValues[field.name] = field.defaultValue ?? "";
}); });
})();
} }
const buildFullExceptionMessage = (exception: any): string => const buildFullExceptionMessage = (exception: any): string =>
@ -91,9 +130,9 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
const testScript = () => const testScript = () =>
{ {
const inputValues = new Map<string, any>(); 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]); inputValues.set(field.name, testInputValues[field.name]);
}); });
@ -108,6 +147,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
try try
{ {
let output; let output;
/*
if(tableName && recordId && fieldName) if(tableName && recordId && fieldName)
{ {
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
@ -115,15 +155,21 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
inputValues.set("apiName", apiName); inputValues.set("apiName", apiName);
inputValues.set("apiVersion", apiVersion); inputValues.set("apiVersion", apiVersion);
output = await qController.testScript(tableName, recordId, fieldName, code, inputValues); output = await qController.testScript(tableName, recordId, fieldName, "todo!", inputValues);
} }
else else
*/
{ {
const formData = new FormData(); const formData = new FormData();
formData.append("scriptId", scriptId); formData.append("scriptId", scriptId);
formData.append("apiName", apiName); formData.append("apiName", apiName);
formData.append("apiVersion", apiVersion); 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()) 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> <Typography variant="h6" p={2} pb={1}>Test Input</Typography>
<Box px={2} pb={2}> <Box px={2} pb={2}>
{ {
scriptDefinition.testInputFields && testInputValues && scriptDefinition.testInputFields.map((field: QFieldMetaData) => testInputFields && testInputValues && testInputFields.map((field: QFieldMetaData) =>
{ {
return (<TextField return (<TextField
key={field.name} key={field.name}
@ -234,16 +280,20 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
</Typography> </Typography>
} }
{ {
scriptDefinition.testOutputFields && scriptDefinition.testOutputFields.map((f: any) => testOutputFields && testOutputFields.map((f: any) =>
{ {
const field = new QFieldMetaData(f); const field = new QFieldMetaData(f);
return ( return (
<Box key={field.name} flexDirection="row" pr={2}> <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}: {field.label}:
</Typography> </Typography>
<MDTypography variant="button" fontWeight="regular" color="text"> <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> </MDTypography>
</Box> </Box>
); );

View File

@ -27,18 +27,22 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy"; import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {SelectChangeEvent} from "@mui/material";
import Alert from "@mui/material/Alert"; import Alert from "@mui/material/Alert";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
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 Chip from "@mui/material/Chip"; import Chip from "@mui/material/Chip";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import FormControl from "@mui/material/FormControl/FormControl";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemAvatar from "@mui/material/ListItemAvatar"; import ListItemAvatar from "@mui/material/ListItemAvatar";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal"; import Modal from "@mui/material/Modal";
import Select from "@mui/material/Select/Select";
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 Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
@ -59,6 +63,7 @@ import ValueUtils from "qqq/utils/qqq/ValueUtils";
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";
import "ace-builds/src-noconflict/mode-velocity";
import "ace-builds/src-noconflict/mode-json"; 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";
@ -94,7 +99,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord); const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
const [scriptLogs, setScriptLogs] = useState({} as any); const [scriptLogs, setScriptLogs] = useState({} as any);
const [scriptTypeRecord, setScriptTypeRecord] = useState(null as QRecord) 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 [currentVersionId , setCurrentVersionId] = useState(null as number);
const [notFoundMessage, setNotFoundMessage] = useState(null); const [notFoundMessage, setNotFoundMessage] = useState(null);
const [selectedTab, setSelectedTab] = useState(0); const [selectedTab, setSelectedTab] = useState(0);
@ -118,17 +125,32 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
const scriptRecord = await qController.get("script", scriptId); const scriptRecord = await qController.get("script", scriptId);
setScriptRecord(scriptRecord); 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: [ const filter = new QQueryFilter([new QFilterCriteria("scriptTypeId", QCriteriaOperator.EQUALS, [scriptRecord.values.get("scriptTypeId")])], [new QFilterOrderBy("id")])
new QFieldMetaData({name: "recordPrimaryKeyList", label: "Record Primary Key List"}) scriptTypeFileSchemaList = await qController.query("scriptTypeFileSchema", filter);
], testOutputFields: []}) }
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])]; const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])];
@ -141,13 +163,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
if(versions && versions.length > 0) if(versions && versions.length > 0)
{ {
setCurrentVersionId(versions[0].values.get("id")); selectVersion(versions[0]);
const latestVersion = await qController.get("scriptRevision", versions[0].values.get("id"));
console.log("Fetched latestVersion:");
console.log(latestVersion);
setSelectedVersionRecord(latestVersion);
loadingSelectedVersion.setNotLoading();
forceUpdate();
} }
} }
catch (e) catch (e)
@ -174,8 +190,8 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
editorProps.tableName = associatedScriptTableName; editorProps.tableName = associatedScriptTableName;
editorProps.fieldName = associatedScriptFieldName; editorProps.fieldName = associatedScriptFieldName;
editorProps.recordId = associatedScriptRecordId; editorProps.recordId = associatedScriptRecordId;
editorProps.scriptDefinition = testScriptDefinitionObject;
editorProps.scriptTypeRecord = scriptTypeRecord; editorProps.scriptTypeRecord = scriptTypeRecord;
editorProps.scriptTypeFileSchemaList = scriptTypeFileSchemaList;
setEditorProps(editorProps); setEditorProps(editorProps);
}; };
@ -223,8 +239,10 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
setCurrentVersionId(version.values.get("id")); setCurrentVersionId(version.values.get("id"));
loadingSelectedVersion.setLoading(); 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"), true);
console.log("Fetched selectedVersion:"); console.log("Fetched selectedVersion:");
console.log(selectedVersion); console.log(selectedVersion);
setSelectedVersionRecord(selectedVersion); setSelectedVersionRecord(selectedVersion);
@ -233,6 +251,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) function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
{ {
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}> return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
@ -344,7 +400,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
*/ */
return ( return (
<Grid container> <Grid container className="scriptViewer">
<Grid item xs={12}> <Grid item xs={12}>
<Box> <Box>
{ {
@ -420,10 +476,22 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
</CustomWidthTooltip> </CustomWidthTooltip>
</Box> </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 <AceEditor
mode="javascript" mode={getSelectedFileType()}
theme="github" theme="github"
name={"viewData"} name={"viewData"}
readOnly readOnly
@ -431,8 +499,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
editorProps={{$blockScrolling: true}} editorProps={{$blockScrolling: true}}
setOptions={{useWorker: false}} setOptions={{useWorker: false}}
width="100%" width="100%"
height="400px" height="368px"
value={selectedVersionRecord?.values?.get("contents")} value={getSelectedFileCode()}
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
/> />
</> </>
) : null ) : null
@ -473,11 +542,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
<TabPanel index={2} value={selectedTab}> <TabPanel index={2} value={selectedTab}>
<Box sx={{height: "455px"}} px={2} pb={1}> <Box sx={{height: "455px"}} px={2} pb={1}>
<ScriptTestForm scriptId={scriptId} <ScriptTestForm scriptId={scriptId}
scriptDefinition={testScriptDefinitionObject} scriptType={scriptTypeRecord}
tableName={associatedScriptTableName} tableName={associatedScriptTableName}
fieldName={associatedScriptFieldName} fieldName={associatedScriptFieldName}
recordId={associatedScriptRecordId} recordId={associatedScriptRecordId}
code={selectedVersionRecord?.values.get("contents")} fileContents={getSelectedVersionCode()}
apiName={selectedVersionRecord?.values.get("apiName")} apiName={selectedVersionRecord?.values.get("apiName")}
apiVersion={selectedVersionRecord?.values.get("apiVersion")} /> apiVersion={selectedVersionRecord?.values.get("apiVersion")} />
</Box> </Box>

View File

@ -929,12 +929,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
} }
}; };
const handleSortChange = (gridSort: GridSortModel) => const handleSortChangeForDataGrid = (gridSort: GridSortModel) =>
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this method just wraps handleSortChange, but w/o the optional 2nd param, so we can use it in data grid //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
handleSortChange(gridSort);
}
const handleSortChange = (gridSort: GridSortModel, overrideFilterModel?: GridFilterModel) =>
{ {
if (gridSort && gridSort.length > 0) if (gridSort && gridSort.length > 0)
{ {
setColumnSortModel(gridSort); setColumnSortModel(gridSort);
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, gridSort, rowsPerPage)); const gridFilterModelToUse = overrideFilterModel ?? filterModel;
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, gridFilterModelToUse, gridSort, rowsPerPage));
localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort)); localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort));
} }
}; };
@ -1286,13 +1295,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null); const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null);
handleFilterChange(models.filter); handleFilterChange(models.filter);
handleSortChange(models.sort); handleSortChange(models.sort, models.filter);
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString()); localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
} }
else else
{ {
handleFilterChange({items: []} as GridFilterModel); handleFilterChange({items: []} as GridFilterModel);
handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}]); handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}], {items: []} as GridFilterModel);
localStorage.removeItem(currentSavedFilterLocalStorageKey); localStorage.removeItem(currentSavedFilterLocalStorageKey);
} }
} }
@ -1692,7 +1701,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>); menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
} }
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate("dev")}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>); menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableName)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
if (tableProcesses && tableProcesses.length) if (tableProcesses && tableProcesses.length)
{ {
@ -1919,7 +1928,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
onColumnVisibilityModelChange={handleColumnVisibilityChange} onColumnVisibilityModelChange={handleColumnVisibilityChange}
onColumnOrderChange={handleColumnOrderChange} onColumnOrderChange={handleColumnOrderChange}
onSelectionModelChange={selectionChanged} onSelectionModelChange={selectionChanged}
onSortModelChange={handleSortChange} onSortModelChange={handleSortChangeForDataGrid}
sortingOrder={["asc", "desc"]} sortingOrder={["asc", "desc"]}
sortModel={columnSortModel} sortModel={columnSortModel}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}

View File

@ -329,10 +329,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
setMetaData(metaData); setMetaData(metaData);
ValueUtils.qInstance = metaData; ValueUtils.qInstance = metaData;
///////////////////////////////////////////////////
// load the processes to show in the action menu //
///////////////////////////////////////////////////
const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName); const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName);
processesForTable.sort((a, b) => a.label.localeCompare(b.label)); processesForTable.sort((a, b) => a.label.localeCompare(b.label));
setTableProcesses(processesForTable); setTableProcesses(processesForTable);
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
//////////////////////////////////////////////////////
// load processes that the routing needs to respect //
//////////////////////////////////////////////////////
const allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true) // these include hidden ones (e.g., to find the bulks)
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
if (runRecordScriptProcess)
{
allTableProcesses.unshift(runRecordScriptProcess)
}
setAllTableProcesses(allTableProcesses);
if (launchingProcess) if (launchingProcess)
{ {
@ -570,6 +584,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission); let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
function gotoCreate()
{
const path = `${pathParts.slice(0, -1).join("/")}/create`;
navigate(path);
}
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
const renderActionsMenu = ( const renderActionsMenu = (
<Menu <Menu
anchorEl={actionsMenu} anchorEl={actionsMenu}
@ -626,8 +648,15 @@ function RecordView({table, launchProcess}: Props): JSX.Element
</MenuItem> </MenuItem>
))} ))}
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />} {(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
{
runRecordScriptProcess &&
<MenuItem key={runRecordScriptProcess.name} onClick={() => processClicked(runRecordScriptProcess)}>
<ListItemIcon><Icon>{runRecordScriptProcess.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{runRecordScriptProcess.label}
</MenuItem>
}
<MenuItem onClick={() => navigate("dev")}> <MenuItem onClick={() => navigate("dev")}>
<ListItemIcon><Icon>data_object</Icon></ListItemIcon> <ListItemIcon><Icon>code</Icon></ListItemIcon>
Developer Mode Developer Mode
</MenuItem> </MenuItem>
{ {
@ -853,7 +882,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
activeModalProcess && activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}> <Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<div className="modalProcess"> <div className="modalProcess">
<ProcessRun process={activeModalProcess} isModal={true} recordIds={id} closeModalHandler={closeModalProcess} /> <ProcessRun process={activeModalProcess} isModal={true} table={tableMetaData} recordIds={id} closeModalHandler={closeModalProcess} />
</div> </div>
</Modal> </Modal>
} }

View File

@ -523,3 +523,25 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
padding-right: 0.25rem; padding-right: 0.25rem;
} }
/* file-select box in script editor & viewer - make it look tabby */
.scriptEditor .selectedFileTab div.MuiSelect-select,
.scriptViewer .selectedFileTab div.MuiSelect-select
{
padding: 0.25rem 1.5rem 0.25rem 1rem !important;
border: 1px solid lightgray;
border-radius: 0.375rem 0.375rem 0 0;
}
.scriptEditor .selectedFileTab,
.scriptViewer .selectedFileTab
{
margin-bottom: -1px;
}
/* show the down-arrow in the file-select box in script editor & viewer */
.scriptEditor .selectedFileTab .MuiSelect-iconStandard,
.scriptViewer .selectedFileTab .MuiSelect-iconStandard
{
display: inline;
right: .5rem
}

View File

@ -62,10 +62,21 @@ export default class HtmlUtils
}; };
/******************************************************************************* /*******************************************************************************
** Download a server-side generated file. ** Download a server-side generated file (or the contents of a data: url)
*******************************************************************************/ *******************************************************************************/
static downloadUrlViaIFrame = (url: string) => static downloadUrlViaIFrame = (url: string, filename: string) =>
{ {
if(url.startsWith("data:"))
{
const link = document.createElement("a");
link.download = filename;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return;
}
if (document.getElementById("downloadIframe")) if (document.getElementById("downloadIframe"))
{ {
document.body.removeChild(document.getElementById("downloadIframe")); document.body.removeChild(document.getElementById("downloadIframe"));
@ -101,10 +112,21 @@ export default class HtmlUtils
}; };
/******************************************************************************* /*******************************************************************************
** Open a server-side generated file from a url in a new window. ** Open a server-side generated file from a url in a new window (or a data: url)
*******************************************************************************/ *******************************************************************************/
static openInNewWindow = (url: string, filename: string) => static openInNewWindow = (url: string, filename: string) =>
{ {
if(url.startsWith("data:"))
{
const openInWindow = window.open("", "_blank");
openInWindow.document.write(`<html lang="en">
<body style="margin: 0">
<iframe src="${url}" width="100%" height="100%" style="border: 0">
</body>
`);
return;
}
const openInWindow = window.open("", "_blank"); const openInWindow = window.open("", "_blank");
openInWindow.document.write(`<html lang="en"> openInWindow.document.write(`<html lang="en">
<head> <head>

View File

@ -39,6 +39,7 @@ import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/mode-sql"; import "ace-builds/src-noconflict/mode-sql";
import "ace-builds/src-noconflict/mode-velocity";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Values ** Utility class for working with QQQ Values
@ -637,7 +638,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
const download = (event: React.MouseEvent<HTMLSpanElement>) => const download = (event: React.MouseEvent<HTMLSpanElement>) =>
{ {
event.stopPropagation(); event.stopPropagation();
HtmlUtils.downloadUrlViaIFrame(url); HtmlUtils.downloadUrlViaIFrame(url, filename);
}; };
const open = (event: React.MouseEvent<HTMLSpanElement>) => const open = (event: React.MouseEvent<HTMLSpanElement>) =>

View File

@ -44,6 +44,7 @@ public class ScriptTableTest extends QBaseSeleniumTest
.withRouteToFile("/data/script/1", "data/script/1.json") .withRouteToFile("/data/script/1", "data/script/1.json")
.withRouteToFile("/data/scriptType/1", "data/scriptType/1.json") .withRouteToFile("/data/scriptType/1", "data/scriptType/1.json")
.withRouteToFile("/data/scriptRevision/query", "data/scriptRevision/query.json") .withRouteToFile("/data/scriptRevision/query", "data/scriptRevision/query.json")
.withRouteToFile("/data/scriptLog/query", "data/scriptLog/query.json")
.withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json") .withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json")
.withRouteToFile("/metaData/table/script", "metaData/table/script.json") .withRouteToFile("/metaData/table/script", "metaData/table/script.json")
.withRouteToFile("/widget/scriptViewer", "widget/scriptViewer.json") .withRouteToFile("/widget/scriptViewer", "widget/scriptViewer.json")

View File

@ -0,0 +1,3 @@
{
"records": []
}

View File

@ -1,7 +1,6 @@
{ {
"tableName": "scriptRevision", "tableName": "scriptRevision",
"values": { "values": {
"contents": "var hello;",
"id": 100, "id": 100,
"commitMessage": "Initial checkin", "commitMessage": "Initial checkin",
"author": "Jon Programmer", "author": "Jon Programmer",
@ -10,5 +9,20 @@
}, },
"displayValues": { "displayValues": {
},
"associatedRecords": {
"files": [
{
"tableName": "scriptRevisionFile",
"values": {
"id": 101,
"fileName": "Script.js",
"contents": "var hello;",
"scriptRevisionId": 100,
"createDate": "2023-06-23T21:59:57Z",
"modifyDate": "2023-06-23T21:59:57Z"
}
}
]
} }
} }

View File

@ -5,7 +5,8 @@
"name": "Record Script", "name": "Record Script",
"id": 1, "id": 1,
"createDate": "2023-02-18T00:47:51Z", "createDate": "2023-02-18T00:47:51Z",
"modifyDate": "2023-02-18T00:47:51Z" "modifyDate": "2023-02-18T00:47:51Z",
"fileMode": 1
}, },
"displayValues": { "displayValues": {
} }