CE-551 Implement default values from fieldMetaData for record create; scroll to error; add errors to possible-value (DynamicSelect);

This commit is contained in:
2023-07-25 14:14:27 -05:00
parent 9c51b3949e
commit 4984ddbf73
4 changed files with 148 additions and 90 deletions

View File

@ -135,7 +135,7 @@ function QDynamicFormField({
/> />
<Box mt={0.75}> <Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular"> <MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} /></div>} {!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography> </MDTypography>
</Box> </Box>
</> </>

View File

@ -104,7 +104,18 @@ class DynamicFormUtils
{ {
if (field.isRequired) if (field.isRequired)
{ {
return (Yup.string().required(`${field.label} is required.`)); if(field.possibleValueSourceName)
{
////////////////////////////////////////////////////////////////////////////////////////////
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
// rather, it's more like "null is how empty will be treated" or some-such... //
////////////////////////////////////////////////////////////////////////////////////////////
return (Yup.string().required(`${field.label} is required.`).nullable(true));
}
else
{
return (Yup.string().required(`${field.label} is required.`));
}
} }
return (null); return (null);
} }

View File

@ -27,8 +27,9 @@ import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {useFormikContext} from "formik"; import {ErrorMessage, useFormikContext} from "formik";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import MDTypography from "qqq/components/legacy/MDTypography";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
interface Props interface Props
@ -246,78 +247,85 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
// console.log(`default value: ${JSON.stringify(defaultValue)}`); // console.log(`default value: ${JSON.stringify(defaultValue)}`);
const autocomplete = ( const autocomplete = (
<Autocomplete <Box>
id={overrideId ?? fieldName} <Autocomplete
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}} id={overrideId ?? fieldName}
open={open} sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
fullWidth open={open}
onOpen={() => fullWidth
{ onOpen={() =>
setOpen(true);
// console.log("setting open...");
if(options.length == 0)
{ {
// console.log("no options yet, so setting search term to ''..."); setOpen(true);
setSearchTerm(""); // console.log("setting open...");
} if(options.length == 0)
}} {
onClose={() => // console.log("no options yet, so setting search term to ''...");
{ setSearchTerm("");
setOpen(false); }
}} }}
isOptionEqualToValue={(option, value) => option.id === value.id} onClose={() =>
getOptionLabel={(option) => {
{ setOpen(false);
// @ts-ignore }}
if(option && option.length) isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option) =>
{ {
// @ts-ignore // @ts-ignore
option = option[0]; if(option && option.length)
} {
// @ts-ignore
option = option[0];
}
// @ts-ignore
return option.label
}}
options={options}
loading={loading}
onInputChange={inputChanged}
onBlur={handleBlur}
defaultValue={defaultValue}
// @ts-ignore // @ts-ignore
return option.label onChange={handleChanged}
}} noOptionsText={"No matches found"}
options={options} onKeyPress={e =>
loading={loading}
onInputChange={inputChanged}
onBlur={handleBlur}
defaultValue={defaultValue}
// @ts-ignore
onChange={handleChanged}
noOptionsText={"No matches found"}
onKeyPress={e =>
{
if (e.key === "Enter")
{ {
e.preventDefault(); if (e.key === "Enter")
} {
}} e.preventDefault();
renderOption={renderOption} }
filterOptions={filterOptions} }}
disabled={isDisabled} renderOption={renderOption}
multiple={isMultiple} filterOptions={filterOptions}
disableCloseOnSelect={isMultiple} disabled={isDisabled}
limitTags={5} multiple={isMultiple}
slotProps={{popper: {className: "DynamicSelectPopper"}}} disableCloseOnSelect={isMultiple}
renderInput={(params) => ( limitTags={5}
<TextField slotProps={{popper: {className: "DynamicSelectPopper"}}}
{...params} renderInput={(params) => (
label={fieldLabel} <TextField
variant="standard" {...params}
autoComplete="off" label={fieldLabel}
type="search" variant="standard"
InputProps={{ autoComplete="off"
...params.InputProps, type="search"
endAdornment: ( InputProps={{
<React.Fragment> ...params.InputProps,
{loading ? <CircularProgress color="inherit" size={20} /> : null} endAdornment: (
{params.InputProps.endAdornment} <React.Fragment>
</React.Fragment> {loading ? <CircularProgress color="inherit" size={20} /> : null}
), {params.InputProps.endAdornment}
}} </React.Fragment>
/> ),
)} }}
/> />
)}
/>
<Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography>
</Box>
</Box>
); );

