Merged dev into feature/bulk-upload-v2

This commit is contained in:
2024-12-03 10:02:16 -06:00
16 changed files with 20312 additions and 4946 deletions

24408
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<revision>0.23.0-SNAPSHOT</revision> <revision>0.24.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName) if (fieldMetaData.possibleValueSourceName)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
if (results && results.length > 0) if (results && results.length > 0)
{ {
defaultDisplayValues.set(fieldName, results[0].label); defaultDisplayValues.set(fieldName, results[0].label);
@ -818,9 +818,9 @@ function EntityForm(props: Props): JSX.Element
{ {
actions.setSubmitting(true); actions.setSubmitting(true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there anre return. // // if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there and return. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (props.onSubmitCallback) if (props.onSubmitCallback)
{ {
props.onSubmitCallback(values); props.onSubmitCallback(values);
@ -1290,7 +1290,7 @@ function EntityForm(props: Props): JSX.Element
table={showEditChildForm.table} table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues} defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields} disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm} onSubmitCallback={props.onSubmitCallback ? props.onSubmitCallback : submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`} overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/> />
</div> </div>

View File

@ -40,16 +40,17 @@ 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 Tooltip from "@mui/material/Tooltip";
import FormData from "form-data"; import FormData from "form-data";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import DynamicSelect from "qqq/components/forms/DynamicSelect"; import DynamicSelect from "qqq/components/forms/DynamicSelect";
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm"; import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm"; import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-github";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/ext-language_tools"; import "ace-builds/src-noconflict/ext-language_tools";
export interface ScriptEditorProps export interface ScriptEditorProps
@ -69,15 +70,15 @@ const qController = Client.getInstance();
function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string } function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
{ {
const rs: {[name: string]: string} = {}; const rs: { [name: string]: string } = {};
if(!scriptTypeFileSchemaList) if (!scriptTypeFileSchemaList)
{ {
console.log("Missing scriptTypeFileSchemaList"); console.log("Missing scriptTypeFileSchemaList");
} }
else else
{ {
let files = scriptRevisionRecord?.associatedRecords?.get("files") let files = scriptRevisionRecord?.associatedRecords?.get("files");
for (let i = 0; i < scriptTypeFileSchemaList.length; i++) for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
{ {
@ -88,7 +89,7 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
for (let j = 0; j < files?.length; j++) for (let j = 0; j < files?.length; j++)
{ {
let file = files[j]; let file = files[j];
if(file.values.get("fileName") == name) if (file.values.get("fileName") == name)
{ {
contents = file.values.get("contents"); contents = file.values.get("contents");
} }
@ -103,9 +104,9 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string } function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
{ {
const rs: {[name: string]: string} = {}; const rs: { [name: string]: string } = {};
if(!scriptTypeFileSchemaList) if (!scriptTypeFileSchemaList)
{ {
console.log("Missing scriptTypeFileSchemaList"); console.log("Missing scriptTypeFileSchemaList");
} }
@ -125,21 +126,21 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
const [closing, setClosing] = useState(false); const [closing, setClosing] = useState(false);
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 fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema); const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]) const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]);
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList)) const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList));
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList)) const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList));
console.log(`file types: ${JSON.stringify(fileTypes)}`); 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("");
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false); const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const ref = useRef(); const ref = useRef();
@ -241,19 +242,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
// need this to make Ace recognize new height. // need this to make Ace recognize new height.
setTimeout(() => setTimeout(() =>
{ {
window.dispatchEvent(new Event("resize")) window.dispatchEvent(new Event("resize"));
}, 100); }, 100);
}; };
const saveClicked = (overrideCommitMessage?: string) => const saveClicked = (overrideCommitMessage?: string) =>
{ {
if(!apiName || !apiVersion) if (!apiName || !apiVersion)
{ {
setErrorAlert("You must select a value for both API Name and API Version.") setErrorAlert("You must select a value for both API Name and API Version.");
return; return;
} }
if(!commitMessage && !overrideCommitMessage) if (!commitMessage && !overrideCommitMessage)
{ {
setPromptForCommitMessageOpen(true); setPromptForCommitMessageOpen(true);
return; return;
@ -267,18 +268,18 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
formData.append("scriptId", scriptId); formData.append("scriptId", scriptId);
formData.append("commitMessage", overrideCommitMessage ?? commitMessage); formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
if(apiName) if (apiName)
{ {
formData.append("apiName", apiName); formData.append("apiName", apiName);
} }
if(apiVersion) if (apiVersion)
{ {
formData.append("apiVersion", apiVersion); formData.append("apiVersion", apiVersion);
} }
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name")) const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
formData.append("fileNames", fileNamesFromSchema.join(",")); formData.append("fileNames", fileNamesFromSchema.join(","));
for (let fileName in fileContents) for (let fileName in fileContents)
@ -299,58 +300,58 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
if (processResult instanceof QJobError) if (processResult instanceof QJobError)
{ {
const jobError = processResult as QJobError const jobError = processResult as QJobError;
setErrorAlert(jobError.userFacingError ?? jobError.error) setErrorAlert(jobError.userFacingError ?? jobError.error);
setClosing(false); setClosing(false);
return; return;
} }
closeCallback(null, "saved", "Saved New Script Version"); closeCallback(null, "saved", "Saved New Script Version");
} }
catch(e) catch (e)
{ {
// @ts-ignore // @ts-ignore
setErrorAlert(e.message ?? "Unexpected error saving script") setErrorAlert(e.message ?? "Unexpected error saving script");
setClosing(false); setClosing(false);
} }
})(); })();
} };
const cancelClicked = () => const cancelClicked = () =>
{ {
setClosing(true); setClosing(true);
closeCallback(null, "cancelled"); closeCallback(null, "cancelled");
} };
const updateCode = (value: string, event: any, index: number) => const updateCode = (value: string, event: any, index: number) =>
{ {
fileContents[openEditorFileNames[index]] = value; fileContents[openEditorFileNames[index]] = value;
forceUpdate(); forceUpdate();
} };
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) => const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{ {
setCommitMessage(event.target.value); setCommitMessage(event.target.value);
} };
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) => const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
{ {
setPromptForCommitMessageOpen(false); setPromptForCommitMessageOpen(false);
if(wasSaveClicked) if (wasSaveClicked)
{ {
setCommitMessage(message) setCommitMessage(message);
saveClicked(message); saveClicked(message);
} }
else else
{ {
setClosing(false); setClosing(false);
} }
} };
const changeApiName = (apiNamePossibleValue?: QPossibleValue) => const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
{ {
if(apiNamePossibleValue) if (apiNamePossibleValue)
{ {
setApiName(apiNamePossibleValue.id); setApiName(apiNamePossibleValue.id);
} }
@ -358,11 +359,11 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
setApiName(null); setApiName(null);
} }
} };
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) => const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
{ {
if(apiVersionPossibleValue) if (apiVersionPossibleValue)
{ {
setApiVersion(apiVersionPossibleValue.id); setApiVersion(apiVersionPossibleValue.id);
} }
@ -370,33 +371,33 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
setApiVersion(null); setApiVersion(null);
} }
} };
const handleSelectingFile = (event: SelectChangeEvent, index: number) => const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
{ {
openEditorFileNames[index] = event.target.value openEditorFileNames[index] = event.target.value;
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const splitEditorClicked = () => const splitEditorClicked = () =>
{ {
openEditorFileNames.push(availableFileNames[0]) openEditorFileNames.push(availableFileNames[0]);
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const closeEditorClicked = (index: number) => const closeEditorClicked = (index: number) =>
{ {
openEditorFileNames.splice(index, 1) openEditorFileNames.splice(index, 1);
setOpenEditorFileNames(openEditorFileNames); setOpenEditorFileNames(openEditorFileNames);
forceUpdate(); forceUpdate();
} };
const computeEditorWidth = (): string => const computeEditorWidth = (): string =>
{ {
return (100 / openEditorFileNames.length) + "%" return (100 / openEditorFileNames.length) + "%";
} };
return ( return (
<Box className="scriptEditor" 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}>
@ -408,7 +409,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{ {
return; return;
} }
setErrorAlert("") setErrorAlert("");
}} anchorOrigin={{vertical: "top", horizontal: "center"}}> }} anchorOrigin={{vertical: "top", horizontal: "center"}}>
<Alert color="error" onClose={() => setErrorAlert("")}> <Alert color="error" onClose={() => setErrorAlert("")}>
{errorAlert} {errorAlert}
@ -464,19 +465,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
<Box> <Box>
{ {
openEditorFileNames.length > 1 && openEditorFileNames.length > 1 &&
<Tooltip title="Close this editor split" enterDelay={500}> <Tooltip title="Close this editor split" enterDelay={500}>
<IconButton size="small" onClick={() => closeEditorClicked(index)}> <IconButton size="small" onClick={() => closeEditorClicked(index)}>
<Icon>close</Icon> <Icon>close</Icon>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} }
{ {
index == openEditorFileNames.length - 1 && index == openEditorFileNames.length - 1 &&
<Tooltip title="Open a new editor split" enterDelay={500}> <Tooltip title="Open a new editor split" enterDelay={500}>
<IconButton size="small" onClick={splitEditorClicked}> <IconButton size="small" onClick={splitEditorClicked}>
<Icon>vertical_split</Icon> <Icon>vertical_split</Icon>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} }
</Box> </Box>
</Box> </Box>
@ -526,29 +527,29 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
</Grid> </Grid>
</Box> </Box>
<CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage}/> <CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage} />
</Card> </Card>
</Box> </Box>
); );
} }
function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void}) function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void })
{ {
const [commitMessage, setCommitMessage] = useState("No commit message given") const [commitMessage, setCommitMessage] = useState("No commit message given");
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) => const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{ {
setCommitMessage(event.target.value); setCommitMessage(event.target.value);
} };
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) => const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
{ {
if(e.key === "Enter") if (e.key === "Enter")
{ {
props.closeHandler(true, commitMessage); props.closeHandler(true, commitMessage);
} }
} };
return ( return (
<Dialog <Dialog
@ -579,10 +580,10 @@ function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClic
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<QCancelButton onClickHandler={() => props.closeHandler(false)} disabled={false} /> <QCancelButton onClickHandler={() => props.closeHandler(false)} disabled={false} />
<QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false}/> <QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false} />
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) );
} }
export default ScriptEditor; export default ScriptEditor;

View File

@ -18,18 +18,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Skeleton} from "@mui/material"; import {Alert, Skeleton} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Modal from "@mui/material/Modal";
import Tab from "@mui/material/Tab"; import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
import parse from "html-react-parser"; import parse from "html-react-parser";
import QContext from "QContext"; import QContext from "QContext";
import EntityForm from "qqq/components/forms/EntityForm";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import TabPanel from "qqq/components/misc/TabPanel"; import TabPanel from "qqq/components/misc/TabPanel";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import BarChart from "qqq/components/widgets/charts/barchart/BarChart"; import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart"; import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart"; import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
@ -44,7 +46,7 @@ import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidg
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget"; import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget"; import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart"; import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer"; import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
import StepperCard from "qqq/components/widgets/misc/StepperCard"; import StepperCard from "qqq/components/widgets/misc/StepperCard";
import USMapWidget from "qqq/components/widgets/misc/USMapWidget"; import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
@ -72,9 +74,9 @@ interface Props
childUrlParams?: string; childUrlParams?: string;
parentWidgetMetaData?: QWidgetMetaData; parentWidgetMetaData?: QWidgetMetaData;
wrapWidgetsInTabPanels: boolean; wrapWidgetsInTabPanels: boolean;
actionCallback?: (blockData: BlockData) => boolean; actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean;
initialWidgetDataList: any[]; initialWidgetDataList: any[];
values?: {[key: string]: any}; values?: { [key: string]: any };
} }
DashboardWidgets.defaultProps = { DashboardWidgets.defaultProps = {
@ -101,6 +103,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const [haveLoadedParams, setHaveLoadedParams] = useState(false); const [haveLoadedParams, setHaveLoadedParams] = useState(false);
const {accentColor} = useContext(QContext); const {accentColor} = useContext(QContext);
/////////////////////////
// modal form controls //
/////////////////////////
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
let initialSelectedTab = 0; let initialSelectedTab = 0;
let selectedTabKey: string = null; let selectedTabKey: string = null;
if (parentWidgetMetaData && wrapWidgetsInTabPanels) if (parentWidgetMetaData && wrapWidgetsInTabPanels)
@ -121,11 +128,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
useEffect(() => useEffect(() =>
{ {
if(initialWidgetDataList && initialWidgetDataList.length > 0) if (initialWidgetDataList && initialWidgetDataList.length > 0)
{ {
// todo actually, should this check each element of the array, down in the loop? yeah, when we need to, do it that way. // todo actually, should this check each element of the array, down in the loop? yeah, when we need to, do it that way.
console.log("We already have initial widget data, so not fetching from backend."); console.log("We already have initial widget data, so not fetching from backend.");
return return;
} }
setWidgetData([]); setWidgetData([]);
@ -166,7 +173,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const reloadWidget = async (index: number, data: string) => const reloadWidget = async (index: number, data: string) =>
{ {
(async () => await (async () =>
{ {
const urlParams = getQueryParams(widgetMetaDataList[index], data); const urlParams = getQueryParams(widgetMetaDataList[index], data);
setCurrentUrlParams(urlParams); setCurrentUrlParams(urlParams);
@ -285,6 +292,150 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
return (rs); return (rs);
} }
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowEditChildForm(null);
};
/*******************************************************************************
**
*******************************************************************************/
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
{
updateChildRecordList(name, "delete", rowIndex);
actionCallback(widgetData[widgetIndex]);
};
/*******************************************************************************
**
*******************************************************************************/
function openEditChildRecord(name: string, widgetData: any, rowIndex: number)
{
let defaultValues = widgetData.queryOutput.records[rowIndex].values;
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, rowIndex, defaultValues, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function openAddChildRecord(name: string, widgetData: any)
{
let defaultValues = widgetData.defaultValuesForNewChildRecords;
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function doOpenEditChildForm(widgetName: string, table: QTableMetaData, rowIndex: number, defaultValues: any, disabledFields: any)
{
const showEditChildForm: any = {};
showEditChildForm.widgetName = widgetName;
showEditChildForm.table = table;
showEditChildForm.rowIndex = rowIndex;
showEditChildForm.defaultValues = defaultValues;
showEditChildForm.disabledFields = disabledFields;
setShowEditChildForm(showEditChildForm);
}
/*******************************************************************************
**
*******************************************************************************/
function submitEditChildForm(values: any)
{
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
actionCallback(widgetData[widgetIndex]);
}
/*******************************************************************************
**
*******************************************************************************/
function determineChildRecordListIndex(widgetName: string): number
{
let widgetIndex = -1;
for (var i = 0; i < widgetMetaDataList.length; i++)
{
const widgetMetaData = widgetMetaDataList[i];
if (widgetMetaData.name == widgetName)
{
widgetIndex = i;
break;
}
}
return (widgetIndex);
}
/*******************************************************************************
**
*******************************************************************************/
function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
{
////////////////////////////////////////////////
// find the correct child record widget index //
////////////////////////////////////////////////
let widgetIndex = determineChildRecordListIndex(widgetName);
if (!widgetData[widgetIndex].queryOutput.records)
{
widgetData[widgetIndex].queryOutput.records = [];
}
const newChildListWidgetData: ChildRecordListData = widgetData[widgetIndex];
if (!newChildListWidgetData.queryOutput.records)
{
newChildListWidgetData.queryOutput.records = [];
}
switch (action)
{
case "insert":
newChildListWidgetData.queryOutput.records.push({values: values});
break;
case "edit":
newChildListWidgetData.queryOutput.records[rowIndex] = {values: values};
break;
case "delete":
newChildListWidgetData.queryOutput.records.splice(rowIndex, 1);
break;
}
newChildListWidgetData.totalRows = newChildListWidgetData.queryOutput.records.length;
widgetData[widgetIndex] = newChildListWidgetData;
setWidgetData(widgetData);
setShowEditChildForm(null);
}
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element => const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
{ {
const labelAdditionalComponentsRight: LabelComponent[] = []; const labelAdditionalComponentsRight: LabelComponent[] = [];
@ -324,7 +475,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
) )
} }
{ {
widgetMetaData.type === "alert" && widgetData[i]?.html && ( widgetMetaData.type === "alert" && widgetData[i]?.html && !widgetData[i]?.hideWidget && (
<Widget <Widget
omitPadding={true} omitPadding={true}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
@ -334,7 +485,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft} labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
> >
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>{parse(widgetData[i]?.html)}</Alert> <Alert severity={widgetData[i]?.alertType?.toLowerCase()}>
{parse(widgetData[i]?.html)}
{widgetData[i]?.bulletList && (
<div style={{fontSize: "14px"}}>
{widgetData[i].bulletList.map((bullet: string, index: number) =>
<li key={`widget-${i}-${index}`}>{parse(bullet)}</li>
)}
</div>
)}
</Alert>
</Widget> </Widget>
) )
} }
@ -516,9 +676,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
} }
{ {
widgetMetaData.type === "divider" && ( widgetMetaData.type === "divider" && (
<Box> <DividerWidget />
<DividerWidget />
</Box>
) )
} }
{ {
@ -552,6 +710,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
widgetMetaData.type === "childRecordList" && ( widgetMetaData.type === "childRecordList" && (
widgetData && widgetData[i] && widgetData && widgetData[i] &&
<RecordGridWidget <RecordGridWidget
disableRowClick={widgetData[i]?.disableRowClick}
allowRecordEdit={widgetData[i]?.allowRecordEdit}
allowRecordDelete={widgetData[i]?.allowRecordDelete}
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, i, rowIndex)}
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData[i], rowIndex)}
addNewRecordCallback={widgetData[i]?.isInProcess ? () => openAddChildRecord(widgetMetaData.name, widgetData[i]) : null}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
data={widgetData[i]} data={widgetData[i]}
/> />
@ -653,23 +817,23 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
if (!omitWrappingGridContainer) if (!omitWrappingGridContainer)
{ {
const gridProps: {[key: string]: any} = {}; const gridProps: { [key: string]: any } = {};
for(let size of ["xs", "sm", "md", "lg", "xl", "xxl"]) for (let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
{ {
const key = `gridCols:sizeClass:${size}` const key = `gridCols:sizeClass:${size}`;
if(widgetMetaData?.defaultValues?.has(key)) if (widgetMetaData?.defaultValues?.has(key))
{ {
gridProps[size] = widgetMetaData?.defaultValues.get(key); gridProps[size] = widgetMetaData?.defaultValues.get(key);
} }
} }
if(!gridProps["xxl"]) if (!gridProps["xxl"])
{ {
gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12; gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12;
} }
if(!gridProps["xs"]) if (!gridProps["xs"])
{ {
gridProps["xs"] = 12; gridProps["xs"] = 12;
} }
@ -725,6 +889,22 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
</Grid> </Grid>
) )
} }
{
showEditChildForm &&
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/>
</div>
</Modal>
}
</> </>
) : null ) : null
); );

View File

@ -50,6 +50,7 @@ import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
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-json"; import "ace-builds/src-noconflict/mode-json";

View File

@ -19,13 +19,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
function DividerWidget(): JSX.Element function DividerWidget(): JSX.Element
{ {
return ( return (
<Divider sx={{padding: "1px", background: "red"}}/> <Box pl={3} pt={3} pb={3} width="100%">
<Divider sx={{width: "100%", height: "1px", background: "grey"}} />
</Box>
); );
} }

View File

@ -39,15 +39,15 @@ import {Link, useNavigate} from "react-router-dom";
export interface ChildRecordListData extends WidgetData export interface ChildRecordListData extends WidgetData
{ {
title: string; title?: string;
queryOutput: { records: { values: any }[] }; queryOutput?: { records: { values: any }[] };
childTableMetaData: QTableMetaData; childTableMetaData?: QTableMetaData;
tablePath: string; tablePath?: string;
viewAllLink: string; viewAllLink?: string;
totalRows: number; totalRows?: number;
canAddChildRecord: boolean; canAddChildRecord?: boolean;
defaultValuesForNewChildRecords: { [fieldName: string]: any }; defaultValuesForNewChildRecords?: { [fieldName: string]: any };
disabledFieldsForNewChildRecords: { [fieldName: string]: any }; disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
} }
interface Props interface Props
@ -186,7 +186,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
setCsv(csv); setCsv(csv);
setFileName(fileName); setFileName(fileName);
} }
}, [data]); }, [JSON.stringify(data?.queryOutput)]);
/////////////////// ///////////////////
// view all link // // view all link //
@ -305,6 +305,12 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
return (<GridToolbarContainer />); return (<GridToolbarContainer />);
} }
let containerPadding = -3;
if (data?.isInProcess)
{
containerPadding = 0;
}
const grid = ( const grid = (
<DataGridPro <DataGridPro
@ -364,7 +370,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
> >
<Box mx={-3} mb={-3}> <Box mx={containerPadding} mb={containerPadding}>
<Box> <Box>
{grid} {grid}
</Box> </Box>

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Modal from "@mui/material/Modal";
import EntityForm from "qqq/components/forms/EntityForm";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useReducer, useState} from "react";
////////////////////////////////
// structure of expected data //
////////////////////////////////
export interface ModalEditFormData
{
tableName: string;
defaultValues?: { [key: string]: string };
disabledFields?: { [key: string]: boolean } | string[];
overrideHeading?: string;
onSubmitCallback?: (values: any) => void;
initialShowModalValue?: boolean;
}
const qController = Client.getInstance();
function ModalEditForm({tableName, defaultValues, disabledFields, overrideHeading, onSubmitCallback, initialShowModalValue}: ModalEditFormData,): JSX.Element
{
const [showModal, setShowModal] = useState(initialShowModalValue);
const [table, setTable] = useState(null as QTableMetaData);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() =>
{
if (!tableName)
{
return;
}
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTable(tableMetaData);
forceUpdate();
})();
}, [tableName]);
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowModal(null);
};
return (
table && showModal &&
<Modal open={showModal as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={table}
defaultValues={defaultValues}
disabledFields={disabledFields}
onSubmitCallback={onSubmitCallback}
overrideHeading={overrideHeading}
/>
</div>
</Modal>
);
}
export default ModalEditForm;

View File

@ -68,6 +68,7 @@ import ValidationReview from "qqq/components/processes/ValidationReview";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget"; import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery"; import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
@ -160,8 +161,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>); const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } }); const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
const [controlCallbacks, setControlCallbacks] = useState({} as {[name: string]: () => void}) const [controlCallbacks, setControlCallbacks] = useState({} as {[name: string]: () => void});
const [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]) const [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]);
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext); const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -196,7 +197,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
noMoreSteps = true; noMoreSteps = true;
} }
if(processValues["noMoreSteps"]) if (processValues["noMoreSteps"])
{ {
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// this, to allow a non-linear process to request this behavior // // this, to allow a non-linear process to request this behavior //
@ -222,7 +223,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [recordConfig, setRecordConfig] = useState({} as any); const [recordConfig, setRecordConfig] = useState({} as any);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [records, setRecords] = useState([] as QRecord[]); const [records, setRecords] = useState([] as any);
const [childRecordData, setChildRecordData] = useState(null as ChildRecordListData);
////////////////////////////// //////////////////////////////
// state for bulk edit form // // state for bulk edit form //
@ -346,23 +348,24 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
*******************************************************************************/ *******************************************************************************/
function renderWidget(widgetName: string) function renderWidget(widgetName: string)
{ {
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
if (!renderedWidgets[activeStep.name]) if (!renderedWidgets[activeStep.name])
{ {
renderedWidgets[activeStep.name] = {}; renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets); setRenderedWidgets(renderedWidgets);
} }
if (renderedWidgets[activeStep.name][widgetName]) let isChildRecordWidget = widgetMetaData.type == "childRecordList";
if (!isChildRecordWidget && renderedWidgets[activeStep.name][widgetName])
{ {
return renderedWidgets[activeStep.name][widgetName]; return renderedWidgets[activeStep.name][widgetName];
} }
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
const queryStringParts: string[] = []; const queryStringParts: string[] = [];
for (let name in processValues) for (let name in processValues)
{ {
@ -370,14 +373,25 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
} }
let initialWidgetDataList = null; let initialWidgetDataList = null;
if(processValues[widgetName]) if (processValues[widgetName])
{ {
processValues[widgetName].hasPermission = true processValues[widgetName].hasPermission = true;
initialWidgetDataList = [processValues[widgetName]] initialWidgetDataList = [processValues[widgetName]];
}
let actionCallback = blockWidgetActionCallback;
if (isChildRecordWidget)
{
actionCallback = childRecordListWidgetActionCallBack;
if (childRecordData)
{
initialWidgetDataList = [childRecordData];
}
} }
const renderedWidget = (<Box m={-2}> const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} initialWidgetDataList={initialWidgetDataList} values={processValues} actionCallback={blockWidgetActionCallback} /> <DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} initialWidgetDataList={initialWidgetDataList} values={processValues} actionCallback={actionCallback} />
</Box>); </Box>);
renderedWidgets[activeStep.name][widgetName] = renderedWidget; renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget; return renderedWidget;
@ -391,46 +405,57 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
const split = controlCode.split(":", 2); const split = controlCode.split(":", 2);
let controlCallbackName: string; let controlCallbackName: string;
let controlCallbackValue: any let controlCallbackValue: any;
if(split.length == 2) if (split.length == 2)
{ {
if(split[0] == "showModal") if (split[0] == "showModal")
{ {
processValues[split[1]] = true processValues[split[1]] = true;
controlCallbackName = split[1] controlCallbackName = split[1];
controlCallbackValue = true controlCallbackValue = true;
} }
else if(split[0] == "hideModal") else if (split[0] == "hideModal")
{ {
processValues[split[1]] = false processValues[split[1]] = false;
controlCallbackName = split[1] controlCallbackName = split[1];
controlCallbackValue = false controlCallbackValue = false;
} }
else if(split[0] == "toggleModal") else if (split[0] == "toggleModal")
{ {
const currentValue = processValues[split[1]] const currentValue = processValues[split[1]];
processValues[split[1]] = !!!currentValue; processValues[split[1]] = !!!currentValue;
controlCallbackName = split[1] controlCallbackName = split[1];
controlCallbackValue = processValues[split[1]] controlCallbackValue = processValues[split[1]];
} }
else else
{ {
console.log(`Unexpected part[0] (before colon) in controlCode: [${controlCode}]`) console.log(`Unexpected part[0] (before colon) in controlCode: [${controlCode}]`);
} }
} }
else else
{ {
console.log(`Expected controlCode to have 2 colon-delimited parts, but was: [${controlCode}]`) console.log(`Expected controlCode to have 2 colon-delimited parts, but was: [${controlCode}]`);
} }
if(controlCallbackName && controlCallbacks[controlCallbackName]) if (controlCallbackName && controlCallbacks[controlCallbackName])
{ {
// @ts-ignore ... args are hard // @ts-ignore ... args are hard
controlCallbacks[controlCallbackName](controlCallbackValue) controlCallbacks[controlCallbackName](controlCallbackValue);
} }
} }
/***************************************************************************
** callback used by child list widget
***************************************************************************/
function childRecordListWidgetActionCallBack(data: any): boolean
{
console.log(`in childRecordListWidgetActionCallBack: ${JSON.stringify(data)}`);
setChildRecordData(data as ChildRecordListData);
return (true);
}
/*************************************************************************** /***************************************************************************
** callback used by widget blocks, e.g., for input-text-enter-on-submit, ** callback used by widget blocks, e.g., for input-text-enter-on-submit,
** and action buttons. ** and action buttons.
@ -439,11 +464,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`); console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`);
if(eventValues?.registerControlCallbackName && eventValues?.registerControlCallbackFunction) if (eventValues?.registerControlCallbackName && eventValues?.registerControlCallbackFunction)
{ {
controlCallbacks[eventValues.registerControlCallbackName] = eventValues.registerControlCallbackFunction; controlCallbacks[eventValues.registerControlCallbackName] = eventValues.registerControlCallbackFunction;
setControlCallbacks(controlCallbacks) setControlCallbacks(controlCallbacks);
return (true) return (true);
} }
//////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -466,29 +491,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// } // }
let doSubmit = false; let doSubmit = false;
if(blockData?.blockTypeName == "BUTTON" && eventValues?.actionCode) if (blockData?.blockTypeName == "BUTTON" && eventValues?.actionCode)
{ {
doSubmit = true doSubmit = true;
} }
else if(blockData?.blockTypeName == "BUTTON" && eventValues?.controlCode) else if (blockData?.blockTypeName == "BUTTON" && eventValues?.controlCode)
{ {
handleControlCode(eventValues.controlCode); handleControlCode(eventValues.controlCode);
doSubmit = false doSubmit = false;
} }
else if(blockData?.blockTypeName == "INPUT_FIELD") else if (blockData?.blockTypeName == "INPUT_FIELD")
{ {
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// if action callback was fired from an input field, assume that means we're good to submit. // // if action callback was fired from an input field, assume that means we're good to submit. //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
doSubmit = true doSubmit = true;
} }
////////////////// //////////////////
// ok - submit! // // ok - submit! //
////////////////// //////////////////
if(doSubmit) if (doSubmit)
{ {
handleSubmit(eventValues); handleFormSubmit(eventValues);
return (true); return (true);
} }
} }
@ -711,7 +736,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]; let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles); const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />; const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
const isFormatScanner = step?.format?.toLowerCase() == "scanner" const isFormatScanner = step?.format?.toLowerCase() == "scanner";
return ( return (
<> <>
@ -997,7 +1022,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// if neither of those, then programmer error // // if neither of those, then programmer error //
//////////////////////////////////////////////// ////////////////////////////////////////////////
!(component.values?.widgetName || component.values?.isAdHocWidget) && !(component.values?.widgetName || component.values?.isAdHocWidget) &&
<Alert severity="error">Error: Component is marked as WIDGET type, but does not specify a <u>widgetName</u>, nor the <u>isAdHocWidget</u> flag.</Alert> <Alert severity="error">Error: Component is marked as WIDGET type, but does not specify a <u>widgetName</u>, nor the <u>isAdHocWidget</u> flag.</Alert>
} }
</> </>
) )
@ -1180,7 +1205,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// if this is the last step or not - and by default that radio will be true, to make this // // if this is the last step or not - and by default that radio will be true, to make this //
// NOT the last step - so set this value. // // NOT the last step - so set this value. //
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
if(!processValues["validationSummary"] && processValues["supportsFullValidation"]) if (!processValues["validationSummary"] && processValues["supportsFullValidation"])
{ {
setOverrideOnLastStep(false); setOverrideOnLastStep(false);
} }
@ -1199,7 +1224,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData); const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
const validation = DynamicFormUtils.getValidationForField(fieldMetaData); const validation = DynamicFormUtils.getValidationForField(fieldMetaData);
addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation) addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation);
}); });
} }
@ -1399,6 +1424,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setRecords(records); setRecords(records);
setLoadingRecords(false); setLoadingRecords(false);
if (!childRecordData || childRecordData.length == 0)
{
setChildRecordData(convertRecordsToChildRecordData(records));
}
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// re-construct the recordConfig object, so the setState call triggers a new rendering // // re-construct the recordConfig object, so the setState call triggers a new rendering //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
@ -1421,6 +1451,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}, [needRecords]); }, [needRecords]);
/***************************************************************************
**
***************************************************************************/
function convertRecordsToChildRecordData(records: QRecord[])
{
const frontendRecords = [] as any[];
records.forEach((record: QRecord) =>
{
const object = {
"tableName": record.tableName,
"recordLabel": record.recordLabel,
"errors": record.errors,
"warnings": record.warnings,
"values": Object.fromEntries(record.values),
"displayValues": Object.fromEntries(record.displayValues),
};
frontendRecords.push(object);
});
const newChildListData = {} as ChildRecordListData;
newChildListData.queryOutput = {records: frontendRecords};
return (newChildListData);
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
@ -1837,11 +1891,31 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}); });
}; };
////////////////////////////////////////////
// handle user submitting changed records //
////////////////////////////////////////////
const doSubmit = async (formData: FormData) =>
{
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,
activeStep.name,
formData,
qController.defaultMultipartFormDataHeaders()
);
setLastProcessResponse(processResponse);
});
};
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// handle user submitting the form - which in qqq means moving forward from any screen. // // handle user submitting the form - which in qqq means moving forward from any screen. //
// caller can pass in a map of values to be added to the form data too // // caller can pass in a map of values to be added to the form data too //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any) => const handleFormSubmit = async (values: any) =>
{ {
setFormError(null); setFormError(null);
@ -1903,19 +1977,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
clearStatesBeforeHittingBackend(); clearStatesBeforeHittingBackend();
setTimeout(async () => /////////////////////////////////////////////////////////////
// convert to regular objects so that they can be jsonized //
/////////////////////////////////////////////////////////////
if (childRecordData)
{ {
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label}); formData.append("frontendRecords", JSON.stringify(childRecordData.queryOutput.records));
}
const processResponse = await qController.processStep( doSubmit(formData);
processName,
processUUID,
activeStep.name,
formData,
qController.defaultMultipartFormDataHeaders(),
);
setLastProcessResponse(processResponse);
});
}; };
@ -1970,7 +2040,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const formStyles: any = {}; const formStyles: any = {};
if(isWidget) if (isWidget)
{ {
formStyles.display = "flex"; formStyles.display = "flex";
formStyles.flexGrow = 1; formStyles.flexGrow = 1;
@ -1984,7 +2054,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
const mainCardStyles: any = {}; const mainCardStyles: any = {};
if(!isWidget && !isModal) if (!isWidget && !isModal)
{ {
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// remove margin around card for non-widget, non-modal, small // // remove margin around card for non-widget, non-modal, small //
@ -2014,7 +2084,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
mainCardStyles.display = "flex"; mainCardStyles.display = "flex";
} }
return mainCardStyles return mainCardStyles;
} }
let nextButtonLabel = "Next"; let nextButtonLabel = "Next";
@ -2039,7 +2109,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationScheme} validationSchema={validationScheme}
validation={validationFunction} validation={validationFunction}
onSubmit={handleSubmit} onSubmit={handleFormSubmit}
> >
{({ {({
values, errors, touched, isSubmitting, setFieldValue, setTouched values, errors, touched, isSubmitting, setFieldValue, setTouched

View File

@ -34,6 +34,7 @@ import BaseLayout from "qqq/layouts/BaseLayout";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
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-json"; import "ace-builds/src-noconflict/mode-json";

View File

@ -92,7 +92,7 @@ const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: {[name: string]: QFieldMetaData}, styleOverrides?: {label?: SxProps, value?: SxProps}) export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps})
{ {
return <Box key={key} display="flex" flexDirection="column" py={1} pr={2}> return <Box key={key} display="flex" flexDirection="column" py={1} pr={2}>
{ {
@ -131,8 +131,8 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
export function getVisibleJoinTables(tableMetaData: QTableMetaData): Set<string> export function getVisibleJoinTables(tableMetaData: QTableMetaData): Set<string>
{ {
const visibleJoinTables = new Set<string>(); const visibleJoinTables = new Set<string>();
@ -206,6 +206,8 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext);
const CREATE_CHILD_KEY = "createChild";
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey)); tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
@ -308,12 +310,19 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/id/process // // the path for a process looks like: .../table/id/process //
// the path for creating a child record looks like: .../table/id/createChild/:childTableName // // the path for creating a child record looks like: .../table/id/createChild/:childTableName //
// the path for creating a child record in a process looks like: //
// .../table/id/processName#/createChild=... //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
let hasChildRecordKey = pathParts.some(p => p.includes(CREATE_CHILD_KEY));
if (!hasChildRecordKey)
{
hasChildRecordKey = hashParts.some(h => h.includes(CREATE_CHILD_KEY));
}
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if our tableName is in the -3 index, try to open process // // if our tableName is in the -3 index, and there is no token for updating child records, try to open process //
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 3] === tableName) if (!hasChildRecordKey && pathParts[pathParts.length - 3] === tableName)
{ {
const processName = pathParts[pathParts.length - 1]; const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName)); const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
@ -350,7 +359,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form // // if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
// e.g., person/42/createChild/address (to create an address under person 42) // // e.g., person/42/createChild/address (to create an address under person 42) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild") if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -369,7 +378,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
for (let i = 0; i < hashParts.length; i++) for (let i = 0; i < hashParts.length; i++)
{ {
const parts = hashParts[i].split("="); const parts = hashParts[i].split("=");
if (parts.length > 1 && parts[0] == "createChild") if (parts.length > 1 && parts[0] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -491,7 +500,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// if the component took in a record object, then we don't need to GET it // // if the component took in a record object, then we don't need to GET it //
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
if(overrideRecord) if (overrideRecord)
{ {
record = overrideRecord; record = overrideRecord;
} }
@ -827,12 +836,12 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
{ {
let shareDisabled = true; let shareDisabled = true;
let disabledTooltipText = ""; let disabledTooltipText = "";
if(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName && record) if (tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName && record)
{ {
const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName); const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName);
if(ownerId != currentUserId) if (ownerId != currentUserId)
{ {
disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.` disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.`;
shareDisabled = true; shareDisabled = true;
} }
else else

View File

@ -65,7 +65,7 @@ export default class DataGridUtils
{ {
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`); console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
} }
} };
/******************************************************************************* /*******************************************************************************
** **
@ -85,13 +85,13 @@ export default class DataGridUtils
row[field.name] = ValueUtils.getDisplayValue(field, record, "query"); row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
}); });
if(tableMetaData.exposedJoins) if (tableMetaData.exposedJoins)
{ {
for (let i = 0; i < tableMetaData.exposedJoins.length; i++) for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{ {
const join = tableMetaData.exposedJoins[i]; const join = tableMetaData.exposedJoins[i];
if(join?.joinTable?.fields?.values()) if (join?.joinTable?.fields?.values())
{ {
const fields = [...join.joinTable.fields.values()]; const fields = [...join.joinTable.fields.values()];
fields.forEach((field) => fields.forEach((field) =>
@ -103,15 +103,15 @@ export default class DataGridUtils
} }
} }
if(!row["id"]) if (!row["id"])
{ {
row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField]; row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField];
if(row["id"] === null || row["id"] === undefined) if (row["id"] === null || row["id"] === undefined)
{ {
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// DataGrid gets very upset about a null or undefined here, so, try to make it happier // // DataGrid gets very upset about a null or undefined here, so, try to make it happier //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
if(!allowEmptyId) if (!allowEmptyId)
{ {
row["id"] = "--"; row["id"] = "--";
} }
@ -122,7 +122,7 @@ export default class DataGridUtils
}); });
return (rows); return (rows);
} };
/******************************************************************************* /*******************************************************************************
** **
@ -132,24 +132,24 @@ export default class DataGridUtils
const columns = [] as GridColDef[]; const columns = [] as GridColDef[];
this.addColumnsForTable(tableMetaData, linkBase, columns, columnSort, null, null); this.addColumnsForTable(tableMetaData, linkBase, columns, columnSort, null, null);
if(metaData) if (metaData)
{ {
if(tableMetaData.exposedJoins) if (tableMetaData.exposedJoins)
{ {
for (let i = 0; i < tableMetaData.exposedJoins.length; i++) for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{ {
const join = tableMetaData.exposedJoins[i]; const join = tableMetaData.exposedJoins[i];
let joinTableName = join.joinTable.name; let joinTableName = join.joinTable.name;
if(metaData.tables.has(joinTableName) && metaData.tables.get(joinTableName).readPermission) if (metaData.tables.has(joinTableName) && metaData.tables.get(joinTableName).readPermission)
{ {
let joinLinkBase = null; let joinLinkBase = null;
joinLinkBase = metaData.getTablePath(join.joinTable); joinLinkBase = metaData.getTablePath(join.joinTable);
if(joinLinkBase) if (joinLinkBase)
{ {
joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/"; joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/";
} }
if(join?.joinTable?.fields?.values()) if (join?.joinTable?.fields?.values())
{ {
this.addColumnsForTable(join.joinTable, joinLinkBase, columns, columnSort, joinTableName + ".", join.label + ": "); this.addColumnsForTable(join.joinTable, joinLinkBase, columns, columnSort, joinTableName + ".", join.label + ": ");
} }
@ -172,7 +172,7 @@ export default class DataGridUtils
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// this sorted by sections - e.g., manual sorting by the meta-data... // // this sorted by sections - e.g., manual sorting by the meta-data... //
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
if(columnSort === "bySection") if (columnSort === "bySection")
{ {
for (let i = 0; i < tableMetaData.sections.length; i++) for (let i = 0; i < tableMetaData.sections.length; i++)
{ {
@ -193,19 +193,23 @@ export default class DataGridUtils
/////////////////////////// ///////////////////////////
// sort by labels... mmm // // sort by labels... mmm //
/////////////////////////// ///////////////////////////
sortedKeys.push(...tableMetaData.fields.keys()) sortedKeys.push(...tableMetaData.fields.keys());
sortedKeys.sort((a: string, b: string): number => sortedKeys.sort((a: string, b: string): number =>
{ {
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label)) return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label));
}) });
} }
sortedKeys.forEach((key) => sortedKeys.forEach((key) =>
{ {
const field = tableMetaData.fields.get(key); const field = tableMetaData.fields.get(key);
if(field.isHeavy) if (!field)
{ {
if(field.type == QFieldType.BLOB) return;
}
if (field.isHeavy)
{
if (field.type == QFieldType.BLOB)
{ {
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// assume we DO want heavy blobs - as download links. // // assume we DO want heavy blobs - as download links. //
@ -222,7 +226,7 @@ export default class DataGridUtils
const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix); const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix);
if(key === tableMetaData.primaryKeyField && linkBase && namePrefix == null) if (key === tableMetaData.primaryKeyField && linkBase && namePrefix == null)
{ {
columns.splice(0, 0, column); columns.splice(0, 0, column);
} }
@ -291,9 +295,9 @@ export default class DataGridUtils
(cellValues.value) (cellValues.value)
); );
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"] const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here? const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
if(showHelp) if (showHelp)
{ {
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />; const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
column.renderHeader = (params: GridColumnHeaderParams) => ( column.renderHeader = (params: GridColumnHeaderParams) => (
@ -306,7 +310,7 @@ export default class DataGridUtils
} }
return (column); return (column);
} };
/******************************************************************************* /*******************************************************************************
@ -335,7 +339,7 @@ export default class DataGridUtils
} }
} }
if(field.possibleValueSourceName) if (field.possibleValueSourceName)
{ {
return (200); return (200);
} }
@ -360,6 +364,6 @@ export default class DataGridUtils
} }
return (200); return (200);
} };
} }

View File

@ -19,7 +19,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Client from "qqq/utils/qqq/Client"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
/******************************************************************************* /*******************************************************************************
** Utility functions for basic html/webpage/browser things. ** Utility functions for basic html/webpage/browser things.
@ -68,10 +69,16 @@ export default class HtmlUtils
** it was originally built like this when we had to submit full access token to backend... ** it was originally built like this when we had to submit full access token to backend...
** **
*******************************************************************************/ *******************************************************************************/
static downloadUrlViaIFrame = (url: string, filename: string) => static downloadUrlViaIFrame = (field: QFieldMetaData, url: string, filename: string) =>
{ {
if(url.startsWith("data:")) if (url.startsWith("data:") || url.startsWith("http"))
{ {
if (url.startsWith("http"))
{
const separator = url.includes("?") ? "&" : "?";
url += encodeURIComponent(`${separator}response-content-disposition=attachment; ${filename}`);
}
const link = document.createElement("a"); const link = document.createElement("a");
link.download = filename; link.download = filename;
link.href = url; link.href = url;
@ -93,8 +100,14 @@ export default class HtmlUtils
// todo - onload event handler to let us know when done? // todo - onload event handler to let us know when done?
document.body.appendChild(iframe); document.body.appendChild(iframe);
var method = "get";
if (QFieldType.BLOB == field.type)
{
method = "post";
}
const form = document.createElement("form"); const form = document.createElement("form");
form.setAttribute("method", "post"); form.setAttribute("method", method);
form.setAttribute("action", url); form.setAttribute("action", url);
form.setAttribute("target", "downloadIframe"); form.setAttribute("target", "downloadIframe");
iframe.appendChild(form); iframe.appendChild(form);
@ -117,7 +130,7 @@ export default class HtmlUtils
*******************************************************************************/ *******************************************************************************/
static openInNewWindow = (url: string, filename: string) => static openInNewWindow = (url: string, filename: string) =>
{ {
if(url.startsWith("data:")) if (url.startsWith("data:"))
{ {
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
@ -153,4 +166,4 @@ export default class HtmlUtils
}; };
} }

View File

@ -133,7 +133,7 @@ class FilterUtils
} }
else else
{ {
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values); values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter");
} }
} }

