mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1566
This commit is contained in:
commit
d85233cb2c
3
.babelrc
3
.babelrc
|
@ -5,7 +5,7 @@
|
|||
{
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": "> 1%",
|
||||
"browsers": ["last 2 versions"],
|
||||
"uglify": true
|
||||
},
|
||||
"useBuiltIns": true
|
||||
|
@ -17,6 +17,7 @@
|
|||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"syntax-dynamic-import",
|
||||
"transform-react-jsx-source",
|
||||
[
|
||||
"transform-class-properties",
|
||||
{
|
||||
|
|
|
@ -2,12 +2,26 @@ module ClientApi
|
|||
module Users
|
||||
class UsersController < ApplicationController
|
||||
|
||||
def sign_out_user
|
||||
respond_to do |format|
|
||||
if sign_out current_user
|
||||
format.json { render json: {}, status: :ok }
|
||||
else
|
||||
format.json { render json: {}, status: :unauthorized }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def preferences_info
|
||||
settings = current_user.settings
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render template: 'client_api/users/preferences',
|
||||
status: :ok,
|
||||
locals: { user: current_user}
|
||||
locals: {
|
||||
timeZone: settings['time_zone'],
|
||||
notifications: settings['notifications']
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -42,134 +56,52 @@ module ClientApi
|
|||
end
|
||||
end
|
||||
|
||||
def change_password
|
||||
user = User.find(current_user.id)
|
||||
is_saved = user.update(user_params)
|
||||
|
||||
if is_saved
|
||||
bypass_sign_in(user)
|
||||
res = "success"
|
||||
def update
|
||||
user_service = ClientApi::UserService.new(
|
||||
current_user: current_user,
|
||||
params: user_params
|
||||
)
|
||||
if user_service.update_user!
|
||||
bypass_sign_in(current_user)
|
||||
success_response
|
||||
else
|
||||
res = "could not change password"
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if is_saved
|
||||
format.json { render json: { msg: res} }
|
||||
else
|
||||
format.json { render json: { msg: res}, status: 422 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def change_assignements_notification
|
||||
change_notification(:assignments_notification, params)
|
||||
end
|
||||
|
||||
def change_assignements_notification_email
|
||||
change_notification(:assignments_notification_email, params)
|
||||
end
|
||||
|
||||
def change_recent_notification
|
||||
change_notification(:recent_notification, params)
|
||||
end
|
||||
|
||||
def change_recent_notification_email
|
||||
change_notification(:recent_notification_email, params)
|
||||
end
|
||||
|
||||
def change_system_notification_email
|
||||
change_notification(:system_message_notification_email, params)
|
||||
end
|
||||
|
||||
def change_timezone
|
||||
user = current_user
|
||||
errors = { timezone_errors: [] }
|
||||
user.time_zone = params['timezone']
|
||||
|
||||
timezone = if user.save
|
||||
user.time_zone
|
||||
else
|
||||
msg = 'You need to select valid TimeZone.'
|
||||
user.reload.time_zone
|
||||
errors[:timezone_errors] << msg
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: { timezone: timezone, errors: errors}}
|
||||
end
|
||||
end
|
||||
|
||||
def change_email
|
||||
user = current_user
|
||||
current_email = current_user.email
|
||||
errors = { current_password_email_field: []}
|
||||
|
||||
if user.valid_password? params['passwrd']
|
||||
user.email = params['email']
|
||||
saved_email = if user.save
|
||||
user.email
|
||||
else
|
||||
user.reload.email
|
||||
end
|
||||
else
|
||||
errors[:current_password_email_field] << 'Wrong password.'
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
resp = { email: saved_email || current_email, errors: errors }
|
||||
format.json { render json: resp }
|
||||
end
|
||||
end
|
||||
|
||||
def change_full_name
|
||||
user = current_user
|
||||
user.name = params['fullName']
|
||||
saved_name = if user.save
|
||||
user.name
|
||||
else
|
||||
user.reload.name
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
resp = { fullName: saved_name, errors: user.errors.messages }
|
||||
format.json { render json: resp }
|
||||
end
|
||||
end
|
||||
|
||||
def change_initials
|
||||
user = current_user
|
||||
user.initials = params['initials']
|
||||
saved_initials = if user.save
|
||||
user.initials
|
||||
else
|
||||
user.reload.initials
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: { initials: saved_initials } }
|
||||
unsuccess_response(current_user.errors.full_messages,
|
||||
:unprocessable_entity)
|
||||
end
|
||||
rescue CustomUserError => error
|
||||
unsuccess_response(error.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:password)
|
||||
params.require(:user)
|
||||
.permit(:password, :initials, :email, :full_name,
|
||||
:password_confirmation, :current_password, :avatar,
|
||||
:time_zone, :assignments_notification,
|
||||
:assignments_email_notification, :recent_notification,
|
||||
:recent_email_notification,
|
||||
:system_message_email_notification)
|
||||
end
|
||||
|
||||
def change_notification(dinamic_param, params)
|
||||
user = current_user
|
||||
user[dinamic_param] = params['status']
|
||||
|
||||
status =
|
||||
if user.save
|
||||
user[dinamic_param]
|
||||
else
|
||||
user.reload[dinamic_param]
|
||||
end
|
||||
|
||||
def success_response(template = nil, locals = nil)
|
||||
respond_to do |format|
|
||||
format.json { render json: { status: status } }
|
||||
format.json do
|
||||
if template && locals
|
||||
render template: template, status: :ok, locals: locals
|
||||
else
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unsuccess_response(message, status = :unprocessable_entity)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { message: message },
|
||||
status: status
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
BIN
app/javascript/src/assets/missing.png
Normal file
BIN
app/javascript/src/assets/missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -6,7 +6,11 @@ import { connect } from "react-redux";
|
|||
import axios from "../../../config/axios";
|
||||
|
||||
import { LEAVE_TEAM_PATH } from "../../../config/api_endpoints";
|
||||
import { addTeamsData, setCurrentTeam, leaveTeamModalShow } from "../../actions/TeamsActions";
|
||||
import {
|
||||
addTeamsData,
|
||||
setCurrentTeam,
|
||||
leaveTeamModalShow
|
||||
} from "../../actions/TeamsActions";
|
||||
|
||||
class LeaveTeamModal extends Component {
|
||||
constructor(props) {
|
||||
|
@ -51,7 +55,10 @@ class LeaveTeamModal extends Component {
|
|||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<FormattedMessage id="settings_page.leave_team_modal.subtitle" />
|
||||
<FormattedMessage
|
||||
id="settings_page.leave_team_modal.subtitle"
|
||||
values={{ teamName: this.props.team.name }}
|
||||
/>
|
||||
</p>
|
||||
<Alert bsStyle="danger">
|
||||
<Glyphicon glyph="exclamation-sign" />
|
||||
|
|
|
@ -6,6 +6,7 @@ import { NavDropdown, MenuItem, Glyphicon } from "react-bootstrap";
|
|||
import styled from "styled-components";
|
||||
import _ from "lodash";
|
||||
|
||||
import { ROOT_PATH } from "../../../config/routes";
|
||||
import { BORDER_GRAY_COLOR } from "../../../config/constants/colors";
|
||||
import { changeTeam } from "../../actions/TeamsActions";
|
||||
import { getTeamsList } from "../../actions/TeamsActions";
|
||||
|
@ -27,15 +28,18 @@ class TeamSwitch extends Component {
|
|||
|
||||
changeTeam(teamId) {
|
||||
this.props.changeTeam(teamId);
|
||||
window.location = ROOT_PATH;
|
||||
}
|
||||
|
||||
displayTeams() {
|
||||
if (!_.isEmpty(this.props.all_teams)) {
|
||||
return this.props.all_teams.filter(team => !team.current_team).map(team =>
|
||||
<MenuItem onSelect={() => this.changeTeam(team.id)} key={team.id}>
|
||||
{team.name}
|
||||
</MenuItem>
|
||||
);
|
||||
return this.props.all_teams
|
||||
.filter(team => !team.current_team)
|
||||
.map(team => (
|
||||
<MenuItem onSelect={() => this.changeTeam(team.id)} key={team.id}>
|
||||
{team.name}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,44 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { func, shape, string, number } from "prop-types";
|
||||
import { NavDropdown, MenuItem, Image } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SIGN_IN_PATH } from "../../../config/routes";
|
||||
|
||||
import { getCurrentUser } from "../../actions/UsersActions";
|
||||
import { addCurrentUser, destroyState } from "../../actions/UsersActions";
|
||||
import { signOutUser, getCurrentUser } from "../../../services/api/users_api";
|
||||
|
||||
const StyledNavDropdown = styled(NavDropdown)`
|
||||
& #user-account-dropdown {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
& #user-account-dropdown {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledAvatar = styled(Image)`
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
`;
|
||||
|
||||
class UserAccountDropdown extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getCurrentUser();
|
||||
getCurrentUser().then(data => {
|
||||
this.props.addCurrentUser(data);
|
||||
});
|
||||
this.signOut = this.signOut.bind(this);
|
||||
}
|
||||
|
||||
signOut() {
|
||||
document.querySelector('meta[name="csrf-token"]').remove();
|
||||
signOutUser().then(() => {
|
||||
this.props.destroyState();
|
||||
window.location = SIGN_IN_PATH;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fullName, avatarThumb } = this.props.current_user;
|
||||
return (
|
||||
<StyledNavDropdown
|
||||
id="user-account-dropdown"
|
||||
|
@ -29,11 +48,11 @@ class UserAccountDropdown extends Component {
|
|||
<span>
|
||||
<FormattedMessage
|
||||
id="user_account_dropdown.greeting"
|
||||
values={{ name: this.props.current_user.fullName }}
|
||||
values={{ name: fullName }}
|
||||
/>
|
||||
<Image
|
||||
src={this.props.current_user.avatarPath}
|
||||
alt={this.props.current_user.fullName}
|
||||
<StyledAvatar
|
||||
src={`${avatarThumb }?${new Date().getTime()}`}
|
||||
alt={fullName}
|
||||
circle
|
||||
/>
|
||||
</span>
|
||||
|
@ -43,7 +62,7 @@ class UserAccountDropdown extends Component {
|
|||
<FormattedMessage id="user_account_dropdown.settings" />
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem href="/users/sign_out">
|
||||
<MenuItem onClick={this.signOut}>
|
||||
<FormattedMessage id="user_account_dropdown.log_out" />
|
||||
</MenuItem>
|
||||
</StyledNavDropdown>
|
||||
|
@ -52,24 +71,18 @@ class UserAccountDropdown extends Component {
|
|||
}
|
||||
|
||||
UserAccountDropdown.propTypes = {
|
||||
getCurrentUser: PropTypes.func.isRequired,
|
||||
current_user: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
fullName: PropTypes.string.isRequired,
|
||||
avatarPath: PropTypes.string.isRequired
|
||||
addCurrentUser: func.isRequired,
|
||||
destroyState: func.isRequired,
|
||||
current_user: shape({
|
||||
id: number.isRequired,
|
||||
fullName: string.isRequired,
|
||||
avatarThumb: string.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
// Map the states from store to component
|
||||
const mapStateToProps = ({ current_user }) => ({ current_user });
|
||||
|
||||
// Map the fetch activity action to component
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getCurrentUser() {
|
||||
dispatch(getCurrentUser());
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
export default connect(mapStateToProps, { destroyState, addCurrentUser })(
|
||||
UserAccountDropdown
|
||||
);
|
||||
|
|
|
@ -1,266 +1,12 @@
|
|||
import axios from "../../config/axios";
|
||||
import { USER_LOGOUT, SET_CURRENT_USER } from "../../config/action_types";
|
||||
|
||||
import {
|
||||
CHANGE_USER_FULL_NAME_PATH,
|
||||
CURRENT_USER_PATH,
|
||||
CHANGE_USER_INITIALS_PATH,
|
||||
CHANGE_USER_EMAIL_PATH,
|
||||
CHANGE_USER_PASSWORD_PATH,
|
||||
CHANGE_USER_TIMEZONE_PATH,
|
||||
CHANGE_USER_ASSIGNEMENTS_NOTIFICATION_PATH,
|
||||
CHANGE_USER_ASSIGNMENTS_NOTIFICATION_EMAIL_PATH,
|
||||
CHANGE_USER_RECENT_NOTIFICATION_PATH,
|
||||
CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH,
|
||||
CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH
|
||||
} from "../../config/api_endpoints";
|
||||
export function destroyState() {
|
||||
return { type: USER_LOGOUT };
|
||||
}
|
||||
|
||||
import {
|
||||
SET_CURRENT_USER,
|
||||
CHANGE_CURRENT_USER_FULL_NAME,
|
||||
CHANGE_CURRENT_USER_INITIALS,
|
||||
CHANGE_CURRENT_USER_EMAIL,
|
||||
CHANGE_CURRENT_USER_PASSWORD,
|
||||
CHANGE_CURRENT_USER_AVATAR,
|
||||
CHANGE_CURRENT_USER_TIMEZONE,
|
||||
CHANGE_ASSIGNMENTS_NOTIFICATION,
|
||||
CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
|
||||
CHANGE_RECENT_NOTIFICATION,
|
||||
CHANGE_RECENT_NOTIFICATION_EMAIL,
|
||||
CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL
|
||||
} from "../../config/action_types";
|
||||
|
||||
function addCurrentUser(data) {
|
||||
export function addCurrentUser(data) {
|
||||
return {
|
||||
type: SET_CURRENT_USER,
|
||||
payload: data
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrentUser() {
|
||||
return dispatch => {
|
||||
axios
|
||||
.get(CURRENT_USER_PATH, { withCredentials: true })
|
||||
.then(({ data }) => {
|
||||
dispatch(addCurrentUser(data.user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("get Current User Error: ", error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function saveFullName({ fullName }) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_FULL_NAME,
|
||||
payload: fullName
|
||||
};
|
||||
}
|
||||
|
||||
export function changeFullName(name) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_FULL_NAME_PATH, {
|
||||
withCredentials: true,
|
||||
fullName: name
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveFullName(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveInitials({ initials }) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_INITIALS,
|
||||
payload: initials
|
||||
};
|
||||
}
|
||||
|
||||
export function changeInitials(initials) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_INITIALS_PATH, {
|
||||
withCredentials: true,
|
||||
initials
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveInitials(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveEmail({ email }) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_EMAIL,
|
||||
payload: email
|
||||
};
|
||||
}
|
||||
|
||||
export function changeEmail(email) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_EMAIL_PATH, {
|
||||
withCredentials: true,
|
||||
email
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveEmail(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function savePassword(password) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_PASSWORD,
|
||||
payload: password
|
||||
};
|
||||
}
|
||||
|
||||
export function changePassword(password) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_PASSWORD_PATH, {
|
||||
user: {
|
||||
withCredentials: true,
|
||||
password
|
||||
}
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(savePassword(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function changeAvatar(avatarSrc) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_AVATAR,
|
||||
payload: avatarSrc
|
||||
};
|
||||
}
|
||||
|
||||
export function saveTimezone({ timezone }) {
|
||||
return {
|
||||
type: CHANGE_CURRENT_USER_TIMEZONE,
|
||||
payload: timezone
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTimezone(timezone) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_TIMEZONE_PATH, { withCredentials: true, timezone })
|
||||
.then(({ data }) => {
|
||||
dispatch(saveTimezone(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveAssignmentsNotification({ status }) {
|
||||
return {
|
||||
type: CHANGE_ASSIGNMENTS_NOTIFICATION,
|
||||
payload: status
|
||||
};
|
||||
}
|
||||
|
||||
export function changeAssignmentsNotification(status) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_ASSIGNEMENTS_NOTIFICATION_PATH, {
|
||||
withCredentials: true,
|
||||
status
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveAssignmentsNotification(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveAssignmentsNotificationEmail({ status }) {
|
||||
return {
|
||||
type: CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
|
||||
payload: status
|
||||
};
|
||||
}
|
||||
|
||||
export function changeAssignmentsNotificationEmail(status) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_ASSIGNMENTS_NOTIFICATION_EMAIL_PATH, {
|
||||
withCredentials: true,
|
||||
status
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveAssignmentsNotificationEmail(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveRecentNotification({ status }) {
|
||||
return {
|
||||
type: CHANGE_RECENT_NOTIFICATION,
|
||||
payload: status
|
||||
};
|
||||
}
|
||||
|
||||
export function changeRecentNotification(status) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_RECENT_NOTIFICATION_PATH, {
|
||||
withCredentials: true,
|
||||
status
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveRecentNotification(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveRecentNotificationEmail({ status }) {
|
||||
return {
|
||||
type: CHANGE_RECENT_NOTIFICATION_EMAIL,
|
||||
payload: status
|
||||
};
|
||||
}
|
||||
|
||||
export function changeRecentNotificationEmail(status) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH, {
|
||||
withCredentials: true,
|
||||
status
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveRecentNotificationEmail(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveSystemMessageNotificationEmail({ status }) {
|
||||
return {
|
||||
type: CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL,
|
||||
payload: status
|
||||
};
|
||||
}
|
||||
|
||||
export function changeSystemMessageNotificationEmail(status) {
|
||||
return dispatch => {
|
||||
axios
|
||||
.post(CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH, {
|
||||
withCredentials: true,
|
||||
status
|
||||
})
|
||||
.then(({ data }) => {
|
||||
dispatch(saveSystemMessageNotificationEmail(data));
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,71 +1,16 @@
|
|||
import {
|
||||
SET_CURRENT_USER,
|
||||
CHANGE_CURRENT_USER_FULL_NAME,
|
||||
CHANGE_CURRENT_USER_INITIALS,
|
||||
CHANGE_CURRENT_USER_EMAIL,
|
||||
CHANGE_CURRENT_USER_PASSWORD,
|
||||
CHANGE_CURRENT_USER_AVATAR,
|
||||
CHANGE_CURRENT_USER_TIMEZONE,
|
||||
CHANGE_ASSIGNMENTS_NOTIFICATION,
|
||||
CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL,
|
||||
CHANGE_RECENT_NOTIFICATION,
|
||||
CHANGE_RECENT_NOTIFICATION_EMAIL,
|
||||
CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL
|
||||
} from "../../config/action_types";
|
||||
import { SET_CURRENT_USER } from "../../config/action_types";
|
||||
|
||||
export function currentUser(
|
||||
state = {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
initials: "",
|
||||
email: "",
|
||||
avatarPath: "",
|
||||
avatarThumbPath: "",
|
||||
timezone: "",
|
||||
assignmentsNotification: false,
|
||||
assignmentsNotificationEmail: false,
|
||||
recentNotification: false,
|
||||
recentNotificationEmail: false,
|
||||
systemMessageNotificationEmail: false
|
||||
},
|
||||
action
|
||||
) {
|
||||
switch (action.type) {
|
||||
case SET_CURRENT_USER:
|
||||
return Object.assign({}, state, action.payload);
|
||||
case CHANGE_CURRENT_USER_FULL_NAME:
|
||||
return Object.assign({}, state, { fullName: action.payload });
|
||||
case CHANGE_CURRENT_USER_INITIALS:
|
||||
return Object.assign({}, state, { initials: action.payload });
|
||||
case CHANGE_CURRENT_USER_EMAIL:
|
||||
return Object.assign({}, state, { email: action.payload });
|
||||
case CHANGE_CURRENT_USER_PASSWORD:
|
||||
console.log("handle sending password to the server");
|
||||
// return Object.assign({}, state, { password: action.payload });
|
||||
return state;
|
||||
case CHANGE_CURRENT_USER_AVATAR:
|
||||
return Object.assign({}, state, { avatar: action.payload });
|
||||
case CHANGE_CURRENT_USER_TIMEZONE:
|
||||
return Object.assign({}, state, { timezone: action.payload });
|
||||
case CHANGE_ASSIGNMENTS_NOTIFICATION:
|
||||
return Object.assign({}, state, {
|
||||
assignmentsNotification: action.payload
|
||||
});
|
||||
case CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL:
|
||||
return Object.assign({}, state, {
|
||||
assignmentsNotificationEmail: action.payload
|
||||
});
|
||||
case CHANGE_RECENT_NOTIFICATION:
|
||||
return Object.assign({}, state, { recentNotification: action.payload });
|
||||
case CHANGE_RECENT_NOTIFICATION_EMAIL:
|
||||
return Object.assign({}, state, {
|
||||
recentNotificationEmail: action.payload
|
||||
});
|
||||
case CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL:
|
||||
return Object.assign({}, state, {
|
||||
systemMessageNotificationEmail: action.payload
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
const initialState = {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
initials: "",
|
||||
email: "",
|
||||
avatarThumb: ""
|
||||
};
|
||||
|
||||
export function currentUser(state = initialState, action) {
|
||||
if (action.type === SET_CURRENT_USER) {
|
||||
return Object.assign({}, state, action.payload);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -8,22 +8,8 @@ export const GLOBAL_ACTIVITIES_DATA = "GLOBAL_ACTIVITIES_DATA";
|
|||
export const DESTROY_GLOBAL_ACTIVITIES_DATA = "DESTROY_GLOBAL_ACTIVITIES_DATA";
|
||||
|
||||
// users
|
||||
export const USER_LOGOUT = "USER_LOGOUT";
|
||||
export const SET_CURRENT_USER = "SET_CURRENT_USER";
|
||||
export const CHANGE_CURRENT_USER_FULL_NAME = "CHANGE_CURRENT_USER_FULL_NAME";
|
||||
export const CHANGE_CURRENT_USER_INITIALS = "CHANGE_CURRENT_USER_INITIALS";
|
||||
export const CHANGE_CURRENT_USER_EMAIL = "CHANGE_CURRENT_USER_EMAIL";
|
||||
export const CHANGE_CURRENT_USER_PASSWORD = "CHANGE_CURRENT_USER_PASSWORD";
|
||||
export const CHANGE_CURRENT_USER_AVATAR = "CHANGE_CURRENT_USER_AVATAR";
|
||||
export const CHANGE_CURRENT_USER_TIMEZONE = "CHANGE_CURRENT_USER_TIMEZONE";
|
||||
export const CHANGE_ASSIGNMENTS_NOTIFICATION =
|
||||
"CHANGE_ASSIGNMENTS_NOTIFICATION";
|
||||
export const CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL =
|
||||
"CHANGE_ASSIGNMENTS_NOTIFICATION_EMAIL";
|
||||
export const CHANGE_RECENT_NOTIFICATION = "CHANGE_RECENT_NOTIFICATION";
|
||||
export const CHANGE_RECENT_NOTIFICATION_EMAIL =
|
||||
"CHANGE_RECENT_NOTIFICATION_EMAIL";
|
||||
export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
|
||||
"CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL";
|
||||
|
||||
// user teams
|
||||
export const LEAVE_TEAM = "LEAVE_TEAM";
|
||||
|
@ -39,4 +25,4 @@ export const SPINNER_OFF = "SPINNER_OFF";
|
|||
// alerts
|
||||
export const ADD_ALERT = "ADD_ALERT";
|
||||
export const CLEAR_ALERT = "CLEAR_ALERT";
|
||||
export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS";
|
||||
export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS";
|
||||
|
|
|
@ -4,11 +4,6 @@ export const ACTIVITIES_PATH = "/client_api/activities";
|
|||
// settings
|
||||
export const SETTINGS_PATH = "/settings";
|
||||
export const SETTINGS_ACCOUNT_PATH = "/settings/account";
|
||||
export const SETTINGS_TEAMS_PATH = "/settings/teams";
|
||||
export const SETTINGS_ACCOUNT_PROFILE_PATH = "/settings/account/profile";
|
||||
export const SETTINGS_ACCOUNT_PREFERENCES_PATH =
|
||||
"/settings/account/preferences";
|
||||
|
||||
// teams
|
||||
export const TEAMS_PATH = "/client_api/teams";
|
||||
export const TEAMS_NEW_PATH = "/client_api/teams";
|
||||
|
@ -22,25 +17,7 @@ export const SEARCH_PATH = "/search";
|
|||
// notifications
|
||||
export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications";
|
||||
|
||||
// users
|
||||
export const CURRENT_USER_PATH = "/client_api/current_user_info";
|
||||
export const CHANGE_USER_FULL_NAME_PATH = "/client_api/users/change_full_name";
|
||||
export const CHANGE_USER_INITIALS_PATH = "/client_api/users/change_initials";
|
||||
export const CHANGE_USER_EMAIL_PATH = "/client_api/users/change_email";
|
||||
export const CHANGE_USER_PASSWORD_PATH = "/client_api/users/change_password";
|
||||
export const CHANGE_USER_TIMEZONE_PATH = "/client_api/users/change_timezone";
|
||||
export const CHANGE_USER_ASSIGNEMENTS_NOTIFICATION_PATH =
|
||||
"/client_api/users/change_assignements_notification";
|
||||
export const CHANGE_USER_ASSIGNMENTS_NOTIFICATION_EMAIL_PATH =
|
||||
"/client_api/users/change_assignements_notification_email";
|
||||
export const CHANGE_USER_RECENT_NOTIFICATION_PATH =
|
||||
"/client_api/users/change_recent_notification";
|
||||
export const CHANGE_USER_RECENT_NOTIFICATION_EMAIL_PATH =
|
||||
"/client_api/users/change_recent_notification_email";
|
||||
export const CHANGE_USER_SYSTEM_MESSAGE_NOTIFICATION_EMAIL_PATH =
|
||||
"/client_api/users/change_system_notification_email";
|
||||
export const INVITE_USERS_PATH = "/client_api/users/invite_users";
|
||||
|
||||
// info dropdown_title
|
||||
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
||||
export const TUTORIALS_LINK = "http://scinote.net/product/tutorials/";
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
// @TODO remove this file ASAP the preferences/profile refactoring is merged
|
||||
import axios from "axios";
|
||||
import store from "./store";
|
||||
import { SIGN_IN_PATH } from "./routes";
|
||||
import { destroyState } from "../components/actions/UsersActions";
|
||||
|
||||
export default axios.create({
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
validateStatus(status) {
|
||||
if (status === 401) {
|
||||
store.dispatch(destroyState);
|
||||
window.location = SIGN_IN_PATH;
|
||||
}
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
export const ENTER_KEY_CODE = 13;
|
||||
export const NAME_MIN_LENGTH = 2;
|
||||
export const USER_INITIALS_MAX_LENGTH = 4;
|
||||
export const PASSWORD_MIN_LENGTH = 8;
|
||||
export const PASSWORD_MAX_LENGTH = 72;
|
||||
export const NAME_MAX_LENGTH = 255;
|
||||
export const TEXT_MAX_LENGTH = 10000;
|
||||
export const INVITE_USERS_LIMIT = 20;
|
||||
|
|
4
app/javascript/src/config/constants/strings.js
Normal file
4
app/javascript/src/config/constants/strings.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
|
||||
export const RECENT_NOTIFICATION = "RECENT_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,}))$/;
|
|
@ -9,7 +9,10 @@ export default {
|
|||
},
|
||||
error_messages: {
|
||||
text_too_short: "is too short (minimum is {min_length} characters)",
|
||||
text_too_long: "is too long (maximum is {max_length} characters)"
|
||||
text_too_long: "is too long (maximum is {max_length} characters)",
|
||||
cant_be_blank: "can't be blank",
|
||||
invalid_email: "invalid email",
|
||||
passwords_dont_match: "Passwords don't match"
|
||||
},
|
||||
navbar: {
|
||||
page_title: "sciNote",
|
||||
|
@ -68,7 +71,11 @@ export default {
|
|||
avatar: "Avatar",
|
||||
edit_avatar: "Edit Avatar",
|
||||
change: "Change",
|
||||
change_password: "Change Password",
|
||||
change_password: "Password",
|
||||
new_password: "New password",
|
||||
password_confirmation:
|
||||
"Current password (we need your current password to confirm your changes)",
|
||||
new_password_confirmation: "New password confirmation",
|
||||
new_email: "New email",
|
||||
initials: "Initials",
|
||||
full_name: "Full name",
|
||||
|
@ -84,6 +91,7 @@ export default {
|
|||
time_zone: "Time zone",
|
||||
time_zone_warning:
|
||||
"Time zone setting affects all time & date fields throughout application.",
|
||||
notifications: "Notifications",
|
||||
profile: "Profile",
|
||||
preferences: "Preferences",
|
||||
assignement: "Assignement",
|
||||
|
@ -102,7 +110,7 @@ export default {
|
|||
leave_team_modal: {
|
||||
title: "Leave team {teamName}",
|
||||
subtitle:
|
||||
"Are you sure you wish to leave team My projects? This action is irreversible.",
|
||||
"Are you sure you wish to leave team {teamName}? This action is irreversible.",
|
||||
warnings: "Leaving team has following consequences:",
|
||||
warning_message_one:
|
||||
"you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
|
||||
|
@ -152,7 +160,8 @@ export default {
|
|||
title: "New team",
|
||||
name_label: "Team name",
|
||||
name_placeholder: "My team",
|
||||
name_sublabel: "Pick a name that would best describe your team (e.g. 'University of ..., Department of ...').",
|
||||
name_sublabel:
|
||||
"Pick a name that would best describe your team (e.g. 'University of ..., Department of ...').",
|
||||
description_label: "Description",
|
||||
description_sublabel: "Describe your team.",
|
||||
create: "Create team"
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { combineReducers } from "redux";
|
||||
import { USER_LOGOUT } from "./action_types";
|
||||
import {
|
||||
setCurrentTeam,
|
||||
getListOfTeams,
|
||||
showLeaveTeamModal,
|
||||
showLeaveTeamModal
|
||||
} from "../components/reducers/TeamReducers";
|
||||
import { globalActivities } from "../components/reducers/ActivitiesReducers";
|
||||
import { currentUser } from "../components/reducers/UsersReducer";
|
||||
import { alerts } from "../components/reducers/AlertsReducers";
|
||||
|
||||
export default combineReducers({
|
||||
const appReducer = combineReducers({
|
||||
current_team: setCurrentTeam,
|
||||
all_teams: getListOfTeams,
|
||||
global_activities: globalActivities,
|
||||
|
@ -16,3 +17,12 @@ export default combineReducers({
|
|||
showLeaveTeamModal,
|
||||
alerts
|
||||
});
|
||||
|
||||
const rootReducer = (state, action) => {
|
||||
if (action.type === USER_LOGOUT) {
|
||||
state = undefined;
|
||||
}
|
||||
return appReducer(state, action);
|
||||
};
|
||||
|
||||
export default rootReducer;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export const ROOT_PATH = "/";
|
||||
|
||||
export const SIGN_IN_PATH = "/users/sign_in";
|
||||
|
||||
// Settings page
|
||||
export const SETTINGS_TEAMS_ROUTE = "/settings/teams";
|
||||
export const SETTINGS_TEAM_ROUTE = "/settings/teams/:id";
|
||||
export const SETTINGS_NEW_TEAM_ROUTE = "/settings/teams/new";
|
||||
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
|
||||
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
|
||||
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import React from "react";
|
||||
import { node, oneOfType, arrayOf } from "prop-types";
|
||||
import { Nav, NavItem } from "react-bootstrap";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import styled from "styled-components";
|
||||
|
||||
import {
|
||||
BORDER_LIGHT_COLOR,
|
||||
SIDEBAR_HOVER_GRAY_COLOR,
|
||||
LIGHT_BLUE_COLOR
|
||||
} from "../../../config/constants/colors";
|
||||
|
||||
import {
|
||||
SETTINGS_ACCOUNT_PREFERENCES,
|
||||
SETTINGS_ACCOUNT_PROFILE
|
||||
} from "../../../config/routes";
|
||||
|
||||
const StyledLinkContainer = styled(LinkContainer)`
|
||||
a {
|
||||
color: ${LIGHT_BLUE_COLOR};
|
||||
padding-left: 0;
|
||||
}
|
||||
&.active > a:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-top: -19px;
|
||||
border-top: 19px solid transparent;
|
||||
border-left: 13px solid ${LIGHT_BLUE_COLOR};
|
||||
border-bottom: 19px solid transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: ${SIDEBAR_HOVER_GRAY_COLOR} !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
a {
|
||||
background-color: ${LIGHT_BLUE_COLOR} !important;
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-left: 13px solid ${LIGHT_BLUE_COLOR};
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
border-top: none;
|
||||
margin: 0;
|
||||
padding: 16px 0 50px 0;
|
||||
`;
|
||||
|
||||
const SettingsAccountWrapper = ({ children }) =>
|
||||
<Wrapper className="row">
|
||||
<div className="col-xs-12 col-sm-3">
|
||||
<Nav bsStyle="pills" stacked activeKey={1}>
|
||||
<StyledLinkContainer to={SETTINGS_ACCOUNT_PROFILE}>
|
||||
<NavItem>
|
||||
<FormattedMessage id="settings_page.profile" />
|
||||
</NavItem>
|
||||
</StyledLinkContainer>
|
||||
<StyledLinkContainer to={SETTINGS_ACCOUNT_PREFERENCES}>
|
||||
<NavItem>
|
||||
<FormattedMessage id="settings_page.preferences" />
|
||||
</NavItem>
|
||||
</StyledLinkContainer>
|
||||
</Nav>
|
||||
</div>
|
||||
{children}
|
||||
</Wrapper>;
|
||||
|
||||
SettingsAccountWrapper.propTypes = {
|
||||
children: oneOfType([arrayOf(node), node]).isRequired
|
||||
};
|
||||
|
||||
export default SettingsAccountWrapper;
|
|
@ -9,17 +9,15 @@ import {
|
|||
SETTINGS_TEAMS_ROUTE,
|
||||
SETTINGS_TEAM_ROUTE,
|
||||
SETTINGS_ACCOUNT_PROFILE,
|
||||
SETTINGS_ACCOUNT_PREFERENCES,
|
||||
SETTINGS_NEW_TEAM_ROUTE
|
||||
} from "../../config/routes";
|
||||
|
||||
import {
|
||||
SETTINGS_PATH,
|
||||
SETTINGS_TEAMS,
|
||||
SETTINGS_ACCOUNT_PROFILE_PATH
|
||||
} from "../../config/api_endpoints";
|
||||
import { SETTINGS_PATH, SETTINGS_TEAMS } from "../../config/api_endpoints";
|
||||
|
||||
import NotFound from "../../components/404/NotFound";
|
||||
import SettingsAccount from "./scenes/account/SettingsAccount";
|
||||
import SettingsProfile from "./scenes/profile";
|
||||
import SettingsPreferences from "./scenes/preferences";
|
||||
import SettingsTeams from "./scenes/teams";
|
||||
import SettingsTeam from "./scenes/team";
|
||||
import SettingsNewTeam from "./scenes/teams/new";
|
||||
|
@ -62,22 +60,27 @@ export default class SettingsPage extends Component {
|
|||
</LinkContainer>
|
||||
</Nav>
|
||||
<Switch>
|
||||
<Route exact path={ROOT_PATH} component={SettingsAccount} />
|
||||
<Route exact path={ROOT_PATH} component={SettingsPreferences} />
|
||||
<Route
|
||||
exact
|
||||
path={SETTINGS_PATH}
|
||||
render={() =>
|
||||
render={() => (
|
||||
<Redirect
|
||||
to={SETTINGS_ACCOUNT_PROFILE_PATH}
|
||||
component={SettingsAccount}
|
||||
/>}
|
||||
to={SETTINGS_ACCOUNT_PROFILE}
|
||||
component={SettingsPreferences}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path={SETTINGS_NEW_TEAM_ROUTE} component={SettingsNewTeam} />
|
||||
<Route path={SETTINGS_TEAM_ROUTE} component={SettingsTeam} />
|
||||
<Route path={SETTINGS_TEAMS_ROUTE} component={SettingsTeams} />
|
||||
<Route
|
||||
to={SETTINGS_ACCOUNT_PROFILE_PATH}
|
||||
component={SettingsAccount}
|
||||
path={SETTINGS_ACCOUNT_PROFILE}
|
||||
component={SettingsProfile}
|
||||
/>
|
||||
<Route
|
||||
path={SETTINGS_ACCOUNT_PREFERENCES}
|
||||
component={SettingsPreferences}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FormGroup, FormControl, ControlLabel, Button } from "react-bootstrap";
|
||||
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../config/constants/colors";
|
||||
import { ENTER_KEY_CODE } from "../../../../config/constants/numeric";
|
||||
|
||||
const StyledInputEnabled = styled.div`
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
padding: 19px;
|
||||
margin: 20px 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMsg = styled.div`color: red;`;
|
||||
|
||||
class InputEnabled extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.inputType === "password") {
|
||||
this.state = {
|
||||
value: "",
|
||||
value2: ""
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
value: this.props.inputValue
|
||||
};
|
||||
}
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleChange2 = this.handleChange2.bind(this);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
if (event.charCode === ENTER_KEY_CODE) {
|
||||
event.preventDefault();
|
||||
this.handleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.setState({ value: event.target.value });
|
||||
}
|
||||
|
||||
handleChange2(event) {
|
||||
this.setState({ value2: event.target.value });
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
this.props.saveData(this.state.value);
|
||||
this.props.disableEdit();
|
||||
}
|
||||
|
||||
confirmationField() {
|
||||
let inputs;
|
||||
const type = this.props.inputType;
|
||||
|
||||
if (type === "email" || type === "password") {
|
||||
inputs = (
|
||||
<div>
|
||||
<p>
|
||||
Current password (we need your current password to confirm your
|
||||
changes)
|
||||
</p>
|
||||
<FormControl type="password" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
errorMsg() {
|
||||
return this.state.value !== this.state.value2
|
||||
? <ErrorMsg>Passwords do not match!</ErrorMsg>
|
||||
: "";
|
||||
}
|
||||
|
||||
inputField() {
|
||||
let input;
|
||||
|
||||
if (this.props.inputType === "password") {
|
||||
input = (
|
||||
<div>
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
<p>New password confirmation</p>
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
value={this.state.value2}
|
||||
onChange={this.handleChange2}
|
||||
/>
|
||||
{this.errorMsg()}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
input = (
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledInputEnabled>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormGroup>
|
||||
<h4>
|
||||
<FormattedMessage id="settings_page.change" />{" "}
|
||||
<FormattedMessage id={this.props.labelTitle} />
|
||||
</h4>
|
||||
{this.confirmationField()}
|
||||
<ControlLabel>
|
||||
{this.props.labelValue}
|
||||
</ControlLabel>
|
||||
{this.inputField()}
|
||||
<Button bsStyle="primary" onClick={this.props.disableEdit}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
<Button bsStyle="default" onClick={this.handleUpdate}>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</StyledInputEnabled>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputEnabled.propTypes = {
|
||||
inputType: PropTypes.string.isRequired,
|
||||
labelValue: PropTypes.string.isRequired,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
disableEdit: PropTypes.func.isRequired,
|
||||
saveData: PropTypes.func.isRequired,
|
||||
labelTitle: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default InputEnabled;
|
|
@ -1,36 +0,0 @@
|
|||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
|
||||
import SettingsLeftTab from "./SettingsLeftTab";
|
||||
import SettingsProfile from "./profile/SettingsProfile";
|
||||
import SettingsPreferences from "./preferences/SettingsPreferences";
|
||||
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../config/constants/colors";
|
||||
import {
|
||||
SETTINGS_ACCOUNT_PREFERENCES_PATH,
|
||||
SETTINGS_ACCOUNT_PROFILE_PATH
|
||||
} from "../../../../config/api_endpoints";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
border-top: none;
|
||||
margin: 0;
|
||||
padding: 16px 0 50px 0;
|
||||
`;
|
||||
|
||||
export default () =>
|
||||
<Wrapper className="row">
|
||||
<div className="col-xs-12 col-sm-3">
|
||||
<SettingsLeftTab />
|
||||
</div>
|
||||
<Switch>
|
||||
<Route path={SETTINGS_ACCOUNT_PROFILE_PATH} component={SettingsProfile} />
|
||||
<Route
|
||||
path={SETTINGS_ACCOUNT_PREFERENCES_PATH}
|
||||
component={SettingsPreferences}
|
||||
/>
|
||||
</Switch>
|
||||
</Wrapper>;
|
|
@ -1,59 +0,0 @@
|
|||
import React from "react";
|
||||
import { Nav, NavItem } from "react-bootstrap";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import {
|
||||
SETTINGS_ACCOUNT_PROFILE,
|
||||
SETTINGS_ACCOUNT_PREFERENCES
|
||||
} from "../../../../config/routes";
|
||||
|
||||
import {
|
||||
SIDEBAR_HOVER_GRAY_COLOR,
|
||||
LIGHT_BLUE_COLOR
|
||||
} from "../../../../config/constants/colors";
|
||||
|
||||
const MyLinkContainer = styled(LinkContainer)`
|
||||
a {
|
||||
color: ${LIGHT_BLUE_COLOR};
|
||||
padding-left: 0;
|
||||
}
|
||||
&.active > a:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-top: -19px;
|
||||
border-top: 19px solid transparent;
|
||||
border-left: 13px solid ${LIGHT_BLUE_COLOR};
|
||||
border-bottom: 19px solid transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: ${SIDEBAR_HOVER_GRAY_COLOR} !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
a {
|
||||
background-color: ${LIGHT_BLUE_COLOR} !important;
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-left: 13px solid ${LIGHT_BLUE_COLOR};
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default () =>
|
||||
<Nav bsStyle="pills" stacked activeKey={1}>
|
||||
<MyLinkContainer to={SETTINGS_ACCOUNT_PROFILE}>
|
||||
<NavItem>
|
||||
<FormattedMessage id="settings_page.profile" />
|
||||
</NavItem>
|
||||
</MyLinkContainer>
|
||||
<MyLinkContainer to={SETTINGS_ACCOUNT_PREFERENCES}>
|
||||
<NavItem>
|
||||
<FormattedMessage id="settings_page.preferences" />
|
||||
</NavItem>
|
||||
</MyLinkContainer>
|
||||
</Nav>;
|
|
@ -1,3 +0,0 @@
|
|||
export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
|
||||
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
|
||||
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";
|
|
@ -1,83 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import PropType from "prop-types";
|
||||
import { Button } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import TimezonePicker from "react-bootstrap-timezone-picker";
|
||||
import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../../config/constants/colors";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
padding: 19px;
|
||||
margin: 20px 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.settings-warning {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
class InputTimezone extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: props.inputValue
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
}
|
||||
|
||||
handleChange(timezone) {
|
||||
this.setState({ value: timezone });
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
if (this.state.value !== "") {
|
||||
this.props.saveData(this.state.value);
|
||||
}
|
||||
this.props.disableEdit();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Wrapper>
|
||||
<h4>
|
||||
{this.props.labelValue}
|
||||
</h4>
|
||||
<TimezonePicker
|
||||
absolute
|
||||
defaultValue="Europe/London"
|
||||
value={this.props.inputValue}
|
||||
placeholder="Select timezone..."
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<div className="settings-warning">
|
||||
<small>
|
||||
<FormattedMessage id="settings_page.time_zone_warning" />
|
||||
</small>
|
||||
</div>
|
||||
<Button bsStyle="primary" onClick={this.props.disableEdit}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button bsStyle="default" onClick={this.handleUpdate}>
|
||||
Update
|
||||
</Button>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputTimezone.propTypes = {
|
||||
labelValue: PropType.string.isRequired,
|
||||
inputValue: PropType.string.isRequired,
|
||||
disableEdit: PropType.func.isRequired,
|
||||
saveData: PropType.func.isRequired
|
||||
};
|
||||
|
||||
export default InputTimezone;
|
|
@ -1,92 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import NotificationsSwitchGroup from "./NotificationsSwitchGroup";
|
||||
import { WHITE_COLOR } from "../../../../../config/constants/colors";
|
||||
|
||||
const Wrapper = styled.div`margin-bottom: 6px;`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
margin-top: 12px;
|
||||
margin-left: 7px;
|
||||
`;
|
||||
|
||||
const Icon = styled.span`
|
||||
border-radius: 50%;
|
||||
color: ${WHITE_COLOR};
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
height: 30px;
|
||||
margin-right: 15px;
|
||||
padding: 7px;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
width: 30px;
|
||||
`;
|
||||
|
||||
const Image = styled.span`
|
||||
border-radius: 50%;
|
||||
color: ${WHITE_COLOR};
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
height: 30px;
|
||||
margin-right: 15px;
|
||||
width: 30px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
class NotificationsGroup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let imgOrIcon;
|
||||
|
||||
if (this.props.imgUrl === "") {
|
||||
imgOrIcon = (
|
||||
<Icon style={{ backgroundColor: this.props.iconBackground }}>
|
||||
<i className={this.props.iconClasses} />
|
||||
</Icon>
|
||||
);
|
||||
} else {
|
||||
imgOrIcon = (
|
||||
<Image>
|
||||
<img src={this.props.imgUrl} alt="default avatar" />
|
||||
</Image>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper className="row">
|
||||
<IconWrapper className="col-sm-1">
|
||||
{imgOrIcon}
|
||||
</IconWrapper>
|
||||
<div className="col-sm-10">
|
||||
<h5>
|
||||
<strong>
|
||||
<FormattedMessage id={this.props.title} />
|
||||
</strong>
|
||||
</h5>
|
||||
<p>
|
||||
<FormattedMessage id={this.props.subtitle} />
|
||||
</p>
|
||||
<NotificationsSwitchGroup type={this.props.type} />
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationsGroup.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
imgUrl: PropTypes.string.isRequired,
|
||||
iconClasses: PropTypes.string.isRequired,
|
||||
iconBackground: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default NotificationsGroup;
|
|
@ -1,158 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { string, bool, func } from "prop-types";
|
||||
|
||||
import NotificationsSwitch from "./NotificationsSwitch";
|
||||
|
||||
import {
|
||||
ASSIGNMENT_NOTIFICATION,
|
||||
RECENT_NOTIFICATION,
|
||||
SYSTEM_NOTIFICATION
|
||||
} from "../constants";
|
||||
|
||||
import {
|
||||
changeAssignmentsNotification,
|
||||
changeAssignmentsNotificationEmail,
|
||||
changeRecentNotification,
|
||||
changeRecentNotificationEmail,
|
||||
changeSystemMessageNotificationEmail
|
||||
} from "../../../../../components/actions/UsersActions";
|
||||
|
||||
class NotificationsSwitchGroup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isSwitchOn: false,
|
||||
isEmailSwitchOn: false
|
||||
};
|
||||
|
||||
this.toggleFirstSwitch = this.toggleFirstSwitch.bind(this);
|
||||
this.toggleSecondSwitch = this.toggleSecondSwitch.bind(this);
|
||||
this.isSwitchDisabled = this.isSwitchDisabled.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
switch (this.props.type) {
|
||||
case ASSIGNMENT_NOTIFICATION:
|
||||
this.setState({
|
||||
isSwitchOn: this.props.assignmentsNotification,
|
||||
isEmailSwitchOn: this.props.assignmentsNotificationEmail,
|
||||
sciNoteDispatch: state =>
|
||||
this.props.changeAssignmentsNotification(state),
|
||||
emailDispatch: state =>
|
||||
this.props.changeAssignmentsNotificationEmail(state)
|
||||
});
|
||||
break;
|
||||
case RECENT_NOTIFICATION:
|
||||
this.setState({
|
||||
isSwitchOn: this.props.recentNotification,
|
||||
isEmailSwitchOn: this.props.recentNotificationEmail,
|
||||
sciNoteDispatch: state => this.props.changeRecentNotification(state),
|
||||
emailDispatch: state =>
|
||||
this.props.changeRecentNotificationEmail(state)
|
||||
});
|
||||
break;
|
||||
case SYSTEM_NOTIFICATION:
|
||||
this.setState({
|
||||
isSwitchOn: true,
|
||||
isEmailSwitchOn: this.props.systemMessageNotificationEmail,
|
||||
sciNoteDispatch: state => `${state}: Do Nothing`,
|
||||
emailDispatch: state =>
|
||||
this.props.changeSystemMessageNotificationEmail(state)
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.setState({
|
||||
isSwitchOn: false,
|
||||
isEmailSwitchOn: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleFirstSwitch() {
|
||||
if (this.state.isSwitchOn) {
|
||||
this.setState({ isSwitchOn: false, isEmailSwitchOn: false });
|
||||
this.state.sciNoteDispatch(false);
|
||||
this.state.emailDispatch(false);
|
||||
} else {
|
||||
this.setState({ isSwitchOn: true });
|
||||
this.state.sciNoteDispatch(true);
|
||||
}
|
||||
}
|
||||
|
||||
toggleSecondSwitch() {
|
||||
if (this.state.isEmailSwitchOn) {
|
||||
this.setState({ isEmailSwitchOn: false });
|
||||
this.state.emailDispatch(false);
|
||||
} else {
|
||||
this.setState({ isEmailSwitchOn: true });
|
||||
this.state.emailDispatch(true);
|
||||
}
|
||||
}
|
||||
|
||||
isSwitchDisabled() {
|
||||
if (this.props.type === SYSTEM_NOTIFICATION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NotificationsSwitch
|
||||
title="settings_page.show_in_scinote"
|
||||
isSwitchOn={this.state.isSwitchOn}
|
||||
toggleSwitch={this.toggleFirstSwitch}
|
||||
isDisabled={this.isSwitchDisabled()}
|
||||
/>
|
||||
<NotificationsSwitch
|
||||
title="settings_page.notify_me_via_email"
|
||||
isSwitchOn={this.state.isEmailSwitchOn}
|
||||
toggleSwitch={this.toggleSecondSwitch}
|
||||
isDisabled={!this.state.isSwitchOn}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO get rid of unnecesary proptypes
|
||||
NotificationsSwitchGroup.propTypes = {
|
||||
type: string.isRequired,
|
||||
assignmentsNotification: bool.isRequired,
|
||||
assignmentsNotificationEmail: bool.isRequired,
|
||||
recentNotification: bool.isRequired,
|
||||
recentNotificationEmail: bool.isRequired,
|
||||
systemMessageNotificationEmail: bool.isRequired,
|
||||
changeAssignmentsNotification: func.isRequired,
|
||||
changeAssignmentsNotificationEmail: func.isRequired,
|
||||
changeRecentNotification: func.isRequired,
|
||||
changeRecentNotificationEmail: func.isRequired,
|
||||
changeSystemMessageNotificationEmail: func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => state.current_user;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeAssignmentsNotification(status) {
|
||||
dispatch(changeAssignmentsNotification(status));
|
||||
},
|
||||
changeAssignmentsNotificationEmail(status) {
|
||||
dispatch(changeAssignmentsNotificationEmail(status));
|
||||
},
|
||||
changeRecentNotification(status) {
|
||||
dispatch(changeRecentNotification(status));
|
||||
},
|
||||
changeRecentNotificationEmail(status) {
|
||||
dispatch(changeRecentNotificationEmail(status));
|
||||
},
|
||||
changeSystemMessageNotificationEmail(status) {
|
||||
dispatch(changeSystemMessageNotificationEmail(status));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
NotificationsSwitchGroup
|
||||
);
|
|
@ -1,168 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import axios from "../../../../../config/axios";
|
||||
import InputDisabled from "../InputDisabled";
|
||||
import InputTimezone from "./InputTimezone";
|
||||
import { changeTimezone } from "../../../../../components/actions/UsersActions";
|
||||
import NotificationsGroup from "./NotificationsGroup";
|
||||
|
||||
import {
|
||||
ASSIGNMENT_NOTIFICATION,
|
||||
RECENT_NOTIFICATION,
|
||||
SYSTEM_NOTIFICATION
|
||||
} from "../constants";
|
||||
|
||||
import {
|
||||
MAIN_COLOR_BLUE,
|
||||
ICON_GREEN_COLOR,
|
||||
BORDER_LIGHT_COLOR
|
||||
} from "../../../../../config/constants/colors";
|
||||
|
||||
const WrapperInputDisabled = styled.div`
|
||||
margin: 20px 0;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
|
||||
.settings-warning {
|
||||
margin-top: -5px;
|
||||
}
|
||||
`;
|
||||
|
||||
class SettingsPreferences extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isTimeZoneEditable: false,
|
||||
email: "",
|
||||
notifications: {
|
||||
assignmentsNotification: false,
|
||||
assignmentsNotificationEmail: false,
|
||||
recentNotification: false,
|
||||
recentNotificationEmail: false,
|
||||
systemMessageNofificationEmail: false
|
||||
}
|
||||
};
|
||||
|
||||
this.setData = this.setData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getPreferencesInfo();
|
||||
}
|
||||
|
||||
toggleIsEditable(fieldNameEnabled) {
|
||||
const editableState = this.state[fieldNameEnabled];
|
||||
this.setState({ [fieldNameEnabled]: !editableState });
|
||||
}
|
||||
|
||||
setData({ data }) {
|
||||
const user = data.user;
|
||||
|
||||
const newData = {
|
||||
timeZone: user.timeZone,
|
||||
notifications: {
|
||||
assignmentsNotification: user.notifications.assignmentsNotification,
|
||||
assignmentsNotificationEmail:
|
||||
user.notifications.assignmentsNotificationEmail,
|
||||
recentNotification: user.notifications.recentNotification,
|
||||
recentNotificationEmail: user.notifications.recentNotificationEmail,
|
||||
systemMessageNofificationEmail:
|
||||
user.notifications.systemMessageNofificationEmail
|
||||
}
|
||||
};
|
||||
|
||||
this.setState(Object.assign({}, this.state, newData));
|
||||
}
|
||||
|
||||
getPreferencesInfo() {
|
||||
axios
|
||||
.get("/client_api/users/preferences_info")
|
||||
.then(response => this.setData(response))
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
render() {
|
||||
const isTimeZoneEditable = "isTimeZoneEditable";
|
||||
let timezoneField;
|
||||
|
||||
if (this.state.isTimeZoneEditable) {
|
||||
timezoneField = (
|
||||
<InputTimezone
|
||||
labelValue="Time zone"
|
||||
inputValue={this.state.timeZone}
|
||||
disableEdit={() => this.toggleIsEditable(isTimeZoneEditable)}
|
||||
saveData={timeZone => this.props.changeTimezone(timeZone)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
timezoneField = (
|
||||
<WrapperInputDisabled>
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.time_zone"
|
||||
inputValue={this.state.timeZone}
|
||||
inputType="text"
|
||||
enableEdit={() => this.toggleIsEditable(isTimeZoneEditable)}
|
||||
/>
|
||||
<div className="settings-warning">
|
||||
<small>
|
||||
<FormattedMessage id="settings_page.time_zone_warning" />
|
||||
</small>
|
||||
</div>
|
||||
</WrapperInputDisabled>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-xs-12 col-sm-9">
|
||||
{timezoneField}
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<NotificationsGroup
|
||||
type={ASSIGNMENT_NOTIFICATION}
|
||||
title="settings_page.assignement"
|
||||
subtitle="settings_page.assignement_msg"
|
||||
imgUrl=""
|
||||
iconClasses="fa fa-newspaper-o"
|
||||
iconBackground={MAIN_COLOR_BLUE}
|
||||
/>
|
||||
<NotificationsGroup
|
||||
type={RECENT_NOTIFICATION}
|
||||
title="settings_page.recent_changes"
|
||||
subtitle="settings_page.recent_changes_msg"
|
||||
imgUrl={this.props.avatarPath}
|
||||
iconClasses=""
|
||||
iconBackground=""
|
||||
/>
|
||||
<NotificationsGroup
|
||||
type={SYSTEM_NOTIFICATION}
|
||||
title="settings_page.system_message"
|
||||
subtitle="settings_page.system_message_msg"
|
||||
imgUrl=""
|
||||
iconClasses="glyphicon glyphicon-tower"
|
||||
iconBackground={ICON_GREEN_COLOR}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsPreferences.propTypes = {
|
||||
changeTimezone: PropTypes.func.isRequired,
|
||||
avatarPath: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => state.current_user;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeTimezone(timezone) {
|
||||
dispatch(changeTimezone(timezone));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
SettingsPreferences
|
||||
);
|
|
@ -1,42 +0,0 @@
|
|||
import React from "react";
|
||||
import PropTypes, { string } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import {
|
||||
WHITE_COLOR,
|
||||
DARK_GRAY_COLOR
|
||||
} from "../../../../../config/constants/colors";
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const EditAvatar = styled.span`
|
||||
color: ${WHITE_COLOR};
|
||||
background-color: ${DARK_GRAY_COLOR};
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
const Avatar = props =>
|
||||
<AvatarWrapper onClick={props.enableEdit}>
|
||||
<img src={props.imgSource} alt="default avatar" />
|
||||
<EditAvatar className="text-center">
|
||||
<span className="glyphicon glyphicon-pencil" />
|
||||
<FormattedMessage id="settings_page.edit_avatar" />
|
||||
</EditAvatar>
|
||||
</AvatarWrapper>;
|
||||
|
||||
Avatar.propTypes = {
|
||||
imgSource: string.isRequired,
|
||||
enableEdit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Avatar;
|
|
@ -1,247 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import axios from "../../../../../config/axios";
|
||||
import Avatar from "./Avatar";
|
||||
import InputDisabled from "../InputDisabled";
|
||||
import InputEnabled from "../InputEnabled";
|
||||
|
||||
import {
|
||||
changeFullName,
|
||||
changeInitials,
|
||||
changeEmail,
|
||||
changePassword,
|
||||
changeAvatar
|
||||
} from "../../../../../components/actions/UsersActions";
|
||||
|
||||
const AvatarLabel = styled.h4`
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
class MyProfile extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fullName: "",
|
||||
avatarThumb: "",
|
||||
initials: "",
|
||||
email: "",
|
||||
timeZone: "",
|
||||
newEmail: "",
|
||||
isFullNameEditable: false,
|
||||
areInitialsEditable: false,
|
||||
isEmailEditable: false,
|
||||
isPasswordEditable: false,
|
||||
isAvatarEditable: false
|
||||
};
|
||||
|
||||
this.toggleIsEditable = this.toggleIsEditable.bind(this);
|
||||
this.getProfileInfo = this.getProfileInfo.bind(this);
|
||||
this.setData = this.setData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getProfileInfo();
|
||||
}
|
||||
|
||||
setData({ data }) {
|
||||
const user = data.user;
|
||||
|
||||
// TODO move this transformation to seperate method
|
||||
|
||||
const newData = {
|
||||
fullName: user.full_name,
|
||||
initials: user.initials,
|
||||
email: user.email,
|
||||
avatarThumb: user.avatar_thumb_path,
|
||||
timeZone: user.time_zone
|
||||
};
|
||||
|
||||
this.setState(Object.assign({}, this.state, newData));
|
||||
}
|
||||
|
||||
getProfileInfo() {
|
||||
axios
|
||||
.get("/client_api/users/profile_info")
|
||||
.then(response => this.setData(response))
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
toggleIsEditable(fieldNameEnabled) {
|
||||
const editableState = this.state[fieldNameEnabled];
|
||||
this.setState({ [fieldNameEnabled]: !editableState });
|
||||
}
|
||||
|
||||
render() {
|
||||
const areInitialsEditable = "areInitialsEditable";
|
||||
const isFullNameEditable = "isFullNameEditable";
|
||||
const isEmailEditable = "isEmailEditable";
|
||||
const isPasswordEditable = "isPasswordEditable";
|
||||
const isAvatarEditable = "isAvatarEditable";
|
||||
let fullNameField;
|
||||
let initialsField;
|
||||
let emailField;
|
||||
let passwordField;
|
||||
let avatarField;
|
||||
|
||||
if (this.state.isAvatarEditable) {
|
||||
avatarField = (
|
||||
<InputEnabled
|
||||
labelTitle="settings_page.avatar"
|
||||
labelValue="Avatar"
|
||||
inputType="file"
|
||||
inputValue=""
|
||||
disableEdit={() => this.toggleIsEditable(isAvatarEditable)}
|
||||
saveData={avatarSrc => this.props.changeAvatar(avatarSrc)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
avatarField = (
|
||||
<Avatar
|
||||
imgSource={this.state.avatarThumb}
|
||||
enableEdit={() => this.toggleIsEditable(isAvatarEditable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.isPasswordEditable) {
|
||||
passwordField = (
|
||||
<InputEnabled
|
||||
labelTitle="settings_page.change_password"
|
||||
labelValue="Change password"
|
||||
inputType="password"
|
||||
inputValue=""
|
||||
disableEdit={() => this.toggleIsEditable(isPasswordEditable)}
|
||||
saveData={newPassword => this.props.changePassword(newPassword)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
passwordField = (
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.change_password"
|
||||
inputType="password"
|
||||
inputValue=""
|
||||
enableEdit={() => this.toggleIsEditable(isPasswordEditable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.isEmailEditable) {
|
||||
emailField = (
|
||||
<InputEnabled
|
||||
labelTitle="settings_page.new_email"
|
||||
labelValue="New email"
|
||||
inputType="email"
|
||||
inputValue={this.state.email}
|
||||
disableEdit={() => this.toggleIsEditable(isEmailEditable)}
|
||||
saveData={newEmail => this.props.changeEmail(newEmail)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
emailField = (
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.new_email"
|
||||
inputValue={this.state.email}
|
||||
inputType="email"
|
||||
enableEdit={() => this.toggleIsEditable(isEmailEditable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.areInitialsEditable) {
|
||||
initialsField = (
|
||||
<InputEnabled
|
||||
labelTitle="settings_page.initials"
|
||||
labelValue="Initials"
|
||||
inputType="text"
|
||||
inputValue={this.state.initials}
|
||||
disableEdit={() => this.toggleIsEditable(areInitialsEditable)}
|
||||
saveData={newName => this.props.changeInitials(newName)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
initialsField = (
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.initials"
|
||||
inputValue={this.state.initials}
|
||||
inputType="text"
|
||||
enableEdit={() => this.toggleIsEditable(areInitialsEditable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.isFullNameEditable) {
|
||||
fullNameField = (
|
||||
<InputEnabled
|
||||
labelTitle="settings_page.full_name"
|
||||
labelValue="Full name"
|
||||
inputType="text"
|
||||
inputValue={this.state.fullName}
|
||||
disableEdit={() => this.toggleIsEditable(isFullNameEditable)}
|
||||
saveData={newName => this.props.changeFullName(newName)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
fullNameField = (
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.full_name"
|
||||
inputValue={this.state.fullName}
|
||||
inputType="text"
|
||||
enableEdit={() => this.toggleIsEditable(isFullNameEditable)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage id="settings_page.my_profile" />
|
||||
</h2>
|
||||
<AvatarLabel>
|
||||
<FormattedMessage id="settings_page.avatar" />
|
||||
</AvatarLabel>
|
||||
{avatarField}
|
||||
{fullNameField}
|
||||
{initialsField}
|
||||
{emailField}
|
||||
{passwordField}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyProfile.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
changeFullName: PropTypes.func.isRequired,
|
||||
changeInitials: PropTypes.func.isRequired,
|
||||
changeEmail: PropTypes.func.isRequired,
|
||||
changePassword: PropTypes.func.isRequired,
|
||||
changeAvatar: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => state.current_user;
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeFullName(name) {
|
||||
dispatch(changeFullName(name));
|
||||
},
|
||||
changeInitials(initials) {
|
||||
dispatch(changeInitials(initials));
|
||||
},
|
||||
changeEmail(email) {
|
||||
dispatch(changeEmail(email));
|
||||
},
|
||||
changePassword(password) {
|
||||
dispatch(changePassword(password));
|
||||
},
|
||||
changeAvatar(avatarSrc) {
|
||||
dispatch(changeAvatar(avatarSrc));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MyProfile);
|
|
@ -1,119 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import axios from "../../../../../config/axios";
|
||||
import MyStatisticsBox from "./MyStatisticsBox";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-left: -15px;
|
||||
width: 260px;
|
||||
`;
|
||||
|
||||
class MyStatistics extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
statistics: {
|
||||
teamSum: 0,
|
||||
projectsSum: 0,
|
||||
experimentsSum: 0,
|
||||
protocolsSum: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.setData = this.setData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getStatisticsInfo();
|
||||
}
|
||||
|
||||
setData({ data }) {
|
||||
const user = data.user;
|
||||
|
||||
const newData = {
|
||||
statistics: {
|
||||
teamsSum: user.statistics.number_of_teams,
|
||||
projectsSum: user.statistics.number_of_projects,
|
||||
experimentsSum: user.statistics.number_of_experiments,
|
||||
protocolsSum: user.statistics.number_of_protocols
|
||||
}
|
||||
};
|
||||
|
||||
this.setState(Object.assign({}, this.state, newData));
|
||||
}
|
||||
|
||||
getStatisticsInfo() {
|
||||
axios
|
||||
.get("/client_api/users/statistics_info")
|
||||
.then(response => this.setData(response))
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
render() {
|
||||
const stats = this.state.statistics;
|
||||
|
||||
const statBoxes = () => {
|
||||
let boxes = (
|
||||
<div>
|
||||
<FormattedMessage id="general.loading" />
|
||||
</div>
|
||||
);
|
||||
if (stats) {
|
||||
boxes = (
|
||||
<Wrapper>
|
||||
<MyStatisticsBox
|
||||
typeLength={stats.teamsSum}
|
||||
plural="settings_page.teams"
|
||||
singular="settings_page.team"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={stats.projectsSum}
|
||||
plural="settings_page.projects"
|
||||
singular="settings_page.project"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={stats.experimentsSum}
|
||||
plural="settings_page.experiments"
|
||||
singular="settings_page.experiment"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={stats.protocolsSum}
|
||||
plural="settings_page.protocols"
|
||||
singular="settings_page.protocol"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return boxes;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage id="settings_page.my_statistics" />
|
||||
</h2>
|
||||
|
||||
{statBoxes()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyStatistics.propTypes = {
|
||||
statistics: PropTypes.shape({
|
||||
teamsSum: PropTypes.number.isRequired,
|
||||
projectsSum: PropTypes.number.isRequired,
|
||||
experimentsSum: PropTypes.number.isRequired,
|
||||
protocolsSum: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
const mapStateToProps = state => state.current_user;
|
||||
|
||||
export default connect(mapStateToProps, {})(MyStatistics);
|
|
@ -0,0 +1,125 @@
|
|||
import React, { Component } from "react";
|
||||
import { string, func } from "prop-types";
|
||||
import { Button } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import TimezonePicker from "react-bootstrap-timezone-picker";
|
||||
import "react-bootstrap-timezone-picker/dist/react-bootstrap-timezone-picker.min.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { updateUser } from "../../../../../services/api/users_api";
|
||||
import InputDisabled from "../../../components/InputDisabled";
|
||||
import { BORDER_LIGHT_COLOR } from "../../../../../config/constants/colors";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
padding: 19px;
|
||||
margin: 20px 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.settings-warning {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const WrapperInputDisabled = styled.div`
|
||||
margin: 20px 0;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
|
||||
.settings-warning {
|
||||
margin-top: -5px;
|
||||
}
|
||||
`;
|
||||
|
||||
class InputTimezone extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: "",
|
||||
disabled: true
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
this.enableEdit = this.enableEdit.bind(this);
|
||||
this.disableEdit = this.disableEdit.bind(this);
|
||||
}
|
||||
|
||||
handleChange(timezone) {
|
||||
this.setState({ value: timezone });
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
if (this.state.value !== "") {
|
||||
updateUser({ time_zone: this.state.value }).then(() => {
|
||||
this.disableEdit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.setState({ disabled: false, value: this.props.value });
|
||||
}
|
||||
|
||||
disableEdit() {
|
||||
this.setState({ disabled: true });
|
||||
this.props.loadPreferences();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.disabled) {
|
||||
return (
|
||||
<WrapperInputDisabled>
|
||||
<InputDisabled
|
||||
labelTitle="settings_page.time_zone"
|
||||
inputValue={this.props.value}
|
||||
inputType="text"
|
||||
enableEdit={this.enableEdit}
|
||||
/>
|
||||
<div className="settings-warning">
|
||||
<small>
|
||||
<FormattedMessage id="settings_page.time_zone_warning" />
|
||||
</small>
|
||||
</div>
|
||||
</WrapperInputDisabled>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<h4>
|
||||
<FormattedMessage id="settings_page.time_zone" />
|
||||
</h4>
|
||||
<TimezonePicker
|
||||
absolute
|
||||
defaultValue="Europe/London"
|
||||
value={this.state.value}
|
||||
placeholder="Select timezone..."
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<div className="settings-warning">
|
||||
<small>
|
||||
<FormattedMessage id="settings_page.time_zone_warning" />
|
||||
</small>
|
||||
</div>
|
||||
<Button bsStyle="primary" onClick={this.disableEdit}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
<Button bsStyle="default" onClick={this.handleUpdate}>
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputTimezone.propTypes = {
|
||||
value: string.isRequired,
|
||||
loadPreferences: func.isRequired
|
||||
};
|
||||
|
||||
export default InputTimezone;
|
|
@ -0,0 +1,179 @@
|
|||
import React, { Component } from "react";
|
||||
import { string, bool, func } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { updateUser } from "../../../../../services/api/users_api";
|
||||
import NotificationsSwitch from "./NotificationsSwitch";
|
||||
import {
|
||||
RECENT_NOTIFICATION,
|
||||
SYSTEM_NOTIFICATION,
|
||||
ASSIGNMENT_NOTIFICATION
|
||||
} from "../../../../../config/constants/strings";
|
||||
import { WHITE_COLOR } from "../../../../../config/constants/colors";
|
||||
import avatarImg from "../../../../../assets/missing.png";
|
||||
|
||||
const Wrapper = styled.div`margin-bottom: 6px;`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
margin-top: 12px;
|
||||
margin-left: 7px;
|
||||
`;
|
||||
|
||||
const Icon = styled.span`
|
||||
border-radius: 50%;
|
||||
color: ${WHITE_COLOR};
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
height: 30px;
|
||||
margin-right: 15px;
|
||||
padding: 7px;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
width: 30px;
|
||||
`;
|
||||
|
||||
const Image = styled.span`
|
||||
border-radius: 50%;
|
||||
color: ${WHITE_COLOR};
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
height: 30px;
|
||||
margin-right: 15px;
|
||||
width: 30px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
class NotificationsGroup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.notificationImage = this.notificationImage.bind(this);
|
||||
this.inAppNotificationField = this.inAppNotificationField.bind(this);
|
||||
this.emailNotificationField = this.emailNotificationField.bind(this);
|
||||
this.updateStatus = this.updateStatus.bind(this);
|
||||
this.buttonGroupStatus = this.buttonGroupStatus.bind(this);
|
||||
}
|
||||
|
||||
notificationImage() {
|
||||
if (this.props.type === RECENT_NOTIFICATION) {
|
||||
return (
|
||||
<Image>
|
||||
<img src={avatarImg} alt="default avatar" />
|
||||
</Image>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Icon style={{ backgroundColor: this.props.iconBackground }}>
|
||||
<i className={this.props.iconClasses} />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
inAppNotificationField(value) {
|
||||
let params = {};
|
||||
switch (this.props.type) {
|
||||
case ASSIGNMENT_NOTIFICATION:
|
||||
params.assignments_notification = value;
|
||||
if(!value) {
|
||||
params.assignments_email_notification = false;
|
||||
}
|
||||
break;
|
||||
case RECENT_NOTIFICATION:
|
||||
params.recent_notification = value;
|
||||
if(!value) {
|
||||
params.recent_email_notification = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
params = {}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
emailNotificationField() {
|
||||
switch (this.props.type) {
|
||||
case ASSIGNMENT_NOTIFICATION:
|
||||
return "assignments_email_notification";
|
||||
case RECENT_NOTIFICATION:
|
||||
return "recent_email_notification";
|
||||
case SYSTEM_NOTIFICATION:
|
||||
return "system_message_email_notification"
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(actionType, value) {
|
||||
if (actionType === "inAppNotification") {
|
||||
const params = this.inAppNotificationField(value);
|
||||
updateUser(params).then(() => this.props.reloadInfo());
|
||||
} else if (actionType === "emailNotification") {
|
||||
const emailField = this.emailNotificationField();
|
||||
updateUser({ [emailField]: value }).then(() => this.props.reloadInfo());
|
||||
}
|
||||
}
|
||||
|
||||
// check if the in sciNote notification is disabled
|
||||
buttonGroupStatus() {
|
||||
return (
|
||||
this.props.type !== SYSTEM_NOTIFICATION && !this.props.inAppNotification
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper className="row">
|
||||
<IconWrapper className="col-sm-1">
|
||||
{this.notificationImage()}
|
||||
</IconWrapper>
|
||||
<div className="col-sm-10">
|
||||
<h5>
|
||||
<strong>
|
||||
<FormattedMessage id={this.props.title} />
|
||||
</strong>
|
||||
</h5>
|
||||
<p>
|
||||
<FormattedMessage id={this.props.subtitle} />
|
||||
</p>
|
||||
<div>
|
||||
<NotificationsSwitch
|
||||
title="settings_page.show_in_scinote"
|
||||
status={this.props.inAppNotification}
|
||||
updateStatus={value =>
|
||||
this.updateStatus("inAppNotification", value)}
|
||||
isDisabled={this.props.type === SYSTEM_NOTIFICATION}
|
||||
/>
|
||||
<NotificationsSwitch
|
||||
title="settings_page.notify_me_via_email"
|
||||
status={this.props.emailNotification}
|
||||
updateStatus={value =>
|
||||
this.updateStatus("emailNotification", value)}
|
||||
isDisabled={false}
|
||||
isTemporarilyDisabled={this.buttonGroupStatus()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationsGroup.propTypes = {
|
||||
title: string.isRequired,
|
||||
subtitle: string.isRequired,
|
||||
type: string.isRequired,
|
||||
iconClasses: string,
|
||||
iconBackground: string,
|
||||
inAppNotification: bool,
|
||||
emailNotification: bool,
|
||||
reloadInfo: func.isRequired
|
||||
};
|
||||
|
||||
NotificationsGroup.defaultProps = {
|
||||
iconClasses: "",
|
||||
iconBackground: "",
|
||||
emailNotification: false,
|
||||
inAppNotification: false
|
||||
};
|
||||
|
||||
export default NotificationsGroup;
|
|
@ -1,5 +1,5 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { string, bool, func } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
|
@ -27,66 +27,95 @@ const RightButton = styled.button`
|
|||
class NotificationsSwitch extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { status: this.props.status };
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.disabledButton = this.disabledButton.bind(this);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
handleClick(value) {
|
||||
if (!this.props.isDisabled) {
|
||||
this.props.toggleSwitch();
|
||||
this.props.updateStatus(value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let switchBtn;
|
||||
|
||||
if (this.props.isSwitchOn) {
|
||||
switchBtn = (
|
||||
disabledButton() {
|
||||
if(this.props.isTemporarilyDisabled) {
|
||||
return (
|
||||
<div className="btn-group">
|
||||
<LeftButton
|
||||
className="btn btn-danger"
|
||||
disabled
|
||||
>
|
||||
<FormattedMessage id="settings_page.no" />
|
||||
</LeftButton>
|
||||
<RightButton
|
||||
className="btn btn-default"
|
||||
disabled
|
||||
>
|
||||
<FormattedMessage id="settings_page.yes" />
|
||||
</RightButton>
|
||||
</div>
|
||||
);
|
||||
} else if(this.props.isDisabled) {
|
||||
return (
|
||||
<div className="btn-group">
|
||||
<LeftButton
|
||||
className="btn btn-default"
|
||||
disabled={this.props.isDisabled}
|
||||
onClick={this.handleClick}
|
||||
disabled
|
||||
>
|
||||
<FormattedMessage id="settings_page.no" />
|
||||
</LeftButton>
|
||||
<RightButton
|
||||
className="btn btn-primary"
|
||||
disabled
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<FormattedMessage id="settings_page.yes" />
|
||||
</RightButton>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switchBtn = (
|
||||
} else if(this.props.status) {
|
||||
return (
|
||||
<div className="btn-group">
|
||||
<LeftButton
|
||||
className="btn btn-danger"
|
||||
disabled
|
||||
onClick={this.handleClick}
|
||||
className="btn btn-default"
|
||||
onClick={() => this.handleClick(false)}
|
||||
>
|
||||
<FormattedMessage id="settings_page.no" />
|
||||
</LeftButton>
|
||||
<RightButton
|
||||
className="btn btn-default"
|
||||
disabled={this.props.isDisabled}
|
||||
onClick={this.handleClick}
|
||||
className="btn btn-primary"
|
||||
onClick={() => this.handleClick(true)}
|
||||
>
|
||||
<FormattedMessage id="settings_page.yes" />
|
||||
</RightButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="btn-group">
|
||||
<LeftButton
|
||||
className="btn btn-danger"
|
||||
onClick={() => this.handleClick(false)}
|
||||
>
|
||||
<FormattedMessage id="settings_page.no" />
|
||||
</LeftButton>
|
||||
<RightButton
|
||||
className="btn btn-default"
|
||||
onClick={() => this.handleClick(true)}
|
||||
>
|
||||
<FormattedMessage id="settings_page.yes" />
|
||||
</RightButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Wrapper className="row">
|
||||
<div className="col-sm-4 col-sm-offset-1">
|
||||
<FormattedMessage id={this.props.title} />
|
||||
</div>
|
||||
<div className="col-sm-7">
|
||||
{switchBtn}
|
||||
{this.disabledButton()}
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
|
@ -94,10 +123,15 @@ class NotificationsSwitch extends Component {
|
|||
}
|
||||
|
||||
NotificationsSwitch.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
isSwitchOn: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
toggleSwitch: PropTypes.func.isRequired
|
||||
title: string.isRequired,
|
||||
status: bool.isRequired,
|
||||
isDisabled: bool.isRequired,
|
||||
updateStatus: func.isRequired,
|
||||
isTemporarilyDisabled: bool
|
||||
};
|
||||
|
||||
NotificationsSwitch.defaultProps = {
|
||||
isTemporarilyDisabled: false
|
||||
}
|
||||
|
||||
export default NotificationsSwitch;
|
|
@ -0,0 +1,94 @@
|
|||
import React, { Component } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { getUserPreferencesInfo } from "../../../../services/api/users_api";
|
||||
import SettingsAccountWrapper from "../../components/SettingsAccountWrapper";
|
||||
import InputTimezone from "./components/InputTimezone";
|
||||
import NotificationsGroup from "./components/NotificationsGroup";
|
||||
|
||||
import {
|
||||
ASSIGNMENT_NOTIFICATION,
|
||||
RECENT_NOTIFICATION,
|
||||
SYSTEM_NOTIFICATION
|
||||
} from "../../../../config/constants/strings";
|
||||
|
||||
import {
|
||||
MAIN_COLOR_BLUE,
|
||||
ICON_GREEN_COLOR
|
||||
} from "../../../../config/constants/colors";
|
||||
|
||||
class SettingsPreferences extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
timeZone: "",
|
||||
assignments_notification: false,
|
||||
assignments_email_notification: false,
|
||||
recent_notification: false,
|
||||
recent_email_motification: false,
|
||||
system_message_email_notification: false
|
||||
};
|
||||
|
||||
this.getPreferencesInfo = this.getPreferencesInfo.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getPreferencesInfo();
|
||||
}
|
||||
|
||||
getPreferencesInfo() {
|
||||
getUserPreferencesInfo().then(data => {
|
||||
this.setState(data);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SettingsAccountWrapper>
|
||||
<div className="col-xs-12 col-sm-9">
|
||||
<InputTimezone
|
||||
value={this.state.timeZone}
|
||||
loadPreferences={this.getPreferencesInfo}
|
||||
/>
|
||||
<h3>
|
||||
<FormattedMessage id="settings_page.notifications" />
|
||||
</h3>
|
||||
<NotificationsGroup
|
||||
type={ASSIGNMENT_NOTIFICATION}
|
||||
title="settings_page.assignement"
|
||||
subtitle="settings_page.assignement_msg"
|
||||
iconClasses="fa fa-newspaper-o"
|
||||
inAppNotification={this.state.assignments_notification}
|
||||
emailNotification={
|
||||
this.state.assignments_email_notification
|
||||
}
|
||||
iconBackground={MAIN_COLOR_BLUE}
|
||||
reloadInfo={this.getPreferencesInfo}
|
||||
/>
|
||||
<NotificationsGroup
|
||||
type={RECENT_NOTIFICATION}
|
||||
title="settings_page.recent_changes"
|
||||
subtitle="settings_page.recent_changes_msg"
|
||||
inAppNotification={this.state.recent_notification}
|
||||
emailNotification={this.state.recent_email_notification}
|
||||
reloadInfo={this.getPreferencesInfo}
|
||||
/>
|
||||
<NotificationsGroup
|
||||
type={SYSTEM_NOTIFICATION}
|
||||
title="settings_page.system_message"
|
||||
subtitle="settings_page.system_message_msg"
|
||||
emailNotification={
|
||||
this.state.system_message_email_notification
|
||||
}
|
||||
iconClasses="glyphicon glyphicon-tower"
|
||||
iconBackground={ICON_GREEN_COLOR}
|
||||
reloadInfo={this.getPreferencesInfo}
|
||||
/>
|
||||
</div>
|
||||
</SettingsAccountWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsPreferences;
|
|
@ -0,0 +1,91 @@
|
|||
import React, { Component } from "react";
|
||||
import { string, func } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import {
|
||||
WHITE_COLOR,
|
||||
DARK_GRAY_COLOR
|
||||
} from "../../../../../config/constants/colors";
|
||||
|
||||
import InputEnabled from "./InputEnabled";
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&:hover > span {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const EditAvatar = styled.span`
|
||||
display: none;
|
||||
color: ${WHITE_COLOR};
|
||||
background-color: ${DARK_GRAY_COLOR};
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
class AvatarInputField extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { disabled: true, timestamp: "" };
|
||||
this.enableEdit = this.enableEdit.bind(this);
|
||||
this.disableEdit = this.disableEdit.bind(this);
|
||||
this.rerender = this.rerender.bind(this);
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.setState({ disabled: false });
|
||||
}
|
||||
|
||||
disableEdit() {
|
||||
this.setState({ disabled: true });
|
||||
}
|
||||
|
||||
rerender() {
|
||||
this.setState({ timestamp: `?${new Date().getTime()}` });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.disabled) {
|
||||
return (
|
||||
<AvatarWrapper onClick={this.enableEdit}>
|
||||
<img
|
||||
src={this.props.imgSource + this.state.timestamp}
|
||||
alt="default avatar"
|
||||
/>
|
||||
<EditAvatar className="text-center">
|
||||
<span className="glyphicon glyphicon-pencil" />
|
||||
<FormattedMessage id="settings_page.edit_avatar" />
|
||||
</EditAvatar>
|
||||
</AvatarWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputEnabled
|
||||
forceRerender={this.rerender}
|
||||
labelTitle="settings_page.avatar"
|
||||
labelValue="Upload new avatar file"
|
||||
inputType="file"
|
||||
inputValue=""
|
||||
dataField="avatar"
|
||||
disableEdit={this.disableEdit}
|
||||
reloadInfo={this.props.reloadInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AvatarInputField.propTypes = {
|
||||
imgSource: string.isRequired,
|
||||
reloadInfo: func.isRequired
|
||||
};
|
||||
|
||||
export default AvatarInputField;
|
|
@ -0,0 +1,405 @@
|
|||
import React, { Component } from "react";
|
||||
import { string, func } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
ControlLabel,
|
||||
Button,
|
||||
HelpBlock
|
||||
} from "react-bootstrap";
|
||||
import { updateUser } from "../../../../../services/api/users_api";
|
||||
|
||||
import {
|
||||
BORDER_LIGHT_COLOR,
|
||||
COLOR_APPLE_BLOSSOM
|
||||
} from "../../../../../config/constants/colors";
|
||||
import {
|
||||
ENTER_KEY_CODE,
|
||||
USER_INITIALS_MAX_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
PASSWORD_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH
|
||||
} from "../../../../../config/constants/numeric";
|
||||
import { EMAIL_REGEX } from "../../../../../config/constants/strings";
|
||||
|
||||
const StyledInputEnabled = styled.div`
|
||||
border: 1px solid ${BORDER_LIGHT_COLOR};
|
||||
padding: 19px;
|
||||
margin: 20px 0;
|
||||
|
||||
input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHelpBlock = styled(HelpBlock)`color: ${COLOR_APPLE_BLOSSOM};`;
|
||||
|
||||
class InputEnabled extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: this.props.inputValue,
|
||||
current_password: "**********",
|
||||
password_confirmation: "",
|
||||
errorMessage: ""
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handlePasswordConfirmation = this.handlePasswordConfirmation.bind(
|
||||
this
|
||||
);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.confirmationField = this.confirmationField.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.handleFileChange = this.handleFileChange.bind(this);
|
||||
}
|
||||
|
||||
getValidationState() {
|
||||
return this.state.errorMessage.length > 0 ? "error" : null;
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
if (event.charCode === ENTER_KEY_CODE) {
|
||||
event.preventDefault();
|
||||
this.handleSubmit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
event.preventDefault();
|
||||
switch (this.props.dataField) {
|
||||
case "full_name":
|
||||
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 {
|
||||
this.setState({ value, errorMessage: "" });
|
||||
}
|
||||
}
|
||||
|
||||
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: "" });
|
||||
}
|
||||
}
|
||||
|
||||
handleCurrentPassword(event) {
|
||||
const { value } = event.target;
|
||||
if (value.length > PASSWORD_MAX_LENGTH) {
|
||||
this.setState({
|
||||
current_password: value,
|
||||
errorMessage: (
|
||||
<FormattedMessage
|
||||
id="error_messages.text_too_long"
|
||||
values={{ max_length: PASSWORD_MAX_LENGTH }}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} 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) {
|
||||
event.preventDefault();
|
||||
const { dataField } = this.props;
|
||||
let params;
|
||||
let formObj;
|
||||
let formData;
|
||||
switch (dataField) {
|
||||
case "email":
|
||||
params = {
|
||||
[dataField]: this.state.value,
|
||||
current_password: this.state.current_password
|
||||
};
|
||||
break;
|
||||
case "password":
|
||||
params = {
|
||||
[dataField]: this.state.value,
|
||||
current_password: this.state.current_password,
|
||||
password_confirmation: this.state.password_confirmation
|
||||
};
|
||||
break;
|
||||
case "avatar":
|
||||
formData = new FormData();
|
||||
formData.append("user[avatar]", this.state.value);
|
||||
formObj = true;
|
||||
params = formData;
|
||||
break;
|
||||
default:
|
||||
params = { [dataField]: this.state.value };
|
||||
}
|
||||
|
||||
updateUser(params, formObj)
|
||||
.then(() => {
|
||||
this.props.reloadInfo();
|
||||
this.props.disableEdit();
|
||||
if(this.props.forceRerender) {
|
||||
this.props.forceRerender();
|
||||
}
|
||||
})
|
||||
.catch(({ response }) => {
|
||||
this.setState({ errorMessage: response.data.message.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
confirmationField() {
|
||||
const type = this.props.inputType;
|
||||
|
||||
if (type === "email") {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage id="settings_page.password_confirmation" />
|
||||
</p>
|
||||
<FormControl
|
||||
type="password"
|
||||
value={this.state.current_password}
|
||||
onChange={this.handleCurrentPassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
inputField() {
|
||||
const { inputType } = this.props;
|
||||
if (inputType === "password") {
|
||||
return (
|
||||
<div>
|
||||
<i>
|
||||
<FormattedMessage id="settings_page.password_confirmation" />
|
||||
</i>
|
||||
<FormControl
|
||||
type={inputType}
|
||||
value={this.state.current_password}
|
||||
onChange={this.handleCurrentPassword}
|
||||
autoFocus
|
||||
/>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
type={inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
autoFocus
|
||||
/>
|
||||
<ControlLabel>
|
||||
<FormattedMessage id="settings_page.new_password_confirmation" />
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
type={inputType}
|
||||
value={this.state.password_confirmation}
|
||||
onChange={this.handlePasswordConfirmationValidation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (inputType === "file") {
|
||||
return (
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormControl
|
||||
type={this.props.inputType}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledInputEnabled>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormGroup validationState={this.getValidationState()}>
|
||||
<h4>
|
||||
<FormattedMessage id="settings_page.change" />
|
||||
<FormattedMessage id={this.props.labelTitle} />
|
||||
</h4>
|
||||
<ControlLabel>{this.props.labelValue}</ControlLabel>
|
||||
{this.inputField()}
|
||||
{this.confirmationField()}
|
||||
<StyledHelpBlock>{this.state.errorMessage}</StyledHelpBlock>
|
||||
<Button bsStyle="default" onClick={this.props.disableEdit}>
|
||||
<FormattedMessage id="general.cancel" />
|
||||
</Button>
|
||||
<Button bsStyle="primary" type="submit">
|
||||
<FormattedMessage id="general.update" />
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</StyledInputEnabled>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputEnabled.propTypes = {
|
||||
inputType: string.isRequired,
|
||||
labelValue: string.isRequired,
|
||||
inputValue: string.isRequired,
|
||||
disableEdit: func.isRequired,
|
||||
reloadInfo: func.isRequired,
|
||||
labelTitle: string.isRequired,
|
||||
dataField: string.isRequired,
|
||||
forceRerender: func
|
||||
};
|
||||
|
||||
InputEnabled.defaultProps = {
|
||||
forceRerender: () => (false)
|
||||
}
|
||||
|
||||
export default InputEnabled;
|
|
@ -0,0 +1,106 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { func } from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { getUserProfileInfo } from "../../../../../services/api/users_api";
|
||||
import { addCurrentUser } from "../../../../../components/actions/UsersActions";
|
||||
|
||||
import AvatarInputField from "./AvatarInputField";
|
||||
import ProfileInputField from "./ProfileInputField";
|
||||
|
||||
const AvatarLabel = styled.h4`
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
class MyProfile extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fullName: "",
|
||||
avatarThumb: "",
|
||||
initials: "",
|
||||
email: "",
|
||||
timeZone: "",
|
||||
newEmail: ""
|
||||
};
|
||||
this.loadInfo = this.loadInfo.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadInfo();
|
||||
}
|
||||
|
||||
loadInfo() {
|
||||
getUserProfileInfo()
|
||||
.then(data => {
|
||||
const { fullName, initials, email, avatarThumb, timeZone } = data;
|
||||
this.setState({ fullName, initials, email, avatarThumb, timeZone });
|
||||
this.props.addCurrentUser(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage id="settings_page.my_profile" />
|
||||
</h2>
|
||||
<AvatarLabel>
|
||||
<FormattedMessage id="settings_page.avatar" />
|
||||
</AvatarLabel>
|
||||
<AvatarInputField
|
||||
reloadInfo={this.loadInfo}
|
||||
imgSource={this.state.avatarThumb}
|
||||
/>
|
||||
|
||||
<ProfileInputField
|
||||
value={this.state.fullName}
|
||||
inputType="text"
|
||||
labelTitle="settings_page.full_name"
|
||||
labelValue="Full name"
|
||||
reloadInfo={this.loadInfo}
|
||||
dataField="full_name"
|
||||
/>
|
||||
|
||||
<ProfileInputField
|
||||
value={this.state.initials}
|
||||
inputType="text"
|
||||
labelTitle="settings_page.initials"
|
||||
labelValue="Initials"
|
||||
reloadInfo={this.loadInfo}
|
||||
dataField="initials"
|
||||
/>
|
||||
<ProfileInputField
|
||||
value={this.state.email}
|
||||
inputType="email"
|
||||
labelTitle="settings_page.new_email"
|
||||
labelValue="New email"
|
||||
reloadInfo={this.loadInfo}
|
||||
dataField="email"
|
||||
/>
|
||||
|
||||
<ProfileInputField
|
||||
value="********"
|
||||
inputType="password"
|
||||
labelTitle="settings_page.change_password"
|
||||
labelValue="Current password"
|
||||
reloadInfo={this.loadInfo}
|
||||
dataField="password"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyProfile.propTypes = {
|
||||
addCurrentUser: func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { addCurrentUser })(MyProfile);
|
|
@ -0,0 +1,90 @@
|
|||
import React, { Component } from "react";
|
||||
import styled from "styled-components";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { getStatisticsInfo } from "../../../../../services/api/users_api";
|
||||
import MyStatisticsBox from "./MyStatisticsBox";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-left: -15px;
|
||||
width: 260px;
|
||||
`;
|
||||
|
||||
class MyStatistics extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
stats: false,
|
||||
number_of_teams: 0,
|
||||
number_of_projects: 0,
|
||||
number_of_experiments: 0,
|
||||
number_of_protocols: 0
|
||||
};
|
||||
|
||||
this.getStatisticsInfo = this.getStatisticsInfo.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getStatisticsInfo();
|
||||
}
|
||||
|
||||
getStatisticsInfo() {
|
||||
getStatisticsInfo()
|
||||
.then(response => {
|
||||
this.setState(Object.assign({}, response.statistics, { stats: true }));
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ stats: false });
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
renderStatBoxes() {
|
||||
if (this.state.stats) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<MyStatisticsBox
|
||||
typeLength={this.state.number_of_teams}
|
||||
plural="settings_page.teams"
|
||||
singular="settings_page.team"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={this.state.number_of_projects}
|
||||
plural="settings_page.projects"
|
||||
singular="settings_page.project"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={this.state.number_of_experiments}
|
||||
plural="settings_page.experiments"
|
||||
singular="settings_page.experiment"
|
||||
/>
|
||||
<MyStatisticsBox
|
||||
typeLength={this.state.number_of_protocols}
|
||||
plural="settings_page.protocols"
|
||||
singular="settings_page.protocol"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage id="general.loading" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage id="settings_page.my_statistics" />
|
||||
</h2>
|
||||
{this.renderStatBoxes()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyStatistics;
|
|
@ -0,0 +1,52 @@
|
|||
import React, { Component } from "react";
|
||||
import { string, func } from "prop-types";
|
||||
|
||||
import InputDisabled from "../../../components/InputDisabled";
|
||||
import InputEnabled from "./InputEnabled";
|
||||
|
||||
class ProfileInputField extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { disabled: true };
|
||||
this.toggleSate = this.toggleSate.bind(this);
|
||||
}
|
||||
|
||||
toggleSate() {
|
||||
this.setState({ disabled: !this.state.disabled });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.disabled) {
|
||||
return (
|
||||
<InputDisabled
|
||||
labelTitle={this.props.labelTitle}
|
||||
inputValue={this.props.value}
|
||||
inputType={this.props.inputType}
|
||||
enableEdit={this.toggleSate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputEnabled
|
||||
labelTitle={this.props.labelTitle}
|
||||
labelValue={this.props.labelValue}
|
||||
inputType={this.props.inputType}
|
||||
inputValue={this.props.value}
|
||||
disableEdit={this.toggleSate}
|
||||
reloadInfo={this.props.reloadInfo}
|
||||
dataField={this.props.dataField}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProfileInputField.propTypes = {
|
||||
value: string.isRequired,
|
||||
inputType: string.isRequired,
|
||||
labelTitle: string.isRequired,
|
||||
labelValue: string.isRequired,
|
||||
dataField: string.isRequired,
|
||||
reloadInfo: func.isRequired
|
||||
};
|
||||
|
||||
export default ProfileInputField;
|
|
@ -1,16 +1,16 @@
|
|||
import React from "react";
|
||||
|
||||
import MyProfile from "./MyProfile";
|
||||
import MyStatistics from "./MyStatistics";
|
||||
import SettingsAccountWrapper from "../../components/SettingsAccountWrapper";
|
||||
import MyProfile from "./components/MyProfile";
|
||||
import MyStatistics from "./components/MyStatistics";
|
||||
|
||||
const SettingsProfile = () =>
|
||||
<div>
|
||||
<SettingsAccountWrapper>
|
||||
<div className="col-xs-12 col-sm-4">
|
||||
<MyProfile />
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-5">
|
||||
<MyStatistics />
|
||||
</div>
|
||||
</div>;
|
||||
</SettingsAccountWrapper>;
|
||||
|
||||
export default SettingsProfile;
|
|
@ -1,8 +1,20 @@
|
|||
import axios from "axios";
|
||||
import store from "../../config/store";
|
||||
import { SIGN_IN_PATH } from "../../config/routes";
|
||||
import { destroyState } from "../../components/actions/UsersActions";
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
validateStatus(status) {
|
||||
if (status === 401) {
|
||||
setTimeout(() => {
|
||||
store.dispatch(destroyState)
|
||||
window.location = SIGN_IN_PATH;
|
||||
}, 500);
|
||||
}
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,3 +2,45 @@
|
|||
export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications";
|
||||
export const UNREADED_NOTIFICATIONS_PATH =
|
||||
"/client_api/unread_notifications_count";
|
||||
|
||||
// activities
|
||||
export const ACTIVITIES_PATH = "/client_api/activities";
|
||||
|
||||
// settings
|
||||
export const SETTINGS_PATH = "/settings";
|
||||
export const SETTINGS_ACCOUNT_PATH = "/settings/account";
|
||||
// teams
|
||||
export const TEAMS_PATH = "/client_api/teams";
|
||||
export const CHANGE_TEAM_PATH = "/client_api/teams/change_team";
|
||||
export const TEAM_DETAILS_PATH = "/client_api/teams/:team_id/details";
|
||||
export const TEAM_UPDATE_PATH = "/client_api/teams/update";
|
||||
export const CURRENT_USER_PATH = "/client_api/current_user_info"
|
||||
|
||||
// search
|
||||
export const SEARCH_PATH = "/search";
|
||||
|
||||
// notifications
|
||||
export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications";
|
||||
|
||||
// users
|
||||
export const USER_PROFILE_INFO = "/client_api/users/profile_info";
|
||||
export const UPDATE_USER_PATH = "/client_api/users/update";
|
||||
export const PREFERENCES_INFO_PATH = "/client_api/users/preferences_info"
|
||||
export const STATISTICS_INFO_PATH = "/client_api/users/statistics_info"
|
||||
export const SIGN_OUT_PATH = "/client_api/users/sign_out_user"
|
||||
|
||||
// info dropdown_title
|
||||
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
||||
export const TUTORIALS_LINK = "http://scinote.net/product/tutorials/";
|
||||
export const RELEASE_NOTES_LINK = "http://scinote.net/docs/release-notes/";
|
||||
export const PREMIUM_LINK = "http://scinote.net/premium/";
|
||||
export const CONTACT_US_LINK =
|
||||
"http://scinote.net/story-of-scinote/#contact-scinote";
|
||||
|
||||
// user teams
|
||||
export const LEAVE_TEAM_PATH = "/client_api/users/leave_team";
|
||||
export const UPDATE_USER_TEAM_ROLE_PATH = "/client_api/users/update_role";
|
||||
export const REMOVE_USER_FROM_TEAM_PATH = "/client_api/users/remove_user";
|
||||
|
||||
// settings
|
||||
export const SETTINGS_TEAMS = "/settings/teams";
|
||||
|
|
34
app/javascript/src/services/api/users_api.js
Normal file
34
app/javascript/src/services/api/users_api.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { axiosInstance } from "./config";
|
||||
import {
|
||||
USER_PROFILE_INFO,
|
||||
UPDATE_USER_PATH,
|
||||
CURRENT_USER_PATH,
|
||||
PREFERENCES_INFO_PATH,
|
||||
STATISTICS_INFO_PATH,
|
||||
SIGN_OUT_PATH
|
||||
} from "./endpoints";
|
||||
|
||||
export const getUserProfileInfo = () =>
|
||||
axiosInstance.get(USER_PROFILE_INFO).then(({ data }) => data.user);
|
||||
|
||||
export const getUserPreferencesInfo = () =>
|
||||
axiosInstance.get(PREFERENCES_INFO_PATH).then(({ data }) => data);
|
||||
|
||||
export const updateUser = (params, formObj = false) => {
|
||||
if (formObj) {
|
||||
return axiosInstance
|
||||
.post(UPDATE_USER_PATH, params)
|
||||
.then(({ data }) => data.user);
|
||||
}
|
||||
return axiosInstance
|
||||
.post(UPDATE_USER_PATH, { user: params })
|
||||
.then(({ data }) => data.user);
|
||||
};
|
||||
|
||||
export const getCurrentUser = () =>
|
||||
axiosInstance.get(CURRENT_USER_PATH).then(({ data }) => data.user);
|
||||
|
||||
export const getStatisticsInfo = () =>
|
||||
axiosInstance.get(STATISTICS_INFO_PATH).then(({ data }) => data.user);
|
||||
|
||||
export const signOutUser = () => axiosInstance.get(SIGN_OUT_PATH);
|
|
@ -36,7 +36,7 @@ class User < ApplicationRecord
|
|||
size: { less_than: Constants::AVATAR_MAX_SIZE_MB.megabytes }
|
||||
validate :time_zone_check
|
||||
|
||||
store_accessor :settings, :time_zone
|
||||
store_accessor :settings, :time_zone, :notifications
|
||||
|
||||
default_settings(
|
||||
time_zone: 'UTC',
|
||||
|
@ -281,7 +281,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
errors.clear
|
||||
errors.set(:avatar, messages)
|
||||
errors.add(:avatar, messages.join(','))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -413,6 +413,28 @@ class User < ApplicationRecord
|
|||
statistics
|
||||
end
|
||||
|
||||
# json friendly attributes
|
||||
NOTIFICATIONS_TYPES = %w(assignments_notification recent_notification
|
||||
assignments_email_notification
|
||||
recent_email_notification
|
||||
system_message_email_notification)
|
||||
# declare notifications getters
|
||||
NOTIFICATIONS_TYPES.each do |name|
|
||||
define_method(name) do
|
||||
attr_name = name.gsub('_notification', '')
|
||||
self.notifications.fetch(attr_name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
# declare notifications setters
|
||||
NOTIFICATIONS_TYPES.each do |name|
|
||||
define_method("#{name}=") do |value|
|
||||
attr_name = name.gsub('_notification', '').to_sym
|
||||
self.notifications[attr_name] = value
|
||||
save
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def confirmation_required?
|
||||
|
|
28
app/services/client_api/user_service.rb
Normal file
28
app/services/client_api/user_service.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
module ClientApi
|
||||
class UserService < BaseService
|
||||
def update_user!
|
||||
error = I18n.t('client_api.user.passwords_dont_match')
|
||||
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
|
|
@ -1,10 +1,6 @@
|
|||
json.user do
|
||||
json.timeZone user.time_zone
|
||||
json.notifications do
|
||||
json.assignmentsNotification user.assignments_notification
|
||||
json.assignmentsNotificationEmail user.assignments_notification_email
|
||||
json.recentNotification user.recent_notification
|
||||
json.recentNotificationEmail user.recent_notification_email
|
||||
json.systemMessageNofificationEmail user.system_message_notification_email
|
||||
end
|
||||
end
|
||||
json.timeZone timeZone
|
||||
json.assignments_notification notifications['assignments']
|
||||
json.assignments_email_notification notifications['assignments_email']
|
||||
json.recent_notification notifications['recent']
|
||||
json.recent_email_notification notifications['recent_email']
|
||||
json.system_message_email_notification notifications['system_message_email']
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
json.user do
|
||||
json.full_name user.full_name
|
||||
json.fullName user.full_name
|
||||
json.initials user.initials
|
||||
json.email user.email
|
||||
json.avatar_thumb_path avatar_path(user, :thumb)
|
||||
json.time_zone user.time_zone
|
||||
json.avatarThumb avatar_path(user, :thumb)
|
||||
json.timeZone user.time_zone
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
json.user do
|
||||
json.id user.id
|
||||
json.fullName user.full_name
|
||||
json.avatarPath avatar_path(user, :icon_small)
|
||||
json.avatarThumb avatar_path(user, :icon_small)
|
||||
end
|
||||
|
|
|
@ -1825,5 +1825,8 @@ en:
|
|||
user_teams:
|
||||
leave_team_error: "An error occured."
|
||||
leave_flash: "Successfuly left team %{team}."
|
||||
user:
|
||||
blank_password_error: "Password can't be blank!"
|
||||
passwords_dont_match: "Passwords don't match"
|
||||
invite_users:
|
||||
permission_error: "You don't have permission to invite additional users to team. Contact its administrator/s."
|
||||
|
|
|
@ -34,28 +34,14 @@ Rails.application.routes.draw do
|
|||
get '/current_user_info', to: 'users/users#current_user_info'
|
||||
|
||||
namespace :users do
|
||||
get '/sign_out_user', to: 'users#sign_out_user'
|
||||
delete '/remove_user', to: 'user_teams#remove_user'
|
||||
delete '/leave_team', to: 'user_teams#leave_team'
|
||||
put '/update_role', to: 'user_teams#update_role'
|
||||
get '/profile_info', to: 'users#profile_info'
|
||||
get '/statistics_info', to: 'users#statistics_info'
|
||||
get '/preferences_info', to: 'users#preferences_info'
|
||||
delete '/leave_team', to: 'user_teams#leave_team'
|
||||
post '/change_full_name', to: 'users#change_full_name'
|
||||
post '/change_initials', to: 'users#change_initials'
|
||||
post '/change_email', to: 'users#change_email'
|
||||
post '/change_password', to: 'users#change_password'
|
||||
post '/change_timezone', to: 'users#change_timezone'
|
||||
post '/change_assignements_notification',
|
||||
to: 'users#change_assignements_notification'
|
||||
post '/change_assignements_notification_email',
|
||||
to: 'users#change_assignements_notification_email'
|
||||
post '/change_recent_notification',
|
||||
to: 'users#change_recent_notification'
|
||||
post '/change_recent_notification_email',
|
||||
to: 'users#change_recent_notification_email'
|
||||
post '/change_system_notification_email',
|
||||
to: 'users#change_system_notification_email'
|
||||
get '/statistics_info', to: 'users#statistics_info'
|
||||
post '/update', to: 'users#update'
|
||||
devise_scope :user do
|
||||
put '/invite_users', to: 'invitations#invite_users'
|
||||
end
|
||||
|
|
22
package.json
22
package.json
|
@ -20,10 +20,11 @@
|
|||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.2.3",
|
||||
"eslint": "^3.7.1",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-plugin-transform-react-jsx-source": "^6.22.0",
|
||||
"eslint": "^4.7.2",
|
||||
"eslint-config-airbnb": "^15.1.0",
|
||||
"eslint-config-google": "^0.5.0",
|
||||
"eslint-config-google": "^0.9.1",
|
||||
"eslint-config-prettier": "^2.3.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
|
@ -44,9 +45,9 @@
|
|||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"coffee-loader": "^0.7.3",
|
||||
"coffee-loader": "^0.8.0",
|
||||
"coffee-script": "^1.12.6",
|
||||
"compression-webpack-plugin": "^0.4.0",
|
||||
"compression-webpack-plugin": "^1.0.0",
|
||||
"css-loader": "^0.28.4",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
|
@ -60,26 +61,25 @@
|
|||
"postcss-loader": "^2.0.6",
|
||||
"postcss-smart-import": "^0.7.5",
|
||||
"precss": "^2.0.0",
|
||||
"prettysize": "^0.1.0",
|
||||
"prettysize": "^1.1.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"rails-erb-loader": "^5.0.2",
|
||||
"react": "^15.6.1",
|
||||
"react": "^16.0.0",
|
||||
"react-bootstrap": "^0.31.1",
|
||||
"react-bootstrap-table": "^4.0.0",
|
||||
"react-bootstrap-timezone-picker": "^1.0.11",
|
||||
"react-data-grid": "^2.0.2",
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"react-transition-group": "^2.2.0",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-intl": "^2.3.0",
|
||||
"react-intl-redux": "^0.6.0",
|
||||
"react-moment": "^0.6.4",
|
||||
"react-redux": "^5.0.5",
|
||||
"react-router-bootstrap": "^0.24.2",
|
||||
"react-router-dom": "^4.1.2",
|
||||
"react-router-prop-types": "^0.0.1",
|
||||
"react-router-prop-types": "^0.0.2",
|
||||
"react-tagsinput": "^3.17.0",
|
||||
"react-timezone": "^0.2.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"redux": "^3.7.2",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
|
|
|
@ -4,9 +4,24 @@ describe ClientApi::Users::UsersController, type: :controller do
|
|||
login_user
|
||||
|
||||
before do
|
||||
# user password is set in user factory defaults to 'asdf1243'
|
||||
@user = User.first
|
||||
end
|
||||
|
||||
describe '#sign_out_user' do
|
||||
it 'returns unauthorized response' do
|
||||
sign_out @user
|
||||
get :sign_out_user, format: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'responds successfully if the user is signed out' do
|
||||
get :sign_out_user, format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(subject.current_user).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET current_user_info' do
|
||||
it 'responds successfully' do
|
||||
get :current_user_info, format: :json
|
||||
|
@ -14,210 +29,227 @@ describe ClientApi::Users::UsersController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST change_password' do
|
||||
it 'responds successfully' do
|
||||
post :change_password,
|
||||
params: { user: { password: 'secretPassword'} },
|
||||
describe 'POST update' do
|
||||
let(:new_password) { 'secretPassword' }
|
||||
let(:new_email) { 'banana@fruit.com' }
|
||||
|
||||
it 'responds successfully if all password params are set' do
|
||||
post :update,
|
||||
params: { user: { password: new_password,
|
||||
password_confirmation: new_password,
|
||||
current_password: 'asdf1243' } },
|
||||
format: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes password' do
|
||||
expect(@user.valid_password?('secretPassword')).to eq(false)
|
||||
post :change_password,
|
||||
params: { user: { password: 'secretPassword'} },
|
||||
it 'responds unsuccessfully if no current_password is provided' do
|
||||
post :update,
|
||||
params: { user: { password: new_password,
|
||||
password_confirmation: new_password } },
|
||||
format: :json
|
||||
|
||||
expect(@user.reload.valid_password?('secretPassword')).to eq(true)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'does not change short password' do
|
||||
expect(@user.valid_password?('pass')).to eq(false)
|
||||
post :change_password,
|
||||
params: { user: { password: 'pass'} },
|
||||
it 'responds unsuccessfully if no password_confirmation is provided' do
|
||||
post :update,
|
||||
params: { user: { password: new_password,
|
||||
current_password: 'asdf1243' } },
|
||||
format: :json
|
||||
|
||||
expect(@user.reload.valid_password?('pass')).to eq(false)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_timezone' do
|
||||
it 'responds successfully' do
|
||||
user = User.first
|
||||
expect(user.time_zone).to eq('UTC')
|
||||
post :change_timezone, params: { timezone: 'Pacific/Fiji' }, format: :json
|
||||
it 'responds successfully if time_zone is updated' do
|
||||
post :update, params: { user: { time_zone: 'Pacific/Fiji' } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'responds successfully if email is updated' do
|
||||
post :update, params: { user: { email: new_email,
|
||||
current_password: 'asdf1243' } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(@user.reload.email).to eq(new_email)
|
||||
end
|
||||
|
||||
it 'responds unsuccessfully if email is updated without password' do
|
||||
post :update, params: { user: { email: new_email } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(@user.reload.email).to_not eq(new_email)
|
||||
end
|
||||
|
||||
it 'responds unsuccessfully if email is updated with invalid email' do
|
||||
post :update, params: { user: { email: 'bananafruit.com',
|
||||
current_password: 'asdf1243' } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(@user.reload.email).to_not eq(new_email)
|
||||
end
|
||||
|
||||
it 'changes timezone' do
|
||||
user = User.first
|
||||
expect(user.time_zone).to eq('UTC')
|
||||
post :change_timezone, params: { timezone: 'Pacific/Fiji' }, format: :json
|
||||
post :update, params: { user: { time_zone: 'Pacific/Fiji' } },
|
||||
format: :json
|
||||
expect(user.reload.time_zone).to eq('Pacific/Fiji')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_initials' do
|
||||
it 'responds successfully' do
|
||||
post :change_initials, params: { initials: 'TD' }, format: :json
|
||||
it 'responds successfully if initials are provided' do
|
||||
post :update, params: { user: { initials: 'TD' } }, format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'responds successfully' do
|
||||
it 'updates user initials' do
|
||||
user = User.first
|
||||
expect(user.initials).not_to eq('TD')
|
||||
post :change_initials, params: { initials: 'TD' }, format: :json
|
||||
post :update, params: { user: { initials: 'TD' } }, format: :json
|
||||
expect(user.reload.initials).to eq('TD')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_system_notification_email' do
|
||||
it 'responds successfully' do
|
||||
post :change_system_notification_email,
|
||||
params: { status: false },
|
||||
it 'responds successfully if system_message_email_notification provided' do
|
||||
post :update,
|
||||
params: { user: { system_message_email_notificationatus: 'false' } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes notification from false => true' do
|
||||
it 'changes system_message_email_notification from false => true' do
|
||||
user = User.first
|
||||
user.system_message_notification_email = false
|
||||
user.system_message_email_notification = false
|
||||
user.save
|
||||
|
||||
post :change_system_notification_email,
|
||||
params: { status: true },
|
||||
post :update,
|
||||
params: { user: { system_message_email_notification: true } },
|
||||
format: :json
|
||||
expect(user.reload.system_message_notification_email).to eq(true)
|
||||
expect(user.reload.system_message_email_notification).to eql('true')
|
||||
end
|
||||
|
||||
it 'changes system_message_email_notification from true => false' do
|
||||
user = User.first
|
||||
user.system_message_email_notification = true
|
||||
user.save
|
||||
|
||||
post :update,
|
||||
params: { user: { system_message_email_notification: false } },
|
||||
format: :json
|
||||
expect(user.reload.system_message_email_notification).to eql('false')
|
||||
end
|
||||
|
||||
it 'responds successfully if recent_email_notification provided' do
|
||||
post :update,
|
||||
params: { user: { recent_email_notification: false } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes recent_email_notification from false => true' do
|
||||
user = User.first
|
||||
user.recent_email_notification = false
|
||||
user.save
|
||||
|
||||
post :update,
|
||||
params: { user: { recent_email_notification: true } },
|
||||
format: :json
|
||||
expect(user.reload.recent_email_notification).to eql('true')
|
||||
end
|
||||
|
||||
it 'changes notification from true => false' do
|
||||
user = User.first
|
||||
user.system_message_notification_email = true
|
||||
user.recent_email_notification = true
|
||||
user.save
|
||||
|
||||
post :change_system_notification_email,
|
||||
params: { status: false },
|
||||
post :update,
|
||||
params: { user: { recent_email_notification: false } },
|
||||
format: :json
|
||||
expect(user.reload.system_message_notification_email).to eq(false)
|
||||
expect(user.reload.recent_email_notification).to eql('false')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_recent_notification_email' do
|
||||
it 'responds successfully' do
|
||||
post :change_recent_notification_email,
|
||||
params: { status: false },
|
||||
format: :json
|
||||
it 'responds successfully if recent_notification provided' do
|
||||
post :update, params: { user: { recent_notification: false } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes notification from false => true' do
|
||||
user = User.first
|
||||
user.recent_notification_email = false
|
||||
user.save
|
||||
|
||||
post :change_recent_notification_email,
|
||||
params: { status: true },
|
||||
format: :json
|
||||
expect(user.reload.recent_notification_email).to eq(true)
|
||||
end
|
||||
|
||||
it 'changes notification from true => false' do
|
||||
user = User.first
|
||||
user.recent_notification_email = true
|
||||
user.save
|
||||
|
||||
post :change_recent_notification_email,
|
||||
params: { status: false },
|
||||
format: :json
|
||||
expect(user.reload.recent_notification_email).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_recent_notification' do
|
||||
it 'responds successfully' do
|
||||
post :change_recent_notification, params: { status: false }, format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes notification from false => true' do
|
||||
it 'changes recent_notification from false => true' do
|
||||
user = User.first
|
||||
user.recent_notification = false
|
||||
user.save
|
||||
|
||||
post :change_recent_notification, params: { status: true }, format: :json
|
||||
expect(user.reload.recent_notification).to eq(true)
|
||||
post :update, params: { user: { recent_notification: true } },
|
||||
format: :json
|
||||
expect(user.reload.recent_notification).to eql('true')
|
||||
end
|
||||
|
||||
it 'changes notification from true => false' do
|
||||
it 'changes recent_notification from true => false' do
|
||||
user = User.first
|
||||
user.recent_notification = true
|
||||
user.save
|
||||
|
||||
post :change_recent_notification, params: { status: false }, format: :json
|
||||
expect(user.reload.recent_notification).to eq(false)
|
||||
post :update, params: { user: { recent_notification: false } },
|
||||
format: :json
|
||||
expect(user.reload.recent_notification).to eq('false')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_assignements_notification_email' do
|
||||
it 'responds successfully' do
|
||||
post :change_assignements_notification_email,
|
||||
params: { status: false },
|
||||
it 'responds successfully if assignments_email_notification provided' do
|
||||
post :update,
|
||||
params: { user: { assignments_email_notification: false } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes notification from false => true' do
|
||||
it 'changes assignments_email_notification from false => true' do
|
||||
user = User.first
|
||||
user.assignments_notification_email = false
|
||||
user.assignments_email_notification = false
|
||||
user.save
|
||||
|
||||
post :change_assignements_notification_email,
|
||||
params: { status: true },
|
||||
post :update,
|
||||
params: { user: { assignments_email_notification: true } },
|
||||
format: :json
|
||||
expect(user.reload.assignments_notification_email).to eq(true)
|
||||
expect(user.reload.assignments_email_notification).to eq('true')
|
||||
end
|
||||
|
||||
it 'changes notification from true => false' do
|
||||
it 'changes assignments_email_notification from true => false' do
|
||||
user = User.first
|
||||
user.assignments_notification_email = true
|
||||
user.assignments_email_notification = true
|
||||
user.save
|
||||
|
||||
post :change_assignements_notification_email,
|
||||
params: { status: false },
|
||||
post :update,
|
||||
params: { user: { assignments_email_notification: false } },
|
||||
format: :json
|
||||
expect(user.reload.assignments_notification_email).to eq(false)
|
||||
expect(user.reload.assignments_email_notification).to eq('false')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST change_assignements_notification' do
|
||||
it 'responds successfully' do
|
||||
post :change_assignements_notification,
|
||||
params: { status: false },
|
||||
it 'responds successfully if assignments_notification provided' do
|
||||
post :update,
|
||||
params: { user: { assignments_notification: false } },
|
||||
format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'changes notification from false => true' do
|
||||
it 'changes assignments_notification from false => true' do
|
||||
user = User.first
|
||||
user.assignments_notification = false
|
||||
user.save
|
||||
|
||||
post :change_assignements_notification,
|
||||
params: { status: true },
|
||||
post :update,
|
||||
params: { user: { assignments_notification: true } },
|
||||
format: :json
|
||||
expect(user.reload.assignments_notification).to eq(true)
|
||||
expect(user.reload.assignments_notification).to eq('true')
|
||||
end
|
||||
|
||||
it 'changes notification from true => false' do
|
||||
it 'changes assignments_notification from true => false' do
|
||||
user = User.first
|
||||
user.assignments_notification = true
|
||||
user.save
|
||||
|
||||
post :change_assignements_notification,
|
||||
params: { status: false },
|
||||
post :update,
|
||||
params: { user: { assignments_notification: false } },
|
||||
format: :json
|
||||
expect(user.reload.assignments_notification).to eq(false)
|
||||
expect(user.reload.assignments_notification).to eq('false')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,7 +110,6 @@ describe User, type: :model do
|
|||
it { should validate_presence_of :full_name }
|
||||
it { should validate_presence_of :initials }
|
||||
it { should validate_presence_of :email }
|
||||
it { should validate_presence_of :settings }
|
||||
|
||||
it do
|
||||
should validate_length_of(:full_name).is_at_most(
|
||||
|
@ -189,4 +188,13 @@ describe User, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'user settings' do
|
||||
it { is_expected.to respond_to(:time_zone) }
|
||||
it { is_expected.to respond_to(:assignments_notification) }
|
||||
it { is_expected.to respond_to(:assignments_email_notification) }
|
||||
it { is_expected.to respond_to(:recent_notification) }
|
||||
it { is_expected.to respond_to(:recent_email_notification) }
|
||||
it { is_expected.to respond_to(:system_message_email_notification) }
|
||||
end
|
||||
end
|
||||
|
|
80
spec/services/client_api/user_service_spec.rb
Normal file
80
spec/services/client_api/user_service_spec.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
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
|
Loading…
Reference in a new issue