mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
* Add form validations * Spread input form types
This commit is contained in:
parent
8ba93bc902
commit
fa536dc4fa
@ -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;
|
@ -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;
|
@ -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;
|
@ -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}>
|
||||
|
@ -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}>
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user