Commit c867b405 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '227259-migrate-feature-highlight-popover-to-gitlab-ui' into 'master'

Migrate feature highlight to Vue

See merge request gitlab-org/gitlab!53487
parents c2e4320b 001a70d7
import $ from 'jquery';
import { togglePopover, mouseenter, debouncedMouseleave } from '../shared/popover';
import { getSelector, inserted } from './feature_highlight_helper';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
$selector
// Set up popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
// Override the existing template to add custom CSS classes
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-body"></div>
</div>
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave(debounceTimeout))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll, { once: true });
})
// Display feature highlight
.removeAttr('disabled');
}
const getPriority = (e) => parseInt(e.dataset.highlightPriority, 10) || 0;
export function findHighestPriorityFeature() {
let priorityFeature;
const sortedFeatureEls = [].slice
.call(document.querySelectorAll('.js-feature-highlight'))
.sort((a, b) => getPriority(b) - getPriority(a));
const [priorityFeatureEl] = sortedFeatureEls;
if (priorityFeatureEl) {
priorityFeature = priorityFeatureEl.dataset.highlight;
}
return priorityFeature;
}
export function highlightFeatures() {
const priorityFeature = findHighestPriorityFeature();
if (priorityFeature) {
setupFeatureHighlightPopover(priorityFeature);
}
return priorityFeature;
}
import $ from 'jquery'; import { deprecatedCreateFlash as Flash } from '~/flash';
import { deprecatedCreateFlash as Flash } from '../flash';
import LazyLoader from '../lazy_loader';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
import { __ } from '../locale'; import { __ } from '../locale';
import { togglePopover } from '../shared/popover';
export const getSelector = (highlightId) => `.js-feature-highlight[data-highlight=${highlightId}]`; export const getSelector = (highlightId) => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function dismiss(highlightId) { export function dismiss(endpoint, highlightId) {
axios return axios
.post(this.attr('data-dismiss-endpoint'), { .post(endpoint, {
feature_name: highlightId, feature_name: highlightId,
}) })
.catch(() => .catch(() =>
...@@ -19,21 +16,4 @@ export function dismiss(highlightId) { ...@@ -19,21 +16,4 @@ export function dismiss(highlightId) {
), ),
), ),
); );
togglePopover.call(this, false);
this.hide();
}
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, highlightId);
$(`#${popoverId} .dismiss-feature-highlight`).on('click', dismissWrapper);
const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
if (lazyImg) {
LazyLoader.loadImage(lazyImg);
}
} }
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { highlightFeatures } from './feature_highlight';
export default function domContentLoaded() {
if (bp.getBreakpointSize() === 'xl') {
highlightFeatures();
return true;
}
return false;
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
...@@ -27,7 +27,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility'; ...@@ -27,7 +27,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else // everything else
import './feature_highlight/feature_highlight_options'; import initFeatureHighlight from './feature_highlight';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import { __ } from './locale'; import { __ } from './locale';
import initLogoAnimation from './logo'; import initLogoAnimation from './logo';
...@@ -114,6 +114,7 @@ function deferredInitialisation() { ...@@ -114,6 +114,7 @@ function deferredInitialisation() {
initFrequentItemDropdowns(); initFrequentItemDropdowns();
initPersistentUserCallouts(); initPersistentUserCallouts();
initDefaultTrackers(); initDefaultTrackers();
initFeatureHighlight();
const search = document.querySelector('#search'); const search = document.querySelector('#search');
if (search) { if (search) {
......
.feature-highlight { .feature-highlight {
position: relative;
margin-left: $gl-padding;
width: 20px;
height: 20px;
cursor: pointer;
&::before { &::before {
content: ''; content: '';
display: block; display: block;
position: absolute;
top: 6px; top: 6px;
left: 6px; left: 6px;
width: 8px; width: 8px;
...@@ -29,56 +22,22 @@ ...@@ -29,56 +22,22 @@
} }
} }
.feature-highlight-popover-content {
display: none;
hr {
margin: $gl-padding 0;
}
.btn-link {
svg {
@include btn-svg;
path {
fill: currentColor;
}
}
}
.feature-highlight-illustration {
width: 100%;
height: 100px;
padding-top: 12px;
padding-bottom: 12px;
background-color: $indigo-50; .feature-highlight-illustration {
border-top-left-radius: 2px; background-color: $indigo-50;
border-top-right-radius: 2px; border-top-left-radius: 2px;
border-bottom: 1px solid darken($gray-normal, 8%); border-top-right-radius: 2px;
} border-bottom: 1px solid darken($gray-normal, 8%);
}
.popover .feature-highlight-popover-content {
display: block;
} }
.feature-highlight-popover { .feature-highlight-popover {
width: 240px; width: 240px;
&.right > .arrow {
border-right-color: $border-color;
}
.popover-body { .popover-body {
padding: 0; padding: 0;
} }
} }
.feature-highlight-popover-sub-content {
padding: $gl-padding $gl-padding-12;
}
@include keyframes(pulse-highlight) { @include keyframes(pulse-highlight) {
0% { 0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4); box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
......
...@@ -284,27 +284,14 @@ ...@@ -284,27 +284,14 @@
%span %span
= _('Kubernetes') = _('Kubernetes')
- if show_cluster_hint - if show_cluster_hint
.feature-highlight.js-feature-highlight{ disabled: true, .js-feature-highlight{ disabled: true,
data: { trigger: 'manual', data: { trigger: 'manual',
container: 'body', container: 'body',
placement: 'right', placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION, highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION], highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: user_callouts_path } } dismiss_endpoint: user_callouts_path,
- if show_cluster_hint auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
.feature-highlight-popover-content
= image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration', lazy: false, alt: _('Kubernetes popover')
.feature-highlight-popover-sub-content
%p= _('Allows you to add and manage Kubernetes clusters.')
%p
= _('Protip:')
= link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
%button.gl-button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span.gl-mr-2= _("Got it!")
= sprite_icon('thumb-up')
- if project_nav_tab? :environments - if project_nav_tab? :environments
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
......
...@@ -17135,9 +17135,6 @@ msgstr "" ...@@ -17135,9 +17135,6 @@ msgstr ""
msgid "Kubernetes error: %{error_code}" msgid "Kubernetes error: %{error_code}"
msgstr "" msgstr ""
msgid "Kubernetes popover"
msgstr ""
msgid "LDAP" msgid "LDAP"
msgstr "" msgstr ""
...@@ -23979,9 +23976,6 @@ msgstr "" ...@@ -23979,9 +23976,6 @@ msgstr ""
msgid "ProtectedEnvironment|Your environment has been unprotected" msgid "ProtectedEnvironment|Your environment has been unprotected"
msgstr "" msgstr ""
msgid "Protip:"
msgstr ""
msgid "Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!" msgid "Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!"
msgstr "" msgstr ""
...@@ -35645,9 +35639,6 @@ msgstr "" ...@@ -35645,9 +35639,6 @@ msgstr ""
msgid "username" msgid "username"
msgstr "" msgstr ""
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
msgid "v%{version} published %{timeAgo}" msgid "v%{version} published %{timeAgo}"
msgstr "" msgstr ""
......
import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter';
import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { togglePopover } from '~/shared/popover'; import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { deprecatedCreateFlash as Flash } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
describe('feature highlight helper', () => { jest.mock('~/flash');
describe('getSelector', () => {
it('returns js-feature-highlight selector', () => {
const highlightId = 'highlightId';
expect(getSelector(highlightId)).toEqual(
`.js-feature-highlight[data-highlight=${highlightId}]`,
);
});
});
describe('feature highlight helper', () => {
describe('dismiss', () => { describe('dismiss', () => {
const context = { let mockAxios;
hide: () => {}, const endpoint = '/-/callouts/dismiss';
attr: () => '/-/callouts/dismiss', const highlightId = '123';
}; const { CREATED, INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => { beforeEach(() => {
jest.spyOn(axios, 'post').mockResolvedValue(); mockAxios = new MockAdapter(axios);
jest.spyOn(togglePopover, 'call').mockImplementation(() => {});
jest.spyOn(context, 'hide').mockImplementation(() => {});
dismiss.call(context);
}); });
it('calls persistent dismissal endpoint', () => { afterEach(() => {
expect(axios.post).toHaveBeenCalledWith( mockAxios.reset();
'/-/callouts/dismiss',
expect.objectContaining({ feature_name: undefined }),
);
}); });
it('calls hide popover', () => { it('calls persistent dismissal endpoint with highlightId', async () => {
expect(togglePopover.call).toHaveBeenCalledWith(context, false); mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(CREATED);
});
it('calls hide', () => { await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
expect(context.hide).toHaveBeenCalled();
}); });
});
describe('inserted', () => { it('triggers flash when dismiss request fails', async () => {
it('registers click event callback', (done) => { mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(INTERNAL_SERVER_ERROR);
const context = {
getAttribute: () => 'popoverId', await dismiss(endpoint, highlightId);
dataset: {
highlight: 'some-feature', expect(Flash).toHaveBeenCalledWith(
}, 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
}; );
jest.spyOn($.fn, 'on').mockImplementation((event) => {
expect(event).toEqual('click');
done();
});
inserted.call(context);
}); });
}); });
}); });
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import domContentLoaded from '~/feature_highlight/feature_highlight_options';
describe('feature highlight options', () => {
describe('domContentLoaded', () => {
it.each`
breakPoint | shouldCall
${'xs'} | ${false}
${'sm'} | ${false}
${'md'} | ${false}
${'lg'} | ${false}
${'xl'} | ${true}
`(
'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall',
({ breakPoint, shouldCall }) => {
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint);
expect(domContentLoaded()).toBe(shouldCall);
},
);
});
});
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import * as featureHighlight from '~/feature_highlight/feature_highlight';
import axios from '~/lib/utils/axios_utils';
import * as popover from '~/shared/popover';
jest.mock('~/shared/popover');
describe('feature highlight', () => {
beforeEach(() => {
setFixtures(`
<div>
<div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled>
Trigger
</div>
</div>
<div class="feature-highlight-popover-content">
Content
<div class="dismiss-feature-highlight">
Dismiss
</div>
</div>
`);
});
describe('setupFeatureHighlightPopover', () => {
let mock;
const selector = '.js-feature-highlight[data-highlight=test]';
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(200);
jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
featureHighlight.setupFeatureHighlightPopover('test', 0);
});
afterEach(() => {
mock.restore();
});
it('setup popover content', () => {
const $popoverContent = $('.feature-highlight-popover-content');
const outerHTML = $popoverContent.prop('outerHTML');
expect($(selector).data('content')).toEqual(outerHTML);
});
it('setup mouseenter', () => {
$(selector).trigger('mouseenter');
expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object));
});
it('setup debounced mouseleave', () => {
$(selector).trigger('mouseleave');
expect(popover.debouncedMouseleave).toHaveBeenCalled();
});
it('setup show.bs.popover', () => {
$(selector).trigger('show.bs.popover');
expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), {
once: true,
});
});
it('removes disabled attribute', () => {
expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
});
});
describe('findHighestPriorityFeature', () => {
beforeEach(() => {
setFixtures(`
<div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
<div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
<div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
`);
});
it('should pick the highest priority feature highlight', () => {
setFixtures(`
<div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
<div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
<div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
`);
expect($('.js-feature-highlight').length).toBeGreaterThan(1);
expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
});
it('should work when no priority is set', () => {
setFixtures(`
<div class="js-feature-highlight" data-highlight="test" disabled></div>
`);
expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
});
it('should pick the highest priority feature highlight when some have no priority set', () => {
setFixtures(`
<div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
<div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
<div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
<div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
<div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
`);
expect($('.js-feature-highlight').length).toBeGreaterThan(1);
expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
});
});
describe('highlightFeatures', () => {
it('calls setupFeatureHighlightPopover', () => {
expect(featureHighlight.highlightFeatures()).toEqual('test');
});
});
});
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