Commit 763f41de authored by jejacks0n's avatar jejacks0n Committed by Miguel Rincon

Add what’s new notification icon

- This shows on the header help navigation item. This also attempts to
consolidate the notification icons, even though the designs differ.
parent 12bcafff
import $ from 'jquery'; import $ from 'jquery';
import ContextualSidebar from './contextual_sidebar'; import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav'; import initFlyOutNav from './fly_out_nav';
import { setNotification } from './whats_new/utils/notification';
function hideEndFade($scrollingTabs) { function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() { $scrollingTabs.each(function scrollTabsLoop() {
...@@ -14,25 +15,17 @@ function hideEndFade($scrollingTabs) { ...@@ -14,25 +15,17 @@ function hideEndFade($scrollingTabs) {
function initDeferred() { function initDeferred() {
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger'); const appEl = document.getElementById('whats-new-app');
if (whatsNewTriggerEl) { if (!appEl) return;
const storageKey = whatsNewTriggerEl.getAttribute('data-storage-key');
$('.header-help').on('show.bs.dropdown', () => { setNotification(appEl);
const displayNotification = JSON.parse(localStorage.getItem(storageKey)); document.querySelector('.js-whats-new-trigger').addEventListener('click', () => {
if (displayNotification === false) {
$('.js-whats-new-notification-count').remove();
}
});
whatsNewTriggerEl.addEventListener('click', () => {
import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new') import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
.then(({ default: initWhatsNew }) => { .then(({ default: initWhatsNew }) => {
initWhatsNew(); initWhatsNew(appEl);
}) })
.catch(() => {}); .catch(() => {});
}); });
}
} }
export default function initLayoutNav() { export default function initLayoutNav() {
......
import Vue from 'vue'; import Vue from 'vue';
import { mapState } from 'vuex';
import App from './components/app.vue'; import App from './components/app.vue';
import store from './store'; import store from './store';
import { getStorageKey, setNotification } from './utils/notification';
let whatsNewApp; let whatsNewApp;
export default () => { export default el => {
if (whatsNewApp) { if (whatsNewApp) {
store.dispatch('openDrawer'); store.dispatch('openDrawer');
} else { } else {
const whatsNewElm = document.getElementById('whats-new-app'); const storageKey = getStorageKey(el);
whatsNewApp = new Vue({ whatsNewApp = new Vue({
el: whatsNewElm, el,
store, store,
components: { components: {
App, App,
}, },
computed: {
...mapState(['open']),
},
watch: {
open() {
setNotification(el);
},
},
render(createElement) { render(createElement) {
return createElement('app', { return createElement('app', {
props: { props: { storageKey },
storageKey: whatsNewElm.getAttribute('data-storage-key'),
},
}); });
}, },
}); });
......
export const getStorageKey = appEl => appEl.getAttribute('data-storage-key');
export const setNotification = appEl => {
const storageKey = getStorageKey(appEl);
const notificationEl = document.querySelector('.header-help');
let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
if (JSON.parse(localStorage.getItem(storageKey)) === false) {
notificationEl.classList.remove('with-notifications');
if (notificationCountEl) {
notificationCountEl.parentElement.removeChild(notificationCountEl);
notificationCountEl = null;
}
} else {
notificationEl.classList.add('with-notifications');
}
};
...@@ -103,7 +103,8 @@ ...@@ -103,7 +103,8 @@
@include transition(color); @include transition(color);
} }
a { a,
.notification-dot {
@include transition(background-color, color, border); @include transition(background-color, color, border);
} }
......
...@@ -556,12 +556,17 @@ ...@@ -556,12 +556,17 @@
border: 1px solid $gray-normal; border: 1px solid $gray-normal;
} }
.header-user-notification-dot { .notification-dot {
background-color: $orange-300; background-color: $orange-300;
height: 12px; height: 12px;
width: 12px; width: 12px;
right: 8px; margin-top: -15px;
top: -8px; pointer-events: none;
visibility: hidden;
}
.with-notifications .notification-dot {
visibility: visible;
} }
.with-performance-bar .navbar-gitlab { .with-performance-bar .navbar-gitlab {
......
...@@ -64,14 +64,20 @@ ...@@ -64,14 +64,20 @@
color: $search-and-nav-links; color: $search-and-nav-links;
> a { > a {
.notification-dot {
border: 2px solid $nav-svg-color;
}
&.header-help-dropdown-toggle {
.notification-dot {
background-color: $search-and-nav-links;
}
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
.header-user-avatar { .header-user-avatar {
border-color: $search-and-nav-links; border-color: $search-and-nav-links;
} }
.header-user-notification-dot {
border: 2px solid $nav-svg-color;
}
} }
&:hover, &:hover,
...@@ -84,9 +90,14 @@ ...@@ -84,9 +90,14 @@
fill: currentColor; fill: currentColor;
} }
&.header-user-dropdown-toggle .header-user-notification-dot { .notification-dot {
will-change: border-color, background-color;
border-color: $nav-svg-color + 33; border-color: $nav-svg-color + 33;
} }
&.header-help-dropdown-toggle .notification-dot {
background-color: $white;
}
} }
} }
...@@ -101,9 +112,15 @@ ...@@ -101,9 +112,15 @@
} }
} }
&.header-user-dropdown-toggle .header-user-notification-dot { .notification-dot {
border-color: $white; border-color: $white;
} }
&.header-help-dropdown-toggle {
.notification-dot {
background-color: $nav-svg-color;
}
}
} }
.impersonated-user, .impersonated-user,
......
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
%span.gl-sr-only %span.gl-sr-only
= s_('Nav|Help') = s_('Nav|Help')
= sprite_icon('question') = sprite_icon('question')
%span.notification-dot.rounded-circle.gl-absolute
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right .dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown' = render 'layouts/header/help_dropdown'
......
- return unless show_pipeline_minutes_notification_dot?(project, namespace) - return unless show_pipeline_minutes_notification_dot?(project, namespace)
%span.header-user-notification-dot.rounded-circle.position-relative{ data: { track_label: "show_buy_ci_minutes_notification", track_property: current_user.namespace.actual_plan_name, track_event: 'render' } } %span.notification-dot.rounded-circle.gl-absolute.gl-visibility-visible{ data: { track_label: "show_buy_ci_minutes_notification", track_property: current_user.namespace.actual_plan_name, track_event: 'render' } }
...@@ -11,12 +11,14 @@ RSpec.describe "renders a `whats new` dropdown item", :js do ...@@ -11,12 +11,14 @@ RSpec.describe "renders a `whats new` dropdown item", :js do
sign_in(user) sign_in(user)
end end
it 'shows notification count and removes it once viewed' do it 'shows notification dot and count and removes it once viewed' do
visit root_dashboard_path visit root_dashboard_path
page.within '.header-help' do
expect(page).to have_selector('.notification-dot', visible: true)
find('.header-help-dropdown-toggle').click find('.header-help-dropdown-toggle').click
page.within '.header-help' do
expect(page).to have_button(text: "See what's new at GitLab") expect(page).to have_button(text: "See what's new at GitLab")
expect(page).to have_selector('.js-whats-new-notification-count') expect(page).to have_selector('.js-whats-new-notification-count')
...@@ -27,6 +29,7 @@ RSpec.describe "renders a `whats new` dropdown item", :js do ...@@ -27,6 +29,7 @@ RSpec.describe "renders a `whats new` dropdown item", :js do
find('.header-help-dropdown-toggle').click find('.header-help-dropdown-toggle').click
page.within '.header-help' do page.within '.header-help' do
expect(page).not_to have_selector('.notification-dot', visible: true)
expect(page).to have_button(text: "See what's new at GitLab") expect(page).to have_button(text: "See what's new at GitLab")
expect(page).not_to have_selector('.js-whats-new-notification-count') expect(page).not_to have_selector('.js-whats-new-notification-count')
end end
......
...@@ -31,18 +31,22 @@ RSpec.describe 'layouts/application' do ...@@ -31,18 +31,22 @@ RSpec.describe 'layouts/application' do
it 'has the notification dot' do it 'has the notification dot' do
render render
expect(rendered).to have_css('span', class: 'header-user-notification-dot') expect(rendered).to have_css('li', class: 'header-user') do
expect(rendered).to have_css('span', class: 'notification-dot')
expect(rendered).to have_selector(track_selector) expect(rendered).to have_selector(track_selector)
end end
end end
end
context 'when we do not show the notification dot' do context 'when we do not show the notification dot' do
it 'does not have the notification dot' do it 'does not have the notification dot' do
render render
expect(rendered).not_to have_css('span', class: 'header-user-notification-dot') expect(rendered).to have_css('li', class: 'header-user') do
expect(rendered).not_to have_css('span', class: 'notification-dot')
expect(rendered).not_to have_selector(track_selector) expect(rendered).not_to have_selector(track_selector)
end end
end end
end end
end
end end
<div class='whats-new-notification-fixture-root'>
<div class='app' data-storage-key='storage-key'></div>
<div class='header-help'>
<div class='js-whats-new-notification-count'></div>
</div>
</div>
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { setNotification, getStorageKey } from '~/whats_new/utils/notification';
describe('~/whats_new/utils/notification', () => {
useLocalStorageSpy();
let wrapper;
const findNotificationEl = () => wrapper.querySelector('.header-help');
const findNotificationCountEl = () => wrapper.querySelector('.js-whats-new-notification-count');
const getAppEl = () => wrapper.querySelector('.app');
beforeEach(() => {
loadFixtures('static/whats_new_notification.html');
wrapper = document.querySelector('.whats-new-notification-fixture-root');
});
afterEach(() => {
wrapper.remove();
});
describe('setNotification', () => {
const subject = () => setNotification(getAppEl());
it("when storage key doesn't exist it adds notifications class", () => {
const notificationEl = findNotificationEl();
expect(notificationEl.classList).not.toContain('with-notifications');
subject();
expect(findNotificationCountEl()).toExist();
expect(notificationEl.classList).toContain('with-notifications');
});
it('removes class and count element when storage key is true', () => {
const notificationEl = findNotificationEl();
notificationEl.classList.add('with-notifications');
localStorage.setItem('storage-key', 'false');
expect(findNotificationCountEl()).toExist();
subject();
expect(findNotificationCountEl()).not.toExist();
expect(notificationEl.classList).not.toContain('with-notifications');
});
});
describe('getStorageKey', () => {
it('retrieves the storage key data attribute from the el', () => {
expect(getStorageKey(getAppEl())).toBe('storage-key');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment