1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #3951: Add form validations (#196)

* Add form validations
* Spread input form types
This commit is contained in:
Sergio Betanzos 2020-09-10 10:27:48 +02:00 committed by GitHub
parent 8ba93bc902
commit fa536dc4fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 305 additions and 91 deletions

View File

@ -0,0 +1,57 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { FormControl, FormControlLabel, Checkbox } from '@material-ui/core';
import { Controller } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
const CheckboxController = memo(
({ control, cy, name, label, error }) => (
<Controller
render={({ onChange, value }) => (
<FormControl error={Boolean(error)}>
<FormControlLabel
control={
<Checkbox
onChange={e => onChange(e.target.checked)}
name={name}
checked={value}
color="primary"
inputProps={{ 'data-cy': cy }}
/>
}
label={label}
labelPlacement="end"
/>
{Boolean(error) && <ErrorHelper label={error?.message} />}
</FormControl>
)}
name={name}
control={control}
/>
),
(prevProps, nextProps) => prevProps.error === nextProps.error
);
CheckboxController.propTypes = {
control: PropTypes.object,
cy: PropTypes.string,
name: PropTypes.string.isRequired,
label: PropTypes.string,
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.objectOf(PropTypes.any)
])
};
CheckboxController.defaultProps = {
control: {},
cy: 'cy',
name: '',
label: '',
values: [],
error: false
};
export default CheckboxController;

View File

@ -0,0 +1,60 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { TextField, MenuItem } from '@material-ui/core';
import { Controller } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
const SelectController = memo(
({ control, cy, name, label, values, error }) => (
<Controller
as={
<TextField
select
SelectProps={{ displayEmpty: true }}
fullWidth
label={label}
inputProps={{ 'data-cy': cy }}
error={Boolean(error)}
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
style={{ marginTop: 12 }}
>
{Array.isArray(values) &&
values?.map(({ text, value }) => (
<MenuItem key={`${name}-${value}`} value={value ?? ''}>
{text}
</MenuItem>
))}
</TextField>
}
name={name}
control={control}
/>
),
(prevProps, nextProps) => prevProps.error === nextProps.error
);
SelectController.propTypes = {
control: PropTypes.object,
cy: PropTypes.string,
name: PropTypes.string.isRequired,
label: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.objectOf(PropTypes.any)
])
};
SelectController.defaultProps = {
control: {},
cy: 'cy',
name: '',
label: '',
values: [],
error: false
};
export default SelectController;

View File

@ -0,0 +1,48 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { TextField } from '@material-ui/core';
import { Controller } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
const TextController = memo(
({ control, cy, name, label, error }) => (
<Controller
as={
<TextField
fullWidth
label={label}
inputProps={{ 'data-cy': cy }}
error={Boolean(error)}
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
/>
}
name={name}
control={control}
/>
),
(prevProps, nextProps) => prevProps.error === nextProps.error
);
TextController.propTypes = {
control: PropTypes.object,
cy: PropTypes.string,
name: PropTypes.string.isRequired,
label: PropTypes.string,
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.objectOf(PropTypes.any)
])
};
TextController.defaultProps = {
control: {},
cy: 'cy',
name: '',
label: '',
error: false
};
export default TextController;

View File

