CE-1727 - Add support for ad-hoc, composite/block widgets; support processFlow (e.g., non-linear); some mobile-style adjustments

This commit is contained in:
2024-09-20 11:07:22 -05:00
parent aee4becda5
commit 98a02cda96

View File

@ -29,6 +29,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
@ -36,12 +37,14 @@ import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material"; import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Step from "@mui/material/Step"; import Step from "@mui/material/Step";
import StepLabel from "@mui/material/StepLabel"; import StepLabel from "@mui/material/StepLabel";
import Stepper from "@mui/material/Stepper"; import Stepper from "@mui/material/Stepper";
import {Theme} from "@mui/material/styles";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
import FormData from "form-data"; import FormData from "form-data";
@ -60,8 +63,11 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
import ValidationReview from "qqq/components/processes/ValidationReview"; import ValidationReview from "qqq/components/processes/ValidationReview";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
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";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
@ -89,14 +95,18 @@ const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; const BACKOFF_AMOUNT = 1.5;
//////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// define a function that we can make referenes to, which we'll overwrite // // define some functions that we can make referene to, which we'll overwrite //
// with formik's setFieldValue function, once we're inside formik. // // with functions from formik, once we're inside formik. //
//////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{ {
}; };
let formikSetTouched = ({}: any, touched: boolean): void =>
{
};
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {}; const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
@ -157,8 +167,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean); const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
const onLastStep = activeStepIndex === steps.length - 2; /////////////////////////////////////////////////////////////////////////////////////
const noMoreSteps = activeStepIndex === steps.length - 1; // determine if we're on the last-step or not (e.g., to decide "Submit" vs "Next"( //
/////////////////////////////////////////////////////////////////////////////////////
let onLastStep = false;
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 2)
{
onLastStep = true;
}
////////////////////////////////////////////
// determine if any 'next' button appears //
////////////////////////////////////////////
let noMoreSteps = false;
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 1)
{
noMoreSteps = true;
}
if(processValues["noMoreSteps"])
{
//////////////////////////////////////////////////////////////////
// this, to allow a non-linear process to request this behavior //
//////////////////////////////////////////////////////////////////
noMoreSteps = true;
}
//////////////// ////////////////
// form state // // form state //
@ -326,6 +358,69 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return renderedWidget; return renderedWidget;
} }
/***************************************************************************
** callback used by widget blocks, e.g., for input-text-enter-on-submit,
** and action buttons.
***************************************************************************/
function blockWidgetActionCallback(blockData: BlockData, eventValues?: { [name: string]: any }): boolean
{
console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`);
///////////////////////////////////////////////////////////////////////////////
// if the eventValues included an actionCode - validate it before proceeding //
///////////////////////////////////////////////////////////////////////////////
if (eventValues && eventValues.actionCode && !ProcessWidgetBlockUtils.isActionCodeValid(eventValues.actionCode, activeStep, processValues))
{
setFormError("Unrecognized action code: " + eventValues.actionCode);
if (eventValues["_fieldToClearIfError"])
{
/////////////////////////////////////////////////////////////////////////////
// if the eventValues included a _fieldToClearIfError, well, then do that. //
/////////////////////////////////////////////////////////////////////////////
formikSetFieldValueFunction(eventValues["_fieldToClearIfError"], "", false);
}
return (false);
}
//////////////////
// ok - submit! //
//////////////////
handleSubmit(eventValues);
return (true);
}
/***************************************************************************
** in a memoized-fashion (YUNO useMemo?), render a component that is an
** adHoc widget (e.g., composite)
***************************************************************************/
function renderAdHocWidget(componentValues: any, componentIndex: number)
{
const key = activeStep.name + "-" + stepInstanceCounter + "-" + componentIndex;
if (renderedWidgets[key])
{
return renderedWidgets[key];
}
const widgetMetaData = new QWidgetMetaData({name: "adHoc"});
const compositeWidgetData = JSON.parse(JSON.stringify(componentValues)) as CompositeData;
compositeWidgetData.styleOverrides = {py: "0.5rem", display: "flex", flexDirection: "column", gap: "0.5rem"};
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(compositeWidgetData, processValues);
renderedWidgets[key] = <Box key={key} pt={2}>
<CompositeWidget widgetMetaData={widgetMetaData} data={compositeWidgetData} actionCallback={blockWidgetActionCallback} />
</Box>;
setRenderedWidgets(renderedWidgets);
return (renderedWidgets[key]);
}
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// generate the main form body content for a step // // generate the main form body content for a step //
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
@ -384,7 +479,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (qJobRunning || step === null) if (qJobRunning || step === null)
{ {
return ( return (
<Grid m={3} mt={9} container> <Grid m={3} mt={9} container maxWidth="calc(100% - 3rem)">
<Grid item xs={0} lg={3} /> <Grid item xs={0} lg={3} />
<Grid item xs={12} lg={6}> <Grid item xs={12} lg={6}>
<Card> <Card>
@ -765,8 +860,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
} }
{ {
component.type === QComponentType.WIDGET && ( component.type === QComponentType.WIDGET && (
<>
{
///////////////////////////////////////////////////
// if a widget name is given, render that widget //
///////////////////////////////////////////////////
component.values?.widgetName && component.values?.widgetName &&
renderWidget(component.values?.widgetName) renderWidget(component.values?.widgetName)
}
{
/////////////////////////////////////////////////////////
// if the widget is marked as adHoc, render it as such //
/////////////////////////////////////////////////////////
component.values?.isAdHocWidget &&
renderAdHocWidget(component.values, index)
}
{
////////////////////////////////////////////////
// if neither of those, then programmer error //
////////////////////////////////////////////////
!(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>
}
</>
) )
} }
</div> </div>
@ -866,6 +982,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setActiveStepIndex(newIndex); setActiveStepIndex(newIndex);
setOverrideOnLastStep(null); setOverrideOnLastStep(null);
////////////////////////////////////////////////////////////////////////////////////////////////////
// reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state //
////////////////////////////////////////////////////////////////////////////////////////////////////
formikSetTouched({}, false);
if (steps) if (steps)
{ {
const activeStep = steps[newIndex]; const activeStep = steps[newIndex];
@ -911,6 +1032,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null); addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
} }
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
{
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, (fieldMetaData) =>
{
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
const validation = DynamicFormUtils.getValidationForField(fieldMetaData);
addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation)
});
}
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// if this step has form fields, set up the form // // if this step has form fields, set up the form //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
@ -1099,7 +1230,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (updatedFields) if (updatedFields)
{ {
updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field)); updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field));
setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap) setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap);
} }
for (let step of steps) for (let step of steps)
@ -1210,6 +1341,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setNewStep(nextStepName); setNewStep(nextStepName);
setStepInstanceCounter(1 + stepInstanceCounter); setStepInstanceCounter(1 + stepInstanceCounter);
setProcessValues(newValues); setProcessValues(newValues);
setRenderedWidgets({});
setQJobRunning(null); setQJobRunning(null);
if (formikSetFieldValueFunction) if (formikSetFieldValueFunction)
@ -1465,8 +1597,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// 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 //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any, actions: any) => const handleSubmit = async (values: any) =>
{ {
setFormError(null); setFormError(null);
@ -1556,14 +1689,40 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}; };
const mainCardStyles: any = {};
const formStyles: any = {}; const formStyles: any = {};
if(isWidget)
{
formStyles.display = "flex";
formStyles.flexGrow = 1;
}
/***************************************************************************
**
***************************************************************************/
function makeMainCardStyles(theme: Theme)
{
const mainCardStyles: any = {};
if(!isWidget && !isModal)
{
////////////////////////////////////////////////////////////////
// remove margin around card for non-widget, non-modal, small //
////////////////////////////////////////////////////////////////
mainCardStyles[theme.breakpoints.down("sm")] = {
marginLeft: "-1.5rem",
marginRight: "-1.5rem",
borderRadius: "0"
};
}
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`; mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget) if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
{ {
mainCardStyles.background = "#FFFFFF"; mainCardStyles.background = "#FFFFFF";
mainCardStyles.boxShadow = "none"; mainCardStyles.boxShadow = "none";
} }
if (isWidget) if (isWidget)
{ {
mainCardStyles.background = "none"; mainCardStyles.background = "none";
@ -1573,8 +1732,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
mainCardStyles.alignItems = "stretch"; mainCardStyles.alignItems = "stretch";
mainCardStyles.flexGrow = 1; mainCardStyles.flexGrow = 1;
mainCardStyles.display = "flex"; mainCardStyles.display = "flex";
formStyles.display = "flex"; }
formStyles.flexGrow = 1;
return mainCardStyles
} }
let nextButtonLabel = "Next"; let nextButtonLabel = "Next";
@ -1602,20 +1762,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ {({
values, errors, touched, isSubmitting, setFieldValue, values, errors, touched, isSubmitting, setFieldValue, setTouched
}) => }) =>
{ {
/////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// once we're in the formik form, use its setFieldValue function // // once we're in the formik form, capture some of its functions //
// over top of the default one we created globally // // over top of the default ones we created globally //
/////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
formikSetFieldValueFunction = setFieldValue; formikSetFieldValueFunction = setFieldValue;
formikSetTouched = setTouched;
return ( return (
<Form style={formStyles} id={formId} autoComplete="off"> <Form style={formStyles} id={formId} autoComplete="off">
<Card sx={mainCardStyles}> <Card sx={makeMainCardStyles}>
{ {
!isWidget && ( !isWidget && processMetaData?.stepFlow == "LINEAR" && (
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}> <Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
<Stepper activeStep={activeStepIndex} alternativeLabel> <Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => ( {steps.map((step) => (
@ -1650,7 +1811,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{/******************************** {/********************************
** back &| next/submit buttons ** ** back &| next/submit buttons **
********************************/} ********************************/}
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}> <Box mt={3} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
{true || activeStepIndex === 0 ? ( {true || activeStepIndex === 0 ? (
<Box /> <Box />
) : ( ) : (
@ -1660,11 +1821,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
<Box /> <Box />
) : ( ) : (
<> <>
{formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError}
</MDTypography>
)}
{ {
noMoreSteps && <QCancelButton noMoreSteps && <QCancelButton
onClickHandler={() => handleCancelClicked(true)} onClickHandler={() => handleCancelClicked(true)}
@ -1700,9 +1856,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const body = ( const body = (
<Box py={3} mb={20} className="processRun"> <Box py={3} mb={20} className="processRun">
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}> <Grid container justifyContent="center" alignItems="center" mt={{xs: 0, md: 6}} sx={{height: "100%"}}>
<Grid item xs={12} lg={10} xl={8}> <Grid item xs={12} lg={10} xl={8}>
{form} {form}
{formError && <Alert severity="error" onClose={() => setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}</Alert>}
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>