View File

@ -32,8 +32,8 @@ import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {Form, Formik} from "formik"; import {Form, Formik, useFormikContext} from "formik";
import React, {useContext, useReducer, useState} from "react"; import React, {useContext, useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useParams} from "react-router-dom"; import {useLocation, useNavigate, useParams} from "react-router-dom";
import * as Yup from "yup"; import * as Yup from "yup";
import QContext from "QContext"; import QContext from "QContext";
@ -77,7 +77,7 @@ function EntityForm(props: Props): JSX.Element
const [formTitle, setFormTitle] = useState(""); const [formTitle, setFormTitle] = useState("");
const [validations, setValidations] = useState({}); const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); const [initialValues, setInitialValues] = useState({} as { [key: string]: any });
const [formFields, setFormFields] = useState(null as Map<string, any>); const [formFields, setFormFields] = useState(null as Map<string, any>);
const [t1sectionName, setT1SectionName] = useState(null as string); const [t1sectionName, setT1SectionName] = useState(null as string);
const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
@ -233,27 +233,26 @@ function EntityForm(props: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. // // if default values were supplied for a new record, then populate initialValues, for formik. //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
if(defaultValues) for (let i = 0; i < fieldArray.length; i++)
{ {
for (let i = 0; i < fieldArray.length; i++) const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name;
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue)
{ {
const fieldMetaData = fieldArray[i]; initialValues[fieldName] = defaultValue;
const fieldName = fieldMetaData.name;
if (defaultValues[fieldName])
{
initialValues[fieldName] = defaultValues[fieldName];
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// we need to set the initialDisplayValue for possible value fields with a default value // // we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed // // so, look them up here now if needed //
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName) if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); defaultDisplayValues.set(fieldName, results[0].label);
if (results && results.length > 0) initialValues[fieldName] = {id: defaultValue, value: results[0].label}
{
defaultDisplayValues.set(fieldName, results[0].label);
}
} }
} }
} }
@ -598,6 +597,7 @@ function EntityForm(props: Props): JSX.Element
isSubmitting, isSubmitting,
}) => ( }) => (
<Form id={formId} autoComplete="off"> <Form id={formId} autoComplete="off">
<ScrollToFirstError />
<Box pb={3} pt={0}> <Box pb={3} pt={0}>
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}> <Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}>
@ -672,4 +672,43 @@ function EntityForm(props: Props): JSX.Element
} }
} }
function ScrollToFirstError(): JSX.Element
{
const {submitCount, isValid} = useFormikContext()
useEffect(() =>
{
/////////////////////////////////////////////////////////////////////////////
// Wrap the code in setTimeout to make sure it runs after the DOM has been //
// updated and has the error message elements. //
/////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
////////////////////////////////////////
// Only run on submit or if not valid //
////////////////////////////////////////
if (submitCount === 0 || isValid)
{
return;
}
//////////////////////////////////
// Find the first error message //
//////////////////////////////////
const errorMessageSelector = "[data-field-error]";
const firstErrorMessage = document.querySelector(errorMessageSelector);
if (!firstErrorMessage)
{
console.warn(`Form failed validation but no error field was found with selector: ${errorMessageSelector}`);
return;
}
firstErrorMessage.scrollIntoView({block: "center"});
}, 100)
}, [submitCount, isValid])
return null;
}
export default EntityForm; export default EntityForm;