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) { import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
$('.js-whats-new-notification-count').remove(); .then(({ default: initWhatsNew }) => {
} initWhatsNew(appEl);
}); })
.catch(() => {});
whatsNewTriggerEl.addEventListener('click', () => { });
import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
.then(({ default: initWhatsNew }) => {
initWhatsNew();
})
.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
find('.header-help-dropdown-toggle').click
page.within '.header-help' do page.within '.header-help' do
expect(page).to have_selector('.notification-dot', visible: true)
find('.header-help-dropdown-toggle').click
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,8 +31,10 @@ RSpec.describe 'layouts/application' do ...@@ -31,8 +31,10 @@ 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_selector(track_selector) expect(rendered).to have_css('span', class: 'notification-dot')
expect(rendered).to have_selector(track_selector)
end
end end
end end
...@@ -40,8 +42,10 @@ RSpec.describe 'layouts/application' do ...@@ -40,8 +42,10 @@ RSpec.describe 'layouts/application' 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_selector(track_selector) expect(rendered).not_to have_css('span', class: 'notification-dot')
expect(rendered).not_to have_selector(track_selector)
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