mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-07 05:34:55 +08:00
Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1628
This commit is contained in:
commit
943339345a
23 changed files with 286 additions and 52 deletions
1
.babelrc
1
.babelrc
|
@ -18,6 +18,7 @@
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"transform-object-rest-spread",
|
"transform-object-rest-spread",
|
||||||
"syntax-dynamic-import",
|
"syntax-dynamic-import",
|
||||||
|
"transform-react-jsx-source",
|
||||||
[
|
[
|
||||||
"transform-class-properties",
|
"transform-class-properties",
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,23 +1,28 @@
|
||||||
module ClientApi
|
module ClientApi
|
||||||
class NotificationsController < ApplicationController
|
class NotificationsController < ApplicationController
|
||||||
before_action :last_notifications, only: :recent_notifications
|
|
||||||
|
|
||||||
def recent_notifications
|
def recent_notifications
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
render template: '/client_api/notifications/index',
|
render template: '/client_api/notifications/index',
|
||||||
status: :ok,
|
status: :ok,
|
||||||
locals: { notifications: @recent_notifications }
|
locals: {
|
||||||
end
|
notifications:
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def last_notifications
|
|
||||||
@recent_notifications =
|
|
||||||
UserNotification.recent_notifications(current_user)
|
UserNotification.recent_notifications(current_user)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# clean the unseen notifications
|
||||||
UserNotification.seen_by_user(current_user)
|
UserNotification.seen_by_user(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_notifications_count
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
render json: {
|
||||||
|
count: UserNotification.unseen_notification_count(current_user)
|
||||||
|
}, status: :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,16 @@ module ClientApi
|
||||||
module Users
|
module Users
|
||||||
class UsersController < ApplicationController
|
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
|
def preferences_info
|
||||||
settings = current_user.settings
|
settings = current_user.settings
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { NavDropdown } from "react-bootstrap";
|
import { NavDropdown } from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import axios from "axios";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { RECENT_NOTIFICATIONS_PATH } from "../../../config/routes";
|
import {
|
||||||
|
getRecentNotifications,
|
||||||
|
getUnreadNotificationsCount
|
||||||
|
} from "../../../services/api/notifications_api";
|
||||||
import {
|
import {
|
||||||
MAIN_COLOR_BLUE,
|
MAIN_COLOR_BLUE,
|
||||||
WILD_SAND_COLOR,
|
WILD_SAND_COLOR,
|
||||||
MYSTIC_COLOR
|
MYSTIC_COLOR
|
||||||
} from "../../../config/constants/colors";
|
} from "../../../config/constants/colors";
|
||||||
|
import {
|
||||||
|
SETTINGS_ACCOUNT_PREFERENCES
|
||||||
|
} from "../../../config/routes"
|
||||||
|
|
||||||
import NotificationItem from "./NotificationItem";
|
import NotificationItem from "./NotificationItem";
|
||||||
import Spinner from "../../Spinner";
|
import Spinner from "../../Spinner";
|
||||||
|
@ -21,7 +27,9 @@ const StyledListHeader = styled(CustomNavItem)`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
& a, a:hover, a:active {
|
& a,
|
||||||
|
a:hover,
|
||||||
|
a:active {
|
||||||
color: ${WILD_SAND_COLOR};
|
color: ${WILD_SAND_COLOR};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -44,40 +52,81 @@ const StyledNavDropdown = styled(NavDropdown)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSpan = styled.span`
|
||||||
|
background-color: ${MAIN_COLOR_BLUE};
|
||||||
|
border-radius: 5px;
|
||||||
|
color: ${WILD_SAND_COLOR};
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 12px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
right: 19px;
|
||||||
|
top: 3px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
class NotificationsDropdown extends Component {
|
class NotificationsDropdown extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { notifications: [] };
|
this.state = {
|
||||||
|
notifications: [],
|
||||||
|
notificationsCount: 0
|
||||||
|
};
|
||||||
this.getRecentNotifications = this.getRecentNotifications.bind(this);
|
this.getRecentNotifications = this.getRecentNotifications.bind(this);
|
||||||
this.renderNotifications = this.renderNotifications.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) {
|
getRecentNotifications(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
getRecentNotifications()
|
||||||
.get(RECENT_NOTIFICATIONS_PATH, { withCredentials: true })
|
.then(response =>
|
||||||
.then(({ data }) => {
|
this.setState({ notifications: response, notificationsCount: 0 })
|
||||||
this.setState({ notifications: data });
|
)
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("get Notifications Error: ", error); // TODO change this
|
console.log("get Notifications Error: ", error); // TODO change this
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadStatus() {
|
||||||
|
getUnreadNotificationsCount().then(response => {
|
||||||
|
this.setState({ notificationsCount: parseInt(response.count, 10) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderNotifications() {
|
renderNotifications() {
|
||||||
const list = this.state.notifications.map(notification =>
|
const list = this.state.notifications.map(notification => (
|
||||||
<NotificationItem key={notification.id} notification={notification} />
|
<NotificationItem key={notification.id} notification={notification} />
|
||||||
);
|
));
|
||||||
|
|
||||||
const items =
|
const items =
|
||||||
this.state.notifications.length > 0
|
this.state.notifications.length > 0 ? (
|
||||||
? list
|
list
|
||||||
: <CustomNavItem>
|
) : (
|
||||||
|
<CustomNavItem>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</CustomNavItem>;
|
</CustomNavItem>
|
||||||
|
);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNotificationStatus() {
|
||||||
|
if (this.state.notificationsCount > 0) {
|
||||||
|
return <StyledSpan>{this.state.notificationsCount}</StyledSpan>;
|
||||||
|
}
|
||||||
|
return <span />;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StyledNavDropdown
|
<StyledNavDropdown
|
||||||
|
@ -89,6 +138,7 @@ class NotificationsDropdown extends Component {
|
||||||
<span className="visible-xs-inline visible-sm-inline">
|
<span className="visible-xs-inline visible-sm-inline">
|
||||||
<FormattedMessage id="navbar.notifications_label" />
|
<FormattedMessage id="navbar.notifications_label" />
|
||||||
</span>
|
</span>
|
||||||
|
{this.renderNotificationStatus()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
onClick={this.getRecentNotifications}
|
onClick={this.getRecentNotifications}
|
||||||
|
@ -98,9 +148,9 @@ class NotificationsDropdown extends Component {
|
||||||
<FormattedMessage id="notifications.dropdown_title" />
|
<FormattedMessage id="notifications.dropdown_title" />
|
||||||
</span>
|
</span>
|
||||||
<span className="pull-right">
|
<span className="pull-right">
|
||||||
<a href="/users/settings/account/preferences">
|
<Link to={SETTINGS_ACCOUNT_PREFERENCES}>
|
||||||
<FormattedMessage id="notifications.dropdown_settings_link" />
|
<FormattedMessage id="notifications.dropdown_settings_link" />
|
||||||
</a>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</StyledListHeader>
|
</StyledListHeader>
|
||||||
{this.renderNotifications()}
|
{this.renderNotifications()}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { NavDropdown, MenuItem, Glyphicon } from "react-bootstrap";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import { ROOT_PATH } from "../../../config/routes";
|
||||||
import { BORDER_GRAY_COLOR } from "../../../config/constants/colors";
|
import { BORDER_GRAY_COLOR } from "../../../config/constants/colors";
|
||||||
import { changeTeam } from "../../actions/TeamsActions";
|
import { changeTeam } from "../../actions/TeamsActions";
|
||||||
import { getTeamsList } from "../../actions/TeamsActions";
|
import { getTeamsList } from "../../actions/TeamsActions";
|
||||||
|
@ -27,15 +28,18 @@ class TeamSwitch extends Component {
|
||||||
|
|
||||||
changeTeam(teamId) {
|
changeTeam(teamId) {
|
||||||
this.props.changeTeam(teamId);
|
this.props.changeTeam(teamId);
|
||||||
|
window.location = ROOT_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
displayTeams() {
|
displayTeams() {
|
||||||
if (!_.isEmpty(this.props.all_teams)) {
|
if (!_.isEmpty(this.props.all_teams)) {
|
||||||
return this.props.all_teams.filter(team => !team.current_team).map(team =>
|
return this.props.all_teams
|
||||||
|
.filter(team => !team.current_team)
|
||||||
|
.map(team => (
|
||||||
<MenuItem onSelect={() => this.changeTeam(team.id)} key={team.id}>
|
<MenuItem onSelect={() => this.changeTeam(team.id)} key={team.id}>
|
||||||
{team.name}
|
{team.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { connect } from "react-redux";
|
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 { NavDropdown, MenuItem, Image } from "react-bootstrap";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { SIGN_IN_PATH } from "../../../config/routes";
|
||||||
|
|
||||||
import { getCurrentUser } from "../../../services/api/users_api";
|
import { addCurrentUser, destroyState } from "../../actions/UsersActions";
|
||||||
import { addCurrentUser } from "../../actions/UsersActions";
|
import { signOutUser, getCurrentUser } from "../../../services/api/users_api";
|
||||||
|
|
||||||
const StyledNavDropdown = styled(NavDropdown)`
|
const StyledNavDropdown = styled(NavDropdown)`
|
||||||
& #user-account-dropdown {
|
& #user-account-dropdown {
|
||||||
|
@ -25,6 +26,15 @@ class UserAccountDropdown extends Component {
|
||||||
getCurrentUser().then(data => {
|
getCurrentUser().then(data => {
|
||||||
this.props.addCurrentUser(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() {
|
render() {
|
||||||
|
@ -52,7 +62,7 @@ class UserAccountDropdown extends Component {
|
||||||
<FormattedMessage id="user_account_dropdown.settings" />
|
<FormattedMessage id="user_account_dropdown.settings" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem divider />
|
<MenuItem divider />
|
||||||
<MenuItem href="/users/sign_out">
|
<MenuItem onClick={this.signOut}>
|
||||||
<FormattedMessage id="user_account_dropdown.log_out" />
|
<FormattedMessage id="user_account_dropdown.log_out" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</StyledNavDropdown>
|
</StyledNavDropdown>
|
||||||
|
@ -61,17 +71,18 @@ class UserAccountDropdown extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserAccountDropdown.propTypes = {
|
UserAccountDropdown.propTypes = {
|
||||||
addCurrentUser: PropTypes.func.isRequired,
|
addCurrentUser: func.isRequired,
|
||||||
current_user: PropTypes.shape({
|
destroyState: func.isRequired,
|
||||||
id: PropTypes.number.isRequired,
|
current_user: shape({
|
||||||
fullName: PropTypes.string.isRequired,
|
id: number.isRequired,
|
||||||
avatarThumb: PropTypes.string.isRequired
|
fullName: string.isRequired,
|
||||||
|
avatarThumb: string.isRequired
|
||||||
}).isRequired
|
}).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map the states from store to component
|
// Map the states from store to component
|
||||||
const mapStateToProps = ({ current_user }) => ({ current_user });
|
const mapStateToProps = ({ current_user }) => ({ current_user });
|
||||||
|
|
||||||
export default connect(mapStateToProps, { addCurrentUser })(
|
export default connect(mapStateToProps, { destroyState, addCurrentUser })(
|
||||||
UserAccountDropdown
|
UserAccountDropdown
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import axios from "../../config/axios";
|
import { USER_LOGOUT, SET_CURRENT_USER } from "../../config/action_types";
|
||||||
|
|
||||||
import {
|
export function destroyState() {
|
||||||
SET_CURRENT_USER,
|
return { type: USER_LOGOUT };
|
||||||
} from "../../config/action_types";
|
}
|
||||||
|
|
||||||
export function addCurrentUser(data) {
|
export function addCurrentUser(data) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -4,6 +4,7 @@ export const GET_LIST_OF_TEAMS = "GET_LIST_OF_TEAMS";
|
||||||
export const SET_TEAM_DETAILS = "SET_TEAM_DETAILS";
|
export const SET_TEAM_DETAILS = "SET_TEAM_DETAILS";
|
||||||
|
|
||||||
// users
|
// users
|
||||||
|
export const USER_LOGOUT = "USER_LOGOUT";
|
||||||
export const SET_CURRENT_USER = "SET_CURRENT_USER";
|
export const SET_CURRENT_USER = "SET_CURRENT_USER";
|
||||||
|
|
||||||
// user teams
|
// user teams
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
|
// @TODO remove this file ASAP the preferences/profile refactoring is merged
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import store from "./store";
|
||||||
|
import { SIGN_IN_PATH } from "./routes";
|
||||||
|
import { destroyState } from "../components/actions/UsersActions";
|
||||||
|
|
||||||
export default axios.create({
|
export default axios.create({
|
||||||
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
validateStatus(status) {
|
||||||
|
if (status === 401) {
|
||||||
|
store.dispatch(destroyState);
|
||||||
|
window.location = SIGN_IN_PATH;
|
||||||
|
}
|
||||||
|
return status >= 200 && status < 300;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
|
import { USER_LOGOUT } from "./action_types";
|
||||||
import {
|
import {
|
||||||
setCurrentTeam,
|
setCurrentTeam,
|
||||||
getListOfTeams,
|
getListOfTeams,
|
||||||
showLeaveTeamModal,
|
showLeaveTeamModal
|
||||||
} from "../components/reducers/TeamReducers";
|
} from "../components/reducers/TeamReducers";
|
||||||
import { currentUser } from "../components/reducers/UsersReducer";
|
import { currentUser } from "../components/reducers/UsersReducer";
|
||||||
import { alerts } from "../components/reducers/AlertsReducers";
|
import { alerts } from "../components/reducers/AlertsReducers";
|
||||||
|
|
||||||
export default combineReducers({
|
const appReducer = combineReducers({
|
||||||
current_team: setCurrentTeam,
|
current_team: setCurrentTeam,
|
||||||
all_teams: getListOfTeams,
|
all_teams: getListOfTeams,
|
||||||
current_user: currentUser,
|
current_user: currentUser,
|
||||||
showLeaveTeamModal,
|
showLeaveTeamModal,
|
||||||
alerts
|
alerts
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rootReducer = (state, action) => {
|
||||||
|
if (action.type === USER_LOGOUT) {
|
||||||
|
state = undefined;
|
||||||
|
}
|
||||||
|
return appReducer(state, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const ROOT_PATH = "/";
|
export const ROOT_PATH = "/";
|
||||||
|
export const SIGN_IN_PATH = "/users/sign_in";
|
||||||
|
|
||||||
// Settings page
|
// Settings page
|
||||||
export const SETTINGS_TEAMS_ROUTE = "/settings/teams";
|
export const SETTINGS_TEAMS_ROUTE = "/settings/teams";
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
import axios from "axios";
|
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({
|
export const axiosInstance = axios.create({
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content
|
"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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
// notifications
|
||||||
|
export const RECENT_NOTIFICATIONS_PATH = "/client_api/recent_notifications";
|
||||||
|
export const UNREADED_NOTIFICATIONS_PATH =
|
||||||
|
"/client_api/unread_notifications_count";
|
||||||
|
|
||||||
// activities
|
// activities
|
||||||
export const ACTIVITIES_PATH = "/client_api/activities";
|
export const ACTIVITIES_PATH = "/client_api/activities";
|
||||||
|
|
||||||
|
@ -22,6 +27,7 @@ export const USER_PROFILE_INFO = "/client_api/users/profile_info";
|
||||||
export const UPDATE_USER_PATH = "/client_api/users/update";
|
export const UPDATE_USER_PATH = "/client_api/users/update";
|
||||||
export const PREFERENCES_INFO_PATH = "/client_api/users/preferences_info"
|
export const PREFERENCES_INFO_PATH = "/client_api/users/preferences_info"
|
||||||
export const STATISTICS_INFO_PATH = "/client_api/users/statistics_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
|
// info dropdown_title
|
||||||
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
export const CUSTOMER_SUPPORT_LINK = "http://scinote.net/support";
|
||||||
|
|
15
app/javascript/src/services/api/notifications_api.js
Normal file
15
app/javascript/src/services/api/notifications_api.js
Normal file
|
@ -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 getUnreadNotificationsCount = () => {
|
||||||
|
return axiosInstance
|
||||||
|
.get(UNREADED_NOTIFICATIONS_PATH)
|
||||||
|
.then(({ data }) => data);
|
||||||
|
};
|
|
@ -4,7 +4,8 @@ import {
|
||||||
UPDATE_USER_PATH,
|
UPDATE_USER_PATH,
|
||||||
CURRENT_USER_PATH,
|
CURRENT_USER_PATH,
|
||||||
PREFERENCES_INFO_PATH,
|
PREFERENCES_INFO_PATH,
|
||||||
STATISTICS_INFO_PATH
|
STATISTICS_INFO_PATH,
|
||||||
|
SIGN_OUT_PATH
|
||||||
} from "./endpoints";
|
} from "./endpoints";
|
||||||
|
|
||||||
export const getUserProfileInfo = () =>
|
export const getUserProfileInfo = () =>
|
||||||
|
@ -29,3 +30,5 @@ export const getCurrentUser = () =>
|
||||||
|
|
||||||
export const getStatisticsInfo = () =>
|
export const getStatisticsInfo = () =>
|
||||||
axiosInstance.get(STATISTICS_INFO_PATH).then(({ data }) => data.user);
|
axiosInstance.get(STATISTICS_INFO_PATH).then(({ data }) => data.user);
|
||||||
|
|
||||||
|
export const signOutUser = () => axiosInstance.get(SIGN_OUT_PATH);
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
# notifications
|
# notifications
|
||||||
get '/recent_notifications', to: 'notifications#recent_notifications'
|
get '/recent_notifications', to: 'notifications#recent_notifications'
|
||||||
|
get '/unread_notifications_count',
|
||||||
|
to: 'notifications#unread_notifications_count'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
get '/current_user_info', to: 'users/users#current_user_info'
|
get '/current_user_info', to: 'users/users#current_user_info'
|
||||||
|
|
||||||
namespace :users do
|
namespace :users do
|
||||||
|
get '/sign_out_user', to: 'users#sign_out_user'
|
||||||
delete '/remove_user', to: 'user_teams#remove_user'
|
delete '/remove_user', to: 'user_teams#remove_user'
|
||||||
delete '/leave_team', to: 'user_teams#leave_team'
|
delete '/leave_team', to: 'user_teams#leave_team'
|
||||||
put '/update_role', to: 'user_teams#update_role'
|
put '/update_role', to: 'user_teams#update_role'
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^8.0.1",
|
"babel-eslint": "^8.0.1",
|
||||||
|
"babel-plugin-transform-react-jsx-source": "^6.22.0",
|
||||||
"eslint": "^4.7.2",
|
"eslint": "^4.7.2",
|
||||||
"eslint-config-airbnb": "^15.1.0",
|
"eslint-config-airbnb": "^15.1.0",
|
||||||
"eslint-config-google": "^0.9.1",
|
"eslint-config-google": "^0.9.1",
|
||||||
|
|
27
spec/controllers/client_api/notifications_controller_spec.rb
Normal file
27
spec/controllers/client_api/notifications_controller_spec.rb
Normal file
|
@ -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 :unread_notifications_count, format: :json
|
||||||
|
expect(response).to be_success
|
||||||
|
expect(response.body).to include('count')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,6 +8,20 @@ describe ClientApi::Users::UsersController, type: :controller do
|
||||||
@user = User.first
|
@user = User.first
|
||||||
end
|
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
|
describe 'GET current_user_info' do
|
||||||
it 'responds successfully' do
|
it 'responds successfully' do
|
||||||
get :current_user_info, format: :json
|
get :current_user_info, format: :json
|
||||||
|
|
8
spec/factories/notifications.rb
Normal file
8
spec/factories/notifications.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :notification do
|
||||||
|
title '<i>Admin</i> was added as Owner to project ' \
|
||||||
|
'<strong>Demo project - qPCR</strong> by <i>User</i>.'
|
||||||
|
message 'Project: <a href=\"/projects/3\"> Demo project - qPCR</a>'
|
||||||
|
type_of 'assignment'
|
||||||
|
end
|
||||||
|
end
|
5
spec/factories/user_notification.rb
Normal file
5
spec/factories/user_notification.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :user_notification do
|
||||||
|
checked false
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe UserNotification, type: :model do
|
describe UserNotification, type: :model do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
it 'should be of class UserNotification' do
|
it 'should be of class UserNotification' do
|
||||||
expect(subject.class).to eq UserNotification
|
expect(subject.class).to eq UserNotification
|
||||||
end
|
end
|
||||||
|
@ -17,4 +19,37 @@ describe UserNotification, type: :model do
|
||||||
it { should belong_to :user }
|
it { should belong_to :user }
|
||||||
it { should belong_to :notification }
|
it { should belong_to :notification }
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue