Merge branch '13781-Data-operations-Creating-a-project-validation'
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 24 Jul 2018 11:49:55 +0000 (13:49 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 24 Jul 2018 11:49:55 +0000 (13:49 +0200)
refs #13781

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

13 files changed:
package.json
src/common/api/common-resource-service.ts
src/store/favorites/favorites-actions.ts
src/store/project/project-action.ts
src/store/project/project-reducer.ts
src/store/store.ts
src/utils/dialog-validator.tsx [deleted file]
src/validators/create-project/create-project-validator.tsx [new file with mode: 0644]
src/validators/max-length.tsx [new file with mode: 0644]
src/validators/require.tsx [new file with mode: 0644]
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views-components/dialog-create/dialog-project-create.tsx
yarn.lock

index 0c06a6f17a357115da8c6897f0f159de7da51084..fa4bd309df7ad1b6c873128d70192e0c9b97fc88 100644 (file)
@@ -6,6 +6,7 @@
     "@material-ui/core": "1.4.0",
     "@material-ui/icons": "1.1.0",
     "@types/lodash": "4.14.112",
+    "@types/redux-form": "7.4.1",
     "axios": "0.18.0",
     "classnames": "2.2.6",
     "lodash": "4.17.10",
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
+    "@types/redux-form": "7.4.1",
     "axios-mock-adapter": "1.15.0",
     "enzyme": "3.3.0",
     "enzyme-adapter-react-16": "1.1.1",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
+    "redux-form": "7.4.2",
     "typescript": "2.9.2"
   },
   "moduleNameMapper": {
index 2541feab026989228c2cf14521c762c78be97d5c..3956fb7390983824a402456abc2144850b85cda2 100644 (file)
@@ -5,7 +5,7 @@
 import * as _ from "lodash";
 import { FilterBuilder } from "./filter-builder";
 import { OrderBuilder } from "./order-builder";
-import { AxiosInstance } from "axios";
+import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "../../models/resource";
 
 export interface ListArguments {
@@ -26,6 +26,11 @@ export interface ListResults<T> {
     itemsAvailable: number;
 }
 
+export interface Errors {
+    errors: string[];
+    errorToken: string;
+}
+
 export class CommonResourceService<T extends Resource> {
 
     static mapResponseKeys = (response: any): Promise<any> =>
@@ -49,6 +54,12 @@ export class CommonResourceService<T extends Resource> {
             }
         }
 
+    static defaultResponse<R>(promise: AxiosPromise<R>): Promise<R> {
+        return promise
+            .then(CommonResourceService.mapResponseKeys)
+            .catch(({ response }) => Promise.reject<Errors>(CommonResourceService.mapResponseKeys(response)));
+    }
+
     protected serverApi: AxiosInstance;
     protected resourceType: string;
 
@@ -58,21 +69,21 @@ export class CommonResourceService<T extends Resource> {
     }
 
     create(data: Partial<T>) {
-        return this.serverApi
-            .post<T>(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data))
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post<T>(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data)));
     }
 
     delete(uuid: string): Promise<T> {
-        return this.serverApi
-            .delete(this.resourceType + uuid)
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .delete(this.resourceType + uuid));
     }
 
     get(uuid: string) {
-        return this.serverApi
-            .get<T>(this.resourceType + uuid)
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get<T>(this.resourceType + uuid));
     }
 
     list(args: ListArguments = {}): Promise<ListResults<T>> {
@@ -82,11 +93,11 @@ export class CommonResourceService<T extends Resource> {
             filters: filters ? filters.serialize() : undefined,
             order: order ? order.getOrder() : undefined
         };
-        return this.serverApi
-            .get(this.resourceType, {
-                params: CommonResourceService.mapKeys(_.snakeCase)(params)
-            })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get(this.resourceType, {
+                    params: CommonResourceService.mapKeys(_.snakeCase)(params)
+                }));
     }
 
     update(uuid: string) {
index 225c9b35c36ce934721c1c10f5cb2883a15e4594..c38f4d1a1aa2690779692ff70e51c2cf1e276bfa 100644 (file)
@@ -21,12 +21,12 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
-        const promise = isFavorite
+        const promise: (any) = isFavorite
             ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
             : favoriteService.create({ userUuid, resource });
 
         promise
-            .then(fav => {
+            .then(() => {
                 dispatch(favoritesActions.UPDATE_FAVORITES({ [resource.uuid]: !isFavorite }));
             });
     };
index 77223e9e41cd00cc31c073691ac3d2cb50fa7b5c..cf38456109be0b25625214773f771c5eabc51713 100644 (file)
@@ -15,7 +15,6 @@ export const projectActions = unionize({
     CLOSE_PROJECT_CREATOR: ofType<{}>(),
     CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
     CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
-    CREATE_PROJECT_ERROR: ofType<string>(),
     REMOVE_PROJECT: ofType<string>(),
     PROJECTS_REQUEST: ofType<string>(),
     PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
@@ -47,8 +46,7 @@ export const createProject = (project: Partial<ProjectResource>) =>
         dispatch(projectActions.CREATE_PROJECT(projectData));
         return projectService
             .create(projectData)
-            .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)))
-            .catch(() => dispatch(projectActions.CREATE_PROJECT_ERROR("Could not create a project")));
+            .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
     };
 
 export type ProjectAction = UnionOf<typeof projectActions>;
