Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/widget validation #86

Merged
merged 12 commits into from
Nov 13, 2019
7 changes: 4 additions & 3 deletions cogboard-webapp/src/components/AddWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ const AddWidget = ({ closeDialog }) => {
const currentBoardId = useSelector(({ ui }) => ui.currentBoard);
const dispatch = useDispatch();

const handleAddClick = (values) => () => {
const handleSubmit = (values) => {
dispatch(addNewWidget({ currentBoardId, values }));
closeDialog();
};

return (
<WidgetForm
renderActions={values =>
onSubmit={handleSubmit}
renderActions={() =>
<>
<Button
onClick={handleAddClick(values)}
type="submit"
color="primary"
variant="contained"
data-cy="widget-form-submit-button"
Expand Down
2 changes: 1 addition & 1 deletion cogboard-webapp/src/components/BoardForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const BoardForm = ({ onSubmit, renderActions, boardId, ...initialFormValues }) =
const {values, handleChange, handleSubmit, errors} = useFormData(initialFormValues, validationSchema, true);

return (
<form onSubmit={handleSubmit(onSubmit)} novalidate="novalidate">
<form onSubmit={handleSubmit(onSubmit)} noValidate="novalidate">
goeson00 marked this conversation as resolved.
Show resolved Hide resolved
<StyledFieldset component="fieldset">
<TextField
onChange={handleChange('title')}
Expand Down
7 changes: 4 additions & 3 deletions cogboard-webapp/src/components/EditWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ const EditWidget = ({ closeDialog, id, widgetTypeData, ...widgetData }) => {
const initialFormValues = { ...widgetData, ...widgetTypeData };
const dispatch = useDispatch();

const handleSaveClick = (values) => () => {
const handleSubmit = (values) => {
dispatch(saveWidget({ id, values }));
closeDialog();
};

return (
<WidgetForm
onSubmit={handleSubmit}
{...initialFormValues}
renderActions={values =>
renderActions={() =>
goeson00 marked this conversation as resolved.
Show resolved Hide resolved
<>
<Button
onClick={handleSaveClick(values)}
type="submit"
color="primary"
variant="contained"
data-cy="widget-form-submit-button"
Expand Down
4 changes: 2 additions & 2 deletions cogboard-webapp/src/components/ValidationMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const ValidationMessages = ({className, messages, ...others}) => {
return (
<ul className={className} {...others}>
{
messages.map(message =>
<li>{message}</li>)
messages.map((message, index) =>
goeson00 marked this conversation as resolved.
Show resolved Hide resolved
<li key={index}>{message}</li>)
}
</ul>
);
Expand Down
64 changes: 46 additions & 18 deletions cogboard-webapp/src/components/WidgetForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import { useSelector } from 'react-redux';
import widgetTypes from '../widgets';
import { useFormData } from '../../hooks';
import { sortByKey } from "../helpers";
import { COLUMNS_MIN, ROWS_MIN } from '../../constants';

import { createValidationSchema, validationSchemaModificator } from './validators';

import { Box, FormControlLabel, FormControl, TextField, Switch, Tab } from '@material-ui/core';
import DropdownField from '../DropdownField';
import WidgetTypeForm from '../WidgetTypeForm';
import { StyledNumberField, StyledTabPanel, StyledTabs } from './styled';
import { StyledNumberField, StyledTabPanel, StyledTabs, StyledValidationMessages } from './styled';
import { renderWidgetTypesMenu } from './helpers';
import { StyledFieldset } from '../styled';

const WidgetForm = ({ renderActions, ...initialFormValues }) => {
const WidgetForm = ({ onSubmit, renderActions, ...initialFormValues }) => {
const boardColumns = useSelector(
({ ui, boards }) => boards.boardsById[ui.currentBoard].columns
);
const { values, handleChange } = useFormData(initialFormValues);

const initialValidationSchema = createValidationSchema(boardColumns);
const typedValidationSchema = validationSchemaModificator(initialFormValues.type, initialValidationSchema)

const { values, handleChange, handleSubmit, errors, setValidationSchema } = useFormData(initialFormValues, typedValidationSchema, true);
const [tabValue, setTabValue] = useState(0);

const widgetType = widgetTypes[values.type];
Expand All @@ -29,8 +34,17 @@ const WidgetForm = ({ renderActions, ...initialFormValues }) => {
setTabValue(newValue);
};

const handleTypeChange = (handler) => (event) => {
const { target: {value} } = event;

const validationSchemaModified = validationSchemaModificator(value, initialValidationSchema)
setValidationSchema(validationSchemaModified)

handler(event);
}

return (
<>
<form onSubmit={handleSubmit(onSubmit)} noValidate="novalidate">
<StyledTabs
value={tabValue}
onChange={handleTabChange}
Expand All @@ -42,7 +56,7 @@ const WidgetForm = ({ renderActions, ...initialFormValues }) => {
<StyledTabPanel value={tabValue} index={0}>
<StyledFieldset component="fieldset">
<DropdownField
onChange={handleChange("type")}
onChange={handleTypeChange(handleChange("type"))}
label="Type"
id="widget-type"
name="type"
Expand All @@ -61,6 +75,13 @@ const WidgetForm = ({ renderActions, ...initialFormValues }) => {
label="Title"
margin="normal"
value={values.title}
error={errors.title !== undefined}
FormHelperTextProps={{component: 'div'}}
helperText={
<StyledValidationMessages
messages={errors.title}
data-cy={'widget-form-title-error'}
/>}
inputProps={{'data-cy': 'widget-form-title-input'}}
/>
<Box
Expand All @@ -73,31 +94,37 @@ const WidgetForm = ({ renderActions, ...initialFormValues }) => {
InputLabelProps={{
shrink: true
}}
inputProps={{
min: COLUMNS_MIN,
max: boardColumns,
'data-cy': 'widget-form-columns-input'
}}
inputProps={{ 'data-cy': 'widget-form-columns-input' }}
label="Columns"
margin="normal"
value={values.columns}
type="number"
error={errors.columns !== undefined}
FormHelperTextProps={{component: 'div'}}
helperText={
<StyledValidationMessages
messages={errors.columns}
data-cy={'widget-form-columns-error'}
/>}
/>
<StyledNumberField
onChange={handleChange('rows')}
id="rows"
InputLabelProps={{
shrink: true
}}
inputProps={{
min: ROWS_MIN,
max: 4,
'data-cy': 'widget-form-rows-input'
}}
inputProps={{ 'data-cy': 'widget-form-rows-input' }}
label="Rows"
margin="normal"
value={values.rows}
type="number"
error={errors.rows !== undefined}
FormHelperTextProps={{component: 'div'}}
helperText={
<StyledValidationMessages
messages={errors.rows}
data-cy={'widget-form-rows-error'}
/>}
/>
</Box>
<FormControl margin="normal">
Expand Down Expand Up @@ -135,12 +162,13 @@ const WidgetForm = ({ renderActions, ...initialFormValues }) => {
<WidgetTypeForm
type={values.type}
values={values}
errors={errors}
handleChange={handleChange}
/>
</StyledTabPanel>
}
{renderActions(values)}
</>
{renderActions()}
</form>
);
};

Expand Down
7 changes: 7 additions & 0 deletions cogboard-webapp/src/components/WidgetForm/styled.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from '@emotion/styled/macro';
import { TextField, Tabs } from '@material-ui/core';
import TabPanel from '../TabPanel';
import ValidationMessages from '../ValidationMessages';

export const StyledNumberField = styled(TextField)`
flex-basis: calc(50% - 18px);
Expand All @@ -12,4 +13,10 @@ export const StyledTabPanel = styled(TabPanel)`

export const StyledTabs = styled(Tabs)`
margin-bottom: 12px;
`

export const StyledValidationMessages = styled(ValidationMessages)`
list-style-type: none;
margin: 0;
padding: 0;
`
73 changes: 73 additions & 0 deletions cogboard-webapp/src/components/WidgetForm/validators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { object, string, number, boolean } from 'yup';

import { splitPropsGroupName } from '../helpers';

import { WIDGET_COLUMNS_MIN, WIDGET_ROWS_MIN, WIDGET_ROWS_MAX, WIDGET_TITLE_LENGTH_LIMIT } from '../../constants';
import widgetTypes from '../widgets';
import { dialogFieldsValidators } from '../widgets/dialogFields/validators';

const MAX_TITLE_LENGTH = `Title length must be less or equal to ${WIDGET_TITLE_LENGTH_LIMIT}.`;
const MIN_TITLE_LENGTH = 'Title cannot be empty.';
const REQUIRED_TITLE = 'Title is a required field.';

const COLUMNS_NUMBER_MIN = `Columns number cannot be less than ${WIDGET_COLUMNS_MIN}.`;
const COLUMNS_REQUIRED = `Columns is a required field.`;

const ROWS_NUMBER_MIN = `Rows number cannot be less than ${WIDGET_ROWS_MIN}.`;
const ROWS_NUMBER_MAX = `Rows number cannot be more than ${WIDGET_ROWS_MAX}.`;
const ROWS_REQUIRED = `Rows is a required field.`;

export const createValidationSchema = ( boardColumns ) => {

const COLUMNS_NUMBER_MAX = `Columns number cannot be more than ${boardColumns}.`;

return object().shape({
type: string()
.required(),
title: string()
.trim()
.max(WIDGET_TITLE_LENGTH_LIMIT, MAX_TITLE_LENGTH)
.min(1, MIN_TITLE_LENGTH)
.required(REQUIRED_TITLE),
columns: number()
.min(WIDGET_COLUMNS_MIN, COLUMNS_NUMBER_MIN)
.max(boardColumns, COLUMNS_NUMBER_MAX)
.required(COLUMNS_REQUIRED),
rows: number()
.min(WIDGET_ROWS_MIN, ROWS_NUMBER_MIN)
.max(WIDGET_ROWS_MAX, ROWS_NUMBER_MAX)
.required(ROWS_REQUIRED),
goNewLine: boolean()
.required(),
disabled: boolean()
.required()
})
}

export const validationSchemaModificator = (type, validationSchema) => {
const widgetType = widgetTypes[type];
const dialogFieldNames = (widgetType && widgetType.dialogFields) ? widgetType.dialogFields : [];

const validators = dialogFieldNames.reduce((schema, fieldName) => {
const { name, validator } = dialogFieldsValidators[fieldName];
const [groupName, propName] = splitPropsGroupName(name);
let newSchema;

if (groupName) {
const groupSchema = schema[groupName] !== undefined ? schema[groupName] : object({});

newSchema = {
...schema,
[groupName]: groupSchema.shape({[propName]: validator})
};
} else {
newSchema = { ...schema, [propName]: validator};
}

return newSchema;
}, {})

const newValidationSchema = validationSchema.shape(validators)

return newValidationSchema
}
3 changes: 2 additions & 1 deletion cogboard-webapp/src/components/WidgetTypeForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const StyledFieldset = styled(FormControl)`
min-width: 300px;
`;

const WidgetTypeForm = ({ values, type, handleChange }) => {
const WidgetTypeForm = ({ values, type, handleChange, errors }) => {
const widgetType = widgetTypes[type];
const dialogFieldNames = (widgetType && widgetType.dialogFields) ? widgetType.dialogFields : [];
const hasDialogFields = dialogFieldNames.length !== 0;
Expand All @@ -37,6 +37,7 @@ const WidgetTypeForm = ({ values, type, handleChange }) => {
key={name}
value={valueRef}
onChange={handleChange(name)}
error={errors[name]}
{...dialogFieldProps}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';

import { Input, InputLabel, Checkbox, MenuItem, ListItemText, Select } from '@material-ui/core';
import { Input, InputLabel, Checkbox, MenuItem, ListItemText, Select, FormHelperText } from '@material-ui/core';

import { StyledFormControl } from './../../styled';
import { AEM_HEALTH_CHECKS } from '../../../constants';

const AemHealthcheckInput = props => {
const { onChange, value } = props;
const AemHealthcheckInput = ({ onChange, value, error }) => {
const inputId = 'aemhealthcheck-metrics-input';
const hasError = error !== undefined;

return (
<StyledFormControl>
<StyledFormControl error={ hasError }>
<InputLabel htmlFor={inputId}>Health Checks</InputLabel>
<Select
multiple
Expand All @@ -26,6 +26,7 @@ const AemHealthcheckInput = props => {
</MenuItem>
)}
</Select>
{hasError && <FormHelperText>{error}</FormHelperText>}
</StyledFormControl>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React from 'react';

import TextField from '@material-ui/core/TextField';
import { StyledValidationMessages } from '../../WidgetForm/styled';

const MultilineTextInput = props => {
const MultilineTextInput = ({ error, ...other }) => {
return (
<TextField
InputLabelProps={{
shrink: true,
}}
margin="normal"
multiline={true}
{...props}
{...other}
goeson00 marked this conversation as resolved.
Show resolved Hide resolved
error={error !== undefined}
FormHelperTextProps={{component: 'div'}}
helperText={
<StyledValidationMessages
messages={error}
data-cy={'widget-form-rows-error'}
/>}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';

import TextField from '@material-ui/core/TextField';
import { StyledValidationMessages } from '../../WidgetForm/styled';

const NumberInput = (props) => {
const { min, step, ...other } = props;
const NumberInput = ({ min, step, error, ...other }) => {

return (
<TextField
Expand All @@ -14,6 +14,13 @@ const NumberInput = (props) => {
}}
margin="normal"
{...other}
error={error !== undefined}
FormHelperTextProps={{component: 'div'}}
helperText={
<StyledValidationMessages
messages={error}
data-cy={'widget-form-rows-error'}
/>}
/>
);
};
Expand Down
Loading