Creation dialog with redux-form validation
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 23 Jul 2018 11:55:54 +0000 (13:55 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 23 Jul 2018 11:55:54 +0000 (13:55 +0200)
Feature #13781

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

src/store/project/project-action.ts
src/store/project/project-reducer.ts
src/utils/dialog-validator.tsx [deleted file]
src/validators/is-uniq-name.tsx [deleted file]
src/validators/require.tsx
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views-components/dialog-create/dialog-project-create.tsx

index b141736dd60f314c9f72e0252a4a5505c8d4d928..3da60f65c9ae9c3d7a14ccbfe0806ba2d6990394 100644 (file)
@@ -14,7 +14,6 @@ const actions = unionize({
     CLOSE_PROJECT_CREATOR: ofType<{}>(),
     CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
     CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
     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 }>(),
     REMOVE_PROJECT: ofType<string>(),
     PROJECTS_REQUEST: ofType<string>(),
     PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
@@ -45,8 +44,7 @@ export const createProject = (project: Partial<ProjectResource>) =>
         dispatch(actions.CREATE_PROJECT(projectData));
         return projectService
             .create(projectData)
         dispatch(actions.CREATE_PROJECT(projectData));
         return projectService
             .create(projectData)
-            .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project)))
-            .catch(errors => dispatch(actions.CREATE_PROJECT_ERROR(errors.errors.join(''))));
+            .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project)));
     };
 
 export type ProjectAction = UnionOf<typeof actions>;
     };
 
 export type ProjectAction = UnionOf<typeof actions>;
index 6df428c9d773fa18f08d787f2574dff9a2acab8a..a329e81242f4b8d7e4fd0ab37555281297a16c56 100644 (file)
@@ -116,9 +116,8 @@ const projectsReducer = (state: ProjectState = initialState, action: ProjectActi
     return actions.match(action, {
         OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
         CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
     return actions.match(action, {
         OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
         CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
-        CREATE_PROJECT: () => updateCreator(state, { pending: true, error: undefined }),
-        CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "", pending: false }),
-        CREATE_PROJECT_ERROR: error => updateCreator(state, { pending: false, error }),
+        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);
         REMOVE_PROJECT: () => state,
         PROJECTS_REQUEST: itemId => {
             const items = _.cloneDeep(state.items);
diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx
deleted file mode 100644 (file)
index 42a22e1..0000000
+++ /dev/null
@@ -1,50 +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 ValidatorProps = {
-    value: string,
-    render: (hasError: boolean) => React.ReactElement<any>;
-    isUniqName?: boolean;
-    validators: Array<(value: string) => string>;
-};
-
-class Validator extends React.Component<ValidatorProps & WithStyles<CssRules>> {
-    render() {
-        const { classes, value, isUniqName } = this.props;
-
-        return (
-            <span>
-                {this.props.render(!this.isValid(value))}
-                {isUniqName ? <span className={classes.formInputError}>Project with this name already exists</span> : null}
-                {this.props.validators.map(validate => {
-                    const errorMsg = validate(value);
-                    return errorMsg ? <span className={classes.formInputError}>{errorMsg}</span> : null;
-                })}
-            </span>
-        );
-    }
-
-    isValid(value: string) {
-        return this.props.validators.every(validate => validate(value).length === 0);
-    }
-}
-
-export const required = (value: string) => value.length > 0 ? "" : "This value is required";
-export const maxLength = (max: number) => (value: string) => value.length <= max ? "" : `This field should have max ${max} characters.`;
-export const isUniq = (getError: () => string) => (value: string) => getError() ? "Project with this name already exists" : "";
-
-type CssRules = "formInputError";
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    formInputError: {
-        color: "#ff0000",
-        marginLeft: "5px",
-        fontSize: "11px",
-    }
-});
-
-export default withStyles(styles)(Validator);
\ No newline at end of file
diff --git a/src/validators/is-uniq-name.tsx b/src/validators/is-uniq-name.tsx
deleted file mode 100644 (file)
index 521bfa3..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-export const isUniqName = (error: string) => {
-    return sleep(1000).then(() => {
-      if (error.includes("UniqueViolation")) {
-        throw { error: 'Project with this name already exists.' };
-      }
-    });
-  };
-
-const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
index 4e1e662957c0065ef19d5c4f6fcfba0fe781b891..8ac3401c44860d7b6a34e62f16f42f0f335a98ef 100644 (file)
@@ -10,7 +10,7 @@ interface RequireProps {
 
 // TODO types for require
 const require: any = (value: string, errorMessage = ERROR_MESSAGE) => {
 
 // TODO types for require
 const require: any = (value: string, errorMessage = ERROR_MESSAGE) => {
-    return value && value.toString().length > 0 ? void 0 : ERROR_MESSAGE;
+    return value && value.toString().length > 0 ? undefined : ERROR_MESSAGE;
 };
 
 export default require;
 };
 
 export default require;
index 2cdd479033a009e23473e60dbebfc0a4f925cd74..f75c459347500da68ea97b196d7be098691b8bbd 100644 (file)
@@ -3,7 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
-import { Dispatch } from "../../../node_modules/redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+
 import { RootState } from "../../store/store";
 import DialogProjectCreate from "../dialog-create/dialog-project-create";
 import actions, { createProject, getProjectList } from "../../store/project/project-action";
 import { RootState } from "../../store/store";
 import DialogProjectCreate from "../dialog-create/dialog-project-create";
 import actions, { createProject, getProjectList } from "../../store/project/project-action";
@@ -11,15 +13,13 @@ import dataExplorerActions from "../../store/data-explorer/data-explorer-action"
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 
 const mapStateToProps = (state: RootState) => ({
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 
 const mapStateToProps = (state: RootState) => ({
-    open: state.projects.creator.opened,
-    pending: state.projects.creator.pending,
-    error: state.projects.creator.error
+    open: state.projects.creator.opened
 });
 
 });
 
-const submit = (data: { name: string, description: string }) =>
+export const addProject = (data: { name: string, description: string }) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { ownerUuid } = getState().projects.creator;
     (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));
         });
             dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             dispatch<any>(getProjectList(ownerUuid));
         });