index 40356c0c90123d7775484849d8fcf937fdbce87d..94a451a86574e70de24f6d143ede7217ce25cf9c 100644 (file)
@@ -18,6 +18,7 @@ interface ProjectCreator {
     opened: boolean;
     pending: boolean;
     ownerUuid: string;
+    error?: string;
 }
 
 export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
@@ -115,9 +116,8 @@ export const projectsReducer = (state: ProjectState = initialState, action: Proj
     return projectActions.match(action, {
         OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }),
         CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
-        CREATE_PROJECT: () => updateCreator(state, { opened: false, pending: true }),
-        CREATE_PROJECT_SUCCESS: () => updateCreator(state, { ownerUuid: "", pending: false }),
-        CREATE_PROJECT_ERROR: () => updateCreator(state, { ownerUuid: "", pending: false }),
+        CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
+        CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
         REMOVE_PROJECT: () => state,
         PROJECTS_REQUEST: itemId => {
             const items = _.cloneDeep(state.items);
index e7dbe16f49163f7b1cde01b8a08b7463b37bcbce..8a5136c91add44b378db23d3f9439d53d5496cd5 100644 (file)
@@ -14,6 +14,7 @@ import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-exp
 import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
 import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
 import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+import { reducer as formReducer } from 'redux-form';
 import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
 
 const composeEnhancers =
@@ -40,6 +41,7 @@ const rootReducer = combineReducers({
     sidePanel: sidePanelReducer,
     detailsPanel: detailsPanelReducer,
     contextMenu: contextMenuReducer,
+    form: formReducer,
     favorites: favoritesReducer,
 });
 
diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx
deleted file mode 100644 (file)
index 9697a86..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-
-type CssRules = "formInputError";
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    formInputError: {
-        color: "#ff0000",
-        marginLeft: "5px",
-        fontSize: "11px",
-    }
-});
-
-type ValidatorProps = {
-    value: string,
-    onChange: (isValid: boolean | string) => void;
-    render: (hasError: boolean) => React.ReactElement<any>;
-    isRequired: boolean;
-};
-
-interface ValidatorState {
-    isPatternValid: boolean;
-    isLengthValid: boolean;
-}
-
-const nameRegEx = /^[a-zA-Z0-9-_ ]+$/;
-const maxInputLength = 60;
-
-export const Validator = withStyles(styles)(
-    class extends React.Component<ValidatorProps & WithStyles<CssRules>> {
-        state: ValidatorState = {
-            isPatternValid: true,
-            isLengthValid: true
-        };
-
-        componentWillReceiveProps(nextProps: ValidatorProps) {
-            const { value } = nextProps;
-
-            if (this.props.value !== value) {
-                this.setState({
-                    isPatternValid: value.match(nameRegEx),
-                    isLengthValid: value.length < maxInputLength
-                }, () => this.onChange());
-            }
-        }
-
-        onChange() {
-            const { value, onChange, isRequired } = this.props;
-            const { isPatternValid, isLengthValid } = this.state;
-            const isValid = value && isPatternValid && isLengthValid && (isRequired || (!isRequired && value.length > 0));
-
-            onChange(isValid);
-        }
-
-        render() {
-            const { classes, isRequired, value } = this.props;
-            const { isPatternValid, isLengthValid } = this.state;
-
-            return (
-                <span>
-            {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))}
-                    {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ?
-                        <span className={classes.formInputError}>This field allow only alphanumeric characters, dashes, spaces and underscores.<br/></span> : null}
-                    {!isLengthValid ?
-                        <span className={classes.formInputError}>This field should have max 60 characters.</span> : null}
-          </span>
-            );
-        }
-    }
-);
diff --git a/src/validators/create-project/create-project-validator.tsx b/src/validators/create-project/create-project-validator.tsx
new file mode 100644 (file)
index 0000000..928efdd
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { require } from '../require';
+import { maxLength } from '../max-length';
+
+export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
+export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
diff --git a/src/validators/max-length.tsx b/src/validators/max-length.tsx
new file mode 100644 (file)
index 0000000..922e3e5
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const ERROR_MESSAGE = 'Maximum string length of this field is: ';
+export const DEFAULT_MAX_VALUE = 60;
+
+interface MaxLengthProps {
+    maxLengthValue: number;  
+    defaultErrorMessage: string;
+}
+
+// TODO types for maxLength
+export const maxLength: any = (maxLengthValue = DEFAULT_MAX_VALUE, errorMessage = ERROR_MESSAGE) => {
+    return (value: string) => {
+        if (value) {
+            return  value && value.length <= maxLengthValue ? undefined : `${errorMessage || ERROR_MESSAGE} ${maxLengthValue}`;
+        }
+
+        return undefined;
+    };
+};
diff --git a/src/validators/require.tsx b/src/validators/require.tsx
new file mode 100644 (file)
index 0000000..f636850
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const ERROR_MESSAGE = 'This field is required.';
+
+interface RequiredProps {
+    value: string;
+}
+
+// TODO types for require
+export const require: any = (value: string) => {
+    return value && value.length > 0 ? undefined : ERROR_MESSAGE;
+};
index 2f3e0b7fe319a349165da8b86affd683234ebbbf..cf5b24f0945316771ca52bdbb2b441a7423fa606 100644 (file)
@@ -4,8 +4,10 @@
 
 import { connect } from "react-redux";
 import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+
 import { RootState } from "../../store/store";
