Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1566

This commit is contained in:
zmagod 2017-10-09 09:30:06 +02:00
commit d85233cb2c
56 changed files with 2297 additions and 2226 deletions

View file

@ -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",
{

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -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" />&nbsp;

View file

@ -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>
));
}
}

View file

@ -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 }}
/>&nbsp;
<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
);

View file

@ -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));
};
}

View file

@ -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;
}

View file

@ -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";

View file

@ -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/";

View file

@ -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;
}
});

View file

@ -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;

View 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,}))$/;

View file

@ -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"

View file

@ -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;

View file

@ -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";

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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>;

View file

@ -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>;

View file

@ -1,3 +0,0 @@
export const ASSIGNMENT_NOTIFICATION = "ASSIGNMENT";
export const RECENT_NOTIFICATION = "RECENT_NOTIFICATION";
export const SYSTEM_NOTIFICATION = "SYSTEM_NOTIFICATION";

View file

@ -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;

View file

@ -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;

View file

@ -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
);

View file

@ -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
);

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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" />&nbsp;
<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>&nbsp;
<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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
});

View file

@ -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";

View 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);

View file

@ -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?

View 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

View file

@ -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']

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View 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

749
yarn.lock

File diff suppressed because it is too large Load diff