diff --git a/packages/web/src/components/ControlledAutocomplete/index.jsx b/packages/web/src/components/ControlledAutocomplete/index.jsx index 0ade2237..ebb33beb 100644 --- a/packages/web/src/components/ControlledAutocomplete/index.jsx +++ b/packages/web/src/components/ControlledAutocomplete/index.jsx @@ -29,6 +29,8 @@ function ControlledAutocomplete(props) { options = [], dependsOn = [], showOptionValue, + renderInput, + showHelperText = true, ...autocompleteProps } = props; let dependsOnValues = []; @@ -105,16 +107,18 @@ function ControlledAutocomplete(props) { )} )} + renderInput={(params) => renderInput(params, fieldState)} /> - - - {fieldState.isTouched - ? fieldState.error?.message || description - : description} - + {showHelperText && ( + + {fieldState.isTouched + ? fieldState.error?.message || description + : description} + + )} )} /> @@ -132,6 +136,8 @@ ControlledAutocomplete.propTypes = { onBlur: PropTypes.func, onChange: PropTypes.func, options: PropTypes.array, + renderInput: PropTypes.func.isRequired, + showHelperText: PropTypes.bool, }; export default ControlledAutocomplete; diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx index 574674d4..614873f7 100644 --- a/packages/web/src/components/Form/index.jsx +++ b/packages/web/src/components/Form/index.jsx @@ -13,12 +13,14 @@ function Form(props) { resolver, render, mode = 'all', + reValidateMode = 'onBlur', + automaticValidation = true, ...formProps } = props; const methods = useForm({ defaultValues, - reValidateMode: 'onBlur', + reValidateMode, resolver, mode, }); @@ -30,7 +32,9 @@ function Form(props) { * For fields having `dependsOn` fields, we need to re-validate the form. */ React.useEffect(() => { - methods.trigger(); + if (automaticValidation) { + methods.trigger(); + } }, [methods.trigger, form]); React.useEffect(() => { @@ -56,6 +60,8 @@ Form.propTypes = { render: PropTypes.func, resolver: PropTypes.func, mode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit', 'onTouched', 'all']), + reValidateMode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit']), + automaticValidation: PropTypes.bool, }; export default Form; diff --git a/packages/web/src/components/TextField/index.jsx b/packages/web/src/components/TextField/index.jsx index 20ec3aeb..dfe505c2 100644 --- a/packages/web/src/components/TextField/index.jsx +++ b/packages/web/src/components/TextField/index.jsx @@ -31,6 +31,7 @@ function TextField(props) { onBlur, onChange, 'data-test': dataTest, + showError = false, ...textFieldProps } = props; return ( @@ -47,6 +48,7 @@ function TextField(props) { onBlur: controllerOnBlur, ...field }, + fieldState: { error }, }) => ( )} /> @@ -89,6 +92,7 @@ TextField.propTypes = { disabled: PropTypes.bool, onBlur: PropTypes.func, onChange: PropTypes.func, + showError: PropTypes.bool, }; export default TextField; diff --git a/packages/web/src/pages/Authentication/RoleMappings.jsx b/packages/web/src/pages/Authentication/RoleMappings.jsx index 9bfff4db..66a14c38 100644 --- a/packages/web/src/pages/Authentication/RoleMappings.jsx +++ b/packages/web/src/pages/Authentication/RoleMappings.jsx @@ -5,6 +5,8 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import { useMemo } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; import Form from 'components/Form'; import useFormatMessage from 'hooks/useFormatMessage'; @@ -23,6 +25,42 @@ function generateFormRoleMappings(roleMappings) { })); } +const uniqueRemoteRoleName = (array, context, formatMessage) => { + const seen = new Set(); + for (const [index, value] of array.entries()) { + if (seen.has(value.remoteRoleName)) { + const path = `${context.path}[${index}].remoteRoleName`; + return context.createError({ + message: `${formatMessage('roleMappingsForm.remoteRoleName')} must be unique`, + path, + }); + } + seen.add(value.remoteRoleName); + } + return true; +}; + +const getValidationSchema = (formatMessage) => + yup.object({ + roleMappings: yup + .array() + .of( + yup.object({ + roleId: yup + .string() + .required(`${formatMessage('roleMappingsForm.role')} is required`), + remoteRoleName: yup + .string() + .required( + `${formatMessage('roleMappingsForm.remoteRoleName')} is required`, + ), + }), + ) + .test('unique-remoteRoleName', '', (value, ctx) => { + return uniqueRemoteRoleName(value, ctx, formatMessage); + }), + }); + function RoleMappings({ provider, providerLoading }) { const formatMessage = useFormatMessage(); const enqueueSnackbar = useEnqueueSnackbar(); @@ -94,7 +132,15 @@ function RoleMappings({ provider, providerLoading }) { {formatMessage('roleMappingsForm.title')} -
+ ( + renderInput={(params, { error }) => ( )} loading={isRolesLoading} + showHelperText={false} />