[ARVADOS-WORKBENCH2] updated: 1.1.4-297-gd8619a3

Git user git at public.curoverse.com
Fri Jul 20 06:51:43 EDT 2018


Summary of changes:
 package.json                                       |   3 +
 src/store/store.ts                                 |   4 +-
 src/utils/dialog-validator.tsx                     |  75 +++----
 .../create-project/create-project-validator.tsx    |   9 +
 src/validators/is-uniq-name.tsx                    |  13 ++
 src/validators/max-length.tsx                      |  24 ++
 src/validators/require.tsx                         |  16 ++
 .../dialog-create/dialog-project-create.tsx        | 246 +++++++++------------
 src/views/workbench/workbench.tsx                  |   4 +-
 yarn.lock                                          |  34 ++-
 10 files changed, 236 insertions(+), 192 deletions(-)
 create mode 100644 src/validators/create-project/create-project-validator.tsx
 create mode 100644 src/validators/is-uniq-name.tsx
 create mode 100644 src/validators/max-length.tsx
 create mode 100644 src/validators/require.tsx

       via  d8619a37a078f72f4be154a3b82894810ebebf36 (commit)
      from  b113552f312a87d933e17c1ffaca4fbd4707e94e (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit d8619a37a078f72f4be154a3b82894810ebebf36
Author: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>
Date:   Fri Jul 20 12:51:26 2018 +0200

    redux-form-validation
    
    Feature #13781
    
    Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>

diff --git a/package.json b/package.json
index f769acc..a8c5617 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "@material-ui/core": "1.2.1",
     "@material-ui/icons": "1.1.0",
     "@types/lodash": "4.14.109",
+    "@types/redux-form": "^7.4.1",
     "axios": "0.18.0",
     "classnames": "^2.2.6",
     "lodash": "4.17.10",
@@ -40,11 +41,13 @@
     "@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": {
diff --git a/src/store/store.ts b/src/store/store.ts
index 36f9203..956fb46 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -13,6 +13,7 @@ import authReducer, { AuthState } from "./auth/auth-reducer";
 import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
 import { projectPanelMiddleware } from '../store/project-panel/project-panel-middleware';
 import detailsPanelReducer, { DetailsPanelState } from './details-panel/details-panel-reducer';
+import { reducer as formReducer } from 'redux-form';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -34,7 +35,8 @@ const rootReducer = combineReducers({
     router: routerReducer,
     dataExplorer: dataExplorerReducer,
     sidePanel: sidePanelReducer,
-    detailsPanel: detailsPanelReducer
+    detailsPanel: detailsPanelReducer,
+    form: formReducer
 });
 
 
diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx
index b264f96..42a22e1 100644
--- a/src/utils/dialog-validator.tsx
+++ b/src/utils/dialog-validator.tsx
@@ -6,64 +6,45 @@ import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 
 type ValidatorProps = {
-  value: string,
-  onChange: (isValid: boolean | string) => void;
-  render: (hasError: boolean) => React.ReactElement<any>;
-  isRequired: boolean;
-  isUniqName?: boolean;
+    value: string,
+    render: (hasError: boolean) => React.ReactElement<any>;
+    isUniqName?: boolean;
+    validators: Array<(value: string) => string>;
 };
 
-interface ValidatorState {
-  isLengthValid: boolean;
-}
-
 class Validator extends React.Component<ValidatorProps & WithStyles<CssRules>> {
-  state: ValidatorState = {
-    isLengthValid: true
-  };
-
-  componentWillReceiveProps(nextProps: ValidatorProps) {
-    const { value } = nextProps;
-
-    if (this.props.value !== value) {
-      this.setState({
-        isLengthValid: value.length < MAX_INPUT_LENGTH
-      }, () => this.onChange());
+    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>
+        );
     }
-  }
-
-  onChange() {
-    const { value, onChange, isRequired } = this.props;
-    const { isLengthValid } = this.state;
-    const isValid = value && isLengthValid && (isRequired || (!isRequired && value.length > 0));
 
-    onChange(isValid);
-  }
-
-  render() {
-    const { classes, isRequired, value, isUniqName } = this.props;
-    const { isLengthValid } = this.state;
-
-    return (
-      <span>
-        {this.props.render(!isLengthValid && (isRequired || (!isRequired && value.length > 0)))}
-        {!isLengthValid ? <span className={classes.formInputError}>This field should have max 255 characters.</span> : null}
-        {isUniqName ? <span className={classes.formInputError}>Project with this name already exists</span> : null}
-      </span>
-    );
-  }
+    isValid(value: string) {
+        return this.props.validators.every(validate => validate(value).length === 0);
+    }
 }
 
-const MAX_INPUT_LENGTH = 255;
+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",
-  }
+    formInputError: {
+        color: "#ff0000",
+        marginLeft: "5px",
+        fontSize: "11px",
+    }
 });
 
 export default withStyles(styles)(Validator);
