diff --git a/src/components/NotificationsDropdown/NotificationsDropdownContainer.jsx b/src/components/NotificationsDropdown/NotificationsDropdownContainer.jsx index bcf40caf9..aeb8bfafc 100644 --- a/src/components/NotificationsDropdown/NotificationsDropdownContainer.jsx +++ b/src/components/NotificationsDropdown/NotificationsDropdownContainer.jsx @@ -8,8 +8,9 @@ import { Link } from 'react-router-dom' import { connect } from 'react-redux' import _ from 'lodash' import { getNotifications, visitNotifications, toggleNotificationSeen, markAllNotificationsRead, - toggleNotificationRead, toggleBundledNotificationRead, viewOlderNotifications, toggleNotificationsDropdownMobile } from '../../routes/notifications/actions' -import { splitNotificationsBySources, filterReadNotifications, limitQuantityInSources, filterOldNotifications } from '../../routes/notifications/helpers/notifications' + toggleNotificationRead, toggleBundledNotificationRead, viewOlderNotifications, + toggleNotificationsDropdownMobile, hideOlderNotifications } from '../../routes/notifications/actions' +import { splitNotificationsBySources, filterReadNotifications, limitQuantityInSources } from '../../routes/notifications/helpers/notifications' import NotificationsSection from '../NotificationsSection/NotificationsSection' import NotificationsEmpty from '../NotificationsEmpty/NotificationsEmpty' import NotificationsDropdownHeader from '../NotificationsDropdownHeader/NotificationsDropdownHeader' @@ -18,21 +19,11 @@ import NotificationsMobilePage from './NotificationsMobilePage' import NotificationsReadAll from './NotificationsReadAll' import ScrollLock from 'react-scroll-lock-component' import MediaQuery from 'react-responsive' -import { NOTIFICATIONS_DROPDOWN_PER_SOURCE, NOTIFICATIONS_DROPDOWN_MAX_TOTAL, REFRESH_NOTIFICATIONS_INTERVAL, SCREEN_BREAKPOINT_MD } from '../../config/constants' +import { NOTIFICATIONS_DROPDOWN_PER_SOURCE, NOTIFICATIONS_NEW_PER_SOURCE, REFRESH_NOTIFICATIONS_INTERVAL, SCREEN_BREAKPOINT_MD } from '../../config/constants' import './NotificationsDropdown.scss' class NotificationsDropdownContainer extends React.Component { - constructor(props) { - super(props) - - this.state = { - isViewAll: false - } - - this.viewAll = this.viewAll.bind(this) - } - componentDidMount() { this.props.getNotifications() this.autoRefreshNotifications = setInterval(() => this.props.getNotifications(), REFRESH_NOTIFICATIONS_INTERVAL) @@ -42,6 +33,7 @@ class NotificationsDropdownContainer extends React.Component { clearInterval(this.autoRefreshNotifications) // hide notifications dropdown for mobile, when this component is unmounted this.props.toggleNotificationsDropdownMobile(false) + this.props.hideOlderNotifications() } componentWillReceiveProps(nextProps) { @@ -52,13 +44,10 @@ class NotificationsDropdownContainer extends React.Component { // hide notifications dropdown for mobile, // when this component persist but URL changed this.props.toggleNotificationsDropdownMobile(false) + this.props.hideOlderNotifications() } } - viewAll() { - this.setState({isViewAll: true}) - } - render() { if (!this.props.initialized) { return @@ -67,10 +56,9 @@ class NotificationsDropdownContainer extends React.Component { const {lastVisited, sources, notifications, markAllNotificationsRead, toggleNotificationRead, toggleNotificationSeen, pending, toggleBundledNotificationRead, visitNotifications, oldSourceIds, viewOlderNotifications, isDropdownMobileOpen, toggleNotificationsDropdownMobile } = this.props - const {isViewAll} = this.state const getPathname = link => link.split(/[?#]/)[0].replace(/\/?$/, '') - // mark notifications with url mathc current page's url as seen + // mark notifications with url match current page's url as seen if (!pending) { const seenNotificationIds = notifications .filter(({ isRead, seen, goto = '' }) => !isRead && !seen && getPathname(goto) === getPathname(window.location.pathname)) @@ -80,26 +68,9 @@ class NotificationsDropdownContainer extends React.Component { } const notReadNotifications = filterReadNotifications(notifications) - const notOldNotifications = filterOldNotifications(notReadNotifications, oldSourceIds) - const allNotificationsBySources = splitNotificationsBySources(sources, notOldNotifications) - let notificationsBySources - - if (!isViewAll) { - notificationsBySources = limitQuantityInSources( - allNotificationsBySources, - NOTIFICATIONS_DROPDOWN_PER_SOURCE, - NOTIFICATIONS_DROPDOWN_MAX_TOTAL - ) - } else { - notificationsBySources = allNotificationsBySources - } - - const hiddenByLimitCount = _.sumBy(allNotificationsBySources, 'notifications.length') - _.sumBy(notificationsBySources, 'notifications.length') + const allNotificationsBySources = splitNotificationsBySources(sources, notReadNotifications) - const globalSource = notificationsBySources.length > 0 && notificationsBySources[0].id === 'global' ? notificationsBySources[0] : null - const projectSources = notificationsBySources.length > 1 && globalSource ? notificationsBySources.slice(1) : notificationsBySources const hasUnread = notReadNotifications.length > 0 - const olderNotificationsCount = notReadNotifications.length - notOldNotifications.length // we have to give Dropdown component some time // before removing notification item node from the list // otherwise dropdown thinks we clicked outside and closes dropdown @@ -142,92 +113,111 @@ class NotificationsDropdownContainer extends React.Component { return ( - {(matches) => (matches ? ( - - !pending && markAllNotificationsRead()} hasUnread={hasUnread}/> - {!hasUnread ? ( -
- {notificationsEmpty} -
- ) : ([ - -
- {globalSource && globalSource.notifications.length && - - } - {projectSources.filter(source => source.notifications.length > 0).map(source => ( - - ))} -
-
, - - { - olderNotificationsCount > 0 ? - `View ${olderNotificationsCount} older notification${olderNotificationsCount > 1 ? 's' : ''}` : - 'View all notifications' - } - - ])} -
- ) : ( - { - toggleNotificationsDropdownMobile() - visitNotifications() - }} - isOpen={isDropdownMobileOpen} - > - {!hasUnread ? ( - notificationsEmpty - ) : ( -
- {globalSource && (globalSource.notifications.length || isViewAll && globalSource.total) && - viewOlderNotifications(globalSource.id) : null} - onLinkClick={(notificationId) => { - toggleNotificationsDropdownMobile() - markNotificationSeen(notificationId) - }} - />} - {projectSources.filter(source => source.notifications.length || isViewAll && source.total).map(source => ( - viewOlderNotifications(source.id) : null} - onLinkClick={(notificationId) => { - toggleNotificationsDropdownMobile() - markNotificationSeen(notificationId) - }} - /> - ))} - {!isViewAll && (olderNotificationsCount > 0 || hiddenByLimitCount > 0) && - Read all notifications} -
- )} -
- ))} + {(matches) => { + if (matches) { + const notificationsBySources = limitQuantityInSources( + allNotificationsBySources, + NOTIFICATIONS_DROPDOWN_PER_SOURCE, + oldSourceIds + ) + const hiddenByLimitCount = _.sumBy(notificationsBySources, 'total') - _.sumBy(notificationsBySources, 'notifications.length') + const globalSource = notificationsBySources.length > 0 && notificationsBySources[0].id === 'global' ? notificationsBySources[0] : null + const projectSources = notificationsBySources.length > 1 && globalSource ? notificationsBySources.slice(1) : notificationsBySources + + return ( + + !pending && markAllNotificationsRead()} hasUnread={hasUnread}/> + {!hasUnread ? ( +
+ {notificationsEmpty} +
+ ) : ([ + +
+ {globalSource && globalSource.notifications.length && + + } + {projectSources.filter(source => source.notifications.length > 0).map(source => ( + + ))} +
+
, + + { + hiddenByLimitCount > 0 ? + `View ${hiddenByLimitCount} older notification${hiddenByLimitCount > 1 ? 's' : ''}` : + 'View all notifications' + } + + ])} +
+ ) + } else { + const notificationsBySources = limitQuantityInSources( + allNotificationsBySources, + NOTIFICATIONS_NEW_PER_SOURCE, + oldSourceIds + ) + const globalSource = notificationsBySources.length > 0 && notificationsBySources[0].id === 'global' ? notificationsBySources[0] : null + const projectSources = notificationsBySources.length > 1 && globalSource ? notificationsBySources.slice(1) : notificationsBySources + + return ( + { + toggleNotificationsDropdownMobile() + visitNotifications() + }} + isOpen={isDropdownMobileOpen} + > + {!hasUnread ? ( + notificationsEmpty + ) : ( +
+ {globalSource && globalSource.notifications.length > 0 && + viewOlderNotifications(globalSource.id)} + onLinkClick={(notificationId) => { + toggleNotificationsDropdownMobile() + markNotificationSeen(notificationId) + }} + />} + {projectSources.filter(source => source.notifications.length).map(source => ( + viewOlderNotifications(source.id)} + onLinkClick={(notificationId) => { + toggleNotificationsDropdownMobile() + markNotificationSeen(notificationId) + }} + /> + ))} +
+ )} +
+ ) + } + }}
) } @@ -243,6 +233,7 @@ const mapDispatchToProps = { toggleNotificationRead, toggleBundledNotificationRead, viewOlderNotifications, + hideOlderNotifications, toggleNotificationsDropdownMobile } diff --git a/src/components/ScrollToAnchors.jsx b/src/components/ScrollToAnchors.jsx index ba4441d8c..19058226c 100644 --- a/src/components/ScrollToAnchors.jsx +++ b/src/components/ScrollToAnchors.jsx @@ -7,7 +7,7 @@ * If there is any hash we check if component has element with such id and scroll to it. */ import React from 'react' -import { SCROLL_TO_MARGIN, SCROLL_TO_DURATION } from '../config/constants' +import { SCROLL_TO_MARGIN, SCROLL_TO_DURATION, SCREEN_BREAKPOINT_MD } from '../config/constants' import { scroller } from 'react-scroll' /** @@ -24,7 +24,7 @@ export function scrollToHash(hash) { scroller.scrollTo(id, { spy: true, smooth: true, - offset: windowWidth < 768 ? 0 : -SCROLL_TO_MARGIN, + offset: windowWidth < SCREEN_BREAKPOINT_MD ? 0 : -SCROLL_TO_MARGIN, duration: SCROLL_TO_DURATION, activeClass: 'active' }) diff --git a/src/components/TopBar/SectionToolBar.scss b/src/components/TopBar/SectionToolBar.scss index a543a81ae..db60214fa 100644 --- a/src/components/TopBar/SectionToolBar.scss +++ b/src/components/TopBar/SectionToolBar.scss @@ -121,5 +121,9 @@ } } } + + > .section .logo .icon-connect-logo-mono { + margin-top: 0; + } } } diff --git a/src/config/constants.js b/src/config/constants.js index 061cd389c..c74c1de2a 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -14,6 +14,7 @@ export const MARK_ALL_NOTIFICATIONS_READ = 'MARK_ALL_NOTIFICATIONS_READ' export const TOGGLE_NOTIFICATION_READ = 'TOGGLE_NOTIFICATION_READ' export const TOGGLE_NOTIFICATION_SEEN = 'TOGGLE_NOTIFICATION_SEEN' export const VIEW_OLDER_NOTIFICATIONS_SUCCESS = 'VIEW_OLDER_NOTIFICATIONS_SUCCESS' +export const HIDE_OLDER_NOTIFICATIONS_SUCCESS = 'HIDE_OLDER_NOTIFICATIONS_SUCCESS' export const NOTIFICATIONS_PENDING = 'NOTIFICATIONS_PENDING' export const TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE = 'TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE' @@ -379,11 +380,9 @@ export const SORT_OPTIONS = [ export const REFRESH_NOTIFICATIONS_INTERVAL = 1000 * 60 * 1 // 1 minute interval export const REFRESH_UNREAD_UPDATE_INTERVAL = 1000 * 10 * 1 // 10 second interval export const NOTIFICATIONS_DROPDOWN_PER_SOURCE = 5 -export const NOTIFICATIONS_DROPDOWN_MAX_TOTAL = Infinity +export const NOTIFICATIONS_NEW_PER_SOURCE = 10 export const NOTIFICATIONS_LIMIT = 1000 -// old notification time in minutes, a notification is old if its date is later than this time -export const OLD_NOTIFICATION_TIME = 60 * 48 // 2 day2 export const SCROLL_TO_MARGIN = 70 // px - 60px of toolbar height + 10px to make some margin export const SCROLL_TO_DURATION = 500 // ms diff --git a/src/projects/list/components/Projects/ProjectsGridView.scss b/src/projects/list/components/Projects/ProjectsGridView.scss index 66b510e4b..26d455850 100644 --- a/src/projects/list/components/Projects/ProjectsGridView.scss +++ b/src/projects/list/components/Projects/ProjectsGridView.scss @@ -323,11 +323,11 @@ $screen-one-column: 720px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } - &:hover { - background-color: $tc-dark-blue-30; - color: $tc-black; - } + .tooltip-target.active .project-customer { + background-color: $tc-dark-blue-30; + color: $tc-black; } .project-segment { diff --git a/src/routes/notifications/actions/index.js b/src/routes/notifications/actions/index.js index f6e011772..af5ca9de5 100644 --- a/src/routes/notifications/actions/index.js +++ b/src/routes/notifications/actions/index.js @@ -9,6 +9,7 @@ import { MARK_ALL_NOTIFICATIONS_READ, TOGGLE_NOTIFICATION_READ, VIEW_OLDER_NOTIFICATIONS_SUCCESS, + HIDE_OLDER_NOTIFICATIONS_SUCCESS, NOTIFICATIONS_PENDING, TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE } from '../../../config/constants' @@ -112,6 +113,10 @@ export const viewOlderNotifications = (sourceId) => (dispatch) => dispatch({ payload: sourceId }) +export const hideOlderNotifications = () => (dispatch) => dispatch({ + type: HIDE_OLDER_NOTIFICATIONS_SUCCESS +}) + export const toggleNotificationsDropdownMobile = (isOpen) => (dispatch) => dispatch({ type: TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE, payload: isOpen diff --git a/src/routes/notifications/containers/NotificationsContainer.jsx b/src/routes/notifications/containers/NotificationsContainer.jsx index 95c04cdd2..ec07beb31 100644 --- a/src/routes/notifications/containers/NotificationsContainer.jsx +++ b/src/routes/notifications/containers/NotificationsContainer.jsx @@ -8,16 +8,16 @@ import { connect } from 'react-redux' import { Link } from 'react-router-dom' import Sticky from 'react-stickynode' import { getNotifications, setNotificationsFilterBy, markAllNotificationsRead, - toggleNotificationRead, viewOlderNotifications, toggleBundledNotificationRead } from '../actions' + toggleNotificationRead, viewOlderNotifications, toggleBundledNotificationRead, hideOlderNotifications } from '../actions' import FooterV2 from '../../../components/FooterV2/FooterV2' import NotificationsSection from '../../../components/NotificationsSection/NotificationsSection' import NotificationsSectionTitle from '../../../components/NotificationsSectionTitle/NotificationsSectionTitle' import SideFilter from '../../../components/SideFilter/SideFilter' import NotificationsEmpty from '../../../components/NotificationsEmpty/NotificationsEmpty' import spinnerWhileLoading from '../../../components/LoadingSpinner' -import { getNotificationsFilters, splitNotificationsBySources, filterReadNotifications, filterOldNotifications } from '../helpers/notifications' +import { getNotificationsFilters, splitNotificationsBySources, filterReadNotifications, limitQuantityInSources } from '../helpers/notifications' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' -import { REFRESH_NOTIFICATIONS_INTERVAL } from '../../../config/constants' +import { REFRESH_NOTIFICATIONS_INTERVAL, NOTIFICATIONS_NEW_PER_SOURCE } from '../../../config/constants' import './NotificationsContainer.scss' class NotificationsContainer extends React.Component { @@ -29,6 +29,7 @@ class NotificationsContainer extends React.Component { componentWillUnmount() { clearInterval(this.autoRefreshNotifications) + this.props.hideOlderNotifications() } render() { @@ -39,8 +40,12 @@ class NotificationsContainer extends React.Component { markAllNotificationsRead, toggleNotificationRead, viewOlderNotifications, oldSourceIds, pending, toggleBundledNotificationRead } = this.props const notReadNotifications = filterReadNotifications(notifications) - const notOldNotifications = filterOldNotifications(notReadNotifications, oldSourceIds) - const notificationsBySources = splitNotificationsBySources(sources, notOldNotifications) + const allNotificationsBySources = splitNotificationsBySources(sources, notReadNotifications) + const notificationsBySources = limitQuantityInSources( + allNotificationsBySources, + NOTIFICATIONS_NEW_PER_SOURCE, + oldSourceIds + ) let globalSource = notificationsBySources.length > 0 && notificationsBySources[0].id === 'global' ? notificationsBySources[0] : null let projectSources = globalSource ? notificationsBySources.slice(1) : notificationsBySources if (filterBy) { @@ -135,6 +140,7 @@ const mapDispatchToProps = { markAllNotificationsRead, toggleNotificationRead, viewOlderNotifications, + hideOlderNotifications, toggleBundledNotificationRead } diff --git a/src/routes/notifications/helpers/notifications.js b/src/routes/notifications/helpers/notifications.js index 781d14c94..b45e2c8e3 100644 --- a/src/routes/notifications/helpers/notifications.js +++ b/src/routes/notifications/helpers/notifications.js @@ -2,13 +2,9 @@ * Helper methods to filter and preprocess notifications */ import _ from 'lodash' -import { OLD_NOTIFICATION_TIME } from '../../../config/constants' import { NOTIFICATION_RULES } from '../constants/notifications' import Handlebars from 'handlebars' -// how many milliseconds in one minute -const MILLISECONDS_IN_MINUTE = 60000 - /** * Handlebars helper to display limited quantity of item and text +N more * @@ -128,19 +124,6 @@ export const splitNotificationsBySources = (sources, notifications) => { */ export const filterReadNotifications = (notifications) => _.filter(notifications, { isRead: false }) -/** - * Filter notifications to only not old or if their source in special oldSourceIds array - * - * @param {Array} notifications list of notifications - * @param {Array} oldSourceIds list of ids of sources that will also show old notifications - * - * @return {Array} list of filtered notifications - */ -export const filterOldNotifications = (notifications, oldSourceIds) => _.filter(notifications, (notification) => ( - _.includes(oldSourceIds, notification.sourceId) || - new Date().getTime() - OLD_NOTIFICATION_TIME * MILLISECONDS_IN_MINUTE < new Date(notification.date).getTime() -)) - /** * Filter notifications that belongs to project:projectId * @@ -189,31 +172,25 @@ export const filterProjectNotifications = (notifications) => _.filter(notificati /** * Limits notifications quantity per source - * and total quantity of notifications * * @param {Array} notificationsBySources list of sources with notifications * @param {Number} maxPerSource maximum number of notifications to include per source - * @param {Number} maxTotal maximum number of notifications in total + * @param {Array} skipSourceIds list of ids of sources that will have all notifications * * @return {Array} list of sources with related notifications */ -export const limitQuantityInSources = (notificationsBySources, maxPerSource, maxTotal) => { - const notificationsBySourceLimited = [] - let total = 0 - let sourceIndex = 0 - - while (total < maxTotal && sourceIndex < notificationsBySources.length) { - const source = notificationsBySources[sourceIndex] - const maxPerThisSource = Math.min(maxTotal - total, maxPerSource) - source.notifications = source.notifications.slice(0, maxPerThisSource) - notificationsBySourceLimited.push(source) - - total += source.notifications.length - sourceIndex += 1 - } +export const limitQuantityInSources = (notificationsBySources, maxPerSource, skipSourceIds) => ( + notificationsBySources.map((source) => { + // clone sources to avoid updating existent objects + const limitedSource = {...source} - return notificationsBySourceLimited -} + if (!_.includes(skipSourceIds, limitedSource.id)) { + limitedSource.notifications = limitedSource.notifications.slice(0, maxPerSource) + } + + return limitedSource + }) +) /** * Get a rule for notification diff --git a/src/routes/notifications/reducers/index.js b/src/routes/notifications/reducers/index.js index bc045f48a..84cf0735f 100644 --- a/src/routes/notifications/reducers/index.js +++ b/src/routes/notifications/reducers/index.js @@ -9,6 +9,7 @@ import { MARK_ALL_NOTIFICATIONS_READ, TOGGLE_NOTIFICATION_READ, VIEW_OLDER_NOTIFICATIONS_SUCCESS, + HIDE_OLDER_NOTIFICATIONS_SUCCESS, NOTIFICATIONS_PENDING, TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE } from '../../../config/constants' @@ -105,6 +106,11 @@ export default (state = initialState, action) => { oldSourceIds: [...state.oldSourceIds, action.payload] } + case HIDE_OLDER_NOTIFICATIONS_SUCCESS: + return {...state, + oldSourceIds: [] + } + case TOGGLE_NOTIFICATIONS_DROPDOWN_MOBILE: return {...state, isDropdownMobileOpen: !_.isUndefined(action.payload) ? action.payload : !state.isDropdownMobileOpen