Files
qqq-frontend-material-dashb…/src/qqq/pages/process-run/index.tsx

1067 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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}
: &nbsp;
</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;