Commit 42b17a10 authored by Matija Obreza's avatar Matija Obreza
Browse files

Added client-side form validation

parent 1922b566
......@@ -5,8 +5,8 @@ import {log} from 'utilities/debug';
import {Partner} from 'model/partner.model';
import Input, {InputLabel} from 'material-ui/Input';
import {FormControl} from 'material-ui/Form';
import Input from 'material-ui/Input';
import FormControl from 'ui/common/forms/FormControl';
import Select from 'material-ui/Select';
import { MenuItem } from 'material-ui/Menu';
......@@ -25,6 +25,8 @@ interface ISelectPartnerProps extends React.ClassAttributes<any> {
partners: Partner[];
label: string;
editable?: boolean;
required?: boolean;
meta?: any;
}
class SelectPartner extends React.Component<ISelectPartnerProps, any> {
......@@ -39,26 +41,24 @@ class SelectPartner extends React.Component<ISelectPartnerProps, any> {
}
public render() {
const {classes, input, partners, label, editable} = this.props;
const {classes, input, partners, label, editable, required, meta } = this.props;
const allowsEdit = editable === undefined || editable;
if (! allowsEdit) {
return (
<FormControl fullWidth>
<InputLabel>{ label }</InputLabel>
<FormControl fullWidth label={ label }>
<Input value={ input.value && input.value.name } className={ classes.input } disabled />
</FormControl>
);
}
// console.log(`Partner req=${required}`, meta);
// <MenuItem value="" />
return (
<FormControl fullWidth>
<InputLabel>{ label }</InputLabel>
<Select className={ classes.select } value={ input.value && input.value.uuid } onChange={ this.handleChange }
<FormControl fullWidth required={ required } meta={ meta } label={ label }>
<Select error={ meta.touched && meta.error } className={ classes.select } value={ input.value && input.value.uuid } onChange={ this.handleChange }
input={ <Input /> }
>
<MenuItem value="" />
{ partners && partners.map((p) => <MenuItem key={ p.uuid } value={ p.uuid }>{ p.name }</MenuItem>) }
</Select>
</FormControl>
......
......@@ -30,11 +30,11 @@ const renderMembers = ({ fields, itemLabel, itemEditor, addItem, removeItem }) =
<div>
{ fields && fields.map((member, index, fields) => (
<div key={ index } className="items-editor-item">
<Grid container justify="space-between" alignItems="flex-end">
<Grid item xs={ 10 } lg={ 11 }>
<Grid container justify="space-between" alignItems="baseline">
<Grid item xs={ 10 } md={ 11 }>
{ itemEditor(member, index, fields, itemLabel) }
</Grid>
<Grid item xs={ 2 } lg={ 1 }>
<Grid item xs={ 2 } md={ 1 }>
<Button type="button" onClick={ onRemoveMember(member, index) }>Remove</Button>
</Grid>
</Grid>
......
import * as React from 'react';
import { withStyles } from 'material-ui/styles';
import InputLabel from 'material-ui/Input/InputLabel';
import MuiFormControl from 'material-ui/Form/FormControl';
interface IProps {
classes?: any;
fullWidth?: boolean;
required?: boolean;
label?: string;
children: any;
meta?: any;
}
const styles = (theme) => {
console.log(theme);
return ({
errors: {
color: theme.palette.error.A400,
// margin: '2rem',
},
});
};
const FormControl = ({ fullWidth = false, required = false, label = null, children, meta = {}, classes }: IProps) => {
// console.log('Meta', meta);
const { touched, error, warning } = meta;
return (
<MuiFormControl fullWidth={ fullWidth }>
{ label && <InputLabel error={ touched && error && true } required={ required }>{ label }</InputLabel> }
{ children }
{
((error && <h6 className={ touched ? classes.errors : '' }>{ error }</h6>) || (warning && <h6 className={ touched ? classes.errors : '' }>{ warning }</h6>))
}
</MuiFormControl>
);
};
export default withStyles(styles)(FormControl);
import FormControl from './FormControl';
import Toggle from './Toggle';
export { FormControl, Toggle };
import * as React from 'react';
import * as MarkdownComponent from 'react-markdown';
import Input from 'material-ui/Input';
import InputLabel from 'material-ui/Input/InputLabel';
import FormControl from 'material-ui/Form/FormControl';
import FormControl from 'ui/common/forms/FormControl';
interface IMarkdownTextProps extends React.HTMLProps<HTMLElement> {
source: string;
......@@ -37,12 +36,11 @@ export default function Markdown({source, textLength, style, className, basic}:
}
}
export const MarkdownField = ({basicMarkdown, input, InputLabelProps, label, meta: {touched, error}, ...custom}) => {
export const MarkdownField = ({basicMarkdown, input, InputLabelProps, label, required, meta, meta: {touched, error}, ...custom}) => {
const basic: boolean = basicMarkdown === undefined || null ? false : basicMarkdown;
return (
<FormControl fullWidth>
<InputLabel { ...InputLabelProps }>{ label }</InputLabel>
<Input multiline={ !basic } { ...input } { ...custom } />
<FormControl fullWidth required={ required } meta={ meta } label={ label }>
<Input error={ touched && error } multiline={ !basic } { ...input } { ...custom } />
<h6>{ basic ? 'Basic markdown supported: * **' : 'Full markdown supported' }</h6>
</FormControl>
);
......
import * as React from 'react';
import Input from 'material-ui/Input';
import InputLabel from 'material-ui/Input/InputLabel';
import FormControl from 'material-ui/Form/FormControl';
import FormControl from 'ui/common/forms/FormControl';
export const TextField = ({input, InputLabelProps, label, meta: {touched, error}, ...custom}) => (
export const TextField = ({input, InputLabelProps, label, meta, required, ...custom}) => (
<FormControl fullWidth>
<InputLabel { ...InputLabelProps }>{ label }</InputLabel>
<Input { ...input } { ...custom } />
<FormControl fullWidth required={ required } label={ label } meta={ meta }>
<Input error={ meta.touched && meta.error && true } { ...input } { ...custom } />
</FormControl>
);
......@@ -5,6 +5,7 @@ import Button from 'material-ui/Button';
import {CROP_FORM} from 'constants/crop';
import {TextField} from 'ui/common/text-field';
import Validators from 'utilities/Validators';
const CropForm = ({error, handleSubmit, initialValues, onDelete}) => {
......@@ -12,8 +13,8 @@ const CropForm = ({error, handleSubmit, initialValues, onDelete}) => {
<form onSubmit={ handleSubmit }>
{ initialValues && initialValues.version && <div>Version: { initialValues.version }</div> }
<Field name="code" label="Crop code" component={ TextField }/>
<Field required name="title" label="Crop title" component={ TextField }/>
<Field required name="code" label="Crop code" component={ TextField } validate={ [ Validators.required, Validators.maxLength20 ] } />
<Field required name="title" label="Crop title" component={ TextField } validate={ [ Validators.required ] } />
<div>{ error && <strong>{ error }</strong> }</div>
<Button raised type="submit">Save changes</Button>
......
......@@ -11,6 +11,7 @@ interface IStepNavigationProps extends React.ClassAttributes<any> {
uuid: string;
location: any;
disabled: boolean;
onPublish: () => void;
onGotoStep: (i: number) => () => void;
showStepName: boolean;
......@@ -70,7 +71,7 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
public render() {
const {classes, steps, showStepName, topDivider, bottomDivider, onGotoStep, onPublish} = this.props;
const {classes, disabled, steps, showStepName, topDivider, bottomDivider, onGotoStep, onPublish} = this.props;
return (
<Grid container spacing={ 0 } className={ classes.root }>
......@@ -89,12 +90,12 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
)
}
{ this.state.id !== steps.length && (
<Button raised onClick={ onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
<Button disabled={ disabled } raised onClick={ onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
NEXT STEP
</Button>
) }
{ this.state.id === steps.length && (
<Button raised onClick={ onPublish } className={ classes.btnBlue }>
<Button disabled={ disabled } raised onClick={ onPublish } className={ classes.btnBlue }>
ACCEPT AND PUBLISH
</Button>
) }
......
......@@ -91,9 +91,13 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
const {dataset, uuid, navigateTo} = this.props;
const path = steps.find((e) => e.id === id).link;
log('Go to step', path);
navigateTo(`/datasets/${dataset ? dataset.uuid : uuid}/${path}`);
steps.forEach((i) => i.active = false);
steps[id - 1].active = true;
if (uuid || (dataset && dataset.uuid)) {
navigateTo(`/datasets/${dataset ? dataset.uuid : uuid}/${path}`);
steps.forEach((i) => i.active = false);
steps[id - 1].active = true;
} else {
// no navigation!
}
}
public onPublish = (e) => {
......@@ -122,7 +126,7 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
<TopSection/>
<Grid item xs={ 12 } md={ 9 } xl={ 10 } className="back-gray p-20">
<Grid container spacing={ 0 } className="back-white">
<StepNavigation onGotoStep={ this.gotoStep } steps={ steps } location={ location } showStepName bottomDivider onPublish={ this.onPublish } />
<StepNavigation disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } steps={ steps } location={ location } showStepName bottomDivider onPublish={ this.onPublish } />
<Grid item xs={ 12 }>
{ stillLoading ? <Loading /> :
<div>
......@@ -130,11 +134,11 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
</div>
}
</Grid>
<StepNavigation onGotoStep={ this.gotoStep } steps={ steps } location={ location } topDivider onPublish={ this.onPublish } />
<StepNavigation disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } steps={ steps } location={ location } topDivider onPublish={ this.onPublish } />
</Grid>
</Grid>
<Grid item xs={ 12 } md={ 3 } xl={ 2 }>
<ProgressMenu onGotoStep={ this.gotoStep } steps={ steps } location={ location } />
<ProgressMenu disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } steps={ steps } location={ location } />
</Grid>
<BottomSection/>
</Grid>
......
......@@ -7,6 +7,7 @@ import StepProgress from './StepProgress';
interface ICustomListItemProps extends React.ClassAttributes<any> {
classes?: any;
disabled: boolean;
index: number;
name: string;
completed: boolean;
......@@ -34,14 +35,14 @@ class CustomListItem extends React.Component<ICustomListItemProps, any> {
public render() {
const { active, classes, index, name, onClick } = this.props;
const { active, disabled, classes, index, name, onClick } = this.props;
return (
<div className={ classes.root }>
{
active ? <span className={ `arrow` }/> : null
}
<ListItem button divider onClick={ onClick }>
<ListItem button divider disabled={ disabled } onClick={ onClick }>
<ListItemIcon>
<StepProgress value={ index + 1 } completed={ false } active={ active }/>
</ListItemIcon>
......
......@@ -5,6 +5,7 @@ import List, {ListItem, ListItemText} from 'material-ui/List';
import StepsListItem from './StepsListItem';
interface IProgressMenuProps extends React.ClassAttributes<any> {
disabled: boolean;
classes: any;
location: any;
steps: any;
......@@ -30,7 +31,7 @@ class ProgressMenu extends React.Component<IProgressMenuProps, any> {
public render() {
const {classes, steps, onGotoStep, location: {pathname}} = this.props;
const {classes, disabled, steps, onGotoStep, location: {pathname}} = this.props;
const link = pathname.split('/').pop();
return (
......@@ -41,9 +42,10 @@ class ProgressMenu extends React.Component<IProgressMenuProps, any> {
{
steps.map((step, i) => (
<StepsListItem
key={ i }
disabled={ disabled }
onClick={ onGotoStep(i + 1) }
active={ step.link.endsWith(link) }
key={ i }
index={ i }
name = { step.name }
/>
......
import * as React from 'react';
import {Field, reduxForm} from 'redux-form';
import MaterialAutosuggest from 'ui/common/material-autosuggest';
import {DATASET_BASIC_INFO_FORM} from 'constants/datasets';
import languages from 'data/Languages';
import {Partner} from 'model/partner.model';
import {TextField} from 'ui/common/text-field';
import {MarkdownField} from 'ui/common/markdown';
import SelectPartner from 'ui/catalog/partner/SelectPartner';
import {Partner} from 'model/partner.model';
import MaterialAutosuggest from 'ui/common/material-autosuggest';
import Validators from 'utilities/Validators';
import BasicInfoRadioGroup from './BasicInfoRadioGroup';
interface ILoginContainerProps extends React.ClassAttributes<any> {
......@@ -30,7 +34,7 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
return (
<form className="p-20 m-20 even-row">
<Field
<Field required
name="owner"
component={ SelectPartner }
label="Select Partner"
......@@ -38,6 +42,7 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
partners={ partners }
editable={ ! (initialValues.uuid && initialValues.version) }
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field required
name="title"
......@@ -46,6 +51,7 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
label="Title of the dataset"
placeholder="Title"
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field required
name="versionTag"
......@@ -53,6 +59,7 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
label="Dataset version"
placeholder="2018.1"
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field
name="description"
......
......@@ -7,7 +7,9 @@ import Grid from 'material-ui/Grid';
import IconButton from 'material-ui/IconButton';
import DeleteIcon from 'material-ui-icons/Delete';
import Radio, {RadioGroup} from 'material-ui/Radio';
import {FormLabel, FormControl, FormControlLabel} from 'material-ui/Form';
import {FormLabel, FormControlLabel} from 'material-ui/Form';
import FormControl from 'ui/common/forms/FormControl';
import Validators from 'utilities/Validators';
import {log} from 'utilities/debug';
......@@ -34,7 +36,7 @@ const renderRadioGroup = ({input, meta, ...rest}) => {
const onInputChange = (event, value) => input.onChange(value);
return (
<FormControl>
<FormControl fullWidth required meta={ meta }>
<FormLabel>Role:</FormLabel>
<RadioGroup
{ ...input }
......@@ -86,25 +88,30 @@ class DatasetCreatorForm extends React.Component<IDatasetCreatorFormProps, any>
<Field required name={ `${creator}.fullName` } component={ TextField } label="Full name"
placeholder="Jane A. Doe"
onBlur={ this.updateCreator(fields, index) }
validate={ [ Validators.required ] }
/>
<Field name={ `${creator}.email` } component={ TextField } type="text" label="Email*"
placeholder="Email*"
<Field required name={ `${creator}.role` } component={ renderRadioGroup }
onBlur={ this.updateCreator(fields, index) }/>
<Field name={ `${creator}.email` } component={ TextField } type="text" label="Email address"
placeholder="name@domain.com"
onBlur={ this.updateCreator(fields, index) }
validate={ [ Validators.emailAddress ] }
/>
<Field name={ `${creator}.phoneNumber` } component={ TextField } type="number" label="Phone"
placeholder="Phone"
<Field name={ `${creator}.phoneNumber` } component={ TextField } type="text" label="Phone number"
placeholder="+1 555 1231 Ext. 13"
onBlur={ this.updateCreator(fields, index) }
validate={ [ Validators.phoneNumber ] }
/>
<Field name={ `${creator}.fax` } component={ TextField } type="number" label="Fax"
placeholder="Fax"
<Field name={ `${creator}.fax` } component={ TextField } type="text" label="Fax"
placeholder="+1 555 1231 Ext. 42"
onBlur={ this.updateCreator(fields, index) }
validate={ [ Validators.phoneNumber ] }
/>
<Field name={ `${creator}.institutionalAffiliation` } component={ TextField } type="text" label="Institutional affiliation"
placeholder="Institutional affiliation"
onBlur={ this.updateCreator(fields, index) }
/>
<Field name={ `${creator}.role` } component={ renderRadioGroup }
onBlur={ this.updateCreator(fields, index) }/>
<Field name={ `${creator}.instituteAddress` } component={ TextField } type="text" label="Address"
placeholder="Address"
onBlur={ this.updateCreator(fields, index) }
......
......@@ -14,6 +14,7 @@ import {Location} from 'model/location.model';
import {error, log} from 'utilities/debug';
import {LocationService} from 'service/LocationService';
import {isNumeric} from 'utilities';
import Validators from 'utilities/Validators';
import IconButton from 'material-ui/IconButton';
import DeleteIcon from 'material-ui-icons/Delete';
......@@ -108,7 +109,6 @@ class TimingAndLocationForm extends React.Component<ILocationFormProps, any> {
<DeleteIcon onClick={ this.deleteLocation(fields, index) }/>
</IconButton>
</div>
<Field
required
name={ `${location}.userCountry` }
......@@ -118,6 +118,20 @@ class TimingAndLocationForm extends React.Component<ILocationFormProps, any> {
onBlur={ this.save(fields, index) }
suggestions={ countries }
suggestionLabel="name"
validate={ [ Validators.required ] }
/>
<Fields
names={ [
`${location}.userCountry`,
`${location}.stateProvince`,
`${location}.verbatimLocality`,
`${location}.mapCountry`,
`${location}.decimalLatitude`,
`${location}.decimalLongitude`,
] }
checkGeonames={ this.checkGeonames }
component={ FormMap }
onMouseOut={ this.save(fields, index) }
/>
<Field
name={ `${location}.stateProvince` }
......@@ -143,27 +157,17 @@ class TimingAndLocationForm extends React.Component<ILocationFormProps, any> {
name={ `${location}.decimalLatitude` }
component={ TextField }
label="Latitude"
type="number"
onBlur={ this.saveLatLng(fields, index)('decimalLatitude') }
validate={ [ Validators.decimalNumber ] }
/>
<Field
name={ `${location}.decimalLongitude` }
component={ TextField }
label="Longitude"
type="number"
onBlur={ this.saveLatLng(fields, index)('decimalLongitude') }
/>
<Fields
names={ [
`${location}.userCountry`,
`${location}.stateProvince`,
`${location}.verbatimLocality`,
`${location}.mapCountry`,
`${location}.decimalLatitude`,
`${location}.decimalLongitude`,
] }
checkGeonames={ this.checkGeonames }
component={ FormMap }
onMouseOut={ this.save(fields, index) }
validate={ [ Validators.decimalNumber ] }
/>
</div>
)) }
......
......@@ -8,17 +8,18 @@ import {DESCRIPTOR_FORM} from 'constants/descriptors';
import ItemsEditor from 'ui/common/ItemsEditor';
import { VocabularyTerm } from 'model/vocabulary.model';
import Validators from 'utilities/Validators';
import { TextField } from 'ui/common/text-field';
import {MarkdownField} from 'ui/common/markdown';
import Heading from 'ui/common/heading';
import SelectPartner from 'ui/catalog/partner/SelectPartner';
import SelectVocabulary from './SelectVocabulary';
import Toggle from 'ui/common/forms/Toggle';
import Button from 'material-ui/Button';
import Radio, { RadioGroup} from 'material-ui/Radio';
import {FormLabel, FormControl, FormControlLabel} from 'material-ui/Form';
import { Toggle, FormControl } from 'ui/common/forms';
import { FormLabel, FormControlLabel } from 'material-ui/Form';
import Grid from 'material-ui/Grid';
// FIXME Use Descriptor.DATATYPES
......@@ -103,7 +104,7 @@ class DescriptorForm extends React.Component<any, any> {
}
public render() {
const {error, handleSubmit, initialValues, onPublish, partners, anyTouched, currentOwner, currentVocabulary} = this.props;
const {error, handleSubmit, initialValues, onPublish, partners, invalid, submitting, anyTouched, currentOwner, currentVocabulary} = this.props;
if (! initialValues) {
return null;
......@@ -128,14 +129,16 @@ class DescriptorForm extends React.Component<any, any> {
label="Owner of the descriptor" placeholder="Partner"
partners={ partners }
component={ SelectPartner }
validate={ [ Validators.required ] }
/>
<Field required name="crop"
<Field name="crop"
label="Crop code" placeholder="maize"
component={ TextField }
/>
<Field required name="title"
label="Descriptor title" placeholder="Color of magic"
component={ TextField }
validate={ [ Validators.required ] }
/>
<Field name="publisher"
label="Original publisher" placeholder="IPGRI"
......@@ -152,9 +155,11 @@ class DescriptorForm extends React.Component<any, any> {
<Field required name="versionTag"
label="Version tag" placeholder="1.0"
component={ TextField }
validate={ [ Validators.required ] }
/>
<Field required name="category"
component={ renderCategoryRadioGroup }
validate={ [ Validators.required ] }
/>
<Field name="columnName"
......@@ -172,6 +177,7 @@ class DescriptorForm extends React.Component<any, any> {
/>
<Field required name="dataType"
component={ renderDataTypeRadioGroup }
validate={ [ Validators.required ] }
/>
{ this.allowsIntegerOnly() && (
......@@ -207,7 +213,7 @@ class DescriptorForm extends React.Component<any, any> {
) }
<div>{ error && <strong>{ error }</strong> }</div>
<Button raised type="submit" >Save changes</Button>
<Button raised type="submit" disabled={ submitting || invalid }>Save changes</Button>
{ initialValues && initialValues.id > 0 && <Button disabled={ anyTouched } type="button" onClick={ onPublish }>Approve and Publish</Button> }
</form>
</div>
......
......@@ -6,10 +6,11 @@ import { DESCRIPTORLIST_FORM } from 'constants/descriptors';
import { TextField } from 'ui/common/text-field';
import { MarkdownField } from 'ui/common/markdown';