\ No newline at end of file
diff --git a/src/validators/create-project/create-project-validator.tsx b/src/validators/create-project/create-project-validator.tsx
new file mode 100644
index 0000000..3eb636c
--- /dev/null
+++ b/src/validators/create-project/create-project-validator.tsx
@@ -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 NAME = [require, maxLength(255)];
+export const DESCRIPTION = [maxLength(255)];
\ No newline at end of file
diff --git a/src/validators/is-uniq-name.tsx b/src/validators/is-uniq-name.tsx
new file mode 100644
index 0000000..521bfa3
--- /dev/null
+++ b/src/validators/is-uniq-name.tsx
@@ -0,0 +1,13 @@
+// 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));
diff --git a/src/validators/max-length.tsx b/src/validators/max-length.tsx
new file mode 100644
index 0000000..1f8e509
--- /dev/null
+++ b/src/validators/max-length.tsx
@@ -0,0 +1,24 @@
+// 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
+const maxLength: any = (maxLengthValue = DEFAULT_MAX_VALUE, errorMessage = ERROR_MESSAGE) => {
+    return (value: string) => {
+        if (value) {
+            return  value && value && value.length <= maxLengthValue ? undefined : `${errorMessage || ERROR_MESSAGE} ${maxLengthValue}`;
+        }
+
+        return undefined;
+    };
+};
+
+export default maxLength;
\ No newline at end of file
diff --git a/src/validators/require.tsx b/src/validators/require.tsx
new file mode 100644
index 0000000..4e1e662
--- /dev/null
+++ b/src/validators/require.tsx
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const ERROR_MESSAGE = 'This field is required.';
+
+interface RequireProps {
+    value: string;
+}
+
+// TODO types for require
+const require: any = (value: string, errorMessage = ERROR_MESSAGE) => {
+    return value && value.toString().length > 0 ? void 0 : ERROR_MESSAGE;
+};
+
+export default require;
diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx
index 0388e05..c448df3 100644
--- a/src/views-components/dialog-create/dialog-project-create.tsx
+++ b/src/views-components/dialog-create/dialog-project-create.tsx
@@ -3,6 +3,8 @@
 // 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';
@@ -10,157 +12,123 @@ import DialogContent from '@material-ui/core/DialogContent';
 import DialogTitle from '@material-ui/core/DialogTitle';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
-import Validator from '../../utils/dialog-validator';
+import { NAME, DESCRIPTION } from '../../validators/create-project/create-project-validator';
+import { isUniqName } from '../../validators/is-uniq-name';
 
 interface ProjectCreateProps {
-  open: boolean;
-  pending: boolean;
-  error: string;
-  handleClose: () => void;
-  onSubmit: (data: { name: string, description: string }) => void;
+    open: boolean;
+    pending: boolean;
+    handleClose: () => void;
+    onSubmit: (data: { name: string, description: string }) => void;
+    handleSubmit: any;
 }
 