-import { DialogProjectCreate as DialogProjectCreateComponent } from "../dialog-create/dialog-project-create";
+import { DialogProjectCreate } from "../dialog-create/dialog-project-create";
 import { projectActions, createProject, getProjectList } from "../../store/project/project-action";
 import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
@@ -14,10 +16,10 @@ const mapStateToProps = (state: RootState) => ({
     open: state.projects.creator.opened
 });
 
-const submit = (data: { name: string, description: string }) =>
+const addProject = (data: { name: string, description: string }) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { ownerUuid } = getState().projects.creator;
-        dispatch<any>(createProject(data)).then(() => {
+        return dispatch<any>(createProject(data)).then(() => {
             dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             dispatch<any>(getProjectList(ownerUuid));
         });
@@ -28,8 +30,11 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
         dispatch(projectActions.CLOSE_PROJECT_CREATOR());
     },
     onSubmit: (data: { name: string, description: string }) => {
-        dispatch<any>(submit(data));
+        return dispatch<any>(addProject(data))
+            .catch((e: any) => {
+                throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Project with this name already exists." : "" });
+            });
     }
 });
 
-export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreateComponent);
+export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
index aefb8159871677ce2de8fb8ba13f4442d3d7133d..592efc1a388a65f63a3471a2edfaa8a8eb674929 100644 (file)
@@ -3,16 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
 import TextField from '@material-ui/core/TextField';
 import Dialog from '@material-ui/core/Dialog';
 import DialogActions from '@material-ui/core/DialogActions';
 import DialogContent from '@material-ui/core/DialogContent';
 import DialogTitle from '@material-ui/core/DialogTitle';
