From edf44439958304b3ef66e1f6b68f7895422745ca Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 2 Oct 2017 14:51:54 +0200 Subject: [PATCH 1/8] added notification alert [fixes SCI-1566] --- .../client_api/notifications_controller.rb | 27 ++++--- .../components/NotificationsDropdown.jsx | 78 +++++++++++++++---- app/javascript/src/services/api/config.js | 8 ++ app/javascript/src/services/api/endpoints.js | 4 + .../src/services/api/notifications_api.js | 15 ++++ config/routes.rb | 2 + package.json | 2 +- .../notifications_controller_spec.rb | 27 +++++++ spec/factories/notifications.rb | 8 ++ spec/factories/user_notification.rb | 5 ++ spec/models/user_notification_spec.rb | 35 +++++++++ 11 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 app/javascript/src/services/api/config.js create mode 100644 app/javascript/src/services/api/endpoints.js create mode 100644 app/javascript/src/services/api/notifications_api.js create mode 100644 spec/controllers/client_api/notifications_controller_spec.rb create mode 100644 spec/factories/notifications.rb create mode 100644 spec/factories/user_notification.rb diff --git a/app/controllers/client_api/notifications_controller.rb b/app/controllers/client_api/notifications_controller.rb index 3985813ed..4a520ba31 100644 --- a/app/controllers/client_api/notifications_controller.rb +++ b/app/controllers/client_api/notifications_controller.rb @@ -1,23 +1,28 @@ module ClientApi class NotificationsController < ApplicationController - before_action :last_notifications, only: :recent_notifications - def recent_notifications respond_to do |format| format.json do render template: '/client_api/notifications/index', status: :ok, - locals: { notifications: @recent_notifications } + locals: { + notifications: + UserNotification.recent_notifications(current_user) + } + end + end + # clean the unseen notifications + UserNotification.seen_by_user(current_user) + end + + def unreaded_notifications_number + respond_to do |format| + format.json do + render json: { + count: UserNotification.unseen_notification_count(current_user) + }, status: :ok end end end - - private - - def last_notifications - @recent_notifications = - UserNotification.recent_notifications(current_user) - UserNotification.seen_by_user(current_user) - end end end diff --git a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx index 334b8eca8..07671ee90 100644 --- a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx +++ b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx @@ -1,10 +1,12 @@ import React, { Component } from "react"; import { NavDropdown } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import axios from "axios"; import styled from "styled-components"; -import { RECENT_NOTIFICATIONS_PATH } from "../../../config/routes"; +import { + getRecentNotifications, + getUnreadedNotificationsNumber +} from "../../../services/api/notifications_api"; import { MAIN_COLOR_BLUE, WILD_SAND_COLOR, @@ -21,7 +23,9 @@ const StyledListHeader = styled(CustomNavItem)` font-weight: bold; padding: 8px; - & a, a:hover, a:active { + & a, + a:hover, + a:active { color: ${WILD_SAND_COLOR}; } `; @@ -44,40 +48,81 @@ const StyledNavDropdown = styled(NavDropdown)` } `; +const StyledSpan = styled.span` + background-color: #37a0d9; + border-radius: 5px; + color: #f5f5f5; + font-size: 11px; + font-weight: bold; + margin-left: 12px; + padding: 1px 6px; + right: 19px; + top: 3px; + position: relative; +`; + class NotificationsDropdown extends Component { constructor(props) { super(props); - this.state = { notifications: [] }; + this.state = { + notifications: [], + notificationsCount: 0 + }; this.getRecentNotifications = this.getRecentNotifications.bind(this); this.renderNotifications = this.renderNotifications.bind(this); + this.renderNotificationStatus = this.renderNotificationStatus.bind(this); + this.loadStatus = this.loadStatus.bind(this); + } + + componentWillMount() { + this.loadStatus(); + } + + componentDidMount() { + const minutes = 60 * 1000; + setInterval(this.loadStatus, minutes); } getRecentNotifications(e) { e.preventDefault(); - axios - .get(RECENT_NOTIFICATIONS_PATH, { withCredentials: true }) - .then(({ data }) => { - this.setState({ notifications: data }); - }) + getRecentNotifications() + .then(response => + this.setState({ notifications: response, notificationsCount: 0 }) + ) .catch(error => { console.log("get Notifications Error: ", error); // TODO change this }); } + loadStatus() { + getUnreadedNotificationsNumber().then(response => { + this.setState({ notificationsCount: parseInt(response.count, 10) }); + }); + } + renderNotifications() { - const list = this.state.notifications.map(notification => + const list = this.state.notifications.map(notification => ( - ); + )); const items = - this.state.notifications.length > 0 - ? list - : - - ; + this.state.notifications.length > 0 ? ( + list + ) : ( + + + + ); return items; } + renderNotificationStatus() { + if (this.state.notificationsCount > 0) { + return {this.state.notificationsCount}; + } + return ; + } + render() { return ( + {this.renderNotificationStatus()} } onClick={this.getRecentNotifications} diff --git a/app/javascript/src/services/api/config.js b/app/javascript/src/services/api/config.js new file mode 100644 index 000000000..86f47cb97 --- /dev/null +++ b/app/javascript/src/services/api/config.js @@ -0,0 +1,8 @@ +import axios from "axios"; + +export const axiosInstance = axios.create({ + withCredentials: true, + headers: { + "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content + } +}); diff --git a/app/javascript/src/services/api/endpoints.js b/app/javascript/src/services/api/endpoints.js new file mode 100644 index 000000000..547e196f3 --- /dev/null +++ b/app/javascript/src/services/api/endpoints.js @@ -0,0 +1,4 @@ +// notifications +export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications"; +export const UNREADED_NOTIFICATIONS_PATH = + "/client_api/unreaded_notifications_number"; diff --git a/app/javascript/src/services/api/notifications_api.js b/app/javascript/src/services/api/notifications_api.js new file mode 100644 index 000000000..0bad62634 --- /dev/null +++ b/app/javascript/src/services/api/notifications_api.js @@ -0,0 +1,15 @@ +import { axiosInstance } from "./config"; +import { + RECENT_NOTIFICATIONS_PATH, + UNREADED_NOTIFICATIONS_PATH +} from "./endpoints"; + +export const getRecentNotifications = () => { + return axiosInstance.get(RECENT_NOTIFICATIONS_PATH).then(({ data }) => data); +}; + +export const getUnreadedNotificationsNumber = () => { + return axiosInstance + .get(UNREADED_NOTIFICATIONS_PATH) + .then(({ data }) => data); +}; diff --git a/config/routes.rb b/config/routes.rb index 81f0d7304..6fbf50603 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,8 @@ Rails.application.routes.draw do end # notifications get '/recent_notifications', to: 'notifications#recent_notifications' + get '/unreaded_notifications_number', + to: 'notifications#unreaded_notifications_number' # users get '/current_user_info', to: 'users/users#current_user_info' diff --git a/package.json b/package.json index dd89a39d0..6a3085177 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "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-dom": "^15.6.1", "react-intl": "^2.3.0", "react-intl-redux": "^0.6.0", @@ -77,6 +76,7 @@ "react-router-bootstrap": "^0.24.2", "react-router-dom": "^4.1.2", "react-router-prop-types": "^0.0.1", + "react-tagsinput": "^3.17.0", "react-timezone": "^0.2.0", "redux": "^3.7.2", "redux-thunk": "^2.2.0", diff --git a/spec/controllers/client_api/notifications_controller_spec.rb b/spec/controllers/client_api/notifications_controller_spec.rb new file mode 100644 index 000000000..80c98d8a7 --- /dev/null +++ b/spec/controllers/client_api/notifications_controller_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe ClientApi::NotificationsController, type: :controller do + login_user + let(:notification) { create :notification } + let(:user_notification) do + create :user_notification, + user: User.first, + notification: notification + end + + describe '#recent_notifications' do + it 'returns a list of notifications' do + get :recent_notifications, format: :json + expect(response).to be_success + expect(response).to render_template('client_api/notifications/index') + end + end + + describe '#unreaded_notifications_number' do + it 'returns a number of unreaded notifications' do + get :unreaded_notifications_number, format: :json + expect(response).to be_success + expect(response.body).to include('count') + end + end +end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb new file mode 100644 index 000000000..f92723cf6 --- /dev/null +++ b/spec/factories/notifications.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :notification do + title 'Admin was added as Owner to project ' \ + 'Demo project - qPCR by User.' + message 'Project: Demo project - qPCR' + type_of 'assignment' + end +end diff --git a/spec/factories/user_notification.rb b/spec/factories/user_notification.rb new file mode 100644 index 000000000..9341a28f2 --- /dev/null +++ b/spec/factories/user_notification.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :user_notification do + checked false + end +end diff --git a/spec/models/user_notification_spec.rb b/spec/models/user_notification_spec.rb index 70eb9445f..a7ba560f2 100644 --- a/spec/models/user_notification_spec.rb +++ b/spec/models/user_notification_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe UserNotification, type: :model do + let(:user) { create :user } + it 'should be of class UserNotification' do expect(subject.class).to eq UserNotification end @@ -17,4 +19,37 @@ describe UserNotification, type: :model do it { should belong_to :user } it { should belong_to :notification } end + + describe '#unseen_notification_count ' do + let(:notifcation) { create :notification } + it 'returns a number of unseen notifications' do + create :user_notification, user: user, notification: notifcation + expect(UserNotification.unseen_notification_count(user)).to eq 1 + end + end + + describe '#recent_notifications' do + let(:notifcation_one) { create :notification } + let(:notifcation_two) { create :notification } + + it 'returns a list of notifications ordered by created_at DESC' do + create :user_notification, user: user, notification: notifcation_one + create :user_notification, user: user, notification: notifcation_two + notifications = UserNotification.recent_notifications(user) + expect(notifications).to eq [notifcation_two, notifcation_one] + end + end + + describe '#seen_by_user' do + let!(:notification) { create :notification } + let!(:user_notification_one) do + create :user_notification, user: user, notification: notification + end + + it 'set the check status to false' do + expect { + UserNotification.seen_by_user(user) + }.to change { user_notification_one.reload.checked }.from(false).to(true) + end + end end From 182e3dcc37a5ba91941f638ee618dba5d98dcabf Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 2 Oct 2017 14:56:49 +0200 Subject: [PATCH 2/8] added colors constants --- .../Navigation/components/NotificationsDropdown.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx index 07671ee90..ec2147fc8 100644 --- a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx +++ b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx @@ -49,9 +49,9 @@ const StyledNavDropdown = styled(NavDropdown)` `; const StyledSpan = styled.span` - background-color: #37a0d9; + background-color: ${MAIN_COLOR_BLUE}; border-radius: 5px; - color: #f5f5f5; + color: ${WILD_SAND_COLOR}; font-size: 11px; font-weight: bold; margin-left: 12px; From 14bae8def56f50d21653811d8c70f4a785894061 Mon Sep 17 00:00:00 2001 From: zmagod Date: Wed, 4 Oct 2017 14:42:38 +0200 Subject: [PATCH 3/8] trigger redirect to root page on team switch [fixes SCI-1641] --- .../Navigation/components/TeamSwitch.jsx | 14 +++++++++----- package.json | 4 ++-- yarn.lock | 6 +++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/javascript/src/components/Navigation/components/TeamSwitch.jsx b/app/javascript/src/components/Navigation/components/TeamSwitch.jsx index 1e8f4eed0..7ee9259ee 100644 --- a/app/javascript/src/components/Navigation/components/TeamSwitch.jsx +++ b/app/javascript/src/components/Navigation/components/TeamSwitch.jsx @@ -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 => - this.changeTeam(team.id)} key={team.id}> - {team.name} - - ); + return this.props.all_teams + .filter(team => !team.current_team) + .map(team => ( + this.changeTeam(team.id)} key={team.id}> + {team.name} + + )); } } diff --git a/package.json b/package.json index 651b0ceb2..d8c878b50 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,6 @@ "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-intl": "^2.3.0", "react-intl-redux": "^0.6.0", @@ -78,7 +76,9 @@ "react-router-bootstrap": "^0.24.2", "react-router-dom": "^4.1.2", "react-router-prop-types": "^0.0.1", + "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", diff --git a/yarn.lock b/yarn.lock index 062e042d8..f01fa9d74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4829,9 +4829,9 @@ react-timezone@^0.2.0: dependencies: classnames "^2.2.1" -react-transition-group@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.0.tgz#793bf8cb15bfe91b3101b24bce1c1d2891659575" +react-transition-group@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" dependencies: chain-function "^1.0.0" classnames "^2.2.5" From 524e8bffbfaf125bbdc8632f49dc8a290a70eaba Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 5 Oct 2017 10:07:50 +0200 Subject: [PATCH 4/8] fixes sign out action on settings page, clean the redux store, handles redirect to sign in page if session is terminated [fixes SCI-1567] --- .babelrc | 1 + .../client_api/users/users_controller.rb | 10 +++++ .../components/UserAccountDropdown.jsx | 45 ++++++++++--------- .../src/components/actions/UsersActions.js | 5 +++ app/javascript/src/config/action_types.js | 3 +- app/javascript/src/config/axios.js | 12 +++++ app/javascript/src/config/reducers.js | 14 +++++- app/javascript/src/config/routes.js | 4 +- app/javascript/src/services/api/config.js | 21 +++++++++ app/javascript/src/services/api/endpoints.js | 1 + app/javascript/src/services/api/users_api.js | 6 +++ config/routes.rb | 1 + package.json | 5 ++- 13 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 app/javascript/src/services/api/config.js create mode 100644 app/javascript/src/services/api/endpoints.js create mode 100644 app/javascript/src/services/api/users_api.js diff --git a/.babelrc b/.babelrc index 98c364a94..3c71c69e3 100644 --- a/.babelrc +++ b/.babelrc @@ -17,6 +17,7 @@ "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", + "transform-react-jsx-source", [ "transform-class-properties", { diff --git a/app/controllers/client_api/users/users_controller.rb b/app/controllers/client_api/users/users_controller.rb index 3aa443cf1..e9d03c5fb 100644 --- a/app/controllers/client_api/users/users_controller.rb +++ b/app/controllers/client_api/users/users_controller.rb @@ -2,6 +2,16 @@ 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 respond_to do |format| format.json do diff --git a/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx b/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx index caa239fb8..932df3c6b 100644 --- a/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx +++ b/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx @@ -1,22 +1,33 @@ 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 { getCurrentUser, destroyState } from "../../actions/UsersActions"; +import { signOutUser } 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; + } `; class UserAccountDropdown extends Component { componentDidMount() { this.props.getCurrentUser(); + 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() { @@ -43,7 +54,7 @@ class UserAccountDropdown extends Component { - + @@ -52,24 +63,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 + getCurrentUser: func.isRequired, + destroyState: func.isRequired, + current_user: shape({ + id: number.isRequired, + fullName: string.isRequired, + avatarPath: 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, getCurrentUser })( UserAccountDropdown ); diff --git a/app/javascript/src/components/actions/UsersActions.js b/app/javascript/src/components/actions/UsersActions.js index c6a25deba..0cc03c7fc 100644 --- a/app/javascript/src/components/actions/UsersActions.js +++ b/app/javascript/src/components/actions/UsersActions.js @@ -15,6 +15,7 @@ import { } from "../../config/api_endpoints"; import { + USER_LOGOUT, SET_CURRENT_USER, CHANGE_CURRENT_USER_FULL_NAME, CHANGE_CURRENT_USER_INITIALS, @@ -29,6 +30,10 @@ import { CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL } from "../../config/action_types"; +export function destroyState() { + return { type: USER_LOGOUT }; +} + function addCurrentUser(data) { return { type: SET_CURRENT_USER, diff --git a/app/javascript/src/config/action_types.js b/app/javascript/src/config/action_types.js index 732a81db4..1108bf936 100644 --- a/app/javascript/src/config/action_types.js +++ b/app/javascript/src/config/action_types.js @@ -8,6 +8,7 @@ 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"; @@ -39,4 +40,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"; \ No newline at end of file +export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS"; diff --git a/app/javascript/src/config/axios.js b/app/javascript/src/config/axios.js index 7cf8426c7..b514b7001 100644 --- a/app/javascript/src/config/axios.js +++ b/app/javascript/src/config/axios.js @@ -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; } }); diff --git a/app/javascript/src/config/reducers.js b/app/javascript/src/config/reducers.js index 5561d4cb3..ead7707fd 100644 --- a/app/javascript/src/config/reducers.js +++ b/app/javascript/src/config/reducers.js @@ -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; diff --git a/app/javascript/src/config/routes.js b/app/javascript/src/config/routes.js index e25f55853..74abccff0 100644 --- a/app/javascript/src/config/routes.js +++ b/app/javascript/src/config/routes.js @@ -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"; \ No newline at end of file +export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences"; diff --git a/app/javascript/src/services/api/config.js b/app/javascript/src/services/api/config.js new file mode 100644 index 000000000..942739121 --- /dev/null +++ b/app/javascript/src/services/api/config.js @@ -0,0 +1,21 @@ +import axios from "axios"; +// import { dispatch } from "redux"; +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; + } +}); diff --git a/app/javascript/src/services/api/endpoints.js b/app/javascript/src/services/api/endpoints.js new file mode 100644 index 000000000..c53043eec --- /dev/null +++ b/app/javascript/src/services/api/endpoints.js @@ -0,0 +1 @@ +export const SIGN_OUT_PATH = "/client_api/users/sign_out_user" diff --git a/app/javascript/src/services/api/users_api.js b/app/javascript/src/services/api/users_api.js new file mode 100644 index 000000000..87e543e29 --- /dev/null +++ b/app/javascript/src/services/api/users_api.js @@ -0,0 +1,6 @@ +import { axiosInstance } from "./config"; +import { SIGN_OUT_PATH } from "./endpoints"; + +export function signOutUser() { + return axiosInstance.get(SIGN_OUT_PATH); +} diff --git a/config/routes.rb b/config/routes.rb index 81f0d7304..eabfa0808 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,7 @@ 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' diff --git a/package.json b/package.json index 651b0ceb2..78bf30f87 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "babel-eslint": "^7.2.3", + "babel-plugin-transform-react-jsx-source": "^6.22.0", "eslint": "^3.7.1", "eslint-config-airbnb": "^15.1.0", "eslint-config-google": "^0.5.0", @@ -68,8 +69,6 @@ "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-intl": "^2.3.0", "react-intl-redux": "^0.6.0", @@ -78,7 +77,9 @@ "react-router-bootstrap": "^0.24.2", "react-router-dom": "^4.1.2", "react-router-prop-types": "^0.0.1", + "react-tagsinput": "^3.17.0", "react-timezone": "^0.2.0", + "react-transition-group": "^2.2.0", "redux": "^3.7.2", "redux-thunk": "^2.2.0", "resolve-url-loader": "^2.1.0", From 1b03222260bd93fb41f7a48ff3b01cf906a73dc7 Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 5 Oct 2017 10:24:28 +0200 Subject: [PATCH 5/8] adds tests for sign_out_user action on users controller --- .../client_api/users/users_controller_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/controllers/client_api/users/users_controller_spec.rb b/spec/controllers/client_api/users/users_controller_spec.rb index c23d17658..5a41ee6ce 100644 --- a/spec/controllers/client_api/users/users_controller_spec.rb +++ b/spec/controllers/client_api/users/users_controller_spec.rb @@ -7,6 +7,20 @@ describe ClientApi::Users::UsersController, type: :controller do @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 From 5a31df7204a6ca44ddfbfb5c59562c63ab9aa177 Mon Sep 17 00:00:00 2001 From: zmagod Date: Fri, 6 Oct 2017 10:45:22 +0200 Subject: [PATCH 6/8] adds @Ducz0r suggestions --- .../client_api/notifications_controller.rb | 2 +- .../Navigation/components/NotificationsDropdown.jsx | 12 ++++++++---- app/javascript/src/services/api/endpoints.js | 2 +- app/javascript/src/services/api/notifications_api.js | 2 +- config/routes.rb | 4 ++-- .../client_api/notifications_controller_spec.rb | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/controllers/client_api/notifications_controller.rb b/app/controllers/client_api/notifications_controller.rb index 4a520ba31..07bfd7230 100644 --- a/app/controllers/client_api/notifications_controller.rb +++ b/app/controllers/client_api/notifications_controller.rb @@ -15,7 +15,7 @@ module ClientApi UserNotification.seen_by_user(current_user) end - def unreaded_notifications_number + def unread_notifications_count respond_to do |format| format.json do render json: { diff --git a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx index ec2147fc8..d63e1f47c 100644 --- a/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx +++ b/app/javascript/src/components/Navigation/components/NotificationsDropdown.jsx @@ -1,17 +1,21 @@ import React, { Component } from "react"; +import { Link } from "react-router-dom"; import { NavDropdown } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import styled from "styled-components"; import { getRecentNotifications, - getUnreadedNotificationsNumber + getUnreadNotificationsCount } from "../../../services/api/notifications_api"; import { MAIN_COLOR_BLUE, WILD_SAND_COLOR, MYSTIC_COLOR } from "../../../config/constants/colors"; +import { + SETTINGS_ACCOUNT_PREFERENCES +} from "../../../config/routes" import NotificationItem from "./NotificationItem"; import Spinner from "../../Spinner"; @@ -95,7 +99,7 @@ class NotificationsDropdown extends Component { } loadStatus() { - getUnreadedNotificationsNumber().then(response => { + getUnreadNotificationsCount().then(response => { this.setState({ notificationsCount: parseInt(response.count, 10) }); }); } @@ -144,9 +148,9 @@ class NotificationsDropdown extends Component { - + - + {this.renderNotifications()} diff --git a/app/javascript/src/services/api/endpoints.js b/app/javascript/src/services/api/endpoints.js index 547e196f3..2fb105cec 100644 --- a/app/javascript/src/services/api/endpoints.js +++ b/app/javascript/src/services/api/endpoints.js @@ -1,4 +1,4 @@ // notifications export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications"; export const UNREADED_NOTIFICATIONS_PATH = - "/client_api/unreaded_notifications_number"; + "/client_api/unread_notifications_count"; diff --git a/app/javascript/src/services/api/notifications_api.js b/app/javascript/src/services/api/notifications_api.js index 0bad62634..a724a44f0 100644 --- a/app/javascript/src/services/api/notifications_api.js +++ b/app/javascript/src/services/api/notifications_api.js @@ -8,7 +8,7 @@ export const getRecentNotifications = () => { return axiosInstance.get(RECENT_NOTIFICATIONS_PATH).then(({ data }) => data); }; -export const getUnreadedNotificationsNumber = () => { +export const getUnreadNotificationsCount = () => { return axiosInstance .get(UNREADED_NOTIFICATIONS_PATH) .then(({ data }) => data); diff --git a/config/routes.rb b/config/routes.rb index 6fbf50603..6062c8012 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,8 +27,8 @@ Rails.application.routes.draw do end # notifications get '/recent_notifications', to: 'notifications#recent_notifications' - get '/unreaded_notifications_number', - to: 'notifications#unreaded_notifications_number' + get '/unread_notifications_count', + to: 'notifications#unread_notifications_count' # users get '/current_user_info', to: 'users/users#current_user_info' diff --git a/spec/controllers/client_api/notifications_controller_spec.rb b/spec/controllers/client_api/notifications_controller_spec.rb index 80c98d8a7..3801f1d3f 100644 --- a/spec/controllers/client_api/notifications_controller_spec.rb +++ b/spec/controllers/client_api/notifications_controller_spec.rb @@ -19,7 +19,7 @@ describe ClientApi::NotificationsController, type: :controller do describe '#unreaded_notifications_number' do it 'returns a number of unreaded notifications' do - get :unreaded_notifications_number, format: :json + get :unread_notifications_count, format: :json expect(response).to be_success expect(response.body).to include('count') end From 0a8ef8cd9dbe6afe0e68410360678ccd1280d4ed Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 9 Oct 2017 09:23:25 +0200 Subject: [PATCH 7/8] removed unneeded import --- app/javascript/src/components/actions/UsersActions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/src/components/actions/UsersActions.js b/app/javascript/src/components/actions/UsersActions.js index e927961a6..7bb848ec3 100644 --- a/app/javascript/src/components/actions/UsersActions.js +++ b/app/javascript/src/components/actions/UsersActions.js @@ -1,5 +1,4 @@ import { USER_LOGOUT, SET_CURRENT_USER } from "../../config/action_types"; -import { getCurrentUser } from "../../services/api/users_api"; export function destroyState() { return { type: USER_LOGOUT }; From f370d170df717fa7168f547b6a2012c993c16743 Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 9 Oct 2017 09:27:25 +0200 Subject: [PATCH 8/8] fix package.json --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index b71c0a2d6..85531bb67 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,8 @@ "react-bootstrap-table": "^4.0.0", "react-bootstrap-timezone-picker": "^1.0.11", "react-data-grid": "^2.0.2", -<<<<<<< HEAD -======= "react-tagsinput": "^3.17.0", "react-transition-group": "^2.2.0", ->>>>>>> fcea55c2a102470bd4520ea0ac1ef771710c17d8 "react-dom": "^15.6.1", "react-intl": "^2.3.0", "react-intl-redux": "^0.6.0",