From 11a4ca256a1414b98fd90f0a1cd4fb9be1783a92 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 22 Jun 2023 16:24:26 -0500 Subject: [PATCH 1/9] Fix how filter models get set when a saved filter is loaded --- src/qqq/pages/records/query/RecordQuery.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index cb47647..689782d 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -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) { 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)); } }; @@ -1286,7 +1295,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null); handleFilterChange(models.filter); - handleSortChange(models.sort); + handleSortChange(models.sort, models.filter); localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString()); } else @@ -1919,7 +1928,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element onColumnVisibilityModelChange={handleColumnVisibilityChange} onColumnOrderChange={handleColumnOrderChange} onSelectionModelChange={selectionChanged} - onSortModelChange={handleSortChange} + onSortModelChange={handleSortChangeForDataGrid} sortingOrder={["asc", "desc"]} sortModel={columnSortModel} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} From 13cfd29d8ce1bd66cb86f6f3818b84a2f36dacd3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 22 Jun 2023 16:33:50 -0500 Subject: [PATCH 2/9] Same fix as previous for when clearing filter --- src/qqq/pages/records/query/RecordQuery.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 689782d..aad82a4 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -1301,7 +1301,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element else { handleFilterChange({items: []} as GridFilterModel); - handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}]); + handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}], {items: []} as GridFilterModel); localStorage.removeItem(currentSavedFilterLocalStorageKey); } } From 5ce5f8475261e6e44fbadea23a860684900baf07 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 22 Jun 2023 18:43:36 -0500 Subject: [PATCH 3/9] Add call to convertFilterPossibleValuesToIds now that buildQFilterFromGridFilter doesn't do that... --- src/qqq/components/misc/SavedFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qqq/components/misc/SavedFilters.tsx b/src/qqq/components/misc/SavedFilters.tsx index ef52bc6..eead528 100644 --- a/src/qqq/components/misc/SavedFilters.tsx +++ b/src/qqq/components/misc/SavedFilters.tsx @@ -200,7 +200,7 @@ function SavedFilters({qController, metaData, tableMetaData, currentSavedFilter, else { 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) { From 48a44f2605d5311950c34b99da79d0ae485e39e2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 27 Jun 2023 08:13:51 -0500 Subject: [PATCH 4/9] Updating to support multi-file scripts --- package.json | 2 +- src/qqq/components/scripts/ScriptEditor.tsx | 166 +++++++++++++++--- .../components/widgets/misc/ScriptViewer.tsx | 100 ++++++++--- src/qqq/styles/qqq-override-styles.css | 22 +++ src/qqq/utils/HtmlUtils.ts | 28 ++- src/qqq/utils/qqq/ValueUtils.tsx | 3 +- 6 files changed, 267 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index df1703b..04e1b52 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.68", + "@kingsrook/qqq-frontend-core": "1.0.71", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/scripts/ScriptEditor.tsx b/src/qqq/components/scripts/ScriptEditor.tsx index c50b08a..ae25b4b 100644 --- a/src/qqq/components/scripts/ScriptEditor.tsx +++ b/src/qqq/components/scripts/ScriptEditor.tsx @@ -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,60 @@ 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 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 [commitMessage, setCommitMessage] = useState("") const [openTool, setOpenTool] = useState(null); const [errorAlert, setErrorAlert] = useState("") @@ -200,7 +243,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 +255,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 +300,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 +350,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 ( - + @@ -348,29 +424,67 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab - + + {openEditorFileNames.map((fileName, index) => + { + return ( + + + + + + + { + openEditorFileNames.length > 1 && + + closeEditorClicked(index)}> + close + + + } + { + index == openEditorFileNames.length - 1 && + + + vertical_split + + + } + + + updateCode(value, event, index)} + width="100%" + height="calc(100% - 88px)" + value={fileContents[openEditorFileNames[index]]} + style={{border: "1px solid gray"}} + /> + + ); + })} + { openTool && { - openTool == "test" && + openTool == "test" && } { openTool == "docs" && diff --git a/src/qqq/components/widgets/misc/ScriptViewer.tsx b/src/qqq/components/widgets/misc/ScriptViewer.tsx index 2f25223..4040099 100644 --- a/src/qqq/components/widgets/misc/ScriptViewer.tsx +++ b/src/qqq/components/widgets/misc/ScriptViewer.tsx @@ -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 {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {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"; @@ -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 supportred.`); + } + + 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"), true); console.log("Fetched selectedVersion:"); console.log(selectedVersion); setSelectedVersionRecord(selectedVersion); @@ -233,6 +250,30 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc })(); }; + const handleSelectFile = (event: SelectChangeEvent) => + { + setSelectedFileName(event.target.value); + } + + const getSelectedFileCode = (): string => + { + return (getSelectedVersionCode()[selectedFileName] ?? ""); + } + + 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 @@ -344,7 +385,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc */ return ( - + { @@ -420,8 +461,20 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc { - loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("contents") ? ( + loadingSelectedVersion.isNotLoading() && selectedVersionRecord ? ( <> + { + availableFileNames && availableFileNames.length > 1 && + + + + } ) : null @@ -473,11 +527,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index b645a8f..0b791d4 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -523,3 +523,25 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } 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 +} diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts index 1bf0712..602b3a8 100644 --- a/src/qqq/utils/HtmlUtils.ts +++ b/src/qqq/utils/HtmlUtils.ts @@ -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")) { 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) => { + if(url.startsWith("data:")) + { + const openInWindow = window.open("", "_blank"); + openInWindow.document.write(` + +