Update to show user-facing process errors; add support for fields linked to records in other tables (adornment);

This commit is contained in:
2022-09-27 19:06:32 -05:00
parent 1a0d8d9f3b
commit 3ce6a8858e
5 changed files with 119 additions and 19 deletions

View File

@ -13,7 +13,7 @@
"@fullcalendar/interaction": "5.10.0", "@fullcalendar/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0", "@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "5.10.0", "@fullcalendar/timegrid": "5.10.0",
"@kingsrook/qqq-frontend-core": "1.0.22", "@kingsrook/qqq-frontend-core": "1.0.23",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",

View File

@ -623,6 +623,7 @@ function EntityList({table}: Props): JSX.Element
setTableState(tableName); setTableState(tableName);
setFiltersMenu(null); setFiltersMenu(null);
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData;
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));

View File

@ -35,7 +35,7 @@ import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import React, {useReducer, useState} from "react"; import React, {useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
import DashboardWidgets from "qqq/components/DashboardWidgets"; import DashboardWidgets from "qqq/components/DashboardWidgets";
import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons"; import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons";
@ -45,9 +45,9 @@ import MDAlert from "qqq/components/Temporary/MDAlert";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QProcessUtils from "qqq/utils/QProcessUtils";
import QTableUtils from "qqq/utils/QTableUtils"; import QTableUtils from "qqq/utils/QTableUtils";
import QValueUtils from "qqq/utils/QValueUtils"; import QValueUtils from "qqq/utils/QValueUtils";
import QProcessUtils from "../../../../utils/QProcessUtils";
const qController = QClient.getInstance(); const qController = QClient.getInstance();
@ -84,6 +84,11 @@ function ViewContents({id, table}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
useEffect(() =>
{
setAsyncLoadInited(false);
}, [location]);
if (!asyncLoadInited) if (!asyncLoadInited)
{ {
setAsyncLoadInited(true); setAsyncLoadInited(true);
@ -100,6 +105,7 @@ function ViewContents({id, table}: Props): JSX.Element
// load top-level meta-data (e.g., to find processes for table) // // load top-level meta-data (e.g., to find processes for table) //
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData;
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
///////////////////// /////////////////////

View File

@ -93,7 +93,8 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [qInstance, setQInstance] = useState(null as QInstance); const [qInstance, setQInstance] = useState(null as QInstance);
const [processValues, setProcessValues] = useState({} as any); const [processValues, setProcessValues] = useState({} as any);
const [processError, setProcessError] = useState(null as string); const [processError, _setProcessError] = useState(null as string);
const [isUserFacingError, setIsUserFacingError] = useState(false);
const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false); const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false);
const [lastProcessResponse, setLastProcessResponse] = useState( const [lastProcessResponse, setLastProcessResponse] = useState(
null as QJobStarted | QJobComplete | QJobError | QJobRunning, null as QJobStarted | QJobComplete | QJobError | QJobRunning,
@ -101,6 +102,15 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
const [showErrorDetail, setShowErrorDetail] = useState(false); const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = useState(false); const [showFullHelpText, setShowFullHelpText] = useState(false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for setting the processError state - call this function, which will also set the isUserFacingError state //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
const setProcessError = (message: string, isUserFacing: boolean = false) =>
{
_setProcessError(message);
setIsUserFacingError(isUserFacing);
};
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the validation screen - it can change whether next is actually the final step or not... so, use this state field to track that. // // the validation screen - it can change whether next is actually the final step or not... so, use this state field to track that. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -216,17 +226,25 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
An error occurred while running the process: An error occurred while running the process:
{" "} {" "}
{process.label} {process.label}
<MDBox mt={3} display="flex" justifyContent="center"> {
<MDBox display="flex" flexDirection="column" alignItems="center"> isUserFacingError ? (
<Button onClick={toggleShowErrorDetail} startIcon={<Icon>{showErrorDetail ? "expand_less" : "expand_more"}</Icon>}> <MDBox mt={1}>
{showErrorDetail ? "Hide " : "Show "} <b>{processError}</b>
detailed error message
</Button>
<MDBox mt={1} style={{display: showErrorDetail ? "block" : "none"}}>
{processError}
</MDBox> </MDBox>
</MDBox> ) : (
</MDBox> <MDBox mt={3} display="flex" justifyContent="center">
<MDBox display="flex" flexDirection="column" alignItems="center">
<Button onClick={toggleShowErrorDetail} startIcon={<Icon>{showErrorDetail ? "expand_less" : "expand_more"}</Icon>}>
{showErrorDetail ? "Hide " : "Show "}
detailed error message
</Button>
<MDBox mt={1} style={{display: showErrorDetail ? "block" : "none"}}>
{processError}
</MDBox>
</MDBox>
</MDBox>
)
}
</MDTypography> </MDTypography>
</> </>
); );
@ -723,9 +741,16 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
else if (lastProcessResponse instanceof QJobError) else if (lastProcessResponse instanceof QJobError)
{ {
const qJobError = lastProcessResponse as QJobError; const qJobError = lastProcessResponse as QJobError;
console.log(`Got an error from the backend... ${qJobError.error}`); console.log(`Got an error from the backend... ${qJobError.error} : ${qJobError.userFacingError}`);
setJobUUID(null); setJobUUID(null);
setProcessError(qJobError.error); if(qJobError.userFacingError)
{
setProcessError(qJobError.userFacingError, true);
}
else
{
setProcessError(qJobError.error);
}
setQJobRunning(null); setQJobRunning(null);
} }
} }
@ -815,6 +840,7 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
try try
{ {
const qInstance = await QClient.getInstance().loadMetaData(); const qInstance = await QClient.getInstance().loadMetaData();
QValueUtils.qInstance = qInstance;
setQInstance(qInstance); setQInstance(qInstance);
} }
catch (e) catch (e)

View File

@ -22,10 +22,14 @@
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import "datejs"; import "datejs";
import {Chip, Icon} from "@mui/material"; import {Chip, Icon} from "@mui/material";
import {queryByTestId} from "@testing-library/react";
import React, {Fragment} from "react"; import React, {Fragment} from "react";
import {Link} from "react-router-dom";
import QClient from "qqq/utils/QClient";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Values ** Utility class for working with QQQ Values
@ -33,6 +37,31 @@ import React, {Fragment} from "react";
*******************************************************************************/ *******************************************************************************/
class QValueUtils class QValueUtils
{ {
public static qInstance: QInstance = null;
public static loadingQInstance = false;
private static getQInstance(): QInstance
{
if(QValueUtils.qInstance == null)
{
if(QValueUtils.loadingQInstance)
{
return (null);
}
QValueUtils.loadingQInstance = true;
const qController = QClient.getInstance();
(async () =>
{
QValueUtils.qInstance = await qController.loadMetaData();
})();
return (null);
}
return QValueUtils.qInstance;
}
/******************************************************************************* /*******************************************************************************
** When you have a field, and a record - call this method to get a string or ** When you have a field, and a record - call this method to get a string or
@ -55,10 +84,48 @@ class QValueUtils
if (field.hasAdornment(AdornmentType.LINK)) if (field.hasAdornment(AdornmentType.LINK))
{ {
const adornment = field.getAdornment(AdornmentType.LINK); const adornment = field.getAdornment(AdornmentType.LINK);
return (<a target={adornment.getValue("target") ?? "_self"} href={rawValue} onClick={(e) => let href = rawValue;
const toRecordFromTable = adornment.getValue("toRecordFromTable");
if(toRecordFromTable)
{ {
e.stopPropagation(); if(QValueUtils.getQInstance())
}}>{rawValue}</a>) {
let tablePath = QValueUtils.getQInstance().getTablePathByName(toRecordFromTable);
if(!tablePath)
{
console.log("Couldn't find path for table: " + tablePath);
return ("");
}
if(!tablePath.endsWith("/"))
{
tablePath += "/"
}
href = tablePath + rawValue;
}
else
{
//////////////////////////////////////////////////////////////////////////////////
// if no instance, we can't get the table path, so we can't do a to-record link //
//////////////////////////////////////////////////////////////////////////////////
return (QValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
}
}
if(!href)
{
return ("");
}
if(href.startsWith("http"))
{
return (<a target={adornment.getValue("target") ?? "_self"} href={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</a>)
}
else
{
return (<Link target={adornment.getValue("target") ?? "_self"} to={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</Link>)
}
} }
if (field.hasAdornment(AdornmentType.CHIP)) if (field.hasAdornment(AdornmentType.CHIP))