diff --git a/.eslintrc b/.eslintrc
index 988b7f66fb..3a39e55c74 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,16 +8,17 @@
"parser": "@babel/eslint-parser",
"rules": {
"react/jsx-boolean-value": ["error","always"],
+ "react/jsx-no-bind": "off",
+ "react/no-multi-comp": 0,
"react/prefer-es6-class": 0,
"react/prefer-stateless-function": 0,
- "react/no-multi-comp": 0,
+ "react/react-in-jsx-scope": 0,
"array-callback-return": "off",
"arrow-body-style": ["warn", "as-needed"],
"comma-dangle": ["warn", "never"],
"import/extensions": "off",
"import/no-unresolved": "off",
"indent": ["warn", 2, {"SwitchCase": 1}],
- "react/jsx-no-bind": "off",
"max-len": [1, 120, 2, {
"ignoreUrls": true,
"ignoreComments": true
diff --git a/app/pages/notifications/index.jsx b/app/pages/notifications/index.jsx
index 282ef0cc8c..546576708a 100644
--- a/app/pages/notifications/index.jsx
+++ b/app/pages/notifications/index.jsx
@@ -1,12 +1,13 @@
-import PropTypes from 'prop-types';
-import React from 'react';
import counterpart from 'counterpart';
+import { shape, string } from 'prop-types';
+import { Component } from 'react';
+import { Helmet } from 'react-helmet';
import Translate from 'react-translate-component';
import talkClient from 'panoptes-client/lib/talk-client';
-import { Helmet } from 'react-helmet';
-import Loading from '../../components/loading-indicator';
-import NotificationSection from '../notifications/notification-section';
+
+import NotificationSection from './notification-section';
import CollapsableSection from '../../components/collapsable-section';
+import Loading from '../../components/loading-indicator';
counterpart.registerTranslations('en', {
notifications: {
@@ -18,148 +19,212 @@ counterpart.registerTranslations('en', {
}
});
-export default class NotificationsPage extends React.Component {
+export default class NotificationsPage extends Component {
constructor(props) {
super(props);
- this.onChildChanged = this.onChildChanged.bind(this);
+
this.state = {
- projNotifications: [],
- expanded: false
+ error: null,
+ expanded: false,
+ loading: false,
+ notifications: []
};
+
+ this.handleExpand = this.handleExpand.bind(this);
}
componentDidMount() {
- if (this.props.user) {
- this.getProjectNotifications();
+ const { project, user } = this.props;
+
+ if (user) {
+ this.fetchNotifications();
}
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.user !== null && nextProps.user !== this.props.user) {
- this.getProjectNotifications();
+ if (project) {
+ this.setState({ expanded: project.id });
}
}
- onChildChanged(section) {
- this.setState({ expanded: section });
+ componentDidUpdate(prevProps) {
+ const { project, user } = this.props;
+
+ if (user && !prevProps.user) {
+ this.fetchNotifications();
+ }
+
+ if (project !== prevProps.project) {
+ this.setState({ expanded: project.id });
+ }
}
- getProjectNotifications() {
- talkClient.type('notifications').get({ page: 1, page_size: 50 })
- .then((projNotifications) => {
- this.groupNotifications(projNotifications);
- })
- .then(() => {
- if (this.props.project) this.setState({ expanded: `project-${this.props.project.id}` });
- })
- .catch((e) => {
- console.error('Unable to load notifications', e);
- });
+ handleExpand(sectionID) {
+ this.setState({ expanded: sectionID });
}
- groupNotifications(notifications) {
- const projectSections = [];
- const projectNotifications = [];
- notifications.forEach((notification) => {
- if (projectSections.indexOf(notification.section) < 0) {
- if (notification.section === 'zooniverse') {
- projectSections.unshift(notification.section);
- projectNotifications.unshift(notification);
- } else {
- projectSections.push(notification.section);
- projectNotifications.push(notification);
- }
+ async fetchNotifications() {
+ this.setState({ loading: true });
+
+ function requestAllNotifications() {
+ let notifications = [];
+
+ function getNotifications(page) {
+ return talkClient
+ .type('notifications')
+ .get({ page, page_size: 50 })
+ .then((response) => {
+ notifications = notifications.concat(response);
+ const meta = response[0] ? response[0].getMeta() : null;
+ if (meta && meta.next_page) {
+ return getNotifications(meta.next_page);
+ }
+ return Promise.resolve(notifications);
+ })
+ .catch((error) => {
+ throw error;
+ });
}
- });
- if (this.props.project && projectSections.indexOf(`project-${this.props.project.id}`) < 0) {
- talkClient.type('notifications').get({ page: 1, page_size: 1, section: `project-${this.props.project.id}` })
- .then(([notification]) => {
- if (notification) {
- projectNotifications.push(notification);
- this.setState({ projNotifications: projectNotifications });
- this.setState({ expanded: `project-${this.props.project.id}` });
- }
- });
+ return getNotifications(1);
}
- this.setState({ projNotifications: projectNotifications });
- }
- renderNotifications() {
- let notificationView;
-
- if (this.state.projNotifications.length > 0) {
- notificationView = (
-
-
+ const {
+ location, project, user
+ } = this.props;
+ const {
+ error, expanded, loading, notifications
+ } = this.state;
+
+ let groupedNotifications = {};
+ let groupedNotificationsIDs = [];
+ if (notifications?.length > 0) {
+ groupedNotifications = this.groupNotifications(notifications);
+ groupedNotificationsIDs = Object.keys(groupedNotifications);
+ if (groupedNotificationsIDs.includes('zooniverse')) {
+ groupedNotificationsIDs.splice(groupedNotificationsIDs.indexOf('zooniverse'), 1);
+ groupedNotificationsIDs.unshift('zooniverse');
+ }
+ }
+
+ const headerStyle = project ? 'notifications-title talk-module' : 'notifications-title';
+
+ let content = '';
+ if (!user) {
+ content = (
+
+ );
+ } else if (error) {
+ content = (
+
{error.message}
+ );
+ } else if (loading) {
+ content = (
+
+ );
+ } else if (notifications?.length === 0) {
+ content = (
+
+
+ {' '}
+
);
}
return (
-
+
+ {content ? (
+
+ {content}
+
+ ) : (
+
+
+ {groupedNotificationsIDs.map((notificationSectionID) => {
+ const opened = notificationSectionID === expanded || groupedNotificationsIDs.length === 1;
- {signedIn}
+ const slug = groupedNotifications[notificationSectionID][0].project_slug;
+
+ const sectionNotifications = groupedNotifications[notificationSectionID] || [];
+ const uniqueNotifications = sectionNotifications.filter((notification, index, self) => (
+ index === self.findIndex((t) => t.id === notification.id)
+ ));
+
+ return (
+ {
+ const section = expanded === notificationSectionID ? false : notificationSectionID;
+ this.handleExpand(section);
+ }}
+ expanded={opened}
+ section={notificationSectionID}
+ >
+
+
+ );
+ })}
+
+
+ )}
);
}
}
+NotificationsPage.defaultProps = {
+ location: {
+ query: {
+ page: '1'
+ }
+ },
+ project: null,
+ user: null
+};
+
NotificationsPage.propTypes = {
- location: PropTypes.shape({
- query: PropTypes.object
+ location: shape({
+ query: shape({
+ page: string
+ })
}),
- project: PropTypes.shape({
- id: PropTypes.string
+ project: shape({
+ id: string
}),
- user: PropTypes.shape({
- display_name: PropTypes.string,
- login: PropTypes.string
+ user: shape({
+ display_name: string,
+ login: string
})
-};
\ No newline at end of file
+};
diff --git a/app/pages/notifications/index.spec.js b/app/pages/notifications/index.spec.js
index 5882c4fdd0..2e742360c8 100644
--- a/app/pages/notifications/index.spec.js
+++ b/app/pages/notifications/index.spec.js
@@ -1,82 +1,145 @@
-// These tests are skipped until a solution can be found for cjsx imports with the coffee-script test compiler
+/* eslint-disable func-names, prefer-arrow-callback, react/jsx-filename-extension */
import React from 'react';
import assert from 'assert';
-import Notifications from './index';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
+import talkClient from 'panoptes-client/lib/talk-client';
+
+import Loading from '../../components/loading-indicator';
+import NotificationsPage from './index';
+
const testNotifications = [
- { id: '123',
- section: 'project-4321'
+ {
+ id: '123',
+ project_id: '4321',
+ section: 'project-4321',
+ getMeta: () => ({ next_page: null })
},
- { id: '124',
- section: 'project-1234'
+ {
+ id: '124',
+ project_id: '1234',
+ section: 'project-1234',
+ getMeta: () => ({ next_page: null })
},
- { id: '125',
- section: 'zooniverse'
+ {
+ id: '125',
+ project_id: '',
+ section: 'zooniverse',
+ getMeta: () => ({ next_page: null })
},
- { id: '126',
- section: 'project-4321'
+ {
+ id: '126',
+ project_id: '4321',
+ section: 'project-4321',
+ getMeta: () => ({ next_page: null })
}
];
-describe('Notifications', function() {
- let wrapper;
- let notifications;
+describe('NotificationsPage', function () {
+ describe('without a user', function () {
+ let wrapper;
- describe('it will display according to user', function() {
- it('will ask user to sign in', function() {
- wrapper = mount(
);
- assert.equal(wrapper.find('.talk-module').text(), 'You\'re not signed in.');
+ before(function () {
+ wrapper = mount(
);
});
- it('will notify when no notifications present', function() {
- const stub = sinon.stub(Notifications.prototype, 'componentDidMount');
- wrapper = mount(
);
- assert(wrapper.contains(
You have no notifications.));
- stub.restore();
+ it('should render not signed in message', function () {
+ assert.equal(wrapper.find('.centering').text(), 'You\'re not signed in.');
});
});
- describe('it correctly display projects', function() {
- beforeEach(function () {
- wrapper = shallow(
-
,
- { disableLifecycleMethods: true }
- );
- wrapper.instance().groupNotifications(testNotifications);
- notifications = shallow(wrapper.instance().renderNotifications());
- });
+ describe('with a user', function () {
+ describe('with an error', function () {
+ let wrapper;
+ const testError = new Error('Test error');
- it('will place zooniverse section first', function() {
- assert.equal(notifications.find('.list').childAt(0).prop('section'), 'zooniverse');
- });
+ before(function () {
+ sinon.stub(talkClient, 'request').callsFake(() => Promise.reject(testError));
+
+ wrapper = mount(
);
+ });
- it('will display correct number of sections', function() {
- assert.equal(notifications.find('.list').children().length, 3);
+ after(function () {
+ talkClient.request.restore();
+ });
+
+ it('should render an error message', function () {
+ assert.equal(wrapper.find('.centering').text(), 'Test error');
+ });
});
- });
- describe('will open sections correctly', function() {
- beforeEach(function () {
- wrapper = shallow(
-
,
- { disableLifecycleMethods: true }
- );
- wrapper.setState({ expanded: 'project-1234' });
- wrapper.instance().groupNotifications(testNotifications);
- notifications = shallow(wrapper.instance().renderNotifications());
+ describe('while loading', function () {
+ let wrapper;
+
+ before(function () {
+ sinon.stub(talkClient, 'request').callsFake(() => Promise.resolve(null));
+
+ wrapper = mount(
);
+ });
+
+ after(function () {
+ talkClient.request.restore();
+ });
+
+ it('should render a loading message', function () {
+ assert.equal(wrapper.find(Loading).length, 1);
+ });
});
- it('will open the active project', function() {
- const activeProject = notifications.find('CollapsableSection').filterWhere(n => n.prop('section') === 'project-1234');
- assert.equal(activeProject.prop('expanded'), true);
+ describe('without notifications', function () {
+ let wrapper;
+
+ before(function () {
+ sinon.stub(talkClient, 'request').callsFake(() => Promise.resolve([]));
+
+ wrapper = mount(
);
+ });
+
+ after(function () {
+ talkClient.request.restore();
+ });
+
+ it('should render a no notifications message', function () {
+ assert.equal(wrapper.find('.centering').text(), 'You have no notifications. You can receive notifications by participating in Talk, following discussions, and receiving messages.');
+ });
});
- it('will keep other projects closed', function() {
- const activeProject = notifications.find('CollapsableSection').filterWhere(n => n.prop('section') === 'project-4321');
- assert.equal(activeProject.prop('expanded'), false);
+ describe('with notifications', function () {
+ let wrapper;
+
+ before(function () {
+ wrapper = shallow(
+
,
+ { disableLifecycleMethods: true }
+ );
+ wrapper.setState({
+ expanded: '1234',
+ notifications: testNotifications
+ });
+ });
+
+ it('will display correct number of sections', function () {
+ assert.equal(wrapper.find('CollapsableSection').length, 3);
+ });
+
+ it('will place Zooniverse section first', function () {
+ assert.equal(wrapper.find('CollapsableSection').at(0).prop('section'), 'zooniverse');
+ });
+
+ it('will open the active project', function () {
+ assert.equal(wrapper.find('CollapsableSection').at(1).prop('expanded'), true);
+ assert.equal(wrapper.find('CollapsableSection').at(1).prop('section'), '1234');
+ });
+
+ it('will keep other projects closed', function () {
+ assert.equal(wrapper.find('CollapsableSection').at(0).prop('expanded'), false);
+ assert.equal(wrapper.find('CollapsableSection').at(2).prop('expanded'), false);
+ });
});
});
});
diff --git a/app/pages/notifications/notification-section.jsx b/app/pages/notifications/notification-section.jsx
index d5b320c54b..11d06174b5 100644
--- a/app/pages/notifications/notification-section.jsx
+++ b/app/pages/notifications/notification-section.jsx
@@ -1,223 +1,222 @@
import PropTypes from 'prop-types';
-import React, { Component } from 'react';
+import { Component } from 'react';
import apiClient from 'panoptes-client/lib/api-client';
import talkClient from 'panoptes-client/lib/talk-client';
import { Link } from 'react-router';
+
import Notification from './notification.cjsx';
-import Paginator from '../../talk/lib/paginator.cjsx';
-import ZooniverseLogo from '../../partials/zooniverse-logo.cjsx';
-import getNotificationData from '../../lib/get-notification-data';
import Loading from '../../components/loading-indicator';
+import getNotificationData from '../../lib/get-notification-data';
+import ZooniverseLogo from '../../partials/zooniverse-logo.cjsx';
+import Paginator from '../../talk/lib/paginator.cjsx';
export default class NotificationSection extends Component {
constructor(props) {
super(props);
this.state = {
- currentMeta: { },
error: null,
- firstMeta: { },
- lastMeta: { },
loading: false,
+ name: '',
notificationData: [],
- notificationsMap: { },
- page: 1,
project: null
};
this.markAllRead = this.markAllRead.bind(this);
this.markAsRead = this.markAsRead.bind(this);
}
- componentWillMount() {
- if (this.props.section === 'zooniverse') {
- this.setState({
- name: 'Zooniverse'
- });
+ componentDidMount() {
+ const {
+ expanded,
+ location,
+ notifications,
+ section
+ } = this.props;
+
+ if (section === 'zooniverse') {
+ this.setState({ name: 'Zooniverse' });
} else {
- apiClient.type('projects').get(this.props.projectID, { include: 'avatar' })
- .catch((error) => {
- this.setState({ error });
- })
- .then((project) => {
- this.setState({ project })
- if (project.links.avatar) {
- apiClient.type('avatars').get(project.links.avatar.id)
- .then((avatar) => {
- this.setState({
- name: project.display_name,
- avatar: avatar.src
- });
- })
- .catch(() => {
- this.setState({
- name: project.display_name
- });
- });
- } else {
- this.setState({
- name: project.display_name
- });
- }
- });
+ this.fetchProjectData(section);
}
- }
- componentDidMount() {
- this.getUnreadCount();
+ const page = parseInt(location.query.page, 10) || 1;
+
+ if (expanded) {
+ this.fetchNotificationData(notifications, page);
+ }
}
- componentWillReceiveProps(nextProps) {
- const pageChanged = nextProps.location.query.page !== this.state.page;
- const userChanged = nextProps.user && nextProps.user !== this.props.user;
- if ((pageChanged || userChanged) && nextProps.expanded) {
- this.getNotifications(nextProps.location.query.page);
+ componentDidUpdate(prevProps) {
+ const {
+ expanded,
+ location,
+ notifications,
+ section
+ } = this.props;
+
+ if (section !== prevProps.section) {
+ if (section === 'zooniverse') {
+ this.setState({ name: 'Zooniverse' });
+ } else {
+ this.fetchProjectData(section);
+ }
+ }
+
+ const page = parseInt(location.query.page, 10) || 1;
+ const prevPage = parseInt(prevProps.location.query.page, 10) || 1;
+
+ if (expanded && !prevProps.expanded) {
+ this.fetchNotificationData(notifications, page);
+ }
+
+ if (page !== prevPage) {
+ this.fetchNotificationData(notifications, page);
}
}
- getNotifications(page) {
+ fetchNotificationData(notifications, page = 1) {
this.setState({ loading: true });
- let firstMeta;
- let lastMeta;
- return talkClient.type('notifications').get({ page, page_size: 5, section: this.props.section })
- .then((newNotifications) => {
- const meta = newNotifications[0] ? newNotifications[0].getMeta() : { };
- const notificationsMap = this.state.notificationsMap;
- firstMeta = this.state.firstMeta;
- lastMeta = this.state.lastMeta;
-
- meta.notificationIds = [];
- newNotifications.forEach((notification, i) => {
- notificationsMap[notification.id] = notification;
- meta.notificationIds.push(newNotifications[i].id);
- });
-
- if (meta.page > this.state.lastMeta.page) {
- lastMeta = meta;
- } else if (meta.page < this.state.firstMeta.page) {
- firstMeta = meta;
- } else {
- firstMeta = lastMeta = meta;
- }
- getNotificationData(newNotifications).then((notificationData) => {
- this.setState({ notificationData, loading: false });
- });
-
- this.setState({
- notifications: newNotifications,
- currentMeta: meta,
- firstMeta,
- lastMeta,
- notificationsMap,
- page
- });
- this.getUnreadCount();
+ const activeNotifications = notifications.slice((page - 1) * 5, page * 5);
+
+ getNotificationData(activeNotifications)
+ .then((notificationData) => {
+ this.setState({ notificationData, loading: false });
});
}
- getUnreadCount() {
- return talkClient.type('notifications').get({ page: 1, page_size: 1, delivered: false, section: this.props.section })
- .catch((error) => {
- this.setState({ error });
- })
- .then(([project]) => {
- if (project) {
- const count = project.getMeta().count || 0;
- this.setState({ unread: count });
- } else {
- this.setState({ unread: 0 });
- }
- });
+ fetchProjectData(projectID) {
+ apiClient.type('projects')
+ .get(projectID, { include: 'avatar' })
+ .catch((error) => {
+ this.setState({ error });
+ })
+ .then((project) => {
+ this.setState({ project });
+ if (project.links.avatar) {
+ apiClient.type('avatars')
+ .get(project.links.avatar.id)
+ .then((avatar) => {
+ this.setState({
+ name: project.display_name,
+ avatar: avatar.src
+ });
+ })
+ .catch(() => {
+ this.setState({
+ name: project.display_name
+ });
+ });
+ } else {
+ this.setState({
+ name: project.display_name
+ });
+ }
+ });
}
markAllRead() {
- this.state.notificationData.forEach((data) => {
+ const { notificationsCounter } = this.context;
+ const { section, toggleSection } = this.props;
+ const { notificationData } = this.state;
+
+ const requestSection = section === 'zooniverse' ? 'zooniverse' : `project-${section}`;
+
+ notificationData.forEach((data) => {
data.notification.update({ delivered: true });
});
- this.setState({ unread: 0 });
- return talkClient.put('/notifications/read', { section: this.props.section }).then(() => {
- return talkClient.type('notifications').get({ page_size: 1, delivered: false }).then(([notification]) => {
- const count = notification ? notification.getMeta().count : 0;
- if (count === 0) this.context.notificationsCounter.setUnread(0);
- });
- });
+ return talkClient
+ .put('/notifications/read', { section: requestSection })
+ .then(() => talkClient
+ .type('notifications')
+ .get({ page_size: 1, delivered: false })
+ .then(([notification]) => {
+ const count = notification ? notification.getMeta().count : 0;
+ if (count === 0) notificationsCounter.setUnread(0);
+ toggleSection(false);
+ }));
}
markAsRead(readNotification) {
const { notificationsCounter } = this.context;
- const { user } = this.props;
- const { notifications } = this.state;
- const relatedNotifications = notifications.filter(notification => (
+ const { notifications, user } = this.props;
+ const relatedNotifications = notifications.filter((notification) => (
notification.delivered === false
&& (notification.id === readNotification.id
|| notification.source.discussion_id === readNotification.source.discussion_id)
));
const notificationPromises = relatedNotifications
- .map((relatedNotification) => {
- return relatedNotification
- .update({ delivered: true })
- .save()
- .catch((error) => {
- console.warn(error);
- return {};
- });
- });
+ .map((relatedNotification) => relatedNotification
+ .update({ delivered: true })
+ .save()
+ .catch((error) => {
+ console.warn(error);
+ return {};
+ }));
Promise.all(notificationPromises).then(() => notificationsCounter.update(user));
}
- avatarFor() {
- const src = this.state.avatar || '/assets/simple-avatar.jpg';
- let avatar;
+ avatarFor(unread) {
+ const { slug } = this.props;
+ const { avatar, name } = this.state;
- if (this.state.unread > 0) return this.unreadCircle();
+ const src = avatar || '/assets/simple-avatar.jpg';
+ let sectionAvatar;
- if (this.state.name === 'Zooniverse') {
- avatar =
;
+ if (unread > 0) return this.unreadCircle(unread);
+
+ if (name === 'Zooniverse') {
+ sectionAvatar =
;
} else {
- avatar =
;
+ sectionAvatar =
;
}
return (
-
- {avatar}
+
+ {sectionAvatar}
);
}
- unreadCircle() {
+ unreadCircle(unread) {
let centerNum = '40%';
- if (this.state.unread > 99) {
+ if (unread > 99) {
centerNum = '20%';
- } else if (this.state.unread > 9) {
+ } else if (unread > 9) {
centerNum = '30%';
}
- const unreadNotifications = this.state.unread > 99 ? '99+' : this.state.unread;
+ const unreadNotifications = unread > 99 ? '99+' : unread;
return (
);
}
- renderHeader() {
- const buttonType = this.props.expanded ? 'fa fa-chevron-up fa-lg' : 'fa fa-chevron-down fa-lg';
+ renderHeader(unread) {
+ const { expanded, toggleSection } = this.props;
+ const { name } = this.state;
+
+ const buttonType = expanded ? 'fa fa-chevron-up fa-lg' : 'fa fa-chevron-down fa-lg';
return (
-
+
- {this.avatarFor()}
+ {this.avatarFor(unread)}
-
{this.state.name}
+ {name}
-
@@ -226,71 +225,95 @@ export default class NotificationSection extends Component {
);
}
- renderError(item) {
- return (
-
- {item.error}
-
- );
- }
-
render() {
- const l = this.state.currentMeta;
- const firstNotification = (l.page * l.page_size) - (l.page_size - 1) || 0;
- const lastNotification = Math.min(l.page_size * l.page, l.count) || 0;
+ const {
+ expanded, location, notifications, user
+ } = this.props;
+ const {
+ error, loading, notificationData, project
+ } = this.state;
+
+ const unread = notifications.filter((notification) => notification.delivered === false).length;
+
+ const page = parseInt(location.query.page, 10) || 1;
+ const pageCount = Math.ceil(notifications.length / 5);
+ const firstNotification = (page - 1) * 5 + 1;
+ const lastNotification = Math.min(page * 5, notifications.length);
return (
- {this.renderHeader()}
+ {this.renderHeader(unread)}
- {!!this.state.error && (
+ {!!error && (
- {this.state.error.toString()}
+ {error.toString()}
)}
- {this.state.loading && (
+ {loading && (
)}
- {(this.props.expanded && this.state.unread > 0) && (
-
+ {(expanded && unread > 0) && (
+
Mark All Read
)}
- {(this.props.expanded && !this.state.loading) && (
- this.state.notificationData.map((item) => {
+ {(expanded && !loading) && (
+ notificationData.map((item) => {
if (item.notification) {
return (
);
+ project={project}
+ user={user}
+ />
+ );
}
- return this.renderError(item);
+ return (
+
+ {item.error}
+
+ );
})
)}
- {(this.props.expanded) && (
+ {(expanded) && (
older }
- page={+this.state.currentMeta.page}
- pageCount={this.state.lastMeta.page_count}
+ itemCount={true}
+ nextLabel={(
+
+ {' older'}
+
+
+ )}
+ page={page}
+ pageCount={pageCount}
pageSelector={false}
- previousLabel={ previous}
- totalItems={{firstNotification} - {lastNotification} of {l.count}}
+ previousLabel={(
+
+
+ {' previous'}
+
+ )}
+ totalItems={(
+
+ {`${firstNotification} - ${lastNotification} of ${notifications.length}`}
+
+ )}
/>
)}
@@ -302,21 +325,38 @@ export default class NotificationSection extends Component {
NotificationSection.propTypes = {
expanded: PropTypes.bool,
- projectID: PropTypes.string,
+ location: PropTypes.shape({
+ query: PropTypes.shape({
+ page: PropTypes.string
+ })
+ }),
+ notifications: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string
+ })),
section: PropTypes.string,
slug: PropTypes.string,
toggleSection: PropTypes.func,
user: PropTypes.shape({
- display_name: PropTypes.string,
- login: PropTypes.string
+ id: PropTypes.string
})
};
NotificationSection.contextTypes = {
- notificationsCounter: PropTypes.object
+ notificationsCounter: PropTypes.shape({
+ setUnread: PropTypes.func
+ })
};
NotificationSection.defaultProps = {
expanded: false,
- toggleSection: () => {}
+ location: {
+ query: {
+ page: '1'
+ }
+ },
+ notifications: [],
+ section: '',
+ slug: '',
+ toggleSection: () => {},
+ user: null
};
diff --git a/app/pages/notifications/notification-section.spec.js b/app/pages/notifications/notification-section.spec.js
index ad815ccbb6..8e934fe37a 100644
--- a/app/pages/notifications/notification-section.spec.js
+++ b/app/pages/notifications/notification-section.spec.js
@@ -1,45 +1,98 @@
+/* eslint-disable func-names, prefer-arrow-callback, react/jsx-filename-extension */
+
import React from 'react';
import assert from 'assert';
import sinon from 'sinon';
import { shallow } from 'enzyme';
+import talkClient from 'panoptes-client/lib/talk-client';
+
import NotificationSection from './notification-section';
-const notifications = [
- { notification: {
- id: '123',
- source_type: 'DataRequest',
- url: '/',
- message: 'test message',
- created_at: '2016-12-09T16:09:50.641Z',
- },
- data: { projectName: 'TestingProject' },
+const notificationComment = {
+ id: '123',
+ source_type: 'Comment',
+ delivered: true,
+ source: {
+ discussion_id: '789'
},
- { notification: {
- id: '124',
- source_type: 'DataRequest',
- url: '/',
- message: 'test message',
- created_at: '2016-12-10T16:09:50.641Z',
+ update: sinon.stub().returnsThis(),
+ save: sinon.stub().resolves({})
+};
+
+const notificationDataRequest = {
+ id: '124',
+ source_type: 'DataRequest',
+ delivered: true,
+ update: sinon.stub().returnsThis(),
+ save: sinon.stub().resolves({})
+};
+
+const notificationModeration = {
+ id: '126',
+ source_type: 'Moderation',
+ delivered: true,
+ update: sinon.stub().returnsThis(),
+ save: sinon.stub().resolves({})
+};
+
+const notificationCommentUnread1 = {
+ id: '127',
+ source_type: 'Comment',
+ delivered: false,
+ source: {
+ discussion_id: '790'
},
- data: { projectName: 'TestingProject' },
+ update: sinon.stub().returnsThis(),
+ save: sinon.stub().resolves({})
+};
+
+const notificationCommentUnread2 = {
+ id: '128',
+ source_type: 'Comment',
+ delivered: false,
+ source: {
+ discussion_id: '790'
},
+ update: sinon.stub().returnsThis(),
+ save: sinon.stub().resolves({})
+};
+
+const notifications = [
+ notificationComment,
+ notificationDataRequest,
+ notificationModeration
];
-describe('Notification Section', function() {
- let wrapper;
+const notificationsWithUnread = notifications.concat([notificationCommentUnread1, notificationCommentUnread2]);
- before(function () {
- sinon.stub(NotificationSection.prototype, 'getUnreadCount').callsFake(() => null);
- });
+function notificationDataItem(notification) {
+ const dataType = notification.source_type.toLowerCase();
+ return {
+ notification,
+ data: {
+ [dataType]: {
+ id: notification.id.split('').reverse().join('')
+ }
+ }
+ };
+}
- after(function () {
- NotificationSection.prototype.getUnreadCount.restore();
- });
+const notificationData = notifications.map((notification) => notificationDataItem(notification));
+
+const notificationDataUnread = notificationsWithUnread.map((notification) => notificationDataItem(notification));
+describe('NotificationSection', function () {
describe('it can display a Zooniverse section', function () {
- beforeEach(function () {
+ let wrapper;
+
+ before(function () {
wrapper = shallow(
- ,
+
);
});
@@ -47,7 +100,7 @@ describe('Notification Section', function() {
assert.equal(wrapper.find('.notification-section__title').text(), 'Zooniverse');
});
- it('should link to the home page', function () {
+ it('should link to the Zooniverse home page', function () {
assert.equal(wrapper.find('Link').prop('to'), '/');
});
@@ -56,37 +109,76 @@ describe('Notification Section', function() {
});
});
- describe('it correctly displays a project', function () {
+ describe('it can display a project section', function () {
+ let wrapper;
+
before(function () {
- sinon.stub(NotificationSection.prototype, 'componentWillMount').callsFake(() => null);
- wrapper = shallow();
- wrapper.setState({ name: 'Testing' });
+ wrapper = shallow(
+ ,
+ { disableLifecycleMethods: true }
+ );
+ wrapper.setState({
+ avatar: '/project/avatar/url',
+ name: 'Test Project'
+ });
});
- after(function () {
- NotificationSection.prototype.componentWillMount.restore();
+ it('should display the correct title', function () {
+ assert.equal(wrapper.find('.notification-section__title').text(), 'Test Project');
});
+ });
- it('should display the correct title', function () {
- assert.equal(wrapper.find('.notification-section__title').text(), 'Testing');
+ describe('it can display a section with unread notifications', function () {
+ let wrapper;
+
+ before(function () {
+ wrapper = shallow(
+
+ );
+
+ wrapper.setState({
+ loading: false,
+ name: 'Zooniverse'
+ });
+ });
+
+ it('should show an unread notification circle in place of an avatar', function () {
+ assert.equal(wrapper.find('circle').length, 1);
});
});
describe('will render appropriately when open', function () {
- beforeEach(function () {
+ let wrapper;
+
+ before(function () {
wrapper = shallow(
- ,
+ ,
+ { disableLifecycleMethods: true }
);
- wrapper.setState({ notificationData: notifications });
+ wrapper.setState({
+ loading: false,
+ name: 'Zooniverse',
+ notificationData
+ });
});
it('should display the correct number of notifications', function () {
- assert.equal(wrapper.find('Notification').length, 2);
- });
-
- it('should show an unread notification in place of an avatar', function () {
- wrapper.setState({ unread: 1 });
- assert.equal(wrapper.find('circle').length, 1);
+ assert.equal(wrapper.find('Notification').length, 3);
});
it('should show close icon', function () {
@@ -94,76 +186,110 @@ describe('Notification Section', function() {
});
});
- describe('will update notifications as read', function () {
- const newNotifications = [
- {
- id: '123',
- delivered: false,
- source: {
- discussion_id: '456'
- },
- source_type: 'Comment',
- update: sinon.stub().returnsThis(),
- save: sinon.stub().resolves({})
- },
- {
- id: '124',
- delivered: false,
- source: {
- discussion_id: '456'
- },
- source_type: 'Comment',
- update: sinon.stub().returnsThis(),
- save: sinon.stub().rejects()
- },
- {
- id: '125',
- delivered: false,
- source_type: 'Moderation',
- source: {},
- update: sinon.stub().returnsThis(),
- save: sinon.stub().resolves({})
- },
- {
- id: '126',
- delivered: false,
- source: {
- discussion_id: '789'
- },
- source_type: 'Comment',
- update: sinon.stub().returnsThis(),
- save: sinon.stub().resolves({})
- }
- ];
- const notificationsCounter = {
+ describe('will update notification as read', function () {
+ let wrapper;
+
+ const notificationsCounterStub = {
update: sinon.stub()
};
- wrapper = shallow(
- ,
- { context: { notificationsCounter }, disableLifeCycleMethods: true }
- );
- wrapper.setState({ notifications: newNotifications });
- wrapper.instance().markAsRead(newNotifications[0]);
- it('should update read notification as read (delivered)', function () {
- assert.equal(newNotifications[0].update.calledWith({ delivered: true }), true);
- assert.equal(newNotifications[0].save.called, true);
+ before(function () {
+ wrapper = shallow(
+ ,
+ {
+ context: {
+ notificationsCounter: notificationsCounterStub
+ },
+ disableLifecycleMethods: true
+ }
+ );
+ wrapper.setState({
+ loading: false,
+ name: 'Zooniverse',
+ notificationData: notificationDataUnread
+ });
+ wrapper.instance().markAsRead(notificationDataUnread[4].notification);
+ });
+
+ it('should update unread notification as read (delivered)', function () {
+ assert.equal(notificationCommentUnread2.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationCommentUnread2.save.called, true);
});
- it('should update related notifications as read (delivered)', function () {
- assert.equal(newNotifications[1].update.calledWith({ delivered: true }), true);
- assert.equal(newNotifications[1].save.called, true);
+ it('should update related unread notifications as read (delivered)', function () {
+ assert.equal(notificationCommentUnread1.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationCommentUnread1.save.called, true);
});
it('should not update unrelated notifications', function () {
- assert.equal(newNotifications[2].update.called, false);
- assert.equal(newNotifications[2].save.called, false);
- assert.equal(newNotifications[3].update.called, false);
- assert.equal(newNotifications[3].save.called, false);
+ assert.equal(notificationComment.update.called, false);
+ assert.equal(notificationDataRequest.update.called, false);
+ assert.equal(notificationModeration.update.called, false);
+ });
+
+ it('should update the notifications counter', function () {
+ assert.equal(notificationsCounterStub.update.called, true);
+ });
+ });
+
+ describe('will update all notifications as read', function () {
+ let wrapper;
+ let toggleSectionSpy;
+
+ const notificationsCounterStub = {
+ setUnread: sinon.stub()
+ };
+
+ before(function () {
+ sinon.stub(talkClient, 'request').callsFake(() => Promise.resolve([]));
+ toggleSectionSpy = sinon.spy();
+
+ wrapper = shallow(
+ ,
+ {
+ context: {
+ notificationsCounter: notificationsCounterStub
+ },
+ disableLifecycleMethods: true
+ }
+ );
+ wrapper.setState({
+ loading: false,
+ name: 'Zooniverse',
+ notificationData: notificationDataUnread
+ });
+ wrapper.instance().markAllRead();
+ });
+
+ after(function () {
+ talkClient.request.restore();
+ });
+
+ it('should update all notifications as read (delivered)', function () {
+ assert.equal(notificationComment.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationDataRequest.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationModeration.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationCommentUnread1.update.calledWith({ delivered: true }), true);
+ assert.equal(notificationCommentUnread2.update.calledWith({ delivered: true }), true);
+ });
+
+ it('should call toggleSection', function () {
+ assert.equal(toggleSectionSpy.calledWith(false), true);
});
it('should update the notifications counter', function () {
- assert.equal(notificationsCounter.update.called, true);
+ assert.equal(notificationsCounterStub.setUnread.calledWith(0), true);
});
});
});