QQQ-21: initial stab at merging some of form parts of create/edit and processes

This commit is contained in:
Tim Chamberlain
2022-07-07 14:59:24 -05:00
parent 510afb8dc4
commit 22e35cdfb0
6 changed files with 201 additions and 143 deletions

View File

@ -1,26 +1,16 @@
/** // react components
=========================================================
* 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.
*/
// react imports
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import React, { useReducer, useState } from "react"; import React, { useReducer, useState } from "react";
// qqq imports // misc components
import * as Yup from "yup";
import { Form, Formik } from "formik";
// qqq components
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import { QRecord } from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import { QFieldType } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import QDynamicForm from "qqq/components/QDynamicForm";
import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
// @material-ui core components // @material-ui core components
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -29,7 +19,6 @@ import Grid from "@mui/material/Grid";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography"; import MDTypography from "components/MDTypography";
import FormField from "layouts/pages/account/components/FormField";
import MDButton from "../../../components/MDButton"; import MDButton from "../../../components/MDButton";
// Declaring props types for EntityForm // Declaring props types for EntityForm
@ -41,17 +30,24 @@ function EntityForm({ id }: Props): JSX.Element {
const qController = new QController(""); const qController = new QController("");
const { tableName } = useParams(); const { tableName } = useParams();
const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: string });
const [formFields, setFormFields] = useState({});
const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [formValues, setFormValues] = useState({} as { [key: string]: string }); const [formValues, setFormValues] = useState({} as { [key: string]: string });
const [formFields, setFormFields] = useState([] as JSX.Element[]);
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const handleInputChange = (e: { target: { name: any; value: any } }) => { function getDynamicStepContent(formData: any): JSX.Element {
const { name, value } = e.target; const { formFields, values, errors, touched } = formData;
formValues[name] = value;
setFormValues(formValues); if (!Object.keys(formFields).length) {
}; return <div>Loading...</div>;
}
return <QDynamicForm formData={formData} primaryKeyId={tableMetaData.primaryKeyField} />;
}
if (!asyncLoadInited) { if (!asyncLoadInited) {
setAsyncLoadInited(true); setAsyncLoadInited(true);
@ -59,73 +55,46 @@ function EntityForm({ id }: Props): JSX.Element {
const tableMetaData = await qController.loadTableMetaData(tableName); const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData); setTableMetaData(tableMetaData);
const fieldArray = [] as QFieldMetaData[];
const sortedKeys = [...tableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) => {
const fieldMetaData = tableMetaData.fields.get(key);
fieldArray.push(fieldMetaData);
});
if (id !== null) { if (id !== null) {
const foundRecord = await qController.get(tableName, id); const record = await qController.get(tableName, id);
tableMetaData.fields.forEach((fieldMetaData, key) => { tableMetaData.fields.forEach((fieldMetaData, key) => {
formValues[key] = foundRecord.values.get(key); initialValues[key] = record.values.get(key);
}); });
setFormValues(formValues); setFormValues(formValues);
} }
const sortedKeys = [...tableMetaData.fields.keys()].sort(); const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(fieldArray);
sortedKeys.forEach((key) => { setInitialValues(initialValues);
const fieldMetaData = tableMetaData.fields.get(key); setFormFields(dynamicFormFields);
if (fieldMetaData.name !== tableMetaData.primaryKeyField) { setValidations(Yup.object().shape(formValidations));
let fieldType: string;
switch (fieldMetaData.type.toString()) {
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
fieldType = "number";
break;
case QFieldType.DATE_TIME:
fieldType = "datetime-local";
break;
case QFieldType.PASSWORD:
case QFieldType.TIME:
case QFieldType.DATE:
fieldType = fieldMetaData.type.toString();
break;
case QFieldType.TEXT:
case QFieldType.HTML:
case QFieldType.STRING:
default:
fieldType = "text";
}
formFields.push(
<Grid item xs={12} sm={4} key={fieldMetaData.name}>
<FormField
id={fieldMetaData.name}
key={fieldMetaData.name}
name={fieldMetaData.name}
label={fieldMetaData.label}
type={fieldType}
defaultValue={formValues[fieldMetaData.name]}
onChange={handleInputChange}
/>
</Grid>
);
}
});
setFormFields(formFields);
forceUpdate(); forceUpdate();
})(); })();
} }
const handleSubmit = (event: { preventDefault: () => void }) => { const handleSubmit = async (values: any, actions: any) => {
event.preventDefault(); actions.setSubmitting(true);
await (async () => {
(async () => {
if (id !== null) { if (id !== null) {
await qController.update(tableName, id, formValues).then((record) => { await qController.update(tableName, id, values).then((record) => {
window.location.href = `/${tableName}/view/${record.values.get("id")}`; // todo - primaryKeyField window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField
)}`;
}); });
} else { } else {
await qController.create(tableName, formValues).then((record) => { await qController.create(tableName, values).then((record) => {
window.location.href = `/${tableName}/view/${record.values.get("id")}`; // todo - primaryKeyField window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField
)}`;
}); });
} }
})(); })();
@ -133,26 +102,48 @@ function EntityForm({ id }: Props): JSX.Element {
const pageTitle = const pageTitle =
id != null ? `Edit ${tableMetaData?.label} (${id})` : `Create New ${tableMetaData?.label}`; id != null ? `Edit ${tableMetaData?.label} (${id})` : `Create New ${tableMetaData?.label}`;
const formId =
id != null ? `edit-${tableMetaData?.label}-form` : `create-${tableMetaData?.label}-form`;
return ( return (
<Card id="basic-info" sx={{ overflow: "visible" }}> <Card id="edit-form-container" sx={{ overflow: "visible" }}>
<MDBox p={3}> <MDBox p={3}>
<MDTypography variant="h5">{pageTitle}</MDTypography> <MDTypography variant="h5">{pageTitle}</MDTypography>
</MDBox> </MDBox>
<MDBox component="form" pb={3} px={3} onSubmit={handleSubmit}> <MDBox pb={3} px={3}>
<Grid key="fieldsGrid" container spacing={3}> <Grid key="fields-grid" container spacing={3}>
{formFields} <Formik
</Grid> initialValues={initialValues}
</MDBox> validationSchema={validations}
<MDBox p={3}> onSubmit={handleSubmit}
>
{({ values, errors, touched, isSubmitting }) => (
<Form id={formId} autoComplete="off">
<MDBox p={3} width="100%">
<MDBox>
{/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/}
{getDynamicStepContent({
values,
touched,
formFields,
errors,
})}
<Grid key="buttonGrid" container spacing={3}> <Grid key="buttonGrid" container spacing={3}>
<MDBox ml="auto"> <MDBox mt={5} ml="auto">
<MDButton type="submit" variant="gradient" color="dark" size="small"> <MDButton type="submit" variant="gradient" color="dark" size="small">
save {tableMetaData?.label} save {tableMetaData?.label}
</MDButton> </MDButton>
</MDBox> </MDBox>
</Grid> </Grid>
</MDBox> </MDBox>
</MDBox>
</Form>
)}
</Formik>
</Grid>
</MDBox>
</Card> </Card>
); );
} }

View File

@ -25,12 +25,13 @@ import FormField from "layouts/pages/users/new-user/components/FormField";
import { QFrontendStepMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; import { QFrontendStepMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
interface Props { interface Props {
formLabel?: string;
formData: any; formData: any;
step?: QFrontendStepMetaData | undefined; primaryKeyId?: string;
} }
function QDynamicForm(props: Props): JSX.Element { function QDynamicForm(props: Props): JSX.Element {
const { formData, step } = props; const { formData, formLabel, primaryKeyId } = props;
const { formFields, values, errors, touched } = formData; const { formFields, values, errors, touched } = formData;
/* /*
@ -47,7 +48,7 @@ function QDynamicForm(props: Props): JSX.Element {
return ( return (
<MDBox> <MDBox>
<MDBox lineHeight={0}> <MDBox lineHeight={0}>
<MDTypography variant="h5">{step?.label}</MDTypography> <MDTypography variant="h5">{formLabel}</MDTypography>
{/* TODO - help text {/* TODO - help text
<MDTypography variant="button" color="text"> <MDTypography variant="button" color="text">
Mandatory information Mandatory information
@ -60,12 +61,16 @@ function QDynamicForm(props: Props): JSX.Element {
Object.keys(formFields).length > 0 && Object.keys(formFields).length > 0 &&
Object.keys(formFields).map((fieldName: any) => { Object.keys(formFields).map((fieldName: any) => {
const field = formFields[fieldName]; const field = formFields[fieldName];
if (primaryKeyId && fieldName === primaryKeyId) {
return null;
}
if (values[fieldName] === undefined) { if (values[fieldName] === undefined) {
values[fieldName] = ""; values[fieldName] = "";
} }
return ( return (
<Grid item xs={12} sm={6} key={fieldName}> <Grid item xs={12} sm={6} key={fieldName}>
<FormField <FormField
required={field.isRequired}
type={field.type} type={field.type}
label={field.label} label={field.label}
name={fieldName} name={fieldName}
@ -156,7 +161,8 @@ function QDynamicForm(props: Props): JSX.Element {
} }
QDynamicForm.defaultProps = { QDynamicForm.defaultProps = {
step: undefined, formLabel: undefined,
primaryKeyId: undefined,
}; };
export default QDynamicForm; export default QDynamicForm;

View File

@ -0,0 +1,75 @@
/*
* 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 * as Yup from "yup";
import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import { QFieldType } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
/*******************************************************************************
** Meta-data to represent a single field in a table.
**
*******************************************************************************/
class DynamicFormUtils {
public static getFormData(qqqFormFields: QFieldMetaData[]) {
const dynamicFormFields: any = {};
const formValidations: any = {};
qqqFormFields.forEach((field) => {
let fieldType: string;
switch (field.type.toString()) {
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
fieldType = "number";
break;
case QFieldType.DATE_TIME:
fieldType = "datetime-local";
break;
case QFieldType.PASSWORD:
case QFieldType.TIME:
case QFieldType.DATE:
fieldType = field.type.toString();
break;
case QFieldType.TEXT:
case QFieldType.HTML:
case QFieldType.STRING:
default:
fieldType = "text";
}
dynamicFormFields[field.name] = {
name: field.name,
label: field.label ? field.label : field.name,
isRequired: field.isRequired,
type: fieldType,
// todo invalidMsg: "Zipcode is not valid (e.g. 70000).",
};
if (field.isRequired) {
formValidations[field.name] = Yup.string().required(`${field.label} is required.`);
}
});
return { dynamicFormFields, formValidations };
}
}
export default DynamicFormUtils;

View File

@ -22,23 +22,24 @@ import Icon from "@mui/material/Icon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link";
// Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography";
import MDButton from "components/MDButton";
// Material Dashboard 2 PRO React TS examples components // Material Dashboard 2 PRO React TS examples components
import DashboardLayout from "examples/LayoutContainers/DashboardLayout"; import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
import DashboardNavbar from "examples/Navbars/DashboardNavbar"; import DashboardNavbar from "examples/Navbars/DashboardNavbar";
import DataTable from "examples/Tables/DataTable"; import DataTable from "examples/Tables/DataTable";
// Data // QQQ
import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import Link from "@mui/material/Link";
import { QTableMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import { QTableMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
// Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography";
import MDButton from "components/MDButton";
import Footer from "../../components/Footer"; import Footer from "../../components/Footer";
import IdCell from "../../components/EntityForm/components/IdCell"; import IdCell from "../../components/EntityForm/components/IdCell";

View File

@ -13,7 +13,9 @@
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/ */
// react components
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import React, { useReducer, useState } from "react";
// @material-ui core components // @material-ui core components
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -26,17 +28,12 @@ import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
// qqq imports
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography"; import MDTypography from "components/MDTypography";
// Settings page components
// qqq imports
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import { QRecord } from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import React, { useReducer, useState } from "react";
import MDButton from "../../../../../components/MDButton"; import MDButton from "../../../../../components/MDButton";
const qController = new QController(""); const qController = new QController("");
@ -104,20 +101,16 @@ function ViewContents({ id }: Props): JSX.Element {
setOpen(false); setOpen(false);
}; };
/*
const handleDelete = (event: { preventDefault: () => void }) => { const handleDelete = (event: { preventDefault: () => void }) => {
event.preventDefault(); event.preventDefault();
/*
(async () => { (async () => {
await qController.delete(tableName, id).then(() => { await qController.delete(tableName, id).then(() => {
window.location.href = `/${tableName}/list/`; window.location.href = `/${tableName}`;
}); });
})(); })();
}; };
*/
const editPath = `/${tableName}/edit/${id}`; const editPath = `/${tableName}/${id}/edit`;
return ( return (
<Card id="basic-info" sx={{ overflow: "visible" }}> <Card id="basic-info" sx={{ overflow: "visible" }}>
@ -131,7 +124,6 @@ function ViewContents({ id }: Props): JSX.Element {
<Grid key="tres" container spacing={3}> <Grid key="tres" container spacing={3}>
<MDBox ml="auto" mr={3}> <MDBox ml="auto" mr={3}>
<MDButton <MDButton
type="submit"
variant="gradient" variant="gradient"
color="primary" color="primary"
size="small" size="small"
@ -153,14 +145,14 @@ function ViewContents({ id }: Props): JSX.Element {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClickConfirmClose}>No</Button> <Button onClick={handleClickConfirmClose}>No</Button>
<Button onClick={handleClickConfirmClose} autoFocus> <Button onClick={handleDelete} autoFocus>
Yes Yes
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</MDBox> </MDBox>
<MDBox> <MDBox>
<MDButton type="submit" variant="gradient" color="dark" size="small"> <MDButton variant="gradient" color="dark" size="small">
<Link href={editPath}>edit {tableMetaData?.label}</Link> <Link href={editPath}>edit {tableMetaData?.label}</Link>
</MDButton> </MDButton>
</MDBox> </MDBox>

View File

@ -45,6 +45,7 @@ import * as Yup from "yup";
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import { QFrontendStepMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; import { QFrontendStepMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import QDynamicForm from "../../components/QDynamicForm"; import QDynamicForm from "../../components/QDynamicForm";
function getDynamicStepContent(stepIndex: number, stepParam: any, formData: any): JSX.Element { function getDynamicStepContent(stepIndex: number, stepParam: any, formData: any): JSX.Element {
@ -57,7 +58,7 @@ function getDynamicStepContent(stepIndex: number, stepParam: any, formData: any)
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return <QDynamicForm formData={formData} step={step} />; return <QDynamicForm formData={formData} formLabel={step.name} />;
} }
const qController = new QController(""); const qController = new QController("");
@ -90,31 +91,23 @@ function ProcessRun(): JSX.Element {
setActiveStep(activeStep); setActiveStep(activeStep);
setFormId(activeStep.name); setFormId(activeStep.name);
const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(
activeStep.formFields
);
const formFields: any = {}; const formFields: any = {};
const initialValues: any = {}; const initialValues: any = {};
const validations: any = {}; const validations: any = {};
activeStep.formFields.forEach((field) => { activeStep.formFields.forEach((field) => {
formFields[field.name] = {
name: field.name,
label: field.label,
type: "text", // todo better
// todo invalidMsg: "Zipcode is not valid (e.g. 70000).",
};
// todo - not working - also, needs real value. // todo - not working - also, needs real value.
initialValues[field.name] = "Hi"; initialValues[field.name] = "Hi";
// todo - all this based on type and other metadata.
// see src/layouts/pages/users/new-user/schemas/validations.ts
validations[field.name] = Yup.string().required(`${field.label} is required.`);
// validations[field.name] = Yup.string().optional();
}); });
setFormFields(formFields); setFormFields(dynamicFormFields);
setInitialValues(initialValues); setInitialValues(initialValues);
setValidations(Yup.object().shape(validations)); setValidations(Yup.object().shape(formValidations));
console.log(`in updateActiveStep: formFields ${JSON.stringify(formFields)}`); console.log(`in updateActiveStep: formFields ${JSON.stringify(dynamicFormFields)}`);
console.log(`in updateActiveStep: initialValues ${JSON.stringify(initialValues)}`); console.log(`in updateActiveStep: initialValues ${JSON.stringify(initialValues)}`);
} }
}; };