From b0de143e088973715681f7eb6c41f2dccb648c2b Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Mon, 23 Jul 2018 13:55:54 +0200 Subject: [PATCH] Creation dialog with redux-form validation Feature #13781 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- src/store/project/project-action.ts | 4 +- src/store/project/project-reducer.ts | 5 +- src/utils/dialog-validator.tsx | 50 ------------------- src/validators/is-uniq-name.tsx | 13 ----- src/validators/require.tsx | 2 +- .../create-project-dialog.tsx | 17 ++++--- .../dialog-create/dialog-project-create.tsx | 46 ++++++++--------- 7 files changed, 34 insertions(+), 103 deletions(-) delete mode 100644 src/utils/dialog-validator.tsx delete mode 100644 src/validators/is-uniq-name.tsx diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index b141736d..3da60f65 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -14,7 +14,6 @@ const actions = unionize({ CLOSE_PROJECT_CREATOR: ofType<{}>(), CREATE_PROJECT: ofType>(), CREATE_PROJECT_SUCCESS: ofType(), - CREATE_PROJECT_ERROR: ofType(), REMOVE_PROJECT: ofType(), PROJECTS_REQUEST: ofType(), PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(), @@ -45,8 +44,7 @@ export const createProject = (project: Partial) => 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; diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts index 6df428c9..a329e812 100644 --- a/src/store/project/project-reducer.ts +++ b/src/store/project/project-reducer.ts @@ -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 }), - 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); diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx deleted file mode 100644 index 42a22e1c..00000000 --- a/src/utils/dialog-validator.tsx +++ /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; - isUniqName?: boolean; - validators: Array<(value: string) => string>; -}; - -class Validator extends React.Component> { - render() { - const { classes, value, isUniqName } = this.props; - - return ( - - {this.props.render(!this.isValid(value))} - {isUniqName ? Project with this name already exists : null} - {this.props.validators.map(validate => { - const errorMsg = validate(value); - return errorMsg ? {errorMsg} : null; - })} - - ); - } - - 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 = 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 index 521bfa30..00000000 --- a/src/validators/is-uniq-name.tsx +++ /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)); diff --git a/src/validators/require.tsx b/src/validators/require.tsx index 4e1e6629..8ac3401c 100644 --- a/src/validators/require.tsx +++ b/src/validators/require.tsx @@ -10,7 +10,7 @@ interface RequireProps { // 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; diff --git a/src/views-components/create-project-dialog/create-project-dialog.tsx b/src/views-components/create-project-dialog/create-project-dialog.tsx index 2cdd4790..f75c4593 100644 --- a/src/views-components/create-project-dialog/create-project-dialog.tsx +++ b/src/views-components/create-project-dialog/create-project-dialog.tsx @@ -3,7 +3,9 @@ // 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"; @@ -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) => ({ - 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(createProject(data)).then(() => { + return dispatch(createProject(data)).then(() => { dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID })); dispatch(getProjectList(ownerUuid)); }); @@ -30,7 +30,10 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(actions.CLOSE_PROJECT_CREATOR()); }, onSubmit: (data: { name: string, description: string }) => { - dispatch(submit(data)); + return dispatch(addProject(data)) + .catch((e: any) => { + throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Project with this name already exists." : "" }); + }); } }); diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index c448df3b..6fb8a699 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -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 { isUniqName } from '../../validators/is-uniq-name'; -interface ProjectCreateProps { +interface DialogProjectProps { open: boolean; - pending: boolean; handleClose: () => void; onSubmit: (data: { name: string, description: string }) => void; handleSubmit: any; + submitting: boolean; } interface TextFieldProps { @@ -31,23 +30,16 @@ interface TextFieldProps { meta?: any; } -class DialogProjectCreate extends React.Component> { - /*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> { render() { - const { classes, open, handleClose, pending, handleSubmit, onSubmit } = this.props; + const { classes, open, handleClose, handleSubmit, onSubmit, submitting } = this.props; return ( + onClose={handleClose} + disableBackdropClick={true} + disableEscapeKeyDown={true}>
onSubmit(data))}> Create a project @@ -60,21 +52,21 @@ class DialogProjectCreate extends React.Component + label="Description - optional" /> - - + + - {pending && } + {submitting && }
@@ -82,13 +74,12 @@ class DialogProjectCreate extends React.Component ( = theme => ({ button: { @@ -126,9 +117,12 @@ const styles: StyleRulesCallback = theme => ({ minWidth: "20px", right: "95px" }, + dialogActions: { + marginBottom: "24px" + } }); export default compose( - reduxForm({ form: 'projectCreateDialog',/* asyncValidate: isUniqName, asyncBlurFields: ["name"] */}), + reduxForm({ form: 'projectCreateDialog' }), withStyles(styles) )(DialogProjectCreate); \ No newline at end of file -- 2.30.2