From: Pawel Kowalczyk Date: Tue, 17 Jul 2018 07:59:32 +0000 (+0200) Subject: validation from BE X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/060353602bb2cb48639f040a3ff986b8dcbe84da?ds=sidebyside validation from BE Feature #13781 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- diff --git a/src/common/api/common-resource-service.ts b/src/common/api/common-resource-service.ts index 4c05392389..39825c0e3e 100644 --- a/src/common/api/common-resource-service.ts +++ b/src/common/api/common-resource-service.ts @@ -5,7 +5,7 @@ import * as _ from "lodash"; import FilterBuilder from "./filter-builder"; import OrderBuilder from "./order-builder"; -import { AxiosInstance } from "axios"; +import { AxiosInstance, AxiosPromise } from "axios"; import { Resource } from "../../models/resource"; export interface ListArguments { @@ -26,6 +26,11 @@ export interface ListResults { itemsAvailable: number; } +export interface Errors { + errors: string[]; + errorToken: string; +} + export default class CommonResourceService { static mapResponseKeys = (response: any): Promise => @@ -49,6 +54,12 @@ export default class CommonResourceService { } } + static defaultResponse(promise: AxiosPromise): Promise { + return promise + .then(CommonResourceService.mapResponseKeys) + .catch(({ response }) => Promise.reject(CommonResourceService.mapResponseKeys(response))); + } + protected serverApi: AxiosInstance; protected resourceType: string; @@ -58,21 +69,21 @@ export default class CommonResourceService { } create(data: Partial) { - return this.serverApi - .post(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data)) - .then(CommonResourceService.mapResponseKeys); + return CommonResourceService.defaultResponse( + this.serverApi + .post(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data))); } delete(uuid: string): Promise { - return this.serverApi - .delete(this.resourceType + uuid) - .then(CommonResourceService.mapResponseKeys); + return CommonResourceService.defaultResponse( + this.serverApi + .delete(this.resourceType + uuid)); } get(uuid: string) { - return this.serverApi - .get(this.resourceType + uuid) - .then(CommonResourceService.mapResponseKeys); + return CommonResourceService.defaultResponse( + this.serverApi + .get(this.resourceType + uuid)); } list(args: ListArguments = {}): Promise> { @@ -82,11 +93,11 @@ export default class CommonResourceService { filters: filters ? filters.serialize() : undefined, order: order ? order.getOrder() : undefined }; - return this.serverApi - .get(this.resourceType, { - params: CommonResourceService.mapKeys(_.snakeCase)(params) - }) - .then(CommonResourceService.mapResponseKeys); + return CommonResourceService.defaultResponse( + this.serverApi + .get(this.resourceType, { + params: CommonResourceService.mapKeys(_.snakeCase)(params) + })); } update(uuid: string) { diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index 2d59a4842b..b141736dd6 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -46,7 +46,7 @@ export const createProject = (project: Partial) => return projectService .create(projectData) .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project))) - .catch((response) => dispatch(actions.CREATE_PROJECT_ERROR(response.response.data.errors))); + .catch(errors => dispatch(actions.CREATE_PROJECT_ERROR(errors.errors.join('')))); }; export type ProjectAction = UnionOf; diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts index 10d272c23a..6df428c9d7 100644 --- a/src/store/project/project-reducer.ts +++ b/src/store/project/project-reducer.ts @@ -18,6 +18,7 @@ interface ProjectCreator { opened: boolean; pending: boolean; ownerUuid: string; + error?: string; } export function findTreeItem(tree: Array>, itemId: string): TreeItem | undefined { @@ -113,11 +114,11 @@ const initialState: ProjectState = { const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => { return actions.match(action, { - OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }), + OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }), CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }), - CREATE_PROJECT: () => updateCreator(state, { pending: true }), + CREATE_PROJECT: () => updateCreator(state, { pending: true, error: undefined }), CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "", pending: false }), - CREATE_PROJECT_ERROR: () => updateCreator(state, { ownerUuid: "", pending: false }), + CREATE_PROJECT_ERROR: error => updateCreator(state, { pending: false, error }), 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 index 1d1a9214bd..848acec1b1 100644 --- a/src/utils/dialog-validator.tsx +++ b/src/utils/dialog-validator.tsx @@ -10,19 +10,15 @@ type ValidatorProps = { onChange: (isValid: boolean | string) => void; render: (hasError: boolean) => React.ReactElement; isRequired: boolean; + duplicatedName?: string; }; interface ValidatorState { - isPatternValid: boolean; isLengthValid: boolean; } -const nameRegEx = /^[a-zA-Z0-9-_ ]+$/; -const maxInputLength = 60; - class Validator extends React.Component> { state: ValidatorState = { - isPatternValid: true, isLengthValid: true }; @@ -31,34 +27,35 @@ class Validator extends React.Component> { if (this.props.value !== value) { this.setState({ - isPatternValid: value.match(nameRegEx), - isLengthValid: value.length < maxInputLength + isLengthValid: value.length < MAX_INPUT_LENGTH }, () => this.onChange()); } } onChange() { const { value, onChange, isRequired } = this.props; - const { isPatternValid, isLengthValid } = this.state; - const isValid = value && isPatternValid && isLengthValid && (isRequired || (!isRequired && value.length > 0)); + const { isLengthValid } = this.state; + const isValid = value && isLengthValid && (isRequired || (!isRequired && value.length > 0)); onChange(isValid); } render() { - const { classes, isRequired, value } = this.props; - const { isPatternValid, isLengthValid } = this.state; + const { classes, isRequired, value, duplicatedName } = this.props; + const { isLengthValid } = this.state; return ( - {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))} - {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ? This field allow only alphanumeric characters, dashes, spaces and underscores.
: null} - {!isLengthValid ? This field should have max 60 characters. : null} + {this.props.render(!isLengthValid && (isRequired || (!isRequired && value.length > 0)))} + {!isLengthValid ? This field should have max 255 characters. : null} + {duplicatedName ? Project with this name already exists : null}
); } } +const MAX_INPUT_LENGTH = 255; + type CssRules = "formInputError"; const styles: StyleRulesCallback = theme => ({ 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 eb69837626..2cdd479033 100644 --- a/src/views-components/create-project-dialog/create-project-dialog.tsx +++ b/src/views-components/create-project-dialog/create-project-dialog.tsx @@ -13,6 +13,7 @@ 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 }); const submit = (data: { name: string, description: string }) => diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index 48b89115e4..9f4768ac75 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -8,13 +8,14 @@ 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, CircularProgress } from '@material-ui/core'; +import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress, Typography } from '@material-ui/core'; import Validator from '../../utils/dialog-validator'; interface ProjectCreateProps { open: boolean; pending: boolean; + error: string; handleClose: () => void; onSubmit: (data: { name: string, description: string }) => void; } @@ -24,6 +25,7 @@ interface DialogState { description: string; isNameValid: boolean; isDescriptionValid: boolean; + duplicatedName: string; } class DialogProjectCreate extends React.Component> { @@ -31,11 +33,22 @@ class DialogProjectCreate extends React.Component this.isNameValid(e)} isRequired={true} + duplicatedName={duplicatedName} render={hasError => this.handleProjectName(e)} label="Project name" - error={hasError} + error={hasError || !!duplicatedName} fullWidth />} /> - - {pending && } + + {pending && } @@ -98,6 +112,7 @@ class DialogProjectCreate extends React.Component