[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