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 ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
import { setNotification } from './whats_new/utils/notification';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
......@@ -14,25 +15,17 @@ function hideEndFade($scrollingTabs) {
function initDeferred() {
$(document).trigger('init.scrolling-tabs');
const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger');
if (whatsNewTriggerEl) {
const storageKey = whatsNewTriggerEl.getAttribute('data-storage-key');
const appEl = document.getElementById('whats-new-app');
if (!appEl) return;
$('.header-help').on('show.bs.dropdown', () => {
const displayNotification = JSON.parse(localStorage.getItem(storageKey));
if (displayNotification === false) {
$('.js-whats-new-notification-count').remove();
}
});
whatsNewTriggerEl.addEventListener('click', () => {
setNotification(appEl);
document.querySelector('.js-whats-new-trigger').addEventListener('click', () => {
import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
.then(({ default: initWhatsNew }) => {
initWhatsNew();
initWhatsNew(appEl);
})
.catch(() => {});
});
}
}
export default function initLayoutNav() {
......
import Vue from 'vue';
import { mapState } from 'vuex';
import App from './components/app.vue';
import store from './store';
import { getStorageKey, setNotification } from './utils/notification';
let whatsNewApp;
export default () => {
export default el => {
if (whatsNewApp) {
store.dispatch('openDrawer');
} else {
const whatsNewElm = document.getElementById('whats-new-app');
const storageKey = getStorageKey(el);
whatsNewApp = new Vue({
el: whatsNewElm,
el,
store,
components: {
App,
},
computed: {
...mapState(['open']),
},
watch: {
open() {
setNotification(el);
},
},
render(createElement) {
return createElement('app', {
props: {
storageKey: whatsNewElm.getAttribute('data-storage-key'),
},
props: { storageKey },
});
},
});
......
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 @@
@include transition(color);
}
a {
a,
.notification-dot {
@include transition(background-color, color, border);
}
......
......@@ -556,12 +556,17 @@
border: 1px solid $gray-normal;
}
.header-user-notification-dot {
.notification-dot {
background-color: $orange-300;
height: 12px;
width: 12px;
right: 8px;
top: -8px;
margin-top: -15px;
pointer-events: none;
visibility: hidden;
}
.with-notifications .notification-dot {
visibility: visible;
}
.with-performance-bar .navbar-gitlab {
......
......@@ -64,14 +64,20 @@
color: $search-and-nav-links;
> 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-avatar {
border-color: $search-and-nav-links;
}
.header-user-notification-dot {
border: 2px solid $nav-svg-color;
}
}
&:hover,
......@@ -84,9 +90,14 @@
fill: currentColor;
}
&.header-user-dropdown-toggle .header-user-notification-dot {
.notification-dot {
will-change: border-color, background-color;
border-color: $nav-svg-color + 33;
}
&.header-help-dropdown-toggle .notification-dot {
background-color: $white;
}
}
}
......@@ -101,9 +112,15 @@
}
}
&.header-user-dropdown-toggle .header-user-notification-dot {
.notification-dot {
border-color: $white;
}
&.header-help-dropdown-toggle {
.notification-dot {
background-color: $nav-svg-color;
}
}
}
.impersonated-user,
......
......@@ -74,6 +74,7 @@
%span.gl-sr-only
= s_('Nav|Help')
= sprite_icon('question')
%span.notification-dot.rounded-circle.gl-absolute
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
......
- 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
sign_in(user)
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
page.within '.header-help' do
expect(page).to have_selector('.notification-dot', visible: true)
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_selector('.js-whats-new-notification-count')
......@@ -27,6 +29,7 @@ RSpec.describe "renders a `whats new` dropdown item", :js do
find('.header-help-dropdown-toggle').click
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).not_to have_selector('.js-whats-new-notification-count')
end
......
......@@ -31,18 +31,22 @@ RSpec.describe 'layouts/application' do
it 'has the notification dot' do
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)
end
end
end
context 'when we do not show the notification dot' do
it 'does not have the notification dot' do
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)
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