mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
559 lines
21 KiB
TypeScript
559 lines
21 KiB
TypeScript
/**
|
|
=========================================================
|
|
* Material Dashboard 2 PRO React TS - v1.0.0
|
|
=========================================================
|
|
|
|
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
|
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
|
|
|
Coded by www.creative-tim.com
|
|
|
|
=========================================================
|
|
|
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
*/
|
|
|
|
import {useEffect, useState} from "react";
|
|
|
|
// formik components
|
|
import {Formik, Form} from "formik";
|
|
|
|
// @mui material components
|
|
import Grid from "@mui/material/Grid";
|
|
import Card from "@mui/material/Card";
|
|
import Stepper from "@mui/material/Stepper";
|
|
import Step from "@mui/material/Step";
|
|
import StepLabel from "@mui/material/StepLabel";
|
|
|
|
// Material Dashboard 2 PRO React TS components
|
|
import MDBox from "components/MDBox";
|
|
import MDButton from "components/MDButton";
|
|
|
|
// Material Dashboard 2 PRO React TS examples components
|
|
import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
|
|
import DashboardNavbar from "examples/Navbars/DashboardNavbar";
|
|
import Footer from "examples/Footer";
|
|
|
|
// ProcessRun layout schemas for form and form fields
|
|
import * as Yup from "yup";
|
|
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
|
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
|
|
import {useLocation, useParams} from "react-router-dom";
|
|
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
|
|
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
|
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 {
|
|
DataGridPro, GridColDef, GridRowParams, GridRowsProp,
|
|
} from "@mui/x-data-grid-pro";
|
|
import QDynamicForm from "../../components/QDynamicForm";
|
|
import MDTypography from "../../../components/MDTypography";
|
|
|
|
function getDynamicStepContent(
|
|
stepIndex: number,
|
|
step: any,
|
|
formData: any,
|
|
processError: string,
|
|
processValues: any,
|
|
recordConfig: any,
|
|
): JSX.Element
|
|
{
|
|
const {
|
|
formFields, values, errors, touched,
|
|
} = formData;
|
|
// console.log(`in getDynamicStepContent: step label ${step?.label}`);
|
|
|
|
if (processError)
|
|
{
|
|
return (
|
|
<>
|
|
<MDTypography color="error" variant="h3">
|
|
Error
|
|
</MDTypography>
|
|
<div>{processError}</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (step === null)
|
|
{
|
|
console.log("in getDynamicStepContent. No step yet, so returning 'loading'");
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
console.log(`in getDynamicStepContent. the step looks like: ${JSON.stringify(step)}`);
|
|
|
|
return (
|
|
<>
|
|
{step.formFields && <QDynamicForm formData={formData} formLabel={step.label} />}
|
|
{step.viewFields && (
|
|
<div>
|
|
{step.viewFields.map((field: QFieldMetaData) => (
|
|
<div key={field.name}>
|
|
<b>
|
|
{field.label}
|
|
:
|
|
</b>
|
|
{" "}
|
|
{processValues[field.name]}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{step.recordListFields && (
|
|
<div>
|
|
<b>Records</b>
|
|
{" "}
|
|
<br />
|
|
<MDBox height="100%">
|
|
<DataGridPro
|
|
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}
|
|
paginationMode="server"
|
|
density="compact"
|
|
loading={recordConfig.loading}
|
|
/>
|
|
</MDBox>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function trace(name: string, isComponent: boolean = false)
|
|
{
|
|
if (isComponent)
|
|
{
|
|
console.log(`COMPONENT: ${name}`);
|
|
}
|
|
else
|
|
{
|
|
console.log(` function: ${name}`);
|
|
}
|
|
}
|
|
|
|
const qController = new QController("");
|
|
|
|
function ProcessRun(): JSX.Element
|
|
{
|
|
const {processName} = useParams();
|
|
const [processUUID, setProcessUUID] = useState(null as string);
|
|
const [jobUUID, setJobUUID] = useState(null as string);
|
|
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 [processValues, setProcessValues] = useState({} as any);
|
|
const [lastProcessResponse, setLastProcessResponse] = useState(
|
|
null as QJobStarted | QJobComplete | QJobError | QJobRunning,
|
|
);
|
|
const [formId, setFormId] = useState("");
|
|
const [formFields, setFormFields] = useState({});
|
|
const [initialValues, setInitialValues] = useState({});
|
|
const [validationScheme, setValidationScheme] = useState(null);
|
|
const [validationFunction, setValidationFunction] = useState(null);
|
|
const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false);
|
|
const [needRecords, setNeedRecords] = useState(false);
|
|
const [processError, setProcessError] = useState(null as string);
|
|
const [recordConfig, setRecordConfig] = useState({} as any);
|
|
const onLastStep = activeStepIndex === steps.length - 2;
|
|
const noMoreSteps = activeStepIndex === steps.length - 1;
|
|
|
|
trace("ProcessRun", true);
|
|
|
|
function buildNewRecordConfig()
|
|
{
|
|
const newRecordConfig = {} as any;
|
|
newRecordConfig.pageNo = 1;
|
|
newRecordConfig.rowsPerPage = 20;
|
|
newRecordConfig.columns = [] as GridColDef[];
|
|
newRecordConfig.rows = [];
|
|
newRecordConfig.totalRecords = 0;
|
|
newRecordConfig.handleRowsPerPageChange = null;
|
|
newRecordConfig.handlePageChange = null;
|
|
newRecordConfig.handleRowClick = null;
|
|
newRecordConfig.loading = true;
|
|
return (newRecordConfig);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
useEffect(() =>
|
|
{
|
|
trace("updateActiveStep");
|
|
|
|
if (!processMetaData)
|
|
{
|
|
console.log("No process meta data yet, so returning early");
|
|
return;
|
|
}
|
|
|
|
// console.log(`Steps are: ${steps}`);
|
|
// console.log(`Setting step to ${newStep}`);
|
|
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}.`);
|
|
}
|
|
setActiveStepIndex(newIndex);
|
|
|
|
if (steps)
|
|
{
|
|
const activeStep = steps[newIndex];
|
|
setActiveStep(activeStep);
|
|
setFormId(activeStep.name);
|
|
|
|
///////////////////////////////////////////////////
|
|
// if this step has form fields, set up the form //
|
|
///////////////////////////////////////////////////
|
|
if (activeStep.formFields)
|
|
{
|
|
const {dynamicFormFields, formValidations} = DynamicFormUtils.getFormData(
|
|
activeStep.formFields,
|
|
);
|
|
|
|
const initialValues: any = {};
|
|
activeStep.formFields.forEach((field) =>
|
|
{
|
|
initialValues[field.name] = processValues[field.name];
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
// if there are fields to load, build a record config, and set the needRecords state flag //
|
|
////////////////////////////////////////////////////////////////////////////////////////////
|
|
if (activeStep.recordListFields)
|
|
{
|
|
const newRecordConfig = buildNewRecordConfig();
|
|
activeStep.recordListFields.forEach((field) =>
|
|
{
|
|
newRecordConfig.columns.push({field: field.name, headerName: field.label, width: 200});
|
|
});
|
|
setRecordConfig(newRecordConfig);
|
|
setNeedRecords(true);
|
|
}
|
|
}
|
|
}, [newStep]);
|
|
|
|
// when we need to load records, do so, async
|
|
useEffect(() =>
|
|
{
|
|
if (needRecords)
|
|
{
|
|
setNeedRecords(false);
|
|
(async () =>
|
|
{
|
|
const records = await qController.processRecords(
|
|
processName,
|
|
processUUID,
|
|
recordConfig.rowsPerPage * (recordConfig.pageNo - 1),
|
|
recordConfig.rowsPerPage,
|
|
);
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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());
|
|
if (!row.id)
|
|
{
|
|
row.id = ++rowId;
|
|
}
|
|
newRecordConfig.rows.push(row);
|
|
});
|
|
// todo count?
|
|
newRecordConfig.totalRecords = records.length;
|
|
setRecordConfig(newRecordConfig);
|
|
})();
|
|
}
|
|
}, [needRecords]);
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
useEffect(() =>
|
|
{
|
|
if (lastProcessResponse)
|
|
{
|
|
trace("handleProcessResponse");
|
|
setLastProcessResponse(null);
|
|
if (lastProcessResponse instanceof QJobComplete)
|
|
{
|
|
const qJobComplete = lastProcessResponse as QJobComplete;
|
|
console.log("Setting new step.");
|
|
setNewStep(qJobComplete.nextStep);
|
|
setProcessValues(qJobComplete.values);
|
|
// console.log(`Updated process values: ${JSON.stringify(qJobComplete.values)}`);
|
|
}
|
|
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;
|
|
setNeedToCheckJobStatus(true);
|
|
}
|
|
else if (lastProcessResponse instanceof QJobError)
|
|
{
|
|
const qJobError = lastProcessResponse as QJobError;
|
|
console.log(`Got an error from the backend... ${qJobError.error}`);
|
|
setProcessError(qJobError.error);
|
|
}
|
|
}
|
|
}, [lastProcessResponse]);
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
// while a backend async job is running, periodically check its status //
|
|
/////////////////////////////////////////////////////////////////////////
|
|
useEffect(() =>
|
|
{
|
|
if (needToCheckJobStatus)
|
|
{
|
|
trace("checkJobStatus");
|
|
setNeedToCheckJobStatus(false);
|
|
(async () =>
|
|
{
|
|
setTimeout(async () =>
|
|
{
|
|
const processResponse = await qController.processJobStatus(
|
|
processName,
|
|
processUUID,
|
|
jobUUID,
|
|
);
|
|
setLastProcessResponse(processResponse);
|
|
}, 1500);
|
|
})();
|
|
}
|
|
}, [needToCheckJobStatus]);
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// do the initial load of data for the process - that is, meta data, plus the init step //
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
if (needInitialLoad)
|
|
{
|
|
trace("initialLoad");
|
|
setNeedInitialLoad(false);
|
|
(async () =>
|
|
{
|
|
const {search} = useLocation();
|
|
const urlSearchParams = new URLSearchParams(search);
|
|
let queryStringForInit = null;
|
|
if (urlSearchParams.get("recordIds"))
|
|
{
|
|
queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get(
|
|
"recordIds",
|
|
)}`;
|
|
}
|
|
else if (urlSearchParams.get("filterJSON"))
|
|
{
|
|
queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get(
|
|
"filterJSON",
|
|
)}`;
|
|
}
|
|
// todo once saved filters exist
|
|
//else if(urlSearchParams.get("filterId")) {
|
|
// queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}`
|
|
// }
|
|
|
|
console.log(`@dk: Query String for init: ${queryStringForInit}`);
|
|
|
|
const processMetaData = await qController.loadProcessMetaData(processName);
|
|
// console.log(processMetaData);
|
|
setProcessMetaData(processMetaData);
|
|
setSteps(processMetaData.frontendSteps);
|
|
|
|
const processResponse = await qController.processInit(processName, queryStringForInit);
|
|
setProcessUUID(processResponse.processUUID);
|
|
setLastProcessResponse(processResponse);
|
|
// console.log(processResponse);
|
|
})();
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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 = () =>
|
|
{
|
|
trace("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) =>
|
|
{
|
|
trace("handleSubmit");
|
|
|
|
// todo - post?
|
|
let queryString = "";
|
|
Object.keys(values).forEach((key) =>
|
|
{
|
|
queryString += `${key}=${encodeURIComponent(values[key])}&`;
|
|
});
|
|
|
|
actions.setSubmitting(false);
|
|
actions.resetForm();
|
|
|
|
const processResponse = await qController.processStep(
|
|
processName,
|
|
processUUID,
|
|
activeStep.name,
|
|
queryString,
|
|
);
|
|
setLastProcessResponse(processResponse);
|
|
};
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<DashboardNavbar />
|
|
<MDBox py={3} mb={20} height="65vh">
|
|
<Grid
|
|
container
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
sx={{height: "100%", mt: 8}}
|
|
>
|
|
<Grid item xs={12} lg={8}>
|
|
<Formik
|
|
enableReinitialize
|
|
initialValues={initialValues}
|
|
validationSchema={validationScheme}
|
|
validation={validationFunction}
|
|
onSubmit={handleSubmit}
|
|
>
|
|
{({
|
|
values, errors, touched, isSubmitting,
|
|
}) => (
|
|
<Form id={formId} autoComplete="off">
|
|
<Card sx={{height: "100%"}}>
|
|
<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,
|
|
)}
|
|
{/********************************
|
|
** back &| next/submit buttons **
|
|
********************************/}
|
|
<MDBox
|
|
mt={2}
|
|
width="100%"
|
|
display="flex"
|
|
justifyContent="space-between"
|
|
>
|
|
{true || activeStepIndex === 0 ? (
|
|
<MDBox />
|
|
) : (
|
|
<MDButton
|
|
variant="gradient"
|
|
color="light"
|
|
onClick={handleBack}
|
|
>
|
|
back
|
|
</MDButton>
|
|
)}
|
|
{noMoreSteps || processError ? (
|
|
<MDBox />
|
|
) : (
|
|
<MDButton
|
|
disabled={isSubmitting}
|
|
type="submit"
|
|
variant="gradient"
|
|
color="dark"
|
|
>
|
|
{onLastStep ? "submit" : "next"}
|
|
</MDButton>
|
|
)}
|
|
</MDBox>
|
|
</MDBox>
|
|
</MDBox>
|
|
</Card>
|
|
</Form>
|
|
)}
|
|
</Formik>
|
|
</Grid>
|
|
</Grid>
|
|
</MDBox>
|
|
<Footer />
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
export default ProcessRun;
|