-import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
-import { Validator } from '../../utils/dialog-validator';
+import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
 
-type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog" | "dialogTitle";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -22,7 +24,9 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         marginLeft: theme.spacing.unit,
         marginRight: "20px",
     },
-    dialogContent: {
+    formContainer: {
+        display: "flex",
+        flexDirection: "column",
         marginTop: "20px",
     },
     dialogTitle: {
@@ -34,109 +38,91 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     dialog: {
         minWidth: "600px",
         minHeight: "320px"
+    },
+    createProgress: {
+        position: "absolute",
+        minWidth: "20px",
+        right: "95px"
+    },
+    dialogActions: {
+        marginBottom: "24px"
     }
 });
-
-interface ProjectCreateProps {
+interface DialogProjectProps {
     open: boolean;
     handleClose: () => void;
     onSubmit: (data: { name: string, description: string }) => void;
+    handleSubmit: any;
+    submitting: boolean;
 }
 
-interface DialogState {
-    name: string;
-    description: string;
-    isNameValid: boolean;
-    isDescriptionValid: boolean;
+interface TextFieldProps {
+    label: string;
+    floatinglabeltext: string;
+    className?: string;
+    input?: string;
+    meta?: any;
 }
 
-export const DialogProjectCreate = withStyles(styles)(
-    class extends React.Component<ProjectCreateProps & WithStyles<CssRules>> {
-        state: DialogState = {
-            name: '',
-            description: '',
-            isNameValid: false,
-            isDescriptionValid: true
-        };
-
+export const DialogProjectCreate = compose(
+    reduxForm({ form: 'projectCreateDialog' }),
+    withStyles(styles))(
+    class DialogProjectCreate extends React.Component<DialogProjectProps & WithStyles<CssRules>> {
         render() {
-            const { name, description } = this.state;
-            const { classes, open, handleClose } = this.props;
+            const { classes, open, handleClose, handleSubmit, onSubmit, submitting } = this.props;
 
             return (
                 <Dialog
                     open={open}
-                    onClose={handleClose}>
+                    onClose={handleClose}
+                    disableBackdropClick={true}
+                    disableEscapeKeyDown={true}>
                     <div className={classes.dialog}>
-                        <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a project</DialogTitle>
-                        <DialogContent className={classes.dialogContent}>
-                            <Validator
-                                value={name}
-                                onChange={e => this.isNameValid(e)}
-                                isRequired={true}
-                                render={hasError =>
-                                    <TextField
-                                        margin="dense"
-                                        className={classes.textField}
-                                        id="name"
-                                        onChange={e => this.handleProjectName(e)}
-                                        label="Project name"
-                                        error={hasError}
-                                        fullWidth/>}/>
-                            <Validator
-                                value={description}
-                                onChange={e => this.isDescriptionValid(e)}
-                                isRequired={false}
-                                render={hasError =>
-                                    <TextField
-                                        margin="dense"
-                                        className={classes.textField}
-                                        id="description"
-                                        onChange={e => this.handleDescriptionValue(e)}
-                                        label="Description - optional"
-                                        error={hasError}
-                                        fullWidth/>}/>
-                        </DialogContent>
-                        <DialogActions>
-                            <Button onClick={handleClose} className={classes.button} color="primary">CANCEL</Button>
-                            <Button onClick={this.handleSubmit} className={classes.lastButton} color="primary"
-                                    disabled={!this.state.isNameValid || (!this.state.isDescriptionValid && description.length > 0)}
-                                    variant="raised">CREATE A PROJECT</Button>
-                        </DialogActions>
+                        <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+                            <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a
+                                project</DialogTitle>
+                            <DialogContent className={classes.formContainer}>
+                                <Field name="name"
+                                       component={this.renderTextField}
+                                       floatinglabeltext="Project Name"
+                                       validate={PROJECT_NAME_VALIDATION}
+                                       className={classes.textField}
+                                       label="Project Name"/>
+                                <Field name="description"
+                                       component={this.renderTextField}
+                                       floatinglabeltext="Description - optional"
+                                       validate={PROJECT_DESCRIPTION_VALIDATION}
+                                       className={classes.textField}
+                                       label="Description - optional"/>
+                            </DialogContent>
+                            <DialogActions className={classes.dialogActions}>
+                                <Button onClick={handleClose} className={classes.button} color="primary"
+                                        disabled={submitting}>CANCEL</Button>
+                                <Button type="submit"
+                                        className={classes.lastButton}
+                                        color="primary"
+                                        disabled={submitting}
+                                        variant="contained">
+                                    CREATE A PROJECT
+                                </Button>
+                                {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+                            </DialogActions>
+                        </form>
                     </div>
                 </Dialog>
             );
         }
 
-        handleSubmit = () => {
-            this.props.onSubmit({
-                name: this.state.name,
-                description: this.state.description
-            });
-        }
-
-        handleProjectName(e: React.ChangeEvent<HTMLInputElement>) {
-            this.setState({
-                name: e.target.value,
-            });
-        }
-
-        handleDescriptionValue(e: React.ChangeEvent<HTMLInputElement>) {
-            this.setState({
-                description: e.target.value,
-            });
-        }
-
-        isNameValid(value: boolean | string) {
-            this.setState({
-                isNameValid: value,
-            });
-        }
-
-        isDescriptionValid(value: boolean | string) {
-            this.setState({
-                isDescriptionValid: value,
-            });
-        }
+        renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+            <TextField
+                helperText={touched && error}
+                label={label}
+                className={this.props.classes.textField}
+                error={touched && !!error}
+                autoComplete='off'
+                {...input}
+                {...custom}
+            />
+        )
     }
 );
index 6960aaf60099961087c01579ef83f78ffd28bdd9..3557ebefe81e4c1e197f30077f9e8c6892ce28a9 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@types/react" "*"
     redux "^3.6.0"
 
+"@types/redux-form@^7.4.1":
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.1.tgz#df84bbda5f06e4d517210797c3cfdc573c3bda36"
+  dependencies:
+    "@types/react" "*"
+    redux "^3.6.0 || ^4.0.0"
+
 abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -2487,6 +2494,10 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
     es6-symbol "~3.1.1"
     next-tick "1"
 
+es6-error@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
+
 es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
@@ -3337,6 +3348,10 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
   version "2.5.5"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
 
+hoist-non-react-statics@^2.5.4:
+  version "2.5.5"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -4601,7 +4616,7 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
-lodash-es@^4.17.5, lodash-es@^4.2.1:
+lodash-es@^4.17.10, lodash-es@^4.17.5, lodash-es@^4.2.1:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
 
@@ -6385,11 +6400,24 @@ redux-devtools@3.4.1:
     prop-types "^15.5.7"
     redux-devtools-instrument "^1.0.1"
 
+redux-form@^7.4.2:
+  version "7.4.2"
+  resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.4.2.tgz#d6061088fb682eb9fc5fb9749bd8b102f03154b0"
+  dependencies:
+    es6-error "^4.1.1"
+    hoist-non-react-statics "^2.5.4"
+    invariant "^2.2.4"
+    is-promise "^2.1.0"
+    lodash "^4.17.10"
+    lodash-es "^4.17.10"
+    prop-types "^15.6.1"
+    react-lifecycles-compat "^3.0.4"
+
 redux-thunk@2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
 
-redux@4.0.0, "redux@>= 3.7.2", redux@^4.0.0:
+redux@4.0.0, "redux@>= 3.7.2", "redux@^3.6.0 || ^4.0.0", redux@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
   dependencies: