diff --git a/src/qqq/components/forms/DynamicFormField.tsx b/src/qqq/components/forms/DynamicFormField.tsx index e2fb6f8..4a80caa 100644 --- a/src/qqq/components/forms/DynamicFormField.tsx +++ b/src/qqq/components/forms/DynamicFormField.tsx @@ -135,7 +135,7 @@ function QDynamicFormField({ /> - {!isDisabled &&
} + {!isDisabled &&
{msg}} />
}
diff --git a/src/qqq/components/forms/DynamicFormUtils.ts b/src/qqq/components/forms/DynamicFormUtils.ts index 02f3d85..59c060e 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -104,7 +104,18 @@ class DynamicFormUtils { 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); } diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index ac558c8..a11a2b3 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -27,8 +27,9 @@ import Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; -import {useFormikContext} from "formik"; +import {ErrorMessage, useFormikContext} from "formik"; import React, {useEffect, useState} from "react"; +import MDTypography from "qqq/components/legacy/MDTypography"; import Client from "qqq/utils/qqq/Client"; interface Props @@ -246,78 +247,85 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe // console.log(`default value: ${JSON.stringify(defaultValue)}`); const autocomplete = ( - - { - setOpen(true); - // console.log("setting open..."); - if(options.length == 0) + + { - // console.log("no options yet, so setting search term to ''..."); - setSearchTerm(""); - } - }} - onClose={() => - { - setOpen(false); - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - getOptionLabel={(option) => - { - // @ts-ignore - if(option && option.length) + setOpen(true); + // console.log("setting open..."); + if(options.length == 0) + { + // console.log("no options yet, so setting search term to ''..."); + setSearchTerm(""); + } + }} + onClose={() => + { + setOpen(false); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + getOptionLabel={(option) => { // @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 - return option.label - }} - options={options} - loading={loading} - onInputChange={inputChanged} - onBlur={handleBlur} - defaultValue={defaultValue} - // @ts-ignore - onChange={handleChanged} - noOptionsText={"No matches found"} - onKeyPress={e => - { - if (e.key === "Enter") + onChange={handleChanged} + noOptionsText={"No matches found"} + onKeyPress={e => { - e.preventDefault(); - } - }} - renderOption={renderOption} - filterOptions={filterOptions} - disabled={isDisabled} - multiple={isMultiple} - disableCloseOnSelect={isMultiple} - limitTags={5} - slotProps={{popper: {className: "DynamicSelectPopper"}}} - renderInput={(params) => ( - - {loading ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> + if (e.key === "Enter") + { + e.preventDefault(); + } + }} + renderOption={renderOption} + filterOptions={filterOptions} + disabled={isDisabled} + multiple={isMultiple} + disableCloseOnSelect={isMultiple} + limitTags={5} + slotProps={{popper: {className: "DynamicSelectPopper"}}} + renderInput={(params) => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + {!isDisabled &&
{msg}} />
} +
+
+
); diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 1d3c2ef..9d1761a 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -32,8 +32,8 @@ import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; import Grid from "@mui/material/Grid"; import Icon from "@mui/material/Icon"; -import {Form, Formik} from "formik"; -import React, {useContext, useReducer, useState} from "react"; +import {Form, Formik, useFormikContext} from "formik"; +import React, {useContext, useEffect, useReducer, useState} from "react"; import {useLocation, useNavigate, useParams} from "react-router-dom"; import * as Yup from "yup"; import QContext from "QContext"; @@ -77,7 +77,7 @@ function EntityForm(props: Props): JSX.Element const [formTitle, setFormTitle] = 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); const [t1sectionName, setT1SectionName] = useState(null as string); 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(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]; - const fieldName = fieldMetaData.name; - if (defaultValues[fieldName]) - { - initialValues[fieldName] = defaultValues[fieldName]; + initialValues[fieldName] = defaultValue; - /////////////////////////////////////////////////////////////////////////////////////////// - // we need to set the initialDisplayValue for possible value fields with a default value // - // so, look them up here now if needed // - /////////////////////////////////////////////////////////////////////////////////////////// - if (fieldMetaData.possibleValueSourceName) + /////////////////////////////////////////////////////////////////////////////////////////// + // we need to set the initialDisplayValue for possible value fields with a default value // + // so, look them up here now if needed // + /////////////////////////////////////////////////////////////////////////////////////////// + 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]]); - if (results && results.length > 0) - { - defaultDisplayValues.set(fieldName, results[0].label); - } + defaultDisplayValues.set(fieldName, results[0].label); + initialValues[fieldName] = {id: defaultValue, value: results[0].label} } } } @@ -598,6 +597,7 @@ function EntityForm(props: Props): JSX.Element isSubmitting, }) => (
+ @@ -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;