View File

@ -28,18 +28,17 @@ import "datejs"; // https://github.com/datejs/Datejs
import {Chip, ClickAwayListener, Icon} from "@mui/material"; import {Chip, ClickAwayListener, Icon} 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";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import {makeStyles} from "@mui/styles";
import parse from "html-react-parser"; import parse from "html-react-parser";
import React, {Fragment, useReducer, useState} from "react";
import AceEditor from "react-ace";
import {Link} from "react-router-dom";
import HtmlUtils from "qqq/utils/HtmlUtils"; 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/ace";
import "ace-builds/src-noconflict/mode-sql"; import "ace-builds/src-noconflict/mode-sql";
import React, {Fragment, useReducer, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-velocity"; import "ace-builds/src-noconflict/mode-velocity";
import {Link} from "react-router-dom";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Values ** Utility class for working with QQQ Values
@ -198,7 +197,7 @@ class ValueUtils
); );
} }
if (field.type == QFieldType.BLOB) if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
{ {
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />); return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
} }
@ -219,7 +218,7 @@ class ValueUtils
if (field.type === QFieldType.DATE_TIME) if (field.type === QFieldType.DATE_TIME)
{ {
if(displayValue && displayValue != rawValue) if (displayValue && displayValue != rawValue)
{ {
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// if the date-time actually has a displayValue set, and it isn't just the // // if the date-time actually has a displayValue set, and it isn't just the //
@ -276,7 +275,7 @@ class ValueUtils
// to millis) back to it // // to millis) back to it //
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
date = new Date(date); date = new Date(date);
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000) date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
} }
// @ts-ignore // @ts-ignore
return (`${date.toString("yyyy-MM-dd")}`); return (`${date.toString("yyyy-MM-dd")}`);
@ -474,7 +473,7 @@ class ValueUtils
*******************************************************************************/ *******************************************************************************/
public static cleanForCsv(param: any): string public static cleanForCsv(param: any): string
{ {
if(param === undefined || param === null) if (param === undefined || param === null)
{ {
return (""); return ("");
} }
@ -499,7 +498,7 @@ class ValueUtils
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// little private component here, for rendering an AceEditor with some buttons/controls/state // // little private component here, for rendering an AceEditor with some buttons/controls/state //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
function CodeViewer({name, mode, code}: {name: string; mode: string; code: string;}): JSX.Element function CodeViewer({name, mode, code}: { name: string; mode: string; code: string; }): JSX.Element
{ {
const [activeCode, setActiveCode] = useState(code); const [activeCode, setActiveCode] = useState(code);
const [isFormatted, setIsFormatted] = useState(false); const [isFormatted, setIsFormatted] = useState(false);
@ -596,7 +595,7 @@ function CodeViewer({name, mode, code}: {name: string; mode: string; code: strin
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// little private component here, for rendering "secret-ish" values, that you can click to reveal or copy // // little private component here, for rendering "secret-ish" values, that you can click to reveal or copy //
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
function RevealComponent({fieldName, value, usage}: {fieldName: string, value: string, usage: string;}): JSX.Element function RevealComponent({fieldName, value, usage}: { fieldName: string, value: string, usage: string; }): JSX.Element
{ {
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>); const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -653,7 +652,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s
</Tooltip> </Tooltip>
</ClickAwayListener> </ClickAwayListener>
</Box> </Box>
):( ) : (
<Box display="inline"><Icon onClick={(e) => handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off</Icon>{displayValue}</Box> <Box display="inline"><Icon onClick={(e) => handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off</Icon>{displayValue}</Box>
) )
) )
@ -680,7 +679,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, filename); HtmlUtils.downloadUrlViaIFrame(field, url, filename);
}; };
const open = (event: React.MouseEvent<HTMLSpanElement>) => const open = (event: React.MouseEvent<HTMLSpanElement>) =>
@ -689,7 +688,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
HtmlUtils.openInNewWindow(url, filename); HtmlUtils.openInNewWindow(url, filename);
}; };
if(!filename || !url) if (!filename || !url)
{ {
return (<React.Fragment />); return (<React.Fragment />);
} }
@ -704,10 +703,22 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
usage == "view" && filename usage == "view" && filename
} }
<Tooltip placement={tooltipPlacement} title="Open file"> <Tooltip placement={tooltipPlacement} title="Open file">
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon> {
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon>
) : (
<a style={{color: "inherit"}} rel="noopener noreferrer" href={url} target="_blank"><Icon className={"blobIcon"} fontSize="small">open_in_new</Icon></a>
)
}
</Tooltip> </Tooltip>
<Tooltip placement={tooltipPlacement} title="Download file"> <Tooltip placement={tooltipPlacement} title="Download file">
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon> {
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon>
) : (
<a style={{color: "inherit"}} href={url} download="test.pdf"><Icon className={"blobIcon"} fontSize="small">save_alt</Icon></a>
)
}
</Tooltip> </Tooltip>
{ {
usage == "query" && filename usage == "query" && filename
@ -717,5 +728,4 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
} }
export default ValueUtils; export default ValueUtils;