@ -10,6 +10,9 @@ import {
Grid
} from '@material-ui/core';
import { Add } from '@material-ui/icons';
import { useFormContext } from 'react-hook-form';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
const useStyles = makeStyles(() => ({
cardPlus: {
@ -22,6 +25,7 @@ const useStyles = makeStyles(() => ({
function FormDialog({ step, data, setFormData }) {
const classes = useStyles();
const { errors } = useFormContext();
const [dialogFormData, setDialogFormData] = useState({});
const [showDialog, setShowDialog] = useState(false);
@ -78,17 +82,26 @@ function FormDialog({ step, data, setFormData }) {
return (
<Box component="form">
<Grid container spacing={3}>
{addCardAction && (
<Grid item xs={12} sm={4} md={3} lg={2}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={() => handleOpen()}>
<CardContent>
<Add />
</CardContent>
</CardActionArea>
</Card>
{typeof errors[id]?.message === 'string' && (
<Grid item xs={12}>
<ErrorHelper label={errors[id]?.message} />
</Grid>
)}
{addCardAction &&
React.useMemo(
() => (
<Grid item xs={12} sm={4} md={3} lg={2}>
<Card className={classes.cardPlus} raised>
<CardActionArea onClick={() => handleOpen()}>
<CardContent>
<Add />
</CardContent>
</CardActionArea>
</Card>
</Grid>
),
[handleOpen, classes]
)}
{Array.isArray(data) &&
data?.map((info, index) => (
<Grid key={`${id}-${index}`} item xs={12} sm={4} md={3} lg={2}>

View File

@ -2,33 +2,38 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Box, Grid } from '@material-ui/core';
import { useFormContext } from 'react-hook-form';
import ErrorHelper from '../FormControl/ErrorHelper';
function FormListSelect({ step, data, setFormData }) {
const { errors } = useFormContext();
const { id, onlyOneSelect, preRender, list, InfoComponent } = step;
useEffect(() => {
preRender && preRender();
}, []);
const handleSelect = index => {
// select index => add select to data form
const handleSelect = index =>
setFormData(prevData => ({
...prevData,
[id]: onlyOneSelect ? [index] : [...prevData[id], index]
}));
};
const handleUnselect = indexRemove => {
// unselect index => remove selected from data form
const handleUnselect = indexRemove =>
setFormData(prevData => ({
...prevData,
[id]: prevData[id]?.filter(index => index !== indexRemove)
}));
};
return (
<Box component="form">
<Grid container spacing={3}>
{typeof errors[id]?.message === 'string' && (
<Grid item xs={12}>
<ErrorHelper label={errors[id]?.message} />
</Grid>
)}
{Array.isArray(list) &&
list?.map((info, index) => (
<Grid key={`${id}-${index}`} item xs={6} sm={4} md={3} lg={1}>

View File

@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
import { debounce } from '@material-ui/core';
function FormStep({ step, data, setFormData }) {
const { reset, errors } = useFormContext();
@ -13,14 +12,7 @@ function FormStep({ step, data, setFormData }) {
reset({ [id]: data }, { errors: true });
}, []);
useEffect(() => {
// setFormData(prev => ({ ...prev, [id]: watch() }));
console.log('errors', errors);
}, [errors]);
const handleSubmit = dataForm => console.log(dataForm);
return React.useMemo(() => <FormComponent id={id} />, [id, errors]);
return React.useMemo(() => <FormComponent id={id} />, [id]);
}
FormStep.propTypes = {

View File

@ -6,16 +6,12 @@ import { useMediaQuery } from '@material-ui/core';
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper';
import CustomStepper from 'client/components/FormStepper/Stepper';
import { console } from 'window-or-global';
const FIRST_STEP = 0;
const FormStepper = ({ steps, initialValue, onSubmit }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'));
const {
watch,
formState: { isValid }
} = useFormContext();
const { watch, trigger, reset } = useFormContext();
const [activeStep, setActiveStep] = useState(FIRST_STEP);
const [formData, setFormData] = useState(initialValue);
@ -24,14 +20,23 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
const lastStep = useMemo(() => totalSteps - 1, [totalSteps]);
const disabledBack = useMemo(() => activeStep === FIRST_STEP, [activeStep]);
useEffect(() => {
reset({ ...formData }, { errors: true });
}, [formData]);
const handleNext = () => {
// TODO check if errors
if (activeStep === lastStep) {
onSubmit(formData);
} else if (isValid) {
setFormData(prevData => ({ ...prevData, ...watch() }));
setActiveStep(prevActiveStep => prevActiveStep + 1);
}
const { id } = steps[activeStep];
trigger(id).then(isValid => {
if (!isValid) return;
if (activeStep === lastStep) {
onSubmit(formData);
} else {
setFormData(prevData => ({ ...prevData, ...watch() }));
setActiveStep(prevActiveStep => prevActiveStep + 1);
}
});
};
const handleBack = useCallback(() => {
@ -62,7 +67,6 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
handleBack={handleBack}
/>
)}
{/* FORM CONTENT */}
{React.useMemo(() => {
const { id, content: Content } = steps[activeStep];
@ -70,10 +74,9 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
return (
Content && (
<Content
formData={formData}
step={steps[activeStep]}
data={formData[id]}
setFormData={setFormData}
step={steps[activeStep]}
/>
)
);
@ -97,7 +100,7 @@ FormStepper.propTypes = {
FormStepper.defaultProps = {
steps: [],
initialValue: {},
onSubmit: dataForm => console.log(dataForm)
onSubmit: console.log
};
export default FormStepper;

View File

@ -1,55 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Grid,
TextField,
MenuItem,
FormControlLabel,
Checkbox
} from '@material-ui/core';
import { useFormContext, Controller } from 'react-hook-form';
import { Grid } from '@material-ui/core';
import { useFormContext } from 'react-hook-form';
import { TYPE_INPUT } from 'client/constants';
import ErrorHelper from 'client/components/FormControl/ErrorHelper';
import TextController from 'client/components/FormControl/TextController';
import SelectController from 'client/components/FormControl/SelectController';
import CheckboxController from 'client/components/FormControl/CheckboxController';
const InputController = {
[TYPE_INPUT.TEXT]: TextController,
[TYPE_INPUT.SELECT]: SelectController,
[TYPE_INPUT.CHECKBOX]: CheckboxController
};
const FormWithSchema = ({ id, cy, schema }) => {
const { register, control, errors } = useFormContext();
const { control, errors } = useFormContext();
return (
<Grid container spacing={3}>
{schema?.map(({ name, type, label, values }) => (
<Grid key={`${cy}-${name}`} item xs={12} md={6}>
{(type === TYPE_INPUT.TEXT || type === TYPE_INPUT.SELECT) && (
<Controller
as={
<TextField
fullWidth
select={type === TYPE_INPUT.SELECT}
label={label}
inputProps={{ 'data-cy': `${cy}-${name}` }}
error={errors[name]}
helperText={
errors[name] && (
<ErrorHelper label={errors[name]?.message} />
)
}
FormHelperTextProps={{ 'data-cy': `${cy}-${name}-error` }}
>
{type === TYPE_INPUT.SELECT &&
values?.map(({ text, value }) => (
<MenuItem key={`${name}-${value}`} value={`${value}`}>
{text}
</MenuItem>
))}
</TextField>
}
name={`${id}.${name}`}
control={control}
/>
)}
</Grid>
))}
{schema?.map(({ name, type, label, values }) => {
const dataCy = `${cy}-${name}`;
const inputName = id ? `${id}.${name}` : name;
const formError = id ? errors[id] : errors;
const inputError = formError ? formError[name] : false;
return (
<Grid key={`${cy}-${name}`} item xs={12} md={6}>
{React.createElement(InputController[type], {
control,
cy: dataCy,
name: inputName,
label,
values,
error: inputError
})}
</Grid>
);
})}
</Grid>
);
};

View File

@ -50,13 +50,11 @@ const MainLayout = ({ children }) => {
// PROTECTED ROUTE
if (authRoute && !isLogged && !isLoginInProcess) {
console.log('protected route needs redirect to LOGIN');
return <Redirect to={PATH.LOGIN} />;
}
// PUBLIC ROUTE
if (!authRoute && isLogged && !isLoginInProcess) {
console.log('public route needs redirect to DASHBOARD');
return <Redirect to={PATH.DASHBOARD} />;
}

View File

@ -8,13 +8,15 @@ import Steps from 'client/containers/Application/Create/steps';
import { Container } from '@material-ui/core';
function ApplicationCreate() {
const { steps, defaultValues } = Steps();
const { steps, defaultValues, resolvers } = Steps();
const methods = useForm({
mode: 'onBlur',
defaultValues
defaultValues,
resolver: yupResolver(resolvers)
});
const onSubmit = formData => console.log('submit', formData);
const onSubmit = formData => console.log('submit', formData, methods.errors);
return (
<Container disableGutters>

View File

@ -6,7 +6,7 @@ export const STRATEGIES_DEPLOY = [
];
export const SHUTDOWN_ACTIONS = [
{ text: 'None', value: '' },
{ text: 'None', value: 'none' },
{ text: 'Terminate', value: 'shutdown' },
{ text: 'Terminate hard', value: 'shutdown-hard' }
];
@ -36,7 +36,7 @@ export default [
name: 'shutdown_action',
label: 'Select a VM shutdown action',
type: TYPE_INPUT.SELECT,
initial: SHUTDOWN_ACTIONS[1].value,
initial: SHUTDOWN_ACTIONS[0].value,
values: SHUTDOWN_ACTIONS
},
{

View File

@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
import * as yup from 'yup';
import useOpennebula from 'client/hooks/useOpennebula';
@ -9,7 +10,10 @@ import FormDialog from 'client/components/FormStepper/FormDialog';
import FormListSelect from 'client/components/FormStepper/FormListSelect';
import FormWithSchema from 'client/components/Forms/FormWithSchema';
import Schema from 'client/containers/Application/Create/schema';
import Schema, {
SHUTDOWN_ACTIONS,
STRATEGIES_DEPLOY
} from 'client/containers/Application/Create/schema';
function Steps() {
const {
@ -30,6 +34,22 @@ function Steps() {
(val, { name, initial }) => ({ ...val, [name]: initial }),
{}
),
resolver: yup.object().shape({
name: yup
.string()
.min(5)
.trim()
.required('is required'),
description: yup.string().trim(),
deployment: yup
.string()
.required()
.oneOf(STRATEGIES_DEPLOY.map(({ value }) => value)),
shutdown_action: yup
.string()
.oneOf(SHUTDOWN_ACTIONS.map(({ value }) => value)),
ready_status_gate: yup.boolean()
}),
FormComponent: props => (
<FormWithSchema cy="form-flow" schema={Schema} {...props} />
)
@ -43,6 +63,10 @@ function Steps() {
getVNetworksTemplates();
},
defaultValue: [],
resolver: yup
.array()
.min(2)
.required(),
addCardAction: true,
DEFAULT_DATA: {
mandatory: true,
@ -61,6 +85,10 @@ function Steps() {
content: FormDialog,
preRender: getTemplates,
defaultValue: [],
resolver: yup
.array()
.min(1)
.required(),
addCardAction: true,
DEFAULT_DATA: {
name: 'Master_dev',
@ -77,6 +105,11 @@ function Steps() {
label: 'Where will it run?',
content: FormListSelect,
defaultValue: [],
resolver: yup
.array()
.min(1)
.max(1)
.required(),
onlyOneSelect: true,
preRender: getClusters,
list: clusters,
@ -95,7 +128,21 @@ function Steps() {
[steps]
);
return { steps, defaultValues };
const resolvers = useMemo(
() =>
yup
.object()
.shape(
steps.reduce(
(values, { id, resolver }) => ({ ...values, [id]: resolver }),
{}
)
)
.required(),
[steps]
);
return { steps, defaultValues, resolvers };
}
export default Steps;