validation-for-dialog
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 10 Jul 2018 10:30:43 +0000 (12:30 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 10 Jul 2018 10:30:43 +0000 (12:30 +0200)
Feature #13694

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

src/components/dialog-create/dialog-project-create.tsx [deleted file]
src/utils/dialog-validator.tsx [new file with mode: 0644]
src/views-components/dialog-create/dialog-project-create.tsx [new file with mode: 0644]
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

diff --git a/src/components/dialog-create/dialog-project-create.tsx b/src/components/dialog-create/dialog-project-create.tsx
deleted file mode 100644 (file)
index dd8c7d1..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-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';
-
-interface ProjectCreateProps {
-  open: boolean;
-  handleClose: () => void;
-}
-
-const DialogProjectCreate: React.SFC<ProjectCreateProps & WithStyles<CssRules>> = ({ classes, open, handleClose }) => {
-  return (
-    <Dialog
-      open={open}
-      onClose={handleClose}>
-      <div className={classes.dialog}>
-        <DialogTitle id="form-dialog-title">Create a project</DialogTitle>
-        <DialogContent className={classes.dialogContent}>
-          <TextField
-            margin="dense"
-            className={classes.textField}
-            id="name"
-            label="Project name"
-            fullWidth />
-          <TextField
-            margin="dense"
-            id="description"
-            label="Description - optional"
-            fullWidth />
-        </DialogContent>
-        <DialogActions>
-          <Button onClick={handleClose} className={classes.button} color="primary">CANCEL</Button>
-          <Button onClick={handleClose} className={classes.lastButton} color="primary" variant="raised">CREATE A PROJECT</Button>
-        </DialogActions>
-      </div>
-    </Dialog>
-  );
-};
-
-type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog";
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-  button: {
-    marginLeft: theme.spacing.unit
-  },
-  lastButton: {
-    marginLeft: theme.spacing.unit,
-    marginRight: "20px",
-  },
-  dialogContent: {
-    marginTop: "20px",
-  },
-  textField: {
-    marginBottom: "32px",
-  },
-  dialog: {
-    minWidth: "550px",
-    minHeight: "320px"
-  }
-});
-
-export default withStyles(styles)(DialogProjectCreate);
\ No newline at end of file
diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx
new file mode 100644 (file)
index 0000000..1d1a921
--- /dev/null
@@ -0,0 +1,72 @@
+// 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,
+  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;
+
+class Validator 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>
+    );
+  }
+}
+
+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/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx
new file mode 100644 (file)
index 0000000..4cdf746
--- /dev/null
@@ -0,0 +1,135 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+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 Validator from '../../utils/dialog-validator';
+
+interface ProjectCreateProps {
+  open: boolean;
+  handleClose: () => void;
+}
+
+interface DialogState {
+  name: string;
+  description: string;
+  isNameValid: boolean;
+  isDescriptionValid: boolean;
+}
+
+class DialogProjectCreate extends React.Component<ProjectCreateProps & WithStyles<CssRules>> {
+  state: DialogState = {
+    name: '',
+    description: '',
+    isNameValid: true,
+    isDescriptionValid: true
+  };
+
+  render() {
+    const { name, description } = this.state;
+    const { classes, open, handleClose } = 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}
+              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 className={classes.dialogActions}>
+            <Button onClick={handleClose} className={classes.button} color="primary">CANCEL</Button>
+            <Button onClick={handleClose} className={classes.lastButton} color="primary" disabled={!this.state.isNameValid || (!this.state.isDescriptionValid && description.length > 0)} variant="raised">CREATE A PROJECT</Button>
+          </DialogActions>
+        </div>
+      </Dialog>
+    );
+  }
+
+  handleProjectName(e: any) {
+    this.setState({
+      name: e.target.value,
+    });
+  }
+
+  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,
+    });
+  }
+}
+
+type CssRules = "button" | "lastButton" | "dialogContent" | "textField" | "dialog" | "dialogTitle" | "dialogActions";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+  button: {
+    marginLeft: theme.spacing.unit
+  },
+  lastButton: {
+    marginLeft: theme.spacing.unit,
+    marginRight: "20px",
+  },
+  dialogContent: {
+    marginTop: "20px",
+  },
+  dialogTitle: {
+    paddingBottom: "0"
+  },
+  dialogActions: {
+    marginBottom: "5px"
+  },
+  textField: {
+    marginTop: "32px",
+  },
+  dialog: {
+    minWidth: "600px",
+    minHeight: "320px"
+  }
+});
+
+export default withStyles(styles)(DialogProjectCreate);
\ No newline at end of file
index f1b82357ec524de9abe55b76add0dde639350bc6..b7c8b954113e2b522be5627ebf7c67317c6d1c80 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from 'react';
 import { ProjectPanelItem } from './project-panel-item';
-import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from '../../common/formatters';
 import DataExplorer from "../../views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
index 9c1336c0d074884edf590ae550b4606718fe16ac..3ca9acc28fd45a950322f24a7046ec6bc09d1b93 100644 (file)
@@ -29,7 +29,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 DialogProjectCreate from '../../components/dialog-create/dialog-project-create';
+import DialogProjectCreate from '../../views-components/dialog-create/dialog-project-create';
 
 const drawerWidth = 240;
 const appBarHeight = 100;