mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
1067 lines
45 KiB
TypeScript
1067 lines
45 KiB
TypeScript
/*
|
||
* 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 {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||
import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QComponentType";
|
||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||
import {QFrontendComponent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendComponent";
|
||
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
|
||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||
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";
|
||
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||
import {Button, Icon, CircularProgress, TablePagination} from "@mui/material";
|
||
import Box from "@mui/material/Box";
|
||
import Card from "@mui/material/Card";
|
||
import Grid from "@mui/material/Grid";
|
||
import Link from "@mui/material/Link";
|
||
import Step from "@mui/material/Step";
|
||
import StepLabel from "@mui/material/StepLabel";
|
||
import Stepper from "@mui/material/Stepper";
|
||
import Typography from "@mui/material/Typography";
|
||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||
import {GoogleOAuthProvider} from "@react-oauth/google";
|
||
import FormData from "form-data";
|
||
import {Form, Formik} from "formik";
|
||
import React, {useEffect, useState} from "react";
|
||
import {useLocation, useParams, useNavigate} from "react-router-dom";
|
||
import * as Yup from "yup";
|
||
import BaseLayout from "qqq/components/BaseLayout";
|
||
import {QCancelButton, QSubmitButton} from "qqq/components/QButtons";
|
||
import QDynamicForm from "qqq/components/QDynamicForm";
|
||
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
|
||
import MDBox from "qqq/components/Temporary/MDBox";
|
||
import MDButton from "qqq/components/Temporary/MDButton";
|
||
import MDProgress from "qqq/components/Temporary/MDProgress";
|
||
import MDTypography from "qqq/components/Temporary/MDTypography";
|
||
import {QGoogleDriveFolderPickerWrapper} from "qqq/pages/process-run/components/QGoogleDriveFolderPickerWrapper";
|
||
import QValidationReview from "qqq/pages/process-run/components/QValidationReview";
|
||
import QClient from "qqq/utils/QClient";
|
||
import QValueUtils from "qqq/utils/QValueUtils";
|
||
import QProcessSummaryResults from "./components/QProcessSummaryResults";
|
||
|
||
interface Props
|
||
{
|
||
process?: QProcessMetaData;
|
||
defaultProcessValues?: any;
|
||
}
|
||
|
||
const INITIAL_RETRY_MILLIS = 1_500;
|
||
const RETRY_MAX_MILLIS = 12_000;
|
||
const BACKOFF_AMOUNT = 1.5;
|
||
|
||
function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
|
||
{
|
||
const processNameParam = useParams().processName;
|
||
const processName = process === null ? processNameParam : process.name;
|
||
|
||
///////////////////
|
||
// process state //
|
||
///////////////////
|
||
const [processUUID, setProcessUUID] = useState(null as string);
|
||
const [retryMillis, setRetryMillis] = useState(INITIAL_RETRY_MILLIS);
|
||
const [jobUUID, setJobUUID] = useState(null as string);
|
||
const [qJobRunning, setQJobRunning] = useState(null as QJobRunning);
|
||
const [qJobRunningDate, setQJobRunningDate] = useState(null as Date);
|
||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
||
const [newStep, setNewStep] = useState(null);
|
||
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
||
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
||
const [processMetaData, setProcessMetaData] = useState(null);
|
||
const [tableMetaData, setTableMetaData] = useState(null);
|
||
const [qInstance, setQInstance] = useState(null as QInstance);
|
||
const [processValues, setProcessValues] = useState({} as any);
|
||
const [processError, setProcessError] = useState(null as string);
|
||
const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false);
|
||
const [lastProcessResponse, setLastProcessResponse] = useState(
|
||
null as QJobStarted | QJobComplete | QJobError | QJobRunning,
|
||
);
|
||
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// the validation screen - it can change whether next is actually the final step or not... so, use this state field to track that. //
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
||
|
||
const onLastStep = activeStepIndex === steps.length - 2;
|
||
const noMoreSteps = activeStepIndex === steps.length - 1;
|
||
|
||
////////////////
|
||
// form state //
|
||
////////////////
|
||
const [formId, setFormId] = useState("");
|
||
const [formFields, setFormFields] = useState({});
|
||
const [initialValues, setInitialValues] = useState({});
|
||
const [validationScheme, setValidationScheme] = useState(null);
|
||
const [validationFunction, setValidationFunction] = useState(null);
|
||
const [formError, setFormError] = useState(null as string);
|
||
|
||
///////////////////////
|
||
// record list state //
|
||
///////////////////////
|
||
const [needRecords, setNeedRecords] = useState(false);
|
||
const [recordConfig, setRecordConfig] = useState({} as any);
|
||
const [pageNumber, setPageNumber] = useState(0);
|
||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||
const [records, setRecords] = useState([] as QRecord[]);
|
||
|
||
//////////////////////////////
|
||
// state for bulk edit form //
|
||
//////////////////////////////
|
||
const [disabledBulkEditFields, setDisabledBulkEditFields] = useState({} as any);
|
||
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
|
||
const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean =>
|
||
{
|
||
if (step.components)
|
||
{
|
||
for (let i = 0; i < step.components.length; i++)
|
||
{
|
||
if (step.components[i].type === type)
|
||
{
|
||
return (true);
|
||
}
|
||
}
|
||
}
|
||
return (false);
|
||
};
|
||
|
||
// @ts-ignore
|
||
const defaultLabelDisplayedRows = ({from, to, count}) => `${from.toLocaleString()}–${to.toLocaleString()} of ${count !== -1 ? count.toLocaleString() : `more than ${to.toLocaleString()}`}`;
|
||
|
||
// @ts-ignore
|
||
// eslint-disable-next-line react/no-unstable-nested-components
|
||
function CustomPagination()
|
||
{
|
||
return (
|
||
<TablePagination
|
||
component="div"
|
||
count={recordConfig.totalRecords}
|
||
page={pageNumber}
|
||
rowsPerPageOptions={[10, 25, 50]}
|
||
rowsPerPage={rowsPerPage}
|
||
onPageChange={(event, value) => recordConfig.handlePageChange(value)}
|
||
onRowsPerPageChange={(event) => recordConfig.handleRowsPerPageChange(Number(event.target.value))}
|
||
labelDisplayedRows={defaultLabelDisplayedRows}
|
||
/>
|
||
);
|
||
}
|
||
|
||
//////////////////////////////////////////////////////////////
|
||
// event handler for the bulk-edit field-enabled checkboxes //
|
||
//////////////////////////////////////////////////////////////
|
||
const bulkEditSwitchChanged = (name: string, switchValue: boolean) =>
|
||
{
|
||
const newDisabledBulkEditFields = JSON.parse(JSON.stringify(disabledBulkEditFields));
|
||
newDisabledBulkEditFields[name] = !switchValue;
|
||
setDisabledBulkEditFields(newDisabledBulkEditFields);
|
||
};
|
||
|
||
const toggleShowErrorDetail = () =>
|
||
{
|
||
setShowErrorDetail(!showErrorDetail);
|
||
};
|
||
|
||
const toggleShowFullHelpText = () =>
|
||
{
|
||
setShowFullHelpText(!showFullHelpText);
|
||
};
|
||
|
||
////////////////////////////////////////////////////
|
||
// generate the main form body content for a step //
|
||
////////////////////////////////////////////////////
|
||
const getDynamicStepContent = (
|
||
stepIndex: number,
|
||
step: any,
|
||
formData: any,
|
||
processError: string,
|
||
processValues: any,
|
||
recordConfig: any,
|
||
setFieldValue: any,
|
||
): JSX.Element =>
|
||
{
|
||
if (processError)
|
||
{
|
||
return (
|
||
<>
|
||
<MDTypography color="error" variant="h3" component="div">
|
||
Error
|
||
</MDTypography>
|
||
<MDTypography color="body" variant="button">
|
||
An error occurred while running the process:
|
||
{" "}
|
||
{process.label}
|
||
<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>
|
||
</>
|
||
);
|
||
}
|
||
|
||
if (qJobRunning || step === null)
|
||
{
|
||
return (
|
||
<Grid m={3} mt={9} container>
|
||
<Grid item xs={0} lg={3} />
|
||
<Grid item xs={12} lg={6}>
|
||
<Card>
|
||
<MDBox p={3}>
|
||
<MDTypography variant="h5" component="div">
|
||
Working
|
||
</MDTypography>
|
||
<Grid container>
|
||
<Grid item padding={2}>
|
||
<CircularProgress color="info" />
|
||
</Grid>
|
||
<Grid item padding={1}>
|
||
<MDTypography color="body" variant="button">
|
||
{qJobRunning?.message}
|
||
<br />
|
||
{qJobRunning?.current && qJobRunning?.total && (
|
||
<>
|
||
<div>{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}</div>
|
||
<MDBox width="20rem">
|
||
<MDProgress variant="gradient" value={100 * (qJobRunning.current / qJobRunning.total)} color="success" />
|
||
</MDBox>
|
||
</>
|
||
)}
|
||
{
|
||
qJobRunningDate && (<i>{`Updated at ${qJobRunningDate?.toLocaleTimeString()}`}</i>)
|
||
}
|
||
</MDTypography>
|
||
</Grid>
|
||
</Grid>
|
||
</MDBox>
|
||
</Card>
|
||
</Grid>
|
||
</Grid>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<MDTypography variation="h5" component="div" fontWeight="bold">{step?.label}</MDTypography>
|
||
{step.components && (
|
||
step.components.map((component: QFrontendComponent, index: number) => (
|
||
// eslint-disable-next-line react/no-array-index-key
|
||
<div key={index}>
|
||
{
|
||
component.type === QComponentType.HELP_TEXT && (
|
||
component.values.previewText ?
|
||
<>
|
||
<Box mt={1}>
|
||
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}} >
|
||
{showFullHelpText ? "Hide " : "Show "}
|
||
{component.values.previewText}
|
||
</Button>
|
||
</Box>
|
||
<MDBox mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
|
||
<Typography variant="body2" color="info">
|
||
{QValueUtils.breakTextIntoLines(component.values.text)}
|
||
</Typography>
|
||
</MDBox>
|
||
</>
|
||
:
|
||
<MDTypography variant="button" color="info">
|
||
{QValueUtils.breakTextIntoLines(component.values.text)}
|
||
</MDTypography>
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.BULK_EDIT_FORM && (
|
||
<QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.EDIT_FORM && (
|
||
<QDynamicForm formData={formData} />
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||
<div>
|
||
{step.viewFields.map((field: QFieldMetaData) => (
|
||
<MDBox key={field.name} display="flex" py={1} pr={2}>
|
||
<MDTypography variant="button" fontWeight="bold">
|
||
{field.label}
|
||
:
|
||
</MDTypography>
|
||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||
{QValueUtils.getValueForDisplay(field, processValues[field.name])}
|
||
</MDTypography>
|
||
</MDBox>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.DOWNLOAD_FORM && (
|
||
<Grid container display="flex" justifyContent="center">
|
||
<Grid item xs={12} sm={12} xl={8} m={3} p={3} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
|
||
<MDBox mt={-5} mb={1} p={1} sx={{width: "fit-content"}} bgColor="success" borderRadius=".25em" width="initial" color="white">
|
||
<MDBox display="flex" alignItems="center" color="white">
|
||
Download
|
||
</MDBox>
|
||
</MDBox>
|
||
<MDBox display="flex" py={1} pr={2}>
|
||
<MDTypography variant="button" fontWeight="bold">
|
||
<Link target="_blank" download href={`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`} display="flex" alignItems="center">
|
||
<Icon fontSize="large">download_for_offline</Icon>
|
||
<Box pl={1}>
|
||
{processValues.downloadFileName}
|
||
</Box>
|
||
</Link>
|
||
</MDTypography>
|
||
</MDBox>
|
||
</Grid>
|
||
</Grid>
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
|
||
<QValidationReview
|
||
qInstance={qInstance}
|
||
process={processMetaData}
|
||
table={tableMetaData}
|
||
processValues={processValues}
|
||
step={step}
|
||
previewRecords={records}
|
||
formValues={formData.values}
|
||
doFullValidationRadioChangedHandler={(event: any) =>
|
||
{
|
||
const {value} = event.currentTarget;
|
||
|
||
//////////////////////////////////////////////////////////////
|
||
// call the formik function to set the value in this field. //
|
||
//////////////////////////////////////////////////////////////
|
||
setFieldValue("doFullValidation", value);
|
||
|
||
setOverrideOnLastStep(value !== "true");
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
|
||
<QProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
|
||
// todo - make these booleans configurable (values on the component)
|
||
<QGoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} />
|
||
)
|
||
}
|
||
{
|
||
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
|
||
<div>
|
||
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
|
||
{" "}
|
||
<br />
|
||
<MDBox height="100%">
|
||
<DataGridPro
|
||
components={{Pagination: CustomPagination}}
|
||
page={recordConfig.pageNo}
|
||
disableSelectionOnClick
|
||
autoHeight
|
||
rows={recordConfig.rows}
|
||
columns={recordConfig.columns}
|
||
rowBuffer={10}
|
||
rowCount={recordConfig.totalRecords}
|
||
pageSize={recordConfig.rowsPerPage}
|
||
rowsPerPageOptions={[10, 25, 50]}
|
||
onPageSizeChange={recordConfig.handleRowsPerPageChange}
|
||
onPageChange={recordConfig.handlePageChange}
|
||
onRowClick={recordConfig.handleRowClick}
|
||
getRowId={(row) => row.__idForDataGridPro__}
|
||
paginationMode="server"
|
||
pagination
|
||
density="compact"
|
||
loading={recordConfig.loading}
|
||
disableColumnFilter
|
||
/>
|
||
</MDBox>
|
||
</div>
|
||
)
|
||
}
|
||
</div>
|
||
)))}
|
||
</>
|
||
);
|
||
};
|
||
|
||
const handlePageChange = (page: number) =>
|
||
{
|
||
setPageNumber(page);
|
||
};
|
||
|
||
const handleRowsPerPageChange = (size: number) =>
|
||
{
|
||
setRowsPerPage(size);
|
||
};
|
||
|
||
function buildNewRecordConfig()
|
||
{
|
||
const newRecordConfig = {} as any;
|
||
newRecordConfig.pageNo = pageNumber;
|
||
newRecordConfig.rowsPerPage = rowsPerPage;
|
||
newRecordConfig.columns = [] as GridColDef[];
|
||
newRecordConfig.rows = [];
|
||
newRecordConfig.totalRecords = 0;
|
||
newRecordConfig.handleRowsPerPageChange = handleRowsPerPageChange;
|
||
newRecordConfig.handlePageChange = handlePageChange;
|
||
newRecordConfig.handleRowClick = null;
|
||
newRecordConfig.loading = true;
|
||
return (newRecordConfig);
|
||
}
|
||
|
||
const getFullFieldList = (activeStep: QFrontendStepMetaData, processValues: any) =>
|
||
{
|
||
let rs: QFieldMetaData[] = [];
|
||
|
||
if(activeStep && activeStep.formFields)
|
||
{
|
||
for(let i = 0; i<activeStep.formFields.length; i++)
|
||
{
|
||
rs.push(activeStep.formFields[i]);
|
||
}
|
||
}
|
||
|
||
if(processValues.inputFieldList)
|
||
{
|
||
for(let i = 0; i<processValues.inputFieldList.length; i++)
|
||
{
|
||
let inputField = new QFieldMetaData(processValues.inputFieldList[i]);
|
||
rs.push(inputField);
|
||
}
|
||
}
|
||
|
||
return (rs);
|
||
}
|
||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (!processMetaData)
|
||
{
|
||
console.log("No process meta data yet, so returning early");
|
||
return;
|
||
}
|
||
|
||
let newIndex = null;
|
||
if (typeof newStep === "number")
|
||
{
|
||
newIndex = newStep as number;
|
||
}
|
||
else if (typeof newStep === "string")
|
||
{
|
||
for (let i = 0; i < steps.length; i++)
|
||
{
|
||
if (steps[i].name === newStep)
|
||
{
|
||
newIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (newIndex === null)
|
||
{
|
||
setProcessError(`Unknown process step ${newStep}.`);
|
||
return;
|
||
}
|
||
setActiveStepIndex(newIndex);
|
||
setOverrideOnLastStep(null);
|
||
|
||
if (steps)
|
||
{
|
||
const activeStep = steps[newIndex];
|
||
setActiveStep(activeStep);
|
||
setFormId(activeStep.name);
|
||
|
||
let dynamicFormFields: any = {};
|
||
let formValidations: any = {};
|
||
let initialValues: any = {};
|
||
|
||
///////////////////////////////////////////////////
|
||
// if this step has form fields, set up the form //
|
||
///////////////////////////////////////////////////
|
||
if (activeStep.formFields || processValues.inputFieldList)
|
||
{
|
||
let fullFieldList = getFullFieldList(activeStep, processValues);
|
||
const formData = DynamicFormUtils.getFormData(fullFieldList);
|
||
dynamicFormFields = formData.dynamicFormFields;
|
||
formValidations = formData.formValidations;
|
||
|
||
fullFieldList.forEach((field) =>
|
||
{
|
||
initialValues[field.name] = processValues[field.name];
|
||
});
|
||
|
||
////////////////////////////////////////////////////
|
||
// disable all fields if this is a bulk edit form //
|
||
////////////////////////////////////////////////////
|
||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
|
||
{
|
||
const newDisabledBulkEditFields: any = {};
|
||
fullFieldList.forEach((field) =>
|
||
{
|
||
newDisabledBulkEditFields[field.name] = true;
|
||
dynamicFormFields[field.name].isRequired = false;
|
||
formValidations[field.name] = null;
|
||
});
|
||
setDisabledBulkEditFields(newDisabledBulkEditFields);
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// define an inner function here, for adding more fields to the form, if any components have form fields built into them //
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
const addField = (fieldName: string, dynamicFormValue: any, initialValue: any, validation: any) =>
|
||
{
|
||
dynamicFormFields[fieldName] = dynamicFormValue;
|
||
initialValues[fieldName] = initialValue;
|
||
formValidations[fieldName] = validation;
|
||
}
|
||
|
||
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
||
{
|
||
addField("doFullValidation", {type: "radio"}, "true", null);
|
||
setOverrideOnLastStep(false);
|
||
}
|
||
|
||
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
|
||
{
|
||
addField("googleDriveAccessToken", {type: "hidden", omitFromQDynamicForm: true}, null, null);
|
||
addField("googleDriveFolderId", {type: "hidden", omitFromQDynamicForm: true}, null, null);
|
||
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, null, null);
|
||
}
|
||
|
||
if(Object.keys(dynamicFormFields).length > 0)
|
||
{
|
||
///////////////////////////////////////////
|
||
// if there are form fields, set them up //
|
||
///////////////////////////////////////////
|
||
setFormFields(dynamicFormFields);
|
||
setInitialValues(initialValues);
|
||
setValidationScheme(Yup.object().shape(formValidations));
|
||
setValidationFunction(null);
|
||
}
|
||
else
|
||
{
|
||
/////////////////////////////////////////////////////////////////////////
|
||
// if there are no form fields, set a null validationScheme (Yup), and //
|
||
// instead use a validation function that always says true. //
|
||
/////////////////////////////////////////////////////////////////////////
|
||
setValidationScheme(null);
|
||
setValidationFunction(() => true);
|
||
}
|
||
}
|
||
}, [newStep]);
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||
// if there are records to load: build a record config, and set the needRecords state flag //
|
||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (activeStep && activeStep.recordListFields)
|
||
{
|
||
const newRecordConfig = buildNewRecordConfig();
|
||
activeStep.recordListFields.forEach((field) =>
|
||
{
|
||
newRecordConfig.columns.push({
|
||
field: field.name, headerName: field.label, width: 200, sortable: false,
|
||
});
|
||
});
|
||
setRecordConfig(newRecordConfig);
|
||
setNeedRecords(true);
|
||
}
|
||
}, [activeStep, rowsPerPage, pageNumber]);
|
||
|
||
/////////////////////////////////////////////////////
|
||
// handle a bulk-edit enabled-switch being checked //
|
||
/////////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (activeStep && (activeStep.formFields || processValues.inputFieldList))
|
||
{
|
||
let fullFieldList = getFullFieldList(activeStep, processValues);
|
||
const newDynamicFormFields: any = {};
|
||
const newFormValidations: any = {};
|
||
fullFieldList.forEach((field) =>
|
||
{
|
||
const fieldName = field.name;
|
||
const isDisabled = disabledBulkEditFields[fieldName];
|
||
|
||
newDynamicFormFields[field.name] = DynamicFormUtils.getDynamicField(field);
|
||
newFormValidations[field.name] = DynamicFormUtils.getValidationForField(field);
|
||
|
||
if (isDisabled)
|
||
{
|
||
newDynamicFormFields[field.name].isRequired = false;
|
||
newFormValidations[field.name] = null;
|
||
}
|
||
});
|
||
|
||
setFormFields(newDynamicFormFields);
|
||
setValidationScheme(Yup.object().shape(newFormValidations));
|
||
}
|
||
}, [disabledBulkEditFields]);
|
||
|
||
////////////////////////////////////////////////
|
||
// when we need to load records, do so, async //
|
||
////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (needRecords)
|
||
{
|
||
setNeedRecords(false);
|
||
(async () =>
|
||
{
|
||
const response = await QClient.getInstance().processRecords(
|
||
processName,
|
||
processUUID,
|
||
recordConfig.rowsPerPage * recordConfig.pageNo,
|
||
recordConfig.rowsPerPage,
|
||
);
|
||
|
||
const {records} = response;
|
||
setRecords(records);
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
// re-construct the recordConfig object, so the setState call triggers a new rendering //
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
const newRecordConfig = buildNewRecordConfig();
|
||
newRecordConfig.loading = false;
|
||
newRecordConfig.columns = recordConfig.columns;
|
||
newRecordConfig.rows = [];
|
||
let rowId = 0;
|
||
records.forEach((record) =>
|
||
{
|
||
const row = Object.fromEntries(record.values.entries());
|
||
row.__idForDataGridPro__ = ++rowId;
|
||
newRecordConfig.rows.push(row);
|
||
});
|
||
|
||
newRecordConfig.totalRecords = response.totalRecords;
|
||
setRecordConfig(newRecordConfig);
|
||
})();
|
||
}
|
||
}, [needRecords]);
|
||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (lastProcessResponse)
|
||
{
|
||
setLastProcessResponse(null);
|
||
setRetryMillis(INITIAL_RETRY_MILLIS);
|
||
|
||
if (lastProcessResponse instanceof QJobComplete)
|
||
{
|
||
const qJobComplete = lastProcessResponse as QJobComplete;
|
||
setJobUUID(null);
|
||
setNewStep(qJobComplete.nextStep);
|
||
setProcessValues(qJobComplete.values);
|
||
setQJobRunning(null);
|
||
|
||
if (activeStep && activeStep.recordListFields)
|
||
{
|
||
setNeedRecords(true);
|
||
}
|
||
}
|
||
else if (lastProcessResponse instanceof QJobStarted)
|
||
{
|
||
const qJobStarted = lastProcessResponse as QJobStarted;
|
||
setJobUUID(qJobStarted.jobUUID);
|
||
setNeedToCheckJobStatus(true);
|
||
}
|
||
else if (lastProcessResponse instanceof QJobRunning)
|
||
{
|
||
const qJobRunning = lastProcessResponse as QJobRunning;
|
||
setQJobRunning(qJobRunning);
|
||
setQJobRunningDate(new Date());
|
||
setNeedToCheckJobStatus(true);
|
||
}
|
||
else if (lastProcessResponse instanceof QJobError)
|
||
{
|
||
const qJobError = lastProcessResponse as QJobError;
|
||
console.log(`Got an error from the backend... ${qJobError.error}`);
|
||
setJobUUID(null);
|
||
setProcessError(qJobError.error);
|
||
setQJobRunning(null);
|
||
}
|
||
}
|
||
}, [lastProcessResponse]);
|
||
|
||
/////////////////////////////////////////////////////////////////////////
|
||
// while a backend async job is running, periodically check its status //
|
||
/////////////////////////////////////////////////////////////////////////
|
||
useEffect(() =>
|
||
{
|
||
if (needToCheckJobStatus)
|
||
{
|
||
setNeedToCheckJobStatus(false);
|
||
if (!processUUID || !jobUUID)
|
||
{
|
||
console.log(`Missing processUUID[${processUUID}] or jobUUID[${jobUUID}], so returning without checking job status`);
|
||
return;
|
||
}
|
||
|
||
(async () =>
|
||
{
|
||
setTimeout(async () =>
|
||
{
|
||
try
|
||
{
|
||
const processResponse = await QClient.getInstance().processJobStatus(
|
||
processName,
|
||
processUUID,
|
||
jobUUID,
|
||
);
|
||
setLastProcessResponse(processResponse);
|
||
}
|
||
catch (e)
|
||
{
|
||
if (e instanceof QException)
|
||
{
|
||
const qException = e as QException;
|
||
const status = Number(qException.status);
|
||
if (status !== undefined && !Number.isNaN(status) && status >= 500 && status <= 600)
|
||
{
|
||
if (retryMillis < RETRY_MAX_MILLIS)
|
||
{
|
||
console.log(`500 error, attempting to retry in ${retryMillis + retryMillis} millis`);
|
||
setRetryMillis(retryMillis * BACKOFF_AMOUNT);
|
||
setNeedToCheckJobStatus(true);
|
||
return;
|
||
}
|
||
|
||
console.log(`Retry millis [${retryMillis}] is greater or equal to the max retry limit [${RETRY_MAX_MILLIS}], giving up...`);
|
||
setProcessError("Could not connect to server");
|
||
}
|
||
}
|
||
|
||
throw (e);
|
||
}
|
||
}, retryMillis);
|
||
})();
|
||
}
|
||
}, [needToCheckJobStatus, retryMillis]);
|
||
|
||
//////////////////////////////////////////////////////////////////////////////////////////
|
||
// do the initial load of data for the process - that is, meta data, plus the init step //
|
||
//////////////////////////////////////////////////////////////////////////////////////////
|
||
if (needInitialLoad)
|
||
{
|
||
setNeedInitialLoad(false);
|
||
(async () =>
|
||
{
|
||
const urlSearchParams = new URLSearchParams(location.search);
|
||
let queryStringPairsForInit = [];
|
||
if (urlSearchParams.get("recordIds"))
|
||
{
|
||
queryStringPairsForInit.push("recordsParam=recordIds");
|
||
queryStringPairsForInit.push(`recordIds=${urlSearchParams.get("recordIds")}`);
|
||
}
|
||
else if (urlSearchParams.get("filterJSON"))
|
||
{
|
||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||
queryStringPairsForInit.push(`filterJSON=${urlSearchParams.get("filterJSON")}`);
|
||
}
|
||
// todo once saved filters exist
|
||
//else if(urlSearchParams.get("filterId")) {
|
||
// queryStringPairsForInit.push("recordsParam=filterId");
|
||
// queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`);
|
||
// }
|
||
|
||
try
|
||
{
|
||
const qInstance = await QClient.getInstance().loadMetaData();
|
||
setQInstance(qInstance);
|
||
}
|
||
catch (e)
|
||
{
|
||
setProcessError("Error loading process definition.");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
const processMetaData = await QClient.getInstance().loadProcessMetaData(processName);
|
||
setProcessMetaData(processMetaData);
|
||
setSteps(processMetaData.frontendSteps);
|
||
if (processMetaData.tableName)
|
||
{
|
||
try
|
||
{
|
||
const tableMetaData = await QClient.getInstance().loadTableMetaData(processMetaData.tableName);
|
||
setTableMetaData(tableMetaData);
|
||
}
|
||
catch (e)
|
||
{
|
||
setProcessError("Error loading process's table definition.");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch (e)
|
||
{
|
||
setProcessError("Error loading process definition.");
|
||
return;
|
||
}
|
||
|
||
if(defaultProcessValues)
|
||
{
|
||
for(let key in defaultProcessValues)
|
||
{
|
||
queryStringPairsForInit.push(`${key}=${encodeURIComponent(defaultProcessValues[key])}`);
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
const processResponse = await QClient.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
|
||
setProcessUUID(processResponse.processUUID);
|
||
setLastProcessResponse(processResponse);
|
||
}
|
||
catch (e)
|
||
{
|
||
setProcessError("Error initializing process.");
|
||
}
|
||
})();
|
||
}
|
||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
// handle the back button - todo - not really done at all //
|
||
// e.g., qqq needs to say when back is or isn't allowed, and we need to hit the backend upon backs. //
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
const handleBack = () =>
|
||
{
|
||
setNewStep(activeStepIndex - 1);
|
||
};
|
||
|
||
//////////////////////////////////////////////////////////////////////////////////////////
|
||
// handle user submitting the form - which in qqq means moving forward from any screen. //
|
||
//////////////////////////////////////////////////////////////////////////////////////////
|
||
const handleSubmit = async (values: any, actions: any) =>
|
||
{
|
||
setFormError(null);
|
||
|
||
const formData = new FormData();
|
||
Object.keys(values).forEach((key) =>
|
||
{
|
||
formData.append(key, values[key]);
|
||
});
|
||
|
||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
|
||
{
|
||
const bulkEditEnabledFields: string[] = [];
|
||
let fullFieldList = getFullFieldList(activeStep, processValues);
|
||
fullFieldList.forEach((field) =>
|
||
{
|
||
if (!disabledBulkEditFields[field.name])
|
||
{
|
||
bulkEditEnabledFields.push(field.name);
|
||
}
|
||
});
|
||
|
||
if (bulkEditEnabledFields.length === 0)
|
||
{
|
||
setFormError("You must edit at least one field to continue.");
|
||
return;
|
||
}
|
||
formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(","));
|
||
}
|
||
|
||
const formDataHeaders = {
|
||
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
|
||
};
|
||
|
||
setProcessValues({});
|
||
setRecords([]);
|
||
setOverrideOnLastStep(null);
|
||
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
||
|
||
setTimeout(async () =>
|
||
{
|
||
const processResponse = await QClient.getInstance().processStep(
|
||
processName,
|
||
processUUID,
|
||
activeStep.name,
|
||
formData,
|
||
formDataHeaders,
|
||
);
|
||
setLastProcessResponse(processResponse);
|
||
});
|
||
};
|
||
|
||
const handleCancelClicked = () =>
|
||
{
|
||
const pathParts = location.pathname.split(/\//);
|
||
pathParts.pop();
|
||
const path = pathParts.join("/");
|
||
navigate(path, {replace: true});
|
||
};
|
||
|
||
const mainCardStyles: any = {};
|
||
mainCardStyles.minHeight = "calc(100vh - 400px)";
|
||
if (!processError && (qJobRunning || activeStep === null))
|
||
{
|
||
mainCardStyles.background = "none";
|
||
mainCardStyles.boxShadow = "none";
|
||
}
|
||
|
||
let nextButtonLabel = "Next";
|
||
let nextButtonIcon = "arrow_forward";
|
||
if (overrideOnLastStep !== null)
|
||
{
|
||
if (overrideOnLastStep)
|
||
{
|
||
nextButtonLabel = "Submit";
|
||
nextButtonIcon = "check";
|
||
}
|
||
}
|
||
else if (onLastStep)
|
||
{
|
||
nextButtonLabel = "Submit";
|
||
nextButtonIcon = "check";
|
||
}
|
||
|
||
return (
|
||
<BaseLayout>
|
||
<MDBox py={3} mb={20}>
|
||
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
||
<Grid item xs={12} lg={10} xl={8}>
|
||
<Formik
|
||
enableReinitialize
|
||
initialValues={initialValues}
|
||
validationSchema={validationScheme}
|
||
validation={validationFunction}
|
||
onSubmit={handleSubmit}
|
||
>
|
||
{({
|
||
values, errors, touched, isSubmitting, setFieldValue,
|
||
}) => (
|
||
<Form id={formId} autoComplete="off">
|
||
<Card sx={mainCardStyles}>
|
||
<MDBox mx={2} mt={-3}>
|
||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||
{steps.map((step) => (
|
||
<Step key={step.name}>
|
||
<StepLabel>{step.label}</StepLabel>
|
||
</Step>
|
||
))}
|
||
</Stepper>
|
||
</MDBox>
|
||
|
||
<MDBox p={3}>
|
||
<MDBox>
|
||
{/***************************************************************************
|
||
** step content - e.g., the appropriate form or other screen for the step **
|
||
***************************************************************************/}
|
||
{getDynamicStepContent(
|
||
activeStepIndex,
|
||
activeStep,
|
||
{
|
||
values,
|
||
touched,
|
||
formFields,
|
||
errors,
|
||
},
|
||
processError,
|
||
processValues,
|
||
recordConfig,
|
||
setFieldValue,
|
||
)}
|
||
{/********************************
|
||
** back &| next/submit buttons **
|
||
********************************/}
|
||
<MDBox mt={6} width="100%" display="flex" justifyContent="space-between">
|
||
{true || activeStepIndex === 0 ? (
|
||
<MDBox />
|
||
) : (
|
||
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
||
)}
|
||
{processError || qJobRunning || !activeStep ? (
|
||
<MDBox />
|
||
) : (
|
||
<>
|
||
{formError && (
|
||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
||
{formError}
|
||
</MDTypography>
|
||
)}
|
||
{
|
||
noMoreSteps && <QCancelButton onClickHandler={handleCancelClicked} label="Return" iconName="arrow_back" disabled={isSubmitting} />
|
||
}
|
||
{
|
||
!noMoreSteps && (
|
||
<MDBox component="div" py={3}>
|
||
<Grid container justifyContent="flex-end" spacing={3}>
|
||
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
||
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
|
||
</Grid>
|
||
</MDBox>
|
||
)
|
||
}
|
||
</>
|
||
)}
|
||
</MDBox>
|
||
</MDBox>
|
||
</MDBox>
|
||
</Card>
|
||
</Form>
|
||
)}
|
||
</Formik>
|
||
</Grid>
|
||
</Grid>
|
||
</MDBox>
|
||
</BaseLayout>
|
||
);
|
||
}
|
||
|
||
ProcessRun.defaultProps = {
|
||
process: null,
|
||
defaultProcessValues: {}
|
||
};
|
||
|
||
export default ProcessRun;
|