diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index 5a409c8..a58e459 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; 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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; 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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; 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 Grid from "@mui/material/Grid"; import Step from "@mui/material/Step"; import StepLabel from "@mui/material/StepLabel"; import Stepper from "@mui/material/Stepper"; +import {Theme} from "@mui/material/styles"; import Typography from "@mui/material/Typography"; import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; import FormData from "form-data"; @@ -60,8 +63,11 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; 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 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 Client from "qqq/utils/qqq/Client"; import TableUtils from "qqq/utils/qqq/TableUtils"; @@ -89,14 +95,18 @@ const INITIAL_RETRY_MILLIS = 1_500; const RETRY_MAX_MILLIS = 12_000; const BACKOFF_AMOUNT = 1.5; -//////////////////////////////////////////////////////////////////////////// -// define a function that we can make referenes to, which we'll overwrite // -// with formik's setFieldValue function, once we're inside formik. // -//////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +// define some functions that we can make referene to, which we'll overwrite // +// with functions from formik, once we're inside formik. // +/////////////////////////////////////////////////////////////////////////////// let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => { }; +let formikSetTouched = ({}: any, touched: boolean): void => +{ +}; + const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {}; 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 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 // @@ -326,6 +358,69 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is 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] = + + ; + + setRenderedWidgets(renderedWidgets); + + return (renderedWidgets[key]); + } + + //////////////////////////////////////////////////// // 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) { return ( - + @@ -765,8 +860,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is } { component.type === QComponentType.WIDGET && ( - component.values?.widgetName && - renderWidget(component.values?.widgetName) + <> + { + /////////////////////////////////////////////////// + // if a widget name is given, render that widget // + /////////////////////////////////////////////////// + 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) && + Error: Component is marked as WIDGET type, but does not specify a widgetName, nor the isAdHocWidget flag. + } + ) } @@ -866,6 +982,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is setActiveStepIndex(newIndex); setOverrideOnLastStep(null); + //////////////////////////////////////////////////////////////////////////////////////////////////// + // reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state // + //////////////////////////////////////////////////////////////////////////////////////////////////// + formikSetTouched({}, false); + if (steps) { const activeStep = steps[newIndex]; @@ -911,6 +1032,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is 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 // /////////////////////////////////////////////////// @@ -1099,7 +1230,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is if (updatedFields) { updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field)); - setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap) + setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap); } for (let step of steps) @@ -1210,6 +1341,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is setNewStep(nextStepName); setStepInstanceCounter(1 + stepInstanceCounter); setProcessValues(newValues); + setRenderedWidgets({}); setQJobRunning(null); 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. // + // 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); @@ -1556,27 +1689,54 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is }; - const mainCardStyles: any = {}; const formStyles: any = {}; - mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`; - if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget) + if(isWidget) { - mainCardStyles.background = "#FFFFFF"; - mainCardStyles.boxShadow = "none"; - } - if (isWidget) - { - mainCardStyles.background = "none"; - mainCardStyles.boxShadow = "none"; - mainCardStyles.border = "none"; - mainCardStyles.minHeight = ""; - mainCardStyles.alignItems = "stretch"; - mainCardStyles.flexGrow = 1; - mainCardStyles.display = "flex"; 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)`; + if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget) + { + mainCardStyles.background = "#FFFFFF"; + mainCardStyles.boxShadow = "none"; + } + + if (isWidget) + { + mainCardStyles.background = "none"; + mainCardStyles.boxShadow = "none"; + mainCardStyles.border = "none"; + mainCardStyles.minHeight = ""; + mainCardStyles.alignItems = "stretch"; + mainCardStyles.flexGrow = 1; + mainCardStyles.display = "flex"; + } + + return mainCardStyles + } + let nextButtonLabel = "Next"; let nextButtonIcon = "arrow_forward"; if (overrideOnLastStep !== null) @@ -1602,20 +1762,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is onSubmit={handleSubmit} > {({ - values, errors, touched, isSubmitting, setFieldValue, + values, errors, touched, isSubmitting, setFieldValue, setTouched }) => { - /////////////////////////////////////////////////////////////////// - // once we're in the formik form, use its setFieldValue function // - // over top of the default one we created globally // - /////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + // once we're in the formik form, capture some of its functions // + // over top of the default ones we created globally // + ////////////////////////////////////////////////////////////////// formikSetFieldValueFunction = setFieldValue; + formikSetTouched = setTouched; return (
- + { - !isWidget && ( + !isWidget && processMetaData?.stepFlow == "LINEAR" && ( {steps.map((step) => ( @@ -1650,7 +1811,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is {/******************************** ** back &| next/submit buttons ** ********************************/} - + {true || activeStepIndex === 0 ? ( ) : ( @@ -1660,11 +1821,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is ) : ( <> - {formError && ( - - {formError} - - )} { noMoreSteps && handleCancelClicked(true)} @@ -1700,9 +1856,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is const body = ( - + {form} + {formError && setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}}