Commit 0101d4ad authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'jj-notification-icon' into 'master'

Add a top level notification dot for what's new

See merge request gitlab-org/gitlab!48416
parents d86e3c15 763f41de
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