-interface DialogState {
-  name: string;
-  description: string;
-  isNameValid: boolean;
-  isDescriptionValid: boolean;
-  isUniqName: boolean;
+interface TextFieldProps {
+    label: string;
+    floatinglabeltext: string;
+    className?: string;
+    input?: string;
+    meta?: any;
 }
 
 class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyles<CssRules>> {
-  state: DialogState = {
-    name: '',
-    description: '',
-    isNameValid: false,
-    isDescriptionValid: true,
-    isUniqName: false
-  };
-
-  componentWillReceiveProps(nextProps: ProjectCreateProps) {
-    const { error } = nextProps;
-
-    if (this.props.error !== error) {
-      this.setState({ isUniqName: error });
+    /*componentWillReceiveProps(nextProps: ProjectCreateProps) {
+        const { error } = nextProps;
+
+        TODO: Validation for other errors
+        if (this.props.error !== error && error && error.includes("UniqueViolation")) {
+            this.setState({ isUniqName: error });
+        }
+}*/
+
+    render() {
+        const { classes, open, handleClose, pending, handleSubmit, onSubmit } = this.props;
+
+        return (
+            <Dialog
+                open={open}
+                onClose={handleClose}>
+                <div className={classes.dialog}>
+                    <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={NAME}
+                                className={classes.textField}
+                                label="Project Name" />
+                            <Field name="description"
+                                component={this.renderTextField}
+                                floatinglabeltext="Description"
+                                validate={DESCRIPTION}
+                                className={classes.textField}
+                                label="Description" />
+                        </DialogContent>
+                        <DialogActions>
+                            <Button onClick={handleClose} className={classes.button} color="primary" disabled={pending}>CANCEL</Button>
+                            <Button type="submit"
+                                className={classes.lastButton}
+                                color="primary"
+                                disabled={pending}
+                                variant="contained">
+                                CREATE A PROJECT
+                            </Button>
+                            {pending && <CircularProgress size={20} className={classes.createProgress} />}
+                        </DialogActions>
+                    </form>
+                </div>
+            </Dialog>
+        );
     }
-  }
-
-  render() {
-    const { name, description, isNameValid, isDescriptionValid, isUniqName } = this.state;
-    const { classes, open, handleClose, pending } = this.props;
-
-    return (
-      <Dialog
-        open={open}
-        onClose={handleClose}>
-        <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}
-              isUniqName={isUniqName}
-              render={hasError =>
-                <TextField
-                  margin="dense"
-                  className={classes.textField}
-                  id="name"
-                  onChange={e => this.handleProjectName(e)}
-                  label="Project name"
-                  error={hasError || isUniqName}
-                  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" disabled={pending}>CANCEL</Button>
-            <Button onClick={this.handleSubmit}
-              className={classes.lastButton}
-              color="primary"
-              disabled={!isNameValid || (!isDescriptionValid && description.length > 0) || pending}
-              variant="contained">
-              CREATE A PROJECT
-            </Button>
-            {pending && <CircularProgress size={20} className={classes.createProgress} />}
-          </DialogActions>
-        </div>
-      </Dialog>
-    );
-  }
-
-  handleSubmit = () => {
-    this.props.onSubmit({
-      name: this.state.name,
-      description: this.state.description
-    });
-  }
-
-  handleProjectName(e: any) {
-    this.setState({
-      name: e.target.value,
-      isUniqName: ''
-    });
-  }
-
-  handleDescriptionValue(e: any) {
-    this.setState({
-      description: e.target.value,
-    });
-  }
-
-  isNameValid(value: boolean | string) {
-    this.setState({
-      isNameValid: value,
-    });
-  }
 
-  isDescriptionValid(value: boolean | string) {
-    this.setState({
-      isDescriptionValid: value,
-    });
-  }
+    // TODO Make it separate file
+    renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+        <TextField
+            helperText={touched && error ? error : void 0}
+            label={label}
+            className={this.props.classes.textField}
+            error={touched && !!error} 
+            autoComplete='off'
+            {...input}
+            {...custom}
+        />
+    )
 }
 
-type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog" | "dialogTitle" | "createProgress";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
-  button: {
-    marginLeft: theme.spacing.unit
-  },
-  lastButton: {
-    marginLeft: theme.spacing.unit,
-    marginRight: "20px",
-  },
-  dialogContent: {
-    marginTop: "20px",
-  },
-  dialogTitle: {
-    paddingBottom: "0"
-  },
-  textField: {
-    marginTop: "32px",
-  },
-  dialog: {
-    minWidth: "600px",
-    minHeight: "320px"
-  },
-  createProgress: {
-    position: "absolute",
-    minWidth: "20px",
-    right: "95px"
-  }
+    button: {
+        marginLeft: theme.spacing.unit
+    },
+    lastButton: {
+        marginLeft: theme.spacing.unit,
+        marginRight: "20px",
+    },
+    formContainer: {
+        display: "flex",
+        flexDirection: "column",
+        marginTop: "20px",
+    },
+    dialogTitle: {
+        paddingBottom: "0"
+    },
+    textField: {
+        marginTop: "32px",
+    },
+    dialog: {
+        minWidth: "600px",
+        minHeight: "320px"
+    },
+    createProgress: {
+        position: "absolute",
+        minWidth: "20px",
+        right: "95px"
+    },
 });
 
-export default withStyles(styles)(DialogProjectCreate);
\ No newline at end of file
+export default compose(
+    reduxForm({ form: 'projectCreateDialog',/* asyncValidate: isUniqName, asyncBlurFields: ["name"] */}),
+    withStyles(styles)
+)(DialogProjectCreate);
\ No newline at end of file
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 6972b2f..b2bdac8 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -28,7 +28,7 @@ import DetailsPanel from '../../views-components/details-panel/details-panel';
 import { ArvadosTheme } from '../../common/custom-theme';
 import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
 import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
-import CreateProjectDialog from "../../views-components/create-project-dialog/create-project-dialog";
+import DialogProjectCreate from "../../views-components/create-project-dialog/create-project-dialog";
 import { authService } from '../../services/services';
 
 import detailsPanelActions, { loadDetails } from "../../store/details-panel/details-panel-action";
@@ -258,7 +258,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                     actions={contextMenuActions}
                     onActionClick={this.openCreateDialog}
                     onClose={this.closeContextMenu} />
-                <CreateProjectDialog />
+                <DialogProjectCreate />
             </div>
         );
     }
diff --git a/yarn.lock b/yarn.lock
index d71a561..0034169 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -162,6 +162,13 @@
     "@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"
@@ -2489,6 +2496,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"
@@ -3339,6 +3350,10 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f"
 
+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"
@@ -4605,7 +4620,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"
 
@@ -6146,7 +6161,7 @@ react-jss@^8.1.0:
     prop-types "^15.6.0"
     theming "^1.3.0"
 
-react-lifecycles-compat@^3.0.2:
+react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
 
@@ -6397,11 +6412,24 @@ redux-devtools at 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 at 2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
 
-redux at 4.0.0, "redux@>= 3.7.2", redux@^4.0.0:
+redux at 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:

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list