diff --git a/package.json b/package.json index f9906b9..883e1d8 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.69", + "@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/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) { diff --git a/src/qqq/components/scripts/ScriptDocsForm.tsx b/src/qqq/components/scripts/ScriptDocsForm.tsx index b07836e..8296997 100644 --- a/src/qqq/components/scripts/ScriptDocsForm.tsx +++ b/src/qqq/components/scripts/ScriptDocsForm.tsx @@ -62,6 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El width="100%" showPrintMargin={false} height="100%" + style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}} /> diff --git a/src/qqq/components/scripts/ScriptEditor.tsx b/src/qqq/components/scripts/ScriptEditor.tsx index c50b08a..9ee7ca1 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,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 ( - + @@ -348,29 +446,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/scripts/ScriptTestForm.tsx b/src/qqq/components/scripts/ScriptTestForm.tsx index 11b16da..a70fa4c 100644 --- a/src/qqq/components/scripts/ScriptTestForm.tsx +++ b/src/qqq/components/scripts/ScriptTestForm.tsx @@ -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 + { + 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(); - 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 Test Input { - scriptDefinition.testInputFields && testInputValues && scriptDefinition.testInputFields.map((field: QFieldMetaData) => + testInputFields && testInputValues && testInputFields.map((field: QFieldMetaData) => { return ( } { - scriptDefinition.testOutputFields && scriptDefinition.testOutputFields.map((f: any) => + testOutputFields && testOutputFields.map((f: any) => { const field = new QFieldMetaData(f); return ( - + {field.label}: - {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") + } ); diff --git a/src/qqq/components/widgets/misc/ScriptViewer.tsx b/src/qqq/components/widgets/misc/ScriptViewer.tsx index 2f25223..afd01ee 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"; @@ -59,6 +63,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 +99,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 +125,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 +163,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 +190,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 +239,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 +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) { return @@ -344,7 +400,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc */ return ( - + { @@ -420,10 +476,22 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc { - loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("contents") ? ( + loadingSelectedVersion.isNotLoading() && selectedVersionRecord ? ( <> + { + availableFileNames && availableFileNames.length > 1 && + + + + } ) : null @@ -473,11 +542,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index cb47647..63ef5ca 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,13 +1295,13 @@ 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 { handleFilterChange({items: []} as GridFilterModel); - handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}]); + handleSortChange([{field: tableMetaData.primaryKeyField, sort: "desc"}], {items: []} as GridFilterModel); localStorage.removeItem(currentSavedFilterLocalStorageKey); } } @@ -1692,7 +1701,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element menuItems.push( processClicked(process)}>{process.iconName ?? "arrow_forward"}{process.label}); } - menuItems.push( navigate("dev")}>codeDeveloper Mode); + menuItems.push( navigate(`${metaData.getTablePathByName(tableName)}/dev`)}>codeDeveloper Mode); if (tableProcesses && tableProcesses.length) { @@ -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")} diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index 2903e5f..7a3e75e 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -329,10 +329,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element const metaData = await qController.loadMetaData(); setMetaData(metaData); ValueUtils.qInstance = metaData; + + /////////////////////////////////////////////////// + // load the processes to show in the action menu // + /////////////////////////////////////////////////// const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName); processesForTable.sort((a, b) => a.label.localeCompare(b.label)); 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) { @@ -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); + function gotoCreate() + { + const path = `${pathParts.slice(0, -1).join("/")}/create`; + navigate(path); + } + + const runRecordScriptProcess = metaData?.processes.get("runRecordScript"); + const renderActionsMenu = ( ))} {(tableProcesses?.length > 0 || hasEditOrDelete) && } + { + runRecordScriptProcess && + processClicked(runRecordScriptProcess)}> + {runRecordScriptProcess.iconName ?? "arrow_forward"} + {runRecordScriptProcess.label} + + } navigate("dev")}> - data_object + code Developer Mode { @@ -853,7 +882,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element activeModalProcess && closeModalProcess(event, reason)}>
- +
} 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(` + +