mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-12 16:14:58 +08:00
Merge pull request #845 from Ducz0r/lm-sci-1670
Input validation extracting to reusable service [SCI-1670]
This commit is contained in:
commit
be2c3b807f
23 changed files with 974 additions and 655 deletions
|
@ -57,19 +57,21 @@ module ClientApi
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
user_service = ClientApi::UserService.new(
|
service = ClientApi::Users::UpdateService.new(
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
params: user_params
|
params: user_params
|
||||||
)
|
)
|
||||||
if user_service.update_user!
|
result = service.execute
|
||||||
|
|
||||||
|
if result[:status] == :success
|
||||||
bypass_sign_in(current_user)
|
bypass_sign_in(current_user)
|
||||||
success_response
|
success_response
|
||||||
else
|
else
|
||||||
unsuccess_response(current_user.errors.full_messages,
|
error_response(
|
||||||
:unprocessable_entity)
|
message: result[:message],
|
||||||
|
details: service.user.errors
|
||||||
|
)
|
||||||
end
|
end
|
||||||
rescue CustomUserError => error
|
|
||||||
unsuccess_response(error.to_s)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -84,22 +86,35 @@ module ClientApi
|
||||||
:system_message_email_notification)
|
:system_message_email_notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
def success_response(template = nil, locals = nil)
|
def success_response(args = {})
|
||||||
|
template = args.fetch(:template) { nil }
|
||||||
|
locals = args.fetch(:locals) { {} }
|
||||||
|
details = args.fetch(:details) { {} }
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
if template && locals
|
if template
|
||||||
render template: template, status: :ok, locals: locals
|
render template: template,
|
||||||
|
status: :ok,
|
||||||
|
locals: locals
|
||||||
else
|
else
|
||||||
render json: {}, status: :ok
|
render json: { details: details }, status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuccess_response(message, status = :unprocessable_entity)
|
def error_response(args = {})
|
||||||
|
message = args.fetch(:message) { t('client_api.generic_error_message') }
|
||||||
|
details = args.fetch(:details) { {} }
|
||||||
|
status = args.fetch(:status) { :unprocessable_entity }
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
render json: { message: message },
|
render json: {
|
||||||
|
message: message,
|
||||||
|
details: details
|
||||||
|
},
|
||||||
status: status
|
status: status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { HelpBlock } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import shortid from "shortid";
|
||||||
|
|
||||||
|
const MyHelpBlock = styled(HelpBlock)`
|
||||||
|
& > span {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class ValidatedErrorHelpBlock extends Component {
|
||||||
|
static renderErrorMessage(error) {
|
||||||
|
const key = shortid.generate();
|
||||||
|
if (error.intl) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
key={key}
|
||||||
|
id={error.messageId}
|
||||||
|
values={error.values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span key={key}>{error.message}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanProps() {
|
||||||
|
// Remove additional props from the props
|
||||||
|
const { tag, ...cleanProps } = this.props;
|
||||||
|
return cleanProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Remove additional props from the props
|
||||||
|
const { tag, ...cleanProps } = this.props;
|
||||||
|
|
||||||
|
const errors = this.context.errors(tag) || [];
|
||||||
|
return (
|
||||||
|
<MyHelpBlock {...cleanProps}>
|
||||||
|
{errors.map((error) => ValidatedErrorHelpBlock.renderErrorMessage(error))}
|
||||||
|
</MyHelpBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedErrorHelpBlock.propTypes = {
|
||||||
|
tag: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidatedErrorHelpBlock.contextTypes = {
|
||||||
|
errors: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedErrorHelpBlock;
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import update from "immutability-helper";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
class ValidatedForm extends Component {
|
||||||
|
static parseErrors(errors) {
|
||||||
|
// This method is quite smart, in the sense that accepts either
|
||||||
|
// errors in 3 shapes: localized error messages ({}),
|
||||||
|
// unlocalized error messages ({}), or mere strings (unlocalized)
|
||||||
|
const arr = _.isString(errors) ? [errors] : errors;
|
||||||
|
return arr.map((el) => _.isString(el) ? { message: el } : el);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {}
|
||||||
|
|
||||||
|
this.setErrors = this.setErrors.bind(this);
|
||||||
|
this.setErrorsForTag = this.setErrorsForTag.bind(this);
|
||||||
|
this.errors = this.errors.bind(this);
|
||||||
|
this.hasAnyError = this.hasAnyError.bind(this);
|
||||||
|
this.hasErrorForTag = this.hasErrorForTag.bind(this);
|
||||||
|
this.addErrorsForTag = this.addErrorsForTag.bind(this);
|
||||||
|
this.clearErrorsForTag = this.clearErrorsForTag.bind(this);
|
||||||
|
this.clearErrors = this.clearErrors.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
// Pass functions downstream via context
|
||||||
|
return {
|
||||||
|
setErrors: this.setErrors,
|
||||||
|
setErrorsForTag: this.setErrorsForTag,
|
||||||
|
errors: this.errors,
|
||||||
|
hasAnyError: this.hasAnyError,
|
||||||
|
hasErrorForTag: this.hasErrorForTag,
|
||||||
|
addErrorsForTag: this.addErrorsForTag,
|
||||||
|
clearErrorsForTag: this.clearErrorsForTag,
|
||||||
|
clearErrors: this.clearErrors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(errors) {
|
||||||
|
const newState = {};
|
||||||
|
_.entries(errors).forEach(([key, value]) => {
|
||||||
|
newState[key] = ValidatedForm.parseErrors(value);
|
||||||
|
});
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorsForTag(tag, errors) {
|
||||||
|
const newState = update(this.state, {
|
||||||
|
[tag]: { $set: ValidatedForm.parseErrors(errors) }
|
||||||
|
});
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors(tag) {
|
||||||
|
return this.state[tag];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyError() {
|
||||||
|
return _.values(this.state) &&
|
||||||
|
_.flatten(_.values(this.state)).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasErrorForTag(tag) {
|
||||||
|
return _.has(this.state, tag) && this.state[tag].length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addErrorsForTag(tag, errors) {
|
||||||
|
let newState;
|
||||||
|
if (_.has(this.state, tag)) {
|
||||||
|
newState = update(this.state, { [tag]: { $push: errors } });
|
||||||
|
} else {
|
||||||
|
newState = update(this.state, { [tag]: { $set: errors } });
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearErrorsForTag(tag) {
|
||||||
|
const newState = update(this.state, { [tag]: { $set: [] } });
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearErrors() {
|
||||||
|
this.setState({});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<form {...this.props}>
|
||||||
|
{this.props.children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedForm.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedForm.defaultProps = {
|
||||||
|
children: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedForm.childContextTypes = {
|
||||||
|
setErrors: PropTypes.func,
|
||||||
|
setErrorsForTag: PropTypes.func,
|
||||||
|
errors: PropTypes.func,
|
||||||
|
hasAnyError: PropTypes.func,
|
||||||
|
hasErrorForTag: PropTypes.func,
|
||||||
|
addErrorsForTag: PropTypes.func,
|
||||||
|
clearErrorsForTag: PropTypes.func,
|
||||||
|
clearErrors: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedForm;
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { FormControl } from "react-bootstrap";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
class ValidatedFormControl extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.cleanProps = this.cleanProps.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
const tag = this.props.tag;
|
||||||
|
const messageIds = this.props.messageIds;
|
||||||
|
const target = e.target;
|
||||||
|
|
||||||
|
// Pass-through "original" onChange
|
||||||
|
if (_.has(this.props, "onChange") && this.props.onChange !== undefined) {
|
||||||
|
this.props.onChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the field
|
||||||
|
let errors = [];
|
||||||
|
this.props.validatorsOnChange.forEach((validator) => {
|
||||||
|
errors = errors.concat(validator(target, messageIds));
|
||||||
|
});
|
||||||
|
this.context.setErrorsForTag(tag, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanProps() {
|
||||||
|
// Remove additional props from the props
|
||||||
|
const {
|
||||||
|
tag,
|
||||||
|
messageIds,
|
||||||
|
validatorsOnChange,
|
||||||
|
onChange,
|
||||||
|
...cleanProps
|
||||||
|
} = this.props;
|
||||||
|
return cleanProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
onChange={this.handleChange}
|
||||||
|
{...this.cleanProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedFormControl.propTypes = {
|
||||||
|
tag: PropTypes.string.isRequired,
|
||||||
|
messageIds: PropTypes.objectOf(PropTypes.string),
|
||||||
|
validatorsOnChange: PropTypes.arrayOf(PropTypes.func),
|
||||||
|
onChange: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedFormControl.defaultProps = {
|
||||||
|
messageIds: {},
|
||||||
|
validatorsOnChange: [],
|
||||||
|
onChange: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedFormControl.contextTypes = {
|
||||||
|
setErrorsForTag: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedFormControl;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FormGroup } from "react-bootstrap";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const ValidatedFormGroup = (props, context) => {
|
||||||
|
// Remove additional props from the props
|
||||||
|
const { tag, ...cleanProps } = props;
|
||||||
|
|
||||||
|
const hasError = context.hasErrorForTag(tag);
|
||||||
|
const formGroupClass = `form-group ${hasError ? " has-error" : ""}`;
|
||||||
|
const validationState = hasError ? "error" : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
className={formGroupClass}
|
||||||
|
validationState={validationState}
|
||||||
|
{...cleanProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidatedFormGroup.propTypes = {
|
||||||
|
tag: PropTypes.string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedFormGroup.contextTypes = {
|
||||||
|
hasErrorForTag: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedFormGroup;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const ValidatedSubmitButton = (props, context) =>
|
||||||
|
<Button {...props} disabled={context.hasAnyError()}>
|
||||||
|
{props.children}
|
||||||
|
</Button>
|
||||||
|
;
|
||||||
|
|
||||||
|
ValidatedSubmitButton.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedSubmitButton.defaultProps = {
|
||||||
|
children: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatedSubmitButton.contextTypes = {
|
||||||
|
hasAnyError: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatedSubmitButton;
|
5
app/javascript/src/components/validation/index.js
Normal file
5
app/javascript/src/components/validation/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export {default as ValidatedErrorHelpBlock} from "./components/ValidatedErrorHelpBlock";
|
||||||
|
export {default as ValidatedForm} from "./components/ValidatedForm";
|
||||||
|
export {default as ValidatedFormControl} from "./components/ValidatedFormControl";
|
||||||
|
export {default as ValidatedFormGroup} from "./components/ValidatedFormGroup";
|
||||||
|
export {default as ValidatedSubmitButton} from "./components/ValidatedSubmitButton";
|
45
app/javascript/src/components/validation/validators/file.js
Normal file
45
app/javascript/src/components/validation/validators/file.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import { AVATAR_MAX_SIZE_MB } from "../../../config/constants/numeric";
|
||||||
|
import { AVATAR_VALID_EXTENSIONS } from "../../../config/constants/strings";
|
||||||
|
|
||||||
|
export const avatarExtensionValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "invalid_file_extension") ?
|
||||||
|
messageIds.invalid_file_extension :
|
||||||
|
"validators.file.invalid_file_extension";
|
||||||
|
|
||||||
|
const filePath = target.value;
|
||||||
|
const ext = filePath
|
||||||
|
.substring(filePath.lastIndexOf(".") + 1)
|
||||||
|
.toLowerCase();
|
||||||
|
const validExtsString = AVATAR_VALID_EXTENSIONS
|
||||||
|
.map(val => `.${val}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
if (!_.includes(AVATAR_VALID_EXTENSIONS, ext)) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values: { valid_extensions: validExtsString }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const avatarSizeValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "file_too_large") ?
|
||||||
|
messageIds.file_too_large :
|
||||||
|
"validators.file.file_too_large";
|
||||||
|
const maxSizeKb = AVATAR_MAX_SIZE_MB * 1024;
|
||||||
|
|
||||||
|
if (target.files && target.files[0]) {
|
||||||
|
const fileSize = target.files[0].size / 1024; // size in KB
|
||||||
|
if (fileSize > maxSizeKb) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values: { max_size: AVATAR_MAX_SIZE_MB }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
139
app/javascript/src/components/validation/validators/text.js
Normal file
139
app/javascript/src/components/validation/validators/text.js
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
NAME_MIN_LENGTH,
|
||||||
|
NAME_MAX_LENGTH,
|
||||||
|
TEXT_MAX_LENGTH,
|
||||||
|
PASSWORD_MIN_LENGTH,
|
||||||
|
PASSWORD_MAX_LENGTH,
|
||||||
|
USER_INITIALS_MAX_LENGTH
|
||||||
|
} from "../../../config/constants/numeric";
|
||||||
|
import { EMAIL_REGEX } from "../../../config/constants/strings";
|
||||||
|
|
||||||
|
export const nameMinLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "text_too_short") ?
|
||||||
|
messageIds.text_too_short :
|
||||||
|
"validators.text.text_too_short";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length < NAME_MIN_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values: { min_length: NAME_MIN_LENGTH }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nameMaxLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "text_too_long") ?
|
||||||
|
messageIds.text_too_long :
|
||||||
|
"validators.text.text_too_long";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length > NAME_MAX_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values:{ max_length: NAME_MAX_LENGTH }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nameLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const res = nameMinLengthValidator(target, messageIds);
|
||||||
|
if (res.length > 0) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return nameMaxLengthValidator(target, messageIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const textBlankValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "text_blank") ?
|
||||||
|
messageIds.text_blank :
|
||||||
|
"validators.text.text_blank";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length === 0) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const textMaxLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "text_too_long") ?
|
||||||
|
messageIds.text_too_long :
|
||||||
|
"validators.text.text_too_long";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length > TEXT_MAX_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values: { max_length: TEXT_MAX_LENGTH }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const passwordLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const messageIdTooShort = _.has(messageIds, "text_too_short") ?
|
||||||
|
messageIds.text_too_short :
|
||||||
|
"validators.text.text_too_short";
|
||||||
|
const messageIdTooLong = _.has(messageIds, "text_too_long") ?
|
||||||
|
messageIds.text_too_long :
|
||||||
|
"validators.text.text_too_long";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId: messageIdTooShort,
|
||||||
|
values:{ min_length: PASSWORD_MIN_LENGTH }
|
||||||
|
}];
|
||||||
|
} else if (value.length > PASSWORD_MAX_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId: messageIdTooLong,
|
||||||
|
values:{ max_length: PASSWORD_MAX_LENGTH }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userInitialsMaxLengthValidator = (target, messageIds = {}) => {
|
||||||
|
const messageId = _.has(messageIds, "text_too_long") ?
|
||||||
|
messageIds.text_too_long :
|
||||||
|
"validators.text.text_too_long";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (value.length > USER_INITIALS_MAX_LENGTH) {
|
||||||
|
return [{
|
||||||
|
intl: true,
|
||||||
|
messageId,
|
||||||
|
values: { max_length: USER_INITIALS_MAX_LENGTH }
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emailValidator = (target, messageIds = {}) => {
|
||||||
|
const res = textBlankValidator(target, messageIds);
|
||||||
|
if (res.length > 0) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = _.has(messageIds, "invalid_email") ?
|
||||||
|
messageIds.invalid_email :
|
||||||
|
"validators.text.invalid_email";
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
|
if (!EMAIL_REGEX.test(value)) {
|
||||||
|
return [{ intl: true, messageId }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
|
@ -6,3 +6,4 @@ export const PASSWORD_MAX_LENGTH = 72;
|
||||||
export const NAME_MAX_LENGTH = 255;
|
export const NAME_MAX_LENGTH = 255;
|
||||||
export const TEXT_MAX_LENGTH = 10000;
|
export const TEXT_MAX_LENGTH = 10000;
|
||||||
export const INVITE_USERS_LIMIT = 20;
|
export const INVITE_USERS_LIMIT = 20;
|
||||||
|
export const AVATAR_MAX_SIZE_MB = 0.2;
|
|
@ -2,3 +2,4 @@ export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
|
||||||
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
|
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
|
||||||
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";
|
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";
|
||||||
export const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
export const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
export const AVATAR_VALID_EXTENSIONS = ["jpg", "jpeg", "png", "gif"];
|
|
@ -16,12 +16,17 @@ export default {
|
||||||
all_teams_page: "SciNote | Settings | Teams",
|
all_teams_page: "SciNote | Settings | Teams",
|
||||||
new_team_page: "SciNote | Settings | Teams | New"
|
new_team_page: "SciNote | Settings | Teams | New"
|
||||||
},
|
},
|
||||||
error_messages: {
|
validators: {
|
||||||
text_too_short: "is too short (minimum is {min_length} characters)",
|
text: {
|
||||||
text_too_long: "is too long (maximum is {max_length} characters)",
|
text_too_short: "is too short (minimum is {min_length} characters)",
|
||||||
cant_be_blank: "can't be blank",
|
text_too_long: "is too long (maximum is {max_length} characters)",
|
||||||
invalid_email: "invalid email",
|
text_blank: "can't be blank",
|
||||||
passwords_dont_match: "Passwords don't match"
|
invalid_email: "invalid email"
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
invalid_file_extension: "invalid file extension (valid extensions are {valid_extensions})",
|
||||||
|
file_too_large: "file too large (maximum size is {max_size} MB)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
page_title: "sciNote",
|
page_title: "sciNote",
|
||||||
|
|
|
@ -4,29 +4,39 @@ import { string, func } from "prop-types";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
|
||||||
FormControl,
|
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
Button,
|
Button,
|
||||||
ButtonToolbar,
|
ButtonToolbar,
|
||||||
HelpBlock
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import update from "immutability-helper";
|
||||||
import { updateUser } from "../../../../../services/api/users_api";
|
import { updateUser } from "../../../../../services/api/users_api";
|
||||||
import { transformName } from "../../../../../services/helpers/string_helper";
|
import { transformName } from "../../../../../services/helpers/string_helper";
|
||||||
import { addAlert } from "../../../../../components/actions/AlertsActions";
|
import { addAlert } from "../../../../../components/actions/AlertsActions";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BORDER_LIGHT_COLOR,
|
BORDER_LIGHT_COLOR,
|
||||||
COLOR_APPLE_BLOSSOM
|
|
||||||
} from "../../../../../config/constants/colors";
|
} from "../../../../../config/constants/colors";
|
||||||
import {
|
import {
|
||||||
ENTER_KEY_CODE,
|
ENTER_KEY_CODE,
|
||||||
USER_INITIALS_MAX_LENGTH,
|
|
||||||
NAME_MAX_LENGTH,
|
|
||||||
PASSWORD_MAX_LENGTH,
|
|
||||||
PASSWORD_MIN_LENGTH
|
|
||||||
} from "../../../../../config/constants/numeric";
|
} from "../../../../../config/constants/numeric";
|
||||||
import { EMAIL_REGEX } from "../../../../../config/constants/strings";
|
import {
|
||||||
|
ValidatedForm,
|
||||||
|
ValidatedFormGroup,
|
||||||
|
ValidatedFormControl,
|
||||||
|
ValidatedErrorHelpBlock,
|
||||||
|
ValidatedSubmitButton
|
||||||
|
} from "../../../../../components/validation";
|
||||||
|
import {
|
||||||
|
textBlankValidator,
|
||||||
|
nameMaxLengthValidator,
|
||||||
|
passwordLengthValidator,
|
||||||
|
userInitialsMaxLengthValidator,
|
||||||
|
emailValidator
|
||||||
|
} from "../../../../../components/validation/validators/text";
|
||||||
|
import {
|
||||||
|
avatarExtensionValidator,
|
||||||
|
avatarSizeValidator
|
||||||
|
} from "../../../../../components/validation/validators/file";
|
||||||
|
|
||||||
const StyledInputEnabled = styled.div`
|
const StyledInputEnabled = styled.div`
|
||||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||||
|
@ -38,10 +48,6 @@ const StyledInputEnabled = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledHelpBlock = styled(HelpBlock)`
|
|
||||||
color: ${COLOR_APPLE_BLOSSOM};
|
|
||||||
`;
|
|
||||||
|
|
||||||
class InputEnabled extends Component {
|
class InputEnabled extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -49,30 +55,16 @@ class InputEnabled extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
value: this.props.inputValue === "********" ? "" : this.props.inputValue,
|
value: this.props.inputValue === "********" ? "" : this.props.inputValue,
|
||||||
current_password: "",
|
current_password: "",
|
||||||
password_confirmation: "",
|
password_confirmation: ""
|
||||||
errorMessage: ""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handlePasswordConfirmation = this.handlePasswordConfirmation.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.confirmationField = this.confirmationField.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
this.getValidationState = this.getValidationState.bind(this);
|
|
||||||
this.handleFullNameValidation = this.handleFullNameValidation.bind(this);
|
|
||||||
this.handleEmailValidation = this.handleEmailValidation.bind(this);
|
|
||||||
this.handleInitialsValidation = this.handleInitialsValidation.bind(this);
|
|
||||||
this.handlePasswordConfirmationValidation = this.handlePasswordConfirmationValidation.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.handleCurrentPassword = this.handleCurrentPassword.bind(this);
|
this.handleCurrentPassword = this.handleCurrentPassword.bind(this);
|
||||||
this.handleFileChange = this.handleFileChange.bind(this);
|
this.handlePasswordConfirmation = this.handlePasswordConfirmation.bind(this);
|
||||||
}
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.inputField = this.inputField.bind(this);
|
||||||
getValidationState() {
|
this.confirmationField = this.confirmationField.bind(this);
|
||||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(event) {
|
handleKeyPress(event) {
|
||||||
|
@ -83,170 +75,30 @@ class InputEnabled extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(event) {
|
handleChange(event) {
|
||||||
event.preventDefault();
|
let newVal;
|
||||||
switch (this.props.dataField) {
|
if (this.props.dataField === "avatar") {
|
||||||
case "full_name":
|
newVal = event.currentTarget.files[0];
|
||||||
this.handleFullNameValidation(event);
|
|
||||||
break;
|
|
||||||
case "email":
|
|
||||||
this.handleEmailValidation(event);
|
|
||||||
break;
|
|
||||||
case "initials":
|
|
||||||
this.handleInitialsValidation(event);
|
|
||||||
break;
|
|
||||||
case "password":
|
|
||||||
this.handlePasswordValidation(event);
|
|
||||||
break;
|
|
||||||
case "avatar":
|
|
||||||
this.handleFileChange(event);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.setState({ value: event.target.value, errorMessage: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFileChange(event) {
|
|
||||||
this.setState({ value: event.currentTarget.files[0], errorMessage: "" });
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordConfirmation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.length === 0) {
|
|
||||||
this.setState({
|
|
||||||
password_confirmation: value,
|
|
||||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.setState({ password_confirmation: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFullNameValidation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.length > NAME_MAX_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: NAME_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else if (value.length === 0) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({ value, errorMessage: "" });
|
newVal = event.target.value;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEmailValidation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (!EMAIL_REGEX.test(value)) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: <FormattedMessage id="error_messages.invalid_email" />
|
|
||||||
});
|
|
||||||
} else if (value.length === 0) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ value, errorMessage: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInitialsValidation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.length > USER_INITIALS_MAX_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: USER_INITIALS_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else if (value.length === 0) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: <FormattedMessage id="error_messages.cant_be_blank" />
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ value, errorMessage: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordValidation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value.length > PASSWORD_MAX_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: PASSWORD_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else if (value.length < PASSWORD_MIN_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_short"
|
|
||||||
values={{ min_length: PASSWORD_MIN_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ value, errorMessage: "" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordConfirmationValidation(event) {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value !== this.state.value) {
|
|
||||||
this.setState({
|
|
||||||
password_confirmation: value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage id="error_messages.passwords_dont_match" />
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ password_confirmation: value, errorMessage: "" });
|
|
||||||
}
|
}
|
||||||
|
const newState = update(this.state, {
|
||||||
|
value: { $set: newVal }
|
||||||
|
});
|
||||||
|
this.setState(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCurrentPassword(event) {
|
handleCurrentPassword(event) {
|
||||||
const { value } = event.target;
|
const newState = update(this.state, {
|
||||||
if (value.length > PASSWORD_MAX_LENGTH) {
|
current_password: { $set: event.target.value }
|
||||||
this.setState({
|
});
|
||||||
current_password: value,
|
this.setState(newState);
|
||||||
errorMessage: (
|
}
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
handlePasswordConfirmation(event) {
|
||||||
values={{ max_length: PASSWORD_MAX_LENGTH }}
|
const newState = update(this.state, {
|
||||||
/>
|
password_confirmation: { $set: event.target.value }
|
||||||
)
|
});
|
||||||
});
|
this.setState(newState);
|
||||||
} else if (value.length < PASSWORD_MIN_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
current_password: value,
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_short"
|
|
||||||
values={{ min_length: PASSWORD_MIN_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ current_password: value, errorMessage: "" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(event) {
|
handleSubmit(event) {
|
||||||
|
@ -292,7 +144,7 @@ class InputEnabled extends Component {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(({ response }) => {
|
.catch(({ response }) => {
|
||||||
this.setState({ errorMessage: response.data.message.toString() });
|
this.form.setErrors(response.data.details);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,108 +153,156 @@ class InputEnabled extends Component {
|
||||||
|
|
||||||
if (type === "email") {
|
if (type === "email") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<ValidatedFormGroup tag="current_password">
|
||||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
<ControlLabel>
|
||||||
<FormControl
|
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||||
|
</ControlLabel>
|
||||||
|
<ValidatedFormControl
|
||||||
id="settings_page.current_password"
|
id="settings_page.current_password"
|
||||||
|
tag="current_password"
|
||||||
type="password"
|
type="password"
|
||||||
value={this.state.current_password}
|
value={this.state.current_password}
|
||||||
|
validatorsOnChange={[passwordLengthValidator]}
|
||||||
onChange={this.handleCurrentPassword}
|
onChange={this.handleCurrentPassword}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
</div>
|
<ValidatedErrorHelpBlock tag="current_password" />
|
||||||
|
</ValidatedFormGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
inputField() {
|
inputField() {
|
||||||
const { inputType } = this.props;
|
const { inputType, dataField } = this.props;
|
||||||
|
|
||||||
|
let validatorsOnChange = [];
|
||||||
|
if (dataField === "full_name") {
|
||||||
|
validatorsOnChange = [textBlankValidator, nameMaxLengthValidator];
|
||||||
|
} else if (dataField === "initials") {
|
||||||
|
validatorsOnChange = [textBlankValidator, userInitialsMaxLengthValidator];
|
||||||
|
} else if (dataField === "email") {
|
||||||
|
validatorsOnChange = [emailValidator];
|
||||||
|
} else if (dataField === "avatar") {
|
||||||
|
validatorsOnChange = [avatarExtensionValidator, avatarSizeValidator];
|
||||||
|
}
|
||||||
|
|
||||||
if (inputType === "password") {
|
if (inputType === "password") {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
<ValidatedFormGroup tag="current_password">
|
||||||
<FormControl
|
<ControlLabel>
|
||||||
id="settings_page.current_password"
|
<FormattedHTMLMessage id="settings_page.password_confirmation" />
|
||||||
type={inputType}
|
</ControlLabel>
|
||||||
value={this.state.current_password}
|
<ValidatedFormControl
|
||||||
onChange={this.handleCurrentPassword}
|
id="settings_page.current_password"
|
||||||
autoFocus
|
type="password"
|
||||||
/>
|
value={this.state.current_password}
|
||||||
<ControlLabel>
|
tag="current_password"
|
||||||
<FormattedMessage id="settings_page.new_password" />
|
validatorsOnChange={[passwordLengthValidator]}
|
||||||
</ControlLabel>
|
onChange={this.handleCurrentPassword}
|
||||||
<FormControl
|
onKeyPress={this.handleKeyPress}
|
||||||
id="settings_page.new_password"
|
autoFocus
|
||||||
type={inputType}
|
/>
|
||||||
value={this.state.value}
|
<ValidatedErrorHelpBlock tag="current_password" />
|
||||||
onChange={this.handleChange}
|
</ValidatedFormGroup>
|
||||||
autoFocus
|
<ValidatedFormGroup tag="new_password">
|
||||||
/>
|
<ControlLabel>
|
||||||
<ControlLabel>
|
<FormattedMessage id="settings_page.new_password" />
|
||||||
<FormattedMessage id="settings_page.new_password_confirmation" />
|
</ControlLabel>
|
||||||
</ControlLabel>
|
<ValidatedFormControl
|
||||||
<FormControl
|
id="settings_page.new_password"
|
||||||
id="settings_page.new_password_confirmation"
|
type="password"
|
||||||
type={inputType}
|
value={this.state.value}
|
||||||
value={this.state.password_confirmation}
|
onChange={this.handleChange}
|
||||||
onChange={this.handlePasswordConfirmationValidation}
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
tag="new_password"
|
||||||
|
validatorsOnChange={[passwordLengthValidator]}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<ValidatedErrorHelpBlock tag="new_password" />
|
||||||
|
</ValidatedFormGroup>
|
||||||
|
<ValidatedFormGroup tag="new_password_confirmation">
|
||||||
|
<ControlLabel>
|
||||||
|
<FormattedMessage id="settings_page.new_password_confirmation" />
|
||||||
|
</ControlLabel>
|
||||||
|
<ValidatedFormControl
|
||||||
|
id="settings_page.new_password_confirmation"
|
||||||
|
type="password"
|
||||||
|
value={this.state.password_confirmation}
|
||||||
|
onChange={this.handlePasswordConfirmation}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
tag="new_password_confirmation"
|
||||||
|
validatorsOnChange={[passwordLengthValidator]}
|
||||||
|
/>
|
||||||
|
<ValidatedErrorHelpBlock tag="new_password_confirmation" />
|
||||||
|
</ValidatedFormGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (inputType === "file") {
|
if (inputType === "file") {
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<ValidatedFormGroup tag={dataField}>
|
||||||
id="user_avatar_input"
|
<ValidatedFormControl
|
||||||
type={this.props.inputType}
|
id="user_avatar_input"
|
||||||
onChange={this.handleChange}
|
tag={dataField}
|
||||||
onKeyPress={this.handleKeyPress}
|
type={this.props.inputType}
|
||||||
autoFocus
|
onChange={this.handleChange}
|
||||||
/>
|
onKeyPress={this.handleKeyPress}
|
||||||
|
validatorsOnChange={validatorsOnChange}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<ValidatedErrorHelpBlock tag={dataField} />
|
||||||
|
</ValidatedFormGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<ValidatedFormGroup tag={dataField}>
|
||||||
type={this.props.inputType}
|
<ValidatedFormControl
|
||||||
value={this.state.value}
|
tag={dataField}
|
||||||
onChange={this.handleChange}
|
type={this.props.inputType}
|
||||||
onKeyPress={this.handleKeyPress}
|
onChange={this.handleChange}
|
||||||
autoFocus
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
validatorsOnChange={validatorsOnChange}
|
||||||
|
value={this.state.value}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<ValidatedErrorHelpBlock tag={dataField} />
|
||||||
|
</ValidatedFormGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StyledInputEnabled id={transformName(this.props.labelTitle)}>
|
<StyledInputEnabled>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<ValidatedForm
|
||||||
<FormGroup validationState={this.getValidationState()}>
|
onSubmit={this.handleSubmit}
|
||||||
<h4>
|
ref={(f) => { this.form = f; }}
|
||||||
<FormattedMessage id="settings_page.change" />
|
>
|
||||||
<FormattedMessage id={this.props.labelTitle} />
|
<h4>
|
||||||
</h4>
|
<FormattedMessage id="settings_page.change" />
|
||||||
{this.props.labelValue !== "none" && (
|
<FormattedMessage id={this.props.labelTitle} />
|
||||||
<ControlLabel>
|
</h4>
|
||||||
<FormattedMessage id={this.props.labelValue} />
|
{this.props.labelValue !== "none" && (
|
||||||
</ControlLabel>
|
<ControlLabel>
|
||||||
)}
|
<FormattedMessage id={this.props.labelValue} />
|
||||||
{this.inputField()}
|
</ControlLabel>
|
||||||
{this.confirmationField()}
|
)}
|
||||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
{this.inputField()}
|
||||||
<ButtonToolbar>
|
{this.confirmationField()}
|
||||||
<Button bsStyle="primary" type="submit">
|
<ButtonToolbar>
|
||||||
<FormattedMessage
|
<ValidatedSubmitButton bsStyle="primary" type="submit">
|
||||||
id={`general.${this.props.dataField === "avatar"
|
<FormattedMessage
|
||||||
? "upload"
|
id={`general.${this.props.dataField === "avatar"
|
||||||
: "update"}`}
|
? "upload"
|
||||||
/>
|
: "update"}`}
|
||||||
</Button>
|
/>
|
||||||
<Button bsStyle="default" onClick={this.props.disableEdit}>
|
</ValidatedSubmitButton>
|
||||||
<FormattedMessage id="general.cancel" />
|
<Button bsStyle="default" onClick={this.props.disableEdit}>
|
||||||
</Button>
|
<FormattedMessage id="general.cancel" />
|
||||||
</ButtonToolbar>
|
</Button>
|
||||||
</FormGroup>
|
</ButtonToolbar>
|
||||||
</form>
|
</ValidatedForm>
|
||||||
</StyledInputEnabled>
|
</StyledInputEnabled>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,55 +3,40 @@ import PropTypes, { bool, number, string, func } from "prop-types";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
HelpBlock
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import _ from "lodash";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import axios from "../../../../../config/axios";
|
import axios from "../../../../../config/axios";
|
||||||
|
import {
|
||||||
|
ValidatedForm,
|
||||||
|
ValidatedFormGroup,
|
||||||
|
ValidatedFormControl,
|
||||||
|
ValidatedErrorHelpBlock,
|
||||||
|
ValidatedSubmitButton
|
||||||
|
} from "../../../../../components/validation";
|
||||||
|
import {
|
||||||
|
textMaxLengthValidator
|
||||||
|
} from "../../../../../components/validation/validators/text";
|
||||||
|
|
||||||
import { TEXT_MAX_LENGTH } from "../../../../../config/constants/numeric";
|
|
||||||
import { TEAM_UPDATE_PATH } from "../../../../../config/api_endpoints";
|
import { TEAM_UPDATE_PATH } from "../../../../../config/api_endpoints";
|
||||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../config/constants/colors";
|
|
||||||
|
|
||||||
const StyledHelpBlock = styled(HelpBlock)`color: ${COLOR_APPLE_BLOSSOM};`;
|
|
||||||
|
|
||||||
class UpdateTeamDescriptionModal extends Component {
|
class UpdateTeamDescriptionModal extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { errorMessage: "", description: "" };
|
this.state = { description: "" };
|
||||||
this.onCloseModal = this.onCloseModal.bind(this);
|
this.onCloseModal = this.onCloseModal.bind(this);
|
||||||
this.updateDescription = this.updateDescription.bind(this);
|
this.updateDescription = this.updateDescription.bind(this);
|
||||||
this.handleDescription = this.handleDescription.bind(this);
|
this.handleDescription = this.handleDescription.bind(this);
|
||||||
this.getValidationState = this.getValidationState.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCloseModal() {
|
onCloseModal() {
|
||||||
this.setState({ errorMessage: "", description: "" });
|
this.setState({ description: "" });
|
||||||
this.props.hideModal();
|
this.props.hideModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidationState() {
|
|
||||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDescription(el) {
|
handleDescription(el) {
|
||||||
const { value } = el.target;
|
this.setState({ description: el.target.value });
|
||||||
if (value.length > TEXT_MAX_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: TEXT_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ errorMessage: "", description: value });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDescription() {
|
updateDescription() {
|
||||||
|
@ -74,40 +59,40 @@ class UpdateTeamDescriptionModal extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||||
<Modal.Header closeButton>
|
<ValidatedForm ref={(f) => { this.form = f; }}>
|
||||||
<Modal.Title>
|
<Modal.Header closeButton>
|
||||||
<FormattedMessage id="settings_page.update_team_description_modal.title" />
|
<Modal.Title>
|
||||||
</Modal.Title>
|
<FormattedMessage id="settings_page.update_team_description_modal.title" />
|
||||||
</Modal.Header>
|
</Modal.Title>
|
||||||
<Modal.Body>
|
</Modal.Header>
|
||||||
<FormGroup
|
<Modal.Body>
|
||||||
controlId="teamDescription"
|
<ValidatedFormGroup tag="description">
|
||||||
validationState={this.getValidationState()}
|
<ControlLabel>
|
||||||
>
|
<FormattedMessage id="settings_page.update_team_description_modal.label" />
|
||||||
<ControlLabel>
|
</ControlLabel>
|
||||||
<FormattedMessage id="settings_page.update_team_description_modal.label" />
|
<ValidatedFormControl
|
||||||
</ControlLabel>
|
componentClass="textarea"
|
||||||
<FormControl
|
tag="description"
|
||||||
componentClass="textarea"
|
defaultValue={this.props.team.description}
|
||||||
defaultValue={this.props.team.description}
|
onChange={this.handleDescription}
|
||||||
onChange={this.handleDescription}
|
validatorsOnChange={[textMaxLengthValidator]}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback />
|
<FormControl.Feedback />
|
||||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
<ValidatedErrorHelpBlock tag="description" />
|
||||||
</FormGroup>
|
</ValidatedFormGroup>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button
|
<ValidatedSubmitButton
|
||||||
bsStyle="primary"
|
bsStyle="primary"
|
||||||
onClick={this.updateDescription}
|
onClick={this.updateDescription}
|
||||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
>
|
||||||
>
|
<FormattedMessage id="general.update" />
|
||||||
<FormattedMessage id="general.update" />
|
</ValidatedSubmitButton>
|
||||||
</Button>
|
<Button onClick={this.onCloseModal}>
|
||||||
<Button onClick={this.onCloseModal}>
|
<FormattedMessage id="general.close" />
|
||||||
<FormattedMessage id="general.close" />
|
</Button>
|
||||||
</Button>
|
</Modal.Footer>
|
||||||
</Modal.Footer>
|
</ValidatedForm>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,55 +3,40 @@ import PropTypes, { bool, number, string, func } from "prop-types";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
HelpBlock
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import _ from "lodash";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import axios from "../../../../../config/axios";
|
import axios from "../../../../../config/axios";
|
||||||
|
import {
|
||||||
|
ValidatedForm,
|
||||||
|
ValidatedFormGroup,
|
||||||
|
ValidatedFormControl,
|
||||||
|
ValidatedErrorHelpBlock,
|
||||||
|
ValidatedSubmitButton
|
||||||
|
} from "../../../../../components/validation";
|
||||||
|
import {
|
||||||
|
nameLengthValidator
|
||||||
|
} from "../../../../../components/validation/validators/text";
|
||||||
|
|
||||||
import { NAME_MAX_LENGTH } from "../../../../../config/constants/numeric";
|
|
||||||
import { TEAM_UPDATE_PATH } from "../../../../../config/api_endpoints";
|
import { TEAM_UPDATE_PATH } from "../../../../../config/api_endpoints";
|
||||||
import { COLOR_APPLE_BLOSSOM } from "../../../../../config/constants/colors";
|
|
||||||
|
|
||||||
const StyledHelpBlock = styled(HelpBlock)`color: ${COLOR_APPLE_BLOSSOM};`;
|
|
||||||
|
|
||||||
class UpdateTeamNameModal extends Component {
|
class UpdateTeamNameModal extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { errorMessage: "", name: props.team.name };
|
this.state = { name: props.team.name };
|
||||||
this.onCloseModal = this.onCloseModal.bind(this);
|
this.onCloseModal = this.onCloseModal.bind(this);
|
||||||
this.updateName = this.updateName.bind(this);
|
this.updateName = this.updateName.bind(this);
|
||||||
this.handleName = this.handleName.bind(this);
|
this.handleName = this.handleName.bind(this);
|
||||||
this.getValidationState = this.getValidationState.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCloseModal() {
|
onCloseModal() {
|
||||||
this.setState({ errorMessage: "", name: "" });
|
this.setState({ name: "" });
|
||||||
this.props.hideModal();
|
this.props.hideModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidationState() {
|
handleName(e) {
|
||||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
this.setState({ name: e.target.value });
|
||||||
}
|
|
||||||
|
|
||||||
handleName(el) {
|
|
||||||
const { value } = el.target;
|
|
||||||
if (value.length > NAME_MAX_LENGTH) {
|
|
||||||
this.setState({
|
|
||||||
errorMessage: (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: NAME_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ errorMessage: "", name: value });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateName() {
|
updateName() {
|
||||||
|
@ -68,46 +53,48 @@ class UpdateTeamNameModal extends Component {
|
||||||
this.props.updateTeamCallback(response.data.team);
|
this.props.updateTeamCallback(response.data.team);
|
||||||
this.onCloseModal();
|
this.onCloseModal();
|
||||||
})
|
})
|
||||||
.catch(error => this.setState({ errorMessage: error.message }));
|
.catch(error => {
|
||||||
|
this.form.setErrorsForTag("name", [error.message]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
<Modal show={this.props.showModal} onHide={this.onCloseModal}>
|
||||||
<Modal.Header closeButton>
|
<ValidatedForm ref={(f) => { this.form = f; }}>
|
||||||
<Modal.Title>
|
<Modal.Header closeButton>
|
||||||
<FormattedMessage id="settings_page.update_team_name_modal.title" />
|
<Modal.Title>
|
||||||
</Modal.Title>
|
<FormattedMessage id="settings_page.update_team_name_modal.title" />
|
||||||
</Modal.Header>
|
</Modal.Title>
|
||||||
<Modal.Body>
|
</Modal.Header>
|
||||||
<FormGroup
|
<Modal.Body>
|
||||||
controlId="teamName"
|
<ValidatedFormGroup tag="name">
|
||||||
validationState={this.getValidationState()}
|
<ControlLabel>
|
||||||
>
|
<FormattedMessage id="settings_page.update_team_name_modal.label" />
|
||||||
<ControlLabel>
|
</ControlLabel>
|
||||||
<FormattedMessage id="settings_page.update_team_name_modal.label" />
|
<ValidatedFormControl
|
||||||
</ControlLabel>
|
type="text"
|
||||||
<FormControl
|
tag="name"
|
||||||
type="text"
|
validatorsOnChange={[nameLengthValidator]}
|
||||||
onChange={this.handleName}
|
onChange={this.handleName}
|
||||||
value={this.state.name}
|
value={this.state.name}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback />
|
<FormControl.Feedback />
|
||||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
<ValidatedErrorHelpBlock tag="name" />
|
||||||
</FormGroup>
|
</ValidatedFormGroup>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button
|
<ValidatedSubmitButton
|
||||||
bsStyle="primary"
|
onClick={this.updateName}
|
||||||
onClick={this.updateName}
|
bsStyle="primary"
|
||||||
disabled={!_.isEmpty(this.state.errorMessage)}
|
>
|
||||||
>
|
<FormattedMessage id="general.update" />
|
||||||
<FormattedMessage id="general.update" />
|
</ValidatedSubmitButton>
|
||||||
</Button>
|
<Button onClick={this.onCloseModal}>
|
||||||
<Button onClick={this.onCloseModal}>
|
<FormattedMessage id="general.close" />
|
||||||
<FormattedMessage id="general.close" />
|
</Button>
|
||||||
</Button>
|
</Modal.Footer>
|
||||||
</Modal.Footer>
|
</ValidatedForm>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||||
import { FormControl } from "react-bootstrap";
|
import { ValidatedFormControl } from "../../../../../../components/validation";
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: "settings_page.new_team.name_placeholder" }
|
placeholder: { id: "settings_page.new_team.name_placeholder" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const NameFormControl = ({ intl, ...props }) =>
|
const NameFormControl = ({ intl, ...props }) =>
|
||||||
<FormControl
|
<ValidatedFormControl
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|
|
@ -3,10 +3,8 @@ import React, { Component } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
FormGroup,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
HelpBlock,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonToolbar
|
ButtonToolbar
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
@ -22,13 +20,19 @@ import {
|
||||||
SETTINGS_TEAMS_ROUTE,
|
SETTINGS_TEAMS_ROUTE,
|
||||||
SETTINGS_TEAM_ROUTE
|
SETTINGS_TEAM_ROUTE
|
||||||
} from "../../../../../config/routes";
|
} from "../../../../../config/routes";
|
||||||
|
|
||||||
import {
|
|
||||||
NAME_MIN_LENGTH,
|
|
||||||
NAME_MAX_LENGTH,
|
|
||||||
TEXT_MAX_LENGTH
|
|
||||||
} from "../../../../../config/constants/numeric";
|
|
||||||
import { getTeamsList } from "../../../../../components/actions/TeamsActions";
|
import { getTeamsList } from "../../../../../components/actions/TeamsActions";
|
||||||
|
import {
|
||||||
|
ValidatedForm,
|
||||||
|
ValidatedFormGroup,
|
||||||
|
ValidatedFormControl,
|
||||||
|
ValidatedErrorHelpBlock,
|
||||||
|
ValidatedSubmitButton
|
||||||
|
} from "../../../../../components/validation";
|
||||||
|
import {
|
||||||
|
nameLengthValidator,
|
||||||
|
textMaxLengthValidator
|
||||||
|
|
||||||
|
} from "../../../../../components/validation/validators/text";
|
||||||
|
|
||||||
import { BORDER_LIGHT_COLOR } from "../../../../../config/constants/colors";
|
import { BORDER_LIGHT_COLOR } from "../../../../../config/constants/colors";
|
||||||
|
|
||||||
|
@ -53,14 +57,8 @@ type Props = {
|
||||||
getTeamsList: Function
|
getTeamsList: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormErrors = {
|
|
||||||
name: string,
|
|
||||||
description: string
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
team: Teams$NewTeam,
|
team: Teams$NewTeam,
|
||||||
formErrors: FormErrors,
|
|
||||||
redirectTo: string
|
redirectTo: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,15 +70,10 @@ class SettingsNewTeam extends Component<Props, State> {
|
||||||
name: "",
|
name: "",
|
||||||
description: ""
|
description: ""
|
||||||
},
|
},
|
||||||
formErrors: {
|
|
||||||
name: "",
|
|
||||||
description: ""
|
|
||||||
},
|
|
||||||
redirectTo: ""
|
redirectTo: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
(this: any).onSubmit = this.onSubmit.bind(this);
|
(this: any).onSubmit = this.onSubmit.bind(this);
|
||||||
(this: any).validateField = this.validateField.bind(this);
|
|
||||||
(this: any).handleChange = this.handleChange.bind(this);
|
(this: any).handleChange = this.handleChange.bind(this);
|
||||||
(this: any).renderTeamNameFormGroup = this.renderTeamNameFormGroup.bind(
|
(this: any).renderTeamNameFormGroup = this.renderTeamNameFormGroup.bind(
|
||||||
this
|
this
|
||||||
|
@ -100,138 +93,62 @@ class SettingsNewTeam extends Component<Props, State> {
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Redirect to the new team page
|
// Redirect to the new team page
|
||||||
this.props.getTeamsList();
|
this.props.getTeamsList();
|
||||||
(this: any).newState = { ...this.state };
|
|
||||||
(this: any).newState = update((this: any).newState, {
|
const newState = update((this: any).state, {
|
||||||
redirectTo: {
|
redirectTo: {
|
||||||
$set: SETTINGS_TEAM_ROUTE.replace(":id", response.details.id)
|
$set: SETTINGS_TEAM_ROUTE.replace(":id", response.details.id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(this: any).setState((this: any).newState);
|
(this: any).setState(newState);
|
||||||
})
|
})
|
||||||
.catch(er => {
|
.catch(er => {
|
||||||
// Display errors
|
// Display errors
|
||||||
(this: any).newState = { ...this.state };
|
(this: any).newTeamForm.setErrors(er.response.data.details);
|
||||||
["name", "description"].forEach(el => {
|
|
||||||
if (er.response.data.details[el]) {
|
|
||||||
(this: any).newState = update((this: any).newState, {
|
|
||||||
formErrors: {
|
|
||||||
name: { $set: <span>{er.response.data.details[el]}</span> }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
(this: any).setState((this: any).newState);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateField(key: string, value: string) {
|
handleChange(e: SyntheticInputEvent<HTMLInputElement>, tag: string): void {
|
||||||
let errorMessage;
|
|
||||||
if (key === "name") {
|
|
||||||
errorMessage = "";
|
|
||||||
|
|
||||||
if (value.length < NAME_MIN_LENGTH) {
|
|
||||||
errorMessage = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_short"
|
|
||||||
values={{ min_length: NAME_MIN_LENGTH }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (value.length > NAME_MAX_LENGTH) {
|
|
||||||
errorMessage = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: NAME_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
(this: any).newState = update((this: any).newState, {
|
|
||||||
formErrors: { name: { $set: errorMessage } }
|
|
||||||
});
|
|
||||||
} else if (key === "description") {
|
|
||||||
errorMessage = "";
|
|
||||||
|
|
||||||
if (value.length > TEXT_MAX_LENGTH) {
|
|
||||||
errorMessage = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="error_messages.text_too_long"
|
|
||||||
values={{ max_length: TEXT_MAX_LENGTH }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
(this: any).newState = update((this: any).newState, {
|
|
||||||
formErrors: { description: { $set: errorMessage } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(e: SyntheticInputEvent<HTMLInputElement>): void {
|
|
||||||
const key = e.target.name;
|
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
|
|
||||||
(this: any).newState = { ...this.state };
|
const newState = update((this: any).state, {
|
||||||
|
team: { [tag]: { $set: value } }
|
||||||
// Update value in the state
|
|
||||||
(this: any).newState = update((this: any).newState, {
|
|
||||||
team: { [key]: { $set: value } }
|
|
||||||
});
|
});
|
||||||
|
(this: any).setState(newState);
|
||||||
// Validate the input
|
|
||||||
(this: any).validateField(key, value);
|
|
||||||
|
|
||||||
// Refresh state
|
|
||||||
(this: any).setState((this: any).newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTeamNameFormGroup() {
|
renderTeamNameFormGroup() {
|
||||||
const formGroupClass = this.state.formErrors.name
|
|
||||||
? "form-group has-error"
|
|
||||||
: "form-group";
|
|
||||||
const validationState = this.state.formErrors.name ? "error" : null;
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<ValidatedFormGroup tag="name">
|
||||||
controlId="formTeamName"
|
|
||||||
className={formGroupClass}
|
|
||||||
validationState={validationState}
|
|
||||||
>
|
|
||||||
<ControlLabel>
|
<ControlLabel>
|
||||||
<FormattedMessage id="settings_page.new_team.name_label" />
|
<FormattedMessage id="settings_page.new_team.name_label" />
|
||||||
</ControlLabel>
|
</ControlLabel>
|
||||||
<NameFormControl
|
<NameFormControl
|
||||||
value={this.state.team.name}
|
value={this.state.team.name}
|
||||||
onChange={this.handleChange}
|
tag="name"
|
||||||
name="name"
|
validatorsOnChange={[nameLengthValidator]}
|
||||||
|
onChange={(e) => this.handleChange(e, "name")}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback />
|
<FormControl.Feedback />
|
||||||
<HelpBlock>{this.state.formErrors.name}</HelpBlock>
|
<ValidatedErrorHelpBlock tag="name" />
|
||||||
</FormGroup>
|
</ValidatedFormGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTeamDescriptionFormGroup() {
|
renderTeamDescriptionFormGroup() {
|
||||||
const formGroupClass = this.state.formErrors.description
|
|
||||||
? "form-group has-error"
|
|
||||||
: "form-group";
|
|
||||||
const validationState = this.state.formErrors.description ? "error" : null;
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<ValidatedFormGroup tag="description">
|
||||||
controlId="formTeamDescription"
|
|
||||||
className={formGroupClass}
|
|
||||||
validationState={validationState}
|
|
||||||
>
|
|
||||||
<ControlLabel>
|
<ControlLabel>
|
||||||
<FormattedMessage id="settings_page.new_team.description_label" />
|
<FormattedMessage id="settings_page.new_team.description_label" />
|
||||||
</ControlLabel>
|
</ControlLabel>
|
||||||
<FormControl
|
<ValidatedFormControl
|
||||||
componentClass="textarea"
|
componentClass="textarea"
|
||||||
value={this.state.team.description}
|
value={this.state.team.description}
|
||||||
onChange={this.handleChange}
|
tag="description"
|
||||||
name="description"
|
validatorsOnChange={[textMaxLengthValidator]}
|
||||||
|
onChange={(e) => this.handleChange(e, "description")}
|
||||||
/>
|
/>
|
||||||
<FormControl.Feedback />
|
<ValidatedErrorHelpBlock tag="description" />
|
||||||
<HelpBlock>{this.state.formErrors.description}</HelpBlock>
|
</ValidatedFormGroup>
|
||||||
</FormGroup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,10 +158,6 @@ class SettingsNewTeam extends Component<Props, State> {
|
||||||
return <Redirect to={this.state.redirectTo} />;
|
return <Redirect to={this.state.redirectTo} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btnDisabled =
|
|
||||||
!_.isEmpty(this.state.formErrors.name) ||
|
|
||||||
!_.isEmpty(this.state.formErrors.description);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTitle localeID="page_title.new_team_page">
|
<PageTitle localeID="page_title.new_team_page">
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
@ -259,7 +172,11 @@ class SettingsNewTeam extends Component<Props, State> {
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
|
||||||
<form onSubmit={this.onSubmit} style={{ maxWidth: "500px" }}>
|
<ValidatedForm
|
||||||
|
onSubmit={this.onSubmit}
|
||||||
|
ref={(f) => { (this: any).newTeamForm = f; }}
|
||||||
|
style={{ maxWidth: "500px" }}
|
||||||
|
>
|
||||||
<MyFormGroupDiv>
|
<MyFormGroupDiv>
|
||||||
{this.renderTeamNameFormGroup()}
|
{this.renderTeamNameFormGroup()}
|
||||||
<small>
|
<small>
|
||||||
|
@ -274,20 +191,19 @@ class SettingsNewTeam extends Component<Props, State> {
|
||||||
</small>
|
</small>
|
||||||
</MyFormGroupDiv>
|
</MyFormGroupDiv>
|
||||||
<ButtonToolbar>
|
<ButtonToolbar>
|
||||||
<Button
|
<ValidatedSubmitButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
disabled={btnDisabled}
|
|
||||||
>
|
>
|
||||||
<FormattedMessage id="settings_page.new_team.create" />
|
<FormattedMessage id="settings_page.new_team.create" />
|
||||||
</Button>
|
</ValidatedSubmitButton>
|
||||||
<LinkContainer to={SETTINGS_TEAMS_ROUTE}>
|
<LinkContainer to={SETTINGS_TEAMS_ROUTE}>
|
||||||
<Button>
|
<Button>
|
||||||
<FormattedMessage id="general.cancel" />
|
<FormattedMessage id="general.cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</form>
|
</ValidatedForm>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
module ClientApi
|
|
||||||
class UserService < BaseService
|
|
||||||
def update_user!
|
|
||||||
error = I18n.t('client_api.user.password_invalid')
|
|
||||||
raise CustomUserError, error unless check_current_password
|
|
||||||
@params.delete(:current_password) # removes unneeded element
|
|
||||||
@current_user.update(@params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_current_password
|
|
||||||
return true unless @params[:email] || @params[:password]
|
|
||||||
pass_blank_err = I18n.t('client_api.user.blank_password_error')
|
|
||||||
pass_match_err = I18n.t('client_api.user.passwords_dont_match')
|
|
||||||
current_password = @params[:current_password]
|
|
||||||
raise CustomUserError, pass_blank_err unless current_password
|
|
||||||
raise CustomUserError, pass_match_err unless check_password_confirmation
|
|
||||||
@current_user.valid_password? current_password
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_password_confirmation
|
|
||||||
return true if @params[:email]
|
|
||||||
@params[:password] == @params[:password_confirmation]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
CustomUserError = Class.new(StandardError)
|
|
||||||
end
|
|
53
app/services/client_api/users/update_service.rb
Normal file
53
app/services/client_api/users/update_service.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
module ClientApi
|
||||||
|
module Users
|
||||||
|
class UpdateService < BaseService
|
||||||
|
attr_accessor :user
|
||||||
|
|
||||||
|
def execute
|
||||||
|
@user = @current_user
|
||||||
|
|
||||||
|
if current_password_valid? &&
|
||||||
|
password_confirmation_valid? &&
|
||||||
|
@user.update(@params.except(:current_password))
|
||||||
|
success
|
||||||
|
else
|
||||||
|
error(@user.errors.full_messages.uniq.join('. '))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_password_valid?
|
||||||
|
# Only check for current_password when updating
|
||||||
|
# email or password
|
||||||
|
return true unless @params[:email] || @params[:password]
|
||||||
|
|
||||||
|
if @user.valid_password?(@params[:current_password])
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
@user.errors.add(
|
||||||
|
:current_password,
|
||||||
|
I18n.t('client_api.user.current_password_invalid')
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_confirmation_valid?
|
||||||
|
# Only check for password_confirmation when
|
||||||
|
# updating password
|
||||||
|
return true unless @params[:password]
|
||||||
|
|
||||||
|
if @params[:password] == @params[:password_confirmation]
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
@user.errors.add(
|
||||||
|
:password_confirmation,
|
||||||
|
I18n.t('client_api.user.password_confirmation_not_match')
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1822,9 +1822,7 @@ en:
|
||||||
leave_team_error: "An error occured."
|
leave_team_error: "An error occured."
|
||||||
leave_flash: "Successfuly left team %{team}."
|
leave_flash: "Successfuly left team %{team}."
|
||||||
user:
|
user:
|
||||||
blank_password_error: "Password can't be blank!"
|
current_password_invalid: "incorrect password"
|
||||||
passwords_dont_match: "Passwords don't match"
|
password_confirmation_not_match: "doesn't match"
|
||||||
password_invalid: "Password is invalid!"
|
|
||||||
avatar_too_big: "Avatar file size must be less than 0.2 MB"
|
|
||||||
invite_users:
|
invite_users:
|
||||||
permission_error: "You don't have permission to invite additional users to team. Contact its administrator/s."
|
permission_error: "You don't have permission to invite additional users to team. Contact its administrator/s."
|
||||||
|
|
|
@ -24,7 +24,7 @@ Scenario: Unsuccessful avatar image upload, file is too big
|
||||||
Then I click on image within ".avatar-container" element
|
Then I click on image within ".avatar-container" element
|
||||||
And I attach a "Moon.png" file to "user_avatar_input" field
|
And I attach a "Moon.png" file to "user_avatar_input" field
|
||||||
Then I click "Update" button
|
Then I click "Update" button
|
||||||
And I should see "Avatar file size must be less than 0.2 MB" error message under "user_avatar_input" field
|
And I should see "file too large (maximum size is 0.2 MB)" error message under "user_avatar_input" field
|
||||||
|
|
||||||
@javascript
|
@javascript
|
||||||
Scenario: Unsuccessful avatar image upload, file is invalid
|
Scenario: Unsuccessful avatar image upload, file is invalid
|
||||||
|
@ -32,7 +32,7 @@ Scenario: Unsuccessful avatar image upload, file is invalid
|
||||||
Then I click on image within ".avatar-container" element
|
Then I click on image within ".avatar-container" element
|
||||||
And I attach a "File.txt" file to "user_avatar_input" field
|
And I attach a "File.txt" file to "user_avatar_input" field
|
||||||
Then I click "Update" button
|
Then I click "Update" button
|
||||||
And I should see "Avatar content type is invalid" error message under "user_avatar_input" field
|
And I should see "invalid file extension" error message under "user_avatar_input" field
|
||||||
|
|
||||||
@javascript
|
@javascript
|
||||||
Scenario: Successful upload avatar image
|
Scenario: Successful upload avatar image
|
||||||
|
@ -93,7 +93,7 @@ Scenario: Unsuccessful Password Change, passwords does not match
|
||||||
And I fill in "mypassword5678" in New password field
|
And I fill in "mypassword5678" in New password field
|
||||||
And I fill in "mypassword56788" in New password confirmation field
|
And I fill in "mypassword56788" in New password confirmation field
|
||||||
Then I click "Update" button
|
Then I click "Update" button
|
||||||
And I should see "Passwords don't match"
|
And I should see "doesn't match"
|
||||||
|
|
||||||
@javascript
|
@javascript
|
||||||
Scenario: Unsuccessful Password Change, current password is invalid
|
Scenario: Unsuccessful Password Change, current password is invalid
|
||||||
|
@ -103,7 +103,7 @@ Scenario: Unsuccessful Password Change, current password is invalid
|
||||||
And I fill in "mypassword5678" in New password field
|
And I fill in "mypassword5678" in New password field
|
||||||
And I fill in "mypassword5678" in New password confirmation field
|
And I fill in "mypassword5678" in New password confirmation field
|
||||||
Then I click "Update" button
|
Then I click "Update" button
|
||||||
And I should see "Password is invalid!"
|
And I should see "incorrect password"
|
||||||
|
|
||||||
@javascript
|
@javascript
|
||||||
Scenario: Successful Password Change
|
Scenario: Successful Password Change
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe ClientApi::UserService do
|
|
||||||
let(:user) do
|
|
||||||
create :user,
|
|
||||||
full_name: 'User One',
|
|
||||||
initials: 'UO',
|
|
||||||
email: 'user@happy.com',
|
|
||||||
password: 'asdf1234',
|
|
||||||
password_confirmation: 'asdf1234'
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#update_user!' do
|
|
||||||
it 'should update user email if the password is correct' do
|
|
||||||
email = 'new_user@happy.com'
|
|
||||||
params = { email: email, current_password: 'asdf1234' }
|
|
||||||
user_service = ClientApi::UserService.new(current_user: user,
|
|
||||||
params: params)
|
|
||||||
user_service.update_user!
|
|
||||||
expect(user.email).to eq(email)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should raise CustomUserError error if the password is not correct' do
|
|
||||||
email = 'new_user@happy.com'
|
|
||||||
params = { email: email, current_password: 'banana' }
|
|
||||||
user_service = ClientApi::UserService.new(current_user: user,
|
|
||||||
params: params)
|
|
||||||
expect {
|
|
||||||
user_service.update_user!
|
|
||||||
}.to raise_error(ClientApi::CustomUserError)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should update initials and full name without password confirmation' do
|
|
||||||
full_name = 'Happy User'
|
|
||||||
initials = 'HU'
|
|
||||||
user_service = ClientApi::UserService.new(
|
|
||||||
current_user: user,
|
|
||||||
params: { full_name: full_name, initials: initials }
|
|
||||||
)
|
|
||||||
user_service.update_user!
|
|
||||||
expect(user.full_name).to eq(full_name)
|
|
||||||
expect(user.initials).to eq(initials)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should raise an error if current password not present' do
|
|
||||||
user_service = ClientApi::UserService.new(
|
|
||||||
current_user: user,
|
|
||||||
params: { password: 'hello1234', password_confirmation: 'hello1234' }
|
|
||||||
)
|
|
||||||
expect {
|
|
||||||
user_service.update_user!
|
|
||||||
}.to raise_error(ClientApi::CustomUserError)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should raise an error if password_confirmation don\'t match' do
|
|
||||||
user_service = ClientApi::UserService.new(
|
|
||||||
current_user: user,
|
|
||||||
params: { password: 'hello1234',
|
|
||||||
password_confirmation: 'hello1234567890',
|
|
||||||
current_password: 'asdf1234' }
|
|
||||||
)
|
|
||||||
|
|
||||||
expect {
|
|
||||||
user_service.update_user!
|
|
||||||
}.to raise_error(ClientApi::CustomUserError, 'Passwords don\'t match')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should update the password' do
|
|
||||||
new_password = 'hello1234'
|
|
||||||
user_service = ClientApi::UserService.new(
|
|
||||||
current_user: user,
|
|
||||||
params: { password: new_password,
|
|
||||||
password_confirmation: new_password,
|
|
||||||
current_password: 'asdf1234' }
|
|
||||||
)
|
|
||||||
user_service.update_user!
|
|
||||||
expect(user.valid_password?(new_password)).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
79
spec/services/client_api/users/update_service_spec.rb
Normal file
79
spec/services/client_api/users/update_service_spec.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
include ClientApi::Users
|
||||||
|
|
||||||
|
describe ClientApi::Users::UpdateService do
|
||||||
|
let(:user) do
|
||||||
|
create :user,
|
||||||
|
full_name: 'User One',
|
||||||
|
initials: 'UO',
|
||||||
|
email: 'user@happy.com',
|
||||||
|
password: 'asdf1234',
|
||||||
|
password_confirmation: 'asdf1234'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should update user email if the password is correct' do
|
||||||
|
email = 'new_user@happy.com'
|
||||||
|
params = { email: email, current_password: 'asdf1234' }
|
||||||
|
service = UpdateService.new(current_user: user,
|
||||||
|
params: params)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :success
|
||||||
|
expect(user.email).to eq(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise CustomUserError error if the password is not correct' do
|
||||||
|
email = 'new_user@happy.com'
|
||||||
|
params = { email: email, current_password: 'banana' }
|
||||||
|
service = UpdateService.new(current_user: user,
|
||||||
|
params: params)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should update initials and full name without password confirmation' do
|
||||||
|
full_name = 'Happy User'
|
||||||
|
initials = 'HU'
|
||||||
|
service = UpdateService.new(
|
||||||
|
current_user: user,
|
||||||
|
params: { full_name: full_name, initials: initials }
|
||||||
|
)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :success
|
||||||
|
expect(user.full_name).to eq(full_name)
|
||||||
|
expect(user.initials).to eq(initials)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise an error if current password not present' do
|
||||||
|
service = UpdateService.new(
|
||||||
|
current_user: user,
|
||||||
|
params: { password: 'hello1234', password_confirmation: 'hello1234' }
|
||||||
|
)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise an error if password_confirmation don\'t match' do
|
||||||
|
service = UpdateService.new(
|
||||||
|
current_user: user,
|
||||||
|
params: { password: 'hello1234',
|
||||||
|
password_confirmation: 'hello1234567890',
|
||||||
|
current_password: 'asdf1234' }
|
||||||
|
)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should update the password' do
|
||||||
|
new_password = 'hello1234'
|
||||||
|
service = UpdateService.new(
|
||||||
|
current_user: user,
|
||||||
|
params: { password: new_password,
|
||||||
|
password_confirmation: new_password,
|
||||||
|
current_password: 'asdf1234' }
|
||||||
|
)
|
||||||
|
result = service.execute
|
||||||
|
expect(result[:status]).to eq :success
|
||||||
|
expect(user.valid_password?(new_password)).to be(true)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue