From 8e67442fe0553f5983cb6a9409414a68ec9523b7 Mon Sep 17 00:00:00 2001 From: vichansson Date: Tue, 30 Jul 2024 17:14:14 +0300 Subject: [PATCH] F OpenNebula/one#6662: Enhance form validation reporting (#3179) Signed-off-by: Victor Hansson --- .../client/components/FormStepper/Stepper.js | 195 +++++++++++++----- .../client/components/FormStepper/index.js | 20 +- .../CreateForm/Steps/Roles/rolesPanel.js | 6 +- src/fireedge/src/client/utils/translation.js | 6 +- 4 files changed, 165 insertions(+), 62 deletions(-) diff --git a/src/fireedge/src/client/components/FormStepper/Stepper.js b/src/fireedge/src/client/components/FormStepper/Stepper.js index da8a1c0c61..11f63127ef 100644 --- a/src/fireedge/src/client/components/FormStepper/Stepper.js +++ b/src/fireedge/src/client/components/FormStepper/Stepper.js @@ -15,18 +15,28 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' - -import { Box, Button, Typography } from '@mui/material' +import { + Box, + Button, + Typography, + List, + ListItem, + ListItemText, + Paper, + IconButton, + Popover, +} from '@mui/material' import Step from '@mui/material/Step' +import { NavArrowDown, NavArrowUp } from 'iconoir-react' import StepButton from '@mui/material/StepButton' import StepConnector, { stepConnectorClasses, } from '@mui/material/StepConnector' +import { useState } from 'react' import StepIcon, { stepIconClasses } from '@mui/material/StepIcon' import StepLabel from '@mui/material/StepLabel' import Stepper from '@mui/material/Stepper' import { styled } from '@mui/styles' - import { SubmitButton } from 'client/components/FormControl' import { Translate } from 'client/components/HOC' import { SCHEMES, T } from 'client/constants' @@ -66,6 +76,25 @@ const ConnectorStyled = styled(StepConnector)(({ theme }) => ({ }, })) +const ErrorListContainer = styled(Paper)(({ theme }) => ({ + maxHeight: 200, + overflowY: 'auto', + padding: theme.spacing(1), + backgroundColor: theme.palette.background.default, +})) + +const ErrorSummaryContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + padding: theme.spacing(1), + borderRadius: theme.shape.borderRadius, + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, +})) + const StepIconStyled = styled(StepIcon)(({ theme }) => ({ color: theme.palette.text.hint, display: 'block', @@ -97,57 +126,117 @@ const CustomStepper = ({ handleBack, errors, isSubmitting, -}) => ( - <> - } - > - {steps?.map(({ id, label }, stepIdx) => ( - stepIdx}> - handleStep(stepIdx)} - disabled={activeStep + 1 < stepIdx} - optional={ - errors[id] && ( - - - - ) - } - data-cy={`step-${id}`} - > - - - - - - ))} - - - - } - /> - - -) + {steps?.map(({ id, label }, stepIdx) => { + const errorData = errors[id] + const hasError = Boolean(errorData?.message) + const individualMessages = errorData?.individualErrorMessages || [] + + return ( + stepIdx}> + handleStep(stepIdx)} + disabled={activeStep + 1 < stepIdx} + data-cy={`step-${id}`} + > + + + + + {hasError && ( + <> + handleClick(event, stepIdx)} + > + + {`${errorData.message[0].replace( + '%s', + errorData.message[1] + )}`} + + + {currentStep === stepIdx && open ? ( + + ) : ( + + )} + + + + + + {individualMessages.flat().map((msg, index) => ( + + + + ))} + + + + + )} + + ) + })} + + + + + } + /> + + + ) +} CustomStepper.propTypes = { steps: PropTypes.arrayOf( diff --git a/src/fireedge/src/client/components/FormStepper/index.js b/src/fireedge/src/client/components/FormStepper/index.js index 0df91a2257..672a44bb50 100644 --- a/src/fireedge/src/client/components/FormStepper/index.js +++ b/src/fireedge/src/client/components/FormStepper/index.js @@ -208,14 +208,28 @@ const FormStepper = ({ const setErrors = ({ inner = [], message = { word: 'Error' } } = {}) => { const errorsByPath = groupBy(inner, 'path') ?? {} - const totalErrors = Object.keys(errorsByPath).length + const totalErrors = Object.values(errorsByPath).reduce((count, value) => { + if (Array.isArray(value)) { + const filteredValue = value?.filter(Boolean) || [] + + return count + filteredValue?.length || 0 + } + + return count + }, 0) const translationError = totalErrors > 0 ? [T.ErrorsOcurred, totalErrors] : Object.values(message) - setError(stepId, { type: 'manual', message: translationError }) + const individualErrorMessages = inner.map((error) => error?.message ?? '') - inner?.forEach(({ path, type, errors: innerMessage }) => { + setError(stepId, { + type: 'manual', + message: translationError, + individualErrorMessages, + }) + + inner?.forEach(({ path, type, errors: innerMessage }, index) => { setError(`${stepId}.${path}`, { type, message: innerMessage }) }) } diff --git a/src/fireedge/src/client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesPanel.js b/src/fireedge/src/client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesPanel.js index cde67fdfa6..1ebeddc557 100644 --- a/src/fireedge/src/client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesPanel.js +++ b/src/fireedge/src/client/components/Forms/ServiceTemplate/CreateForm/Steps/Roles/rolesPanel.js @@ -40,9 +40,9 @@ const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => { onChange(updatedRole) } - const handleTextFieldChange = (event) => { + const handleTextFieldChange = (event, number = false) => { const { name, value } = event.target - handleInputChange(name, parseInt(value, 10)) + handleInputChange(name, number ? parseInt(value, 10) : value) } const handleAutocompleteChange = (event, value) => { @@ -79,7 +79,7 @@ const RoleVmVmPanel = ({ roles, onChange, selectedRoleIndex }) => { label={Tr(T.NumberOfVms)} name="CARDINALITY" value={selectedRole?.CARDINALITY || 0} - onChange={handleTextFieldChange} + onChange={(event) => handleTextFieldChange(event, true)} disabled={isDisabled} InputProps={{ inputProps: { diff --git a/src/fireedge/src/client/utils/translation.js b/src/fireedge/src/client/utils/translation.js index 77900928b1..896612c219 100644 --- a/src/fireedge/src/client/utils/translation.js +++ b/src/fireedge/src/client/utils/translation.js @@ -99,9 +99,9 @@ const buildTranslationLocale = () => { setLocale({ mixed: { - default: () => T['validation.mixed.default'], - required: () => T['validation.mixed.required'], - defined: () => T['validation.mixed.defined'], + default: ({ path }) => `${path} ${T['validation.mixed.default']}`, + required: ({ path }) => `${path} ${T['validation.mixed.required']}`, + defined: ({ path }) => `${path} ${T['validation.mixed.defined']}`, oneOf: ({ values }) => ({ word: T['validation.mixed.oneOf'], values }), notOneOf: ({ values }) => ({ word: T['validation.mixed.notOneOf'],