diff --git a/src/fireedge/src/public/components/FormControl/CheckboxController.js b/src/fireedge/src/public/components/FormControl/CheckboxController.js new file mode 100644 index 0000000000..e446066839 --- /dev/null +++ b/src/fireedge/src/public/components/FormControl/CheckboxController.js @@ -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 }) => ( + ( + + onChange(e.target.checked)} + name={name} + checked={value} + color="primary" + inputProps={{ 'data-cy': cy }} + /> + } + label={label} + labelPlacement="end" + /> + {Boolean(error) && } + + )} + 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; diff --git a/src/fireedge/src/public/components/FormControl/SelectController.js b/src/fireedge/src/public/components/FormControl/SelectController.js new file mode 100644 index 0000000000..cb82c54bb7 --- /dev/null +++ b/src/fireedge/src/public/components/FormControl/SelectController.js @@ -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 }) => ( + } + FormHelperTextProps={{ 'data-cy': `${cy}-error` }} + style={{ marginTop: 12 }} + > + {Array.isArray(values) && + values?.map(({ text, value }) => ( + + {text} + + ))} + + } + 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; diff --git a/src/fireedge/src/public/components/FormControl/TextController.js b/src/fireedge/src/public/components/FormControl/TextController.js new file mode 100644 index 0000000000..3721c31e50 --- /dev/null +++ b/src/fireedge/src/public/components/FormControl/TextController.js @@ -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 }) => ( + } + 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; diff --git a/src/fireedge/src/public/components/FormStepper/FormDialog.js b/src/fireedge/src/public/components/FormStepper/FormDialog.js index 9cebd7a399..9151fa5a96 100644 --- a/src/fireedge/src/public/components/FormStepper/FormDialog.js +++ b/src/fireedge/src/public/components/FormStepper/FormDialog.js @@ -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 ( - {addCardAction && ( - - - handleOpen()}> - - - - - + {typeof errors[id]?.message === 'string' && ( + + )} + {addCardAction && + React.useMemo( + () => ( + + + handleOpen()}> + + + + + + + ), + [handleOpen, classes] + )} {Array.isArray(data) && data?.map((info, index) => ( diff --git a/src/fireedge/src/public/components/FormStepper/FormListSelect.js b/src/fireedge/src/public/components/FormStepper/FormListSelect.js index e9e433574c..6e6b18f9b7 100644 --- a/src/fireedge/src/public/components/FormStepper/FormListSelect.js +++ b/src/fireedge/src/public/components/FormStepper/FormListSelect.js @@ -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 ( + {typeof errors[id]?.message === 'string' && ( + + + + )} {Array.isArray(list) && list?.map((info, index) => ( diff --git a/src/fireedge/src/public/components/FormStepper/FormStep.js b/src/fireedge/src/public/components/FormStepper/FormStep.js index 9872bdd230..34cd93631d 100644 --- a/src/fireedge/src/public/components/FormStepper/FormStep.js +++ b/src/fireedge/src/public/components/FormStepper/FormStep.js @@ -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(() => , [id, errors]); + return React.useMemo(() => , [id]); } FormStep.propTypes = { diff --git a/src/fireedge/src/public/components/FormStepper/index.js b/src/fireedge/src/public/components/FormStepper/index.js index 637fc0f0af..595d89cd3a 100644 --- a/src/fireedge/src/public/components/FormStepper/index.js +++ b/src/fireedge/src/public/components/FormStepper/index.js @@ -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 && ( ) ); @@ -97,7 +100,7 @@ FormStepper.propTypes = { FormStepper.defaultProps = { steps: [], initialValue: {}, - onSubmit: dataForm => console.log(dataForm) + onSubmit: console.log }; export default FormStepper; diff --git a/src/fireedge/src/public/components/Forms/FormWithSchema.js b/src/fireedge/src/public/components/Forms/FormWithSchema.js index b2f4a384c8..a1bb4e339a 100644 --- a/src/fireedge/src/public/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/public/components/Forms/FormWithSchema.js @@ -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 ( - {schema?.map(({ name, type, label, values }) => ( - - {(type === TYPE_INPUT.TEXT || type === TYPE_INPUT.SELECT) && ( - - ) - } - FormHelperTextProps={{ 'data-cy': `${cy}-${name}-error` }} - > - {type === TYPE_INPUT.SELECT && - values?.map(({ text, value }) => ( - - {text} - - ))} - - } - name={`${id}.${name}`} - control={control} - /> - )} - - ))} + {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 ( + + {React.createElement(InputController[type], { + control, + cy: dataCy, + name: inputName, + label, + values, + error: inputError + })} + + ); + })} ); }; diff --git a/src/fireedge/src/public/components/HOC/MainLayout.js b/src/fireedge/src/public/components/HOC/MainLayout.js index 4bd9d16d7b..80965952fd 100644 --- a/src/fireedge/src/public/components/HOC/MainLayout.js +++ b/src/fireedge/src/public/components/HOC/MainLayout.js @@ -50,13 +50,11 @@ const MainLayout = ({ children }) => { // PROTECTED ROUTE if (authRoute && !isLogged && !isLoginInProcess) { - console.log('protected route needs redirect to LOGIN'); return ; } // PUBLIC ROUTE if (!authRoute && isLogged && !isLoginInProcess) { - console.log('public route needs redirect to DASHBOARD'); return ; } diff --git a/src/fireedge/src/public/containers/Application/Create/index.js b/src/fireedge/src/public/containers/Application/Create/index.js index 998ac536ff..fb720174c1 100644 --- a/src/fireedge/src/public/containers/Application/Create/index.js +++ b/src/fireedge/src/public/containers/Application/Create/index.js @@ -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 ( diff --git a/src/fireedge/src/public/containers/Application/Create/schema.js b/src/fireedge/src/public/containers/Application/Create/schema.js index 03d4767632..2ec70e916a 100644 --- a/src/fireedge/src/public/containers/Application/Create/schema.js +++ b/src/fireedge/src/public/containers/Application/Create/schema.js @@ -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 }, { diff --git a/src/fireedge/src/public/containers/Application/Create/steps.js b/src/fireedge/src/public/containers/Application/Create/steps.js index 4ef54e998e..1129d91581 100644 --- a/src/fireedge/src/public/containers/Application/Create/steps.js +++ b/src/fireedge/src/public/containers/Application/Create/steps.js @@ -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 => ( ) @@ -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;