@@ -30,7 +30,10 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
         dispatch(actions.CLOSE_PROJECT_CREATOR());
     },
     onSubmit: (data: { name: string, description: string }) => {
         dispatch(actions.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." : "" });
+            });
     }
 });
 
     }
 });
 
index c448df3b0c0075ec34664624188537206be9b929..6fb8a699df64e01f35dc5a60a49c35063cd827ff 100644 (file)
@@ -13,14 +13,13 @@ import DialogTitle from '@material-ui/core/DialogTitle';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
 import { NAME, DESCRIPTION } from '../../validators/create-project/create-project-validator';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
 import { NAME, DESCRIPTION } from '../../validators/create-project/create-project-validator';
-import { isUniqName } from '../../validators/is-uniq-name';
 
 
-interface ProjectCreateProps {
+interface DialogProjectProps {
     open: boolean;
     open: boolean;
-    pending: boolean;
     handleClose: () => void;
     onSubmit: (data: { name: string, description: string }) => void;
     handleSubmit: any;
     handleClose: () => void;
     onSubmit: (data: { name: string, description: string }) => void;
     handleSubmit: any;
+    submitting: boolean;
 }
 
 interface TextFieldProps {
 }
 
 interface TextFieldProps {
@@ -31,23 +30,16 @@ interface TextFieldProps {
     meta?: any;
 }
 
     meta?: any;
 }
 
-class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyles<CssRules>> {
-    /*componentWillReceiveProps(nextProps: ProjectCreateProps) {
-        const { error } = nextProps;
-
-        TODO: Validation for other errors
-        if (this.props.error !== error && error && error.includes("UniqueViolation")) {
-            this.setState({ isUniqName: error });
-        }
-}*/
-
+class DialogProjectCreate extends React.Component<DialogProjectProps & WithStyles<CssRules>> {
     render() {
     render() {
-        const { classes, open, handleClose, pending, handleSubmit, onSubmit } = this.props;
+        const { classes, open, handleClose, handleSubmit, onSubmit, submitting } = this.props;
 
         return (
             <Dialog
                 open={open}
 
         return (
             <Dialog
                 open={open}
-                onClose={handleClose}>
+                onClose={handleClose}
+                disableBackdropClick={true}
+                disableEscapeKeyDown={true}>
                 <div className={classes.dialog}>
                     <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
                         <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a project</DialogTitle>
                 <div className={classes.dialog}>
                     <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
                         <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a project</DialogTitle>
@@ -60,21 +52,21 @@ class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyle
                                 label="Project Name" />
                             <Field name="description"
                                 component={this.renderTextField}
                                 label="Project Name" />
                             <Field name="description"
                                 component={this.renderTextField}
-                                floatinglabeltext="Description"
+                                floatinglabeltext="Description - optional"
                                 validate={DESCRIPTION}
                                 className={classes.textField}
                                 validate={DESCRIPTION}
                                 className={classes.textField}
-                                label="Description" />
+                                label="Description - optional" />
                         </DialogContent>
                         </DialogContent>
-                        <DialogActions>
-                            <Button onClick={handleClose} className={classes.button} color="primary" disabled={pending}>CANCEL</Button>
+                        <DialogActions className={classes.dialogActions}>
+                            <Button onClick={handleClose} className={classes.button} color="primary" disabled={submitting}>CANCEL</Button>
                             <Button type="submit"
                                 className={classes.lastButton}
                                 color="primary"
                             <Button type="submit"
                                 className={classes.lastButton}
                                 color="primary"
-                                disabled={pending}
+                                disabled={submitting}
                                 variant="contained">
                                 CREATE A PROJECT
                             </Button>
                                 variant="contained">
                                 CREATE A PROJECT
                             </Button>
-                            {pending && <CircularProgress size={20} className={classes.createProgress} />}
+                            {submitting && <CircularProgress size={20} className={classes.createProgress} />}
                         </DialogActions>
                     </form>
                 </div>
                         </DialogActions>
                     </form>
                 </div>
@@ -82,13 +74,12 @@ class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyle
         );
     }
 
         );
     }
 
-    // TODO Make it separate file
     renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
         <TextField
     renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
         <TextField
-            helperText={touched && error ? error : void 0}
+            helperText={touched && error}
             label={label}
             className={this.props.classes.textField}
             label={label}
             className={this.props.classes.textField}
-            error={touched && !!error} 
+            error={touched && !!error}
             autoComplete='off'
             {...input}
             {...custom}
             autoComplete='off'
             {...input}
             {...custom}
@@ -96,7 +87,7 @@ class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyle
     )
 }
 
     )
 }
 
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -126,9 +117,12 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         minWidth: "20px",
         right: "95px"
     },
         minWidth: "20px",
         right: "95px"
     },
+    dialogActions: {
+        marginBottom: "24px"
+    }
 });
 
 export default compose(
 });
 
 export default compose(
-    reduxForm({ form: 'projectCreateDialog',/* asyncValidate: isUniqName, asyncBlurFields: ["name"] */}),
+    reduxForm({ form: 'projectCreateDialog' }),
     withStyles(styles)
 )(DialogProjectCreate);
\ No newline at end of file
     withStyles(styles)
 )(DialogProjectCreate);
\ No newline at end of file