Merge branch '41672-emphasize-gke-cluster-to-new-users' into 'master'
Add feature highlight blue dot to GKE "Clusters" sidebar item Closes #41672 See merge request gitlab-org/gitlab-ce!16379
This commit is contained in:
commit
7c8e7a8d1f
20 changed files with 687 additions and 7 deletions
|
@ -1 +1 @@
|
|||
{"iconCount":189,"spriteSize":85900,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
|
||||
{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
1
app/assets/images/illustrations/cluster_popover.svg
Normal file
1
app/assets/images/illustrations/cluster_popover.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="104" viewBox="0 0 142 104"><g fill="none" fill-rule="evenodd"><g transform="translate(112 4)"><path fill="#FFF" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#FC6D26" rx="5"/></g><g transform="translate(5 74)"><rect width="30" height="30" fill="#FFF" rx="8"/><path fill="#E1DBF1" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#6B4FBB" rx="5"/></g><path fill="#FFF" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#FC6D26" rx="4"/><g transform="translate(112 77)"><rect width="24" height="24" fill="#FFF" rx="6"/><path fill="#E1DBF1" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#6B4FBB" rx="4"/></g><g transform="translate(46 29)"><rect width="46" height="46" y="2" fill="#E1DBF1" rx="10"/><rect width="46" height="46" fill="#E1DBF1" rx="10"/><path fill="#C3B8E3" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v26a6 6 0 0 0 6 6h26a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h26c5.523 0 10 4.477 10 10v26c0 5.523-4.477 10-10 10H10C4.477 46 0 41.523 0 36V10C0 4.477 4.477 0 10 0z"/><rect width="14" height="14" x="16" y="16" fill="#6B4FBB" rx="2"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M98.413 35.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#C3B8E3" d="M104.78 29.32a2 2 0 0 1-2.826-2.829l2.122-2.12a2 2 0 0 1 2.827 2.83l-2.122 2.12z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M42.413 89.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#E1DBF1" d="M48.78 83.32a2 2 0 1 1-2.826-2.829l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.122 2.12z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M27.713 26.531a2 2 0 1 1 2.574-3.062l2.296 1.93a2 2 0 1 1-2.573 3.062l-2.297-1.93z"/><path fill="#C3B8E3" d="M34.604 32.321a2 2 0 1 1 2.573-3.062l2.297 1.93A2 2 0 0 1 36.9 34.25l-2.297-1.93z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M93.74 74.553a2 2 0 0 1 2.52-3.106l2.33 1.891a2 2 0 1 1-2.521 3.106l-2.33-1.891z"/><path fill="#E1DBF1" d="M100.727 80.225a2 2 0 1 1 2.521-3.105l2.33 1.89a2 2 0 1 1-2.522 3.106l-2.33-1.89z"/></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,65 @@
|
|||
import _ from 'underscore';
|
||||
import {
|
||||
getSelector,
|
||||
togglePopover,
|
||||
inserted,
|
||||
mouseenter,
|
||||
mouseleave,
|
||||
} 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);
|
||||
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
|
||||
|
||||
$selector
|
||||
// Setup 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-content"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
.on('mouseenter', mouseenter)
|
||||
.on('mouseleave', debouncedMouseleave)
|
||||
.on('inserted.bs.popover', inserted)
|
||||
.on('show.bs.popover', () => {
|
||||
window.addEventListener('scroll', hideOnScroll);
|
||||
})
|
||||
.on('hide.bs.popover', () => {
|
||||
window.removeEventListener('scroll', hideOnScroll);
|
||||
})
|
||||
// Display feature highlight
|
||||
.removeAttr('disabled');
|
||||
}
|
||||
|
||||
export function findHighestPriorityFeature() {
|
||||
let priorityFeature;
|
||||
|
||||
const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) =>
|
||||
(a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
|
||||
|
||||
const [priorityFeatureEl] = sortedFeatureEls;
|
||||
if (priorityFeatureEl) {
|
||||
priorityFeature = priorityFeatureEl.dataset.highlight;
|
||||
}
|
||||
|
||||
return priorityFeature;
|
||||
}
|
||||
|
||||
export function highlightFeatures() {
|
||||
const priorityFeature = findHighestPriorityFeature();
|
||||
|
||||
if (priorityFeature) {
|
||||
setupFeatureHighlightPopover(priorityFeature);
|
||||
}
|
||||
|
||||
return priorityFeature;
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import axios from '../lib/utils/axios_utils';
|
||||
import { __ } from '../locale';
|
||||
import Flash from '../flash';
|
||||
import LazyLoader from '../lazy_loader';
|
||||
|
||||
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
|
||||
|
||||
export function togglePopover(show) {
|
||||
const isAlreadyShown = this.hasClass('js-popover-show');
|
||||
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
|
||||
return false;
|
||||
}
|
||||
this.popover(show ? 'show' : 'hide');
|
||||
this.toggleClass('disable-animation js-popover-show', show);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function dismiss(highlightId) {
|
||||
axios.post(this.attr('data-dismiss-endpoint'), {
|
||||
feature_name: highlightId,
|
||||
})
|
||||
.catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.')));
|
||||
|
||||
togglePopover.call(this, false);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
export function mouseleave() {
|
||||
if (!$('.popover:hover').length > 0) {
|
||||
const $featureHighlight = $(this);
|
||||
togglePopover.call($featureHighlight, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function mouseenter() {
|
||||
const $featureHighlight = $(this);
|
||||
|
||||
const showedPopover = togglePopover.call($featureHighlight, true);
|
||||
if (showedPopover) {
|
||||
$('.popover')
|
||||
.on('mouseleave', mouseleave.bind($featureHighlight));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { highlightFeatures } from './feature_highlight';
|
||||
import bp from '../breakpoints';
|
||||
|
||||
export default function domContentLoaded() {
|
||||
if (bp.getBreakpointSize() === 'lg') {
|
||||
highlightFeatures();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', domContentLoaded);
|
|
@ -26,6 +26,7 @@ import './gl_dropdown';
|
|||
import initTodoToggle from './header';
|
||||
import initImporterStatus from './importer_status';
|
||||
import initLayoutNav from './layout_nav';
|
||||
import './feature_highlight/feature_highlight_options';
|
||||
import LazyLoader from './lazy_loader';
|
||||
import initLogoAnimation from './logo';
|
||||
import './milestone_select';
|
||||
|
|
|
@ -61,3 +61,4 @@
|
|||
@import "framework/responsive_tables";
|
||||
@import "framework/stacked-progress-bar";
|
||||
@import "framework/ci_variable_list";
|
||||
@import "framework/feature_highlight";
|
||||
|
|
103
app/assets/stylesheets/framework/feature_highlight.scss
Normal file
103
app/assets/stylesheets/framework/feature_highlight.scss
Normal file
|
@ -0,0 +1,103 @@
|
|||
.feature-highlight {
|
||||
position: relative;
|
||||
margin-left: $gl-padding;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: $blue-500;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 rgba($blue-500, 0.4);
|
||||
animation: pulse-highlight 2s infinite;
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&.disable-animation::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&[disabled]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-showing-fly-out {
|
||||
.feature-highlight {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-highlight-popover-content {
|
||||
display: none;
|
||||
|
||||
hr {
|
||||
margin: $gl-padding * 0.5 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;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom: 1px solid darken($gray-normal, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.popover .feature-highlight-popover-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feature-highlight-popover {
|
||||
width: 240px;
|
||||
padding: 0;
|
||||
border: 1px solid $dropdown-border-color;
|
||||
box-shadow: 0 2px 4px $dropdown-shadow-color;
|
||||
|
||||
&.right > .arrow {
|
||||
border-right-color: $dropdown-border-color;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-highlight-popover-sub-content {
|
||||
padding: 9px 14px;
|
||||
}
|
||||
|
||||
@include keyframes(pulse-highlight) {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px transparent;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,6 @@ module UserCalloutsHelper
|
|||
private
|
||||
|
||||
def user_dismissed?(feature_name)
|
||||
current_user&.callouts&.find_by(feature_name: feature_name)
|
||||
current_user&.callouts&.find_by(feature_name: UserCallout.feature_names[feature_name])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -184,10 +184,34 @@
|
|||
Environments
|
||||
|
||||
- if project_nav_tab? :clusters
|
||||
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
|
||||
= nav_link(controller: [:clusters, :user, :gcp]) do
|
||||
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
|
||||
%span
|
||||
Clusters
|
||||
- if show_cluster_hint
|
||||
.feature-highlight.js-feature-highlight{ disabled: true,
|
||||
data: { trigger: 'manual',
|
||||
container: 'body',
|
||||
toggle: 'popover',
|
||||
placement: 'right',
|
||||
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
|
||||
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
|
||||
dismiss_endpoint: user_callouts_path } }
|
||||
- if show_cluster_hint
|
||||
.feature-highlight-popover-content
|
||||
= image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration'
|
||||
.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 clusters to deploy your code!')
|
||||
%hr
|
||||
%button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
|
||||
%span= _("Got it!")
|
||||
= sprite_icon('thumb-up')
|
||||
|
||||
|
||||
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
|
||||
= nav_link(path: 'pipelines#charts') do
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add blue dot feature highlight to make GKE Clusters more visible to users
|
||||
merge_request: 16379
|
||||
author:
|
||||
type: added
|
15
doc/user/feature_highlight.md
Normal file
15
doc/user/feature_highlight.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Feature highlight
|
||||
|
||||
> [Introduced][ce-16379] in GitLab 10.5
|
||||
|
||||
Feature highlights are represented by a pulsing blue dot. Hovering over the dot
|
||||
will open up callout with more information.
|
||||
They are used to emphasize a certain feature and make something more visible to the user.
|
||||
|
||||
You can dismiss any feature highlight permanently by clicking the "Got it" link
|
||||
at the bottom of the callout. There isn't a way to restore the feature highlight
|
||||
after it has been dismissed.
|
||||
|
||||
![Clusters feature highlight](img/feature_highlight_example.png)
|
||||
|
||||
[ce-16379]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16379
|
BIN
doc/user/img/feature_highlight_example.png
Normal file
BIN
doc/user/img/feature_highlight_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -107,6 +107,8 @@ personal access tokens, authorized applications, etc.
|
|||
methods available in GitLab.
|
||||
- [Permissions](permissions.md): Learn the different set of permissions levels for each
|
||||
user type (guest, reporter, developer, master, owner).
|
||||
- [Feature highlight](feature_highlight.md): Learn more about the little blue dots
|
||||
around the app that explain certain features
|
||||
|
||||
## Groups
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
"worker-loader": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitlab-org/gitlab-svgs": "^1.7.0",
|
||||
"@gitlab-org/gitlab-svgs": "^1.8.0",
|
||||
"axios-mock-adapter": "^1.10.0",
|
||||
"babel-plugin-istanbul": "^4.1.5",
|
||||
"eslint": "^3.18.0",
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import {
|
||||
getSelector,
|
||||
togglePopover,
|
||||
dismiss,
|
||||
mouseleave,
|
||||
mouseenter,
|
||||
inserted,
|
||||
} from '~/feature_highlight/feature_highlight_helper';
|
||||
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
|
||||
|
||||
describe('feature highlight helper', () => {
|
||||
describe('getSelector', () => {
|
||||
it('returns js-feature-highlight selector', () => {
|
||||
const highlightId = 'highlightId';
|
||||
expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePopover', () => {
|
||||
describe('togglePopover(true)', () => {
|
||||
it('returns true when popover is shown', () => {
|
||||
const context = {
|
||||
hasClass: () => false,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
expect(togglePopover.call(context, true)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when popover is already shown', () => {
|
||||
const context = {
|
||||
hasClass: () => true,
|
||||
};
|
||||
|
||||
expect(togglePopover.call(context, true)).toEqual(false);
|
||||
});
|
||||
|
||||
it('shows popover', (done) => {
|
||||
const context = {
|
||||
hasClass: () => false,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
spyOn(context, 'popover').and.callFake((method) => {
|
||||
expect(method).toEqual('show');
|
||||
done();
|
||||
});
|
||||
|
||||
togglePopover.call(context, true);
|
||||
});
|
||||
|
||||
it('adds disable-animation and js-popover-show class', (done) => {
|
||||
const context = {
|
||||
hasClass: () => false,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
|
||||
expect(classNames).toEqual('disable-animation js-popover-show');
|
||||
expect(show).toEqual(true);
|
||||
done();
|
||||
});
|
||||
|
||||
togglePopover.call(context, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePopover(false)', () => {
|
||||
it('returns true when popover is hidden', () => {
|
||||
const context = {
|
||||
hasClass: () => true,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
expect(togglePopover.call(context, false)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when popover is already hidden', () => {
|
||||
const context = {
|
||||
hasClass: () => false,
|
||||
};
|
||||
|
||||
expect(togglePopover.call(context, false)).toEqual(false);
|
||||
});
|
||||
|
||||
it('hides popover', (done) => {
|
||||
const context = {
|
||||
hasClass: () => true,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
spyOn(context, 'popover').and.callFake((method) => {
|
||||
expect(method).toEqual('hide');
|
||||
done();
|
||||
});
|
||||
|
||||
togglePopover.call(context, false);
|
||||
});
|
||||
|
||||
it('removes disable-animation and js-popover-show class', (done) => {
|
||||
const context = {
|
||||
hasClass: () => true,
|
||||
popover: () => {},
|
||||
toggleClass: () => {},
|
||||
};
|
||||
|
||||
spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
|
||||
expect(classNames).toEqual('disable-animation js-popover-show');
|
||||
expect(show).toEqual(false);
|
||||
done();
|
||||
});
|
||||
|
||||
togglePopover.call(context, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
let mock;
|
||||
const context = {
|
||||
hide: () => {},
|
||||
attr: () => '/-/callouts/dismiss',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
spyOn(togglePopover, 'call').and.callFake(() => {});
|
||||
spyOn(context, 'hide').and.callFake(() => {});
|
||||
dismiss.call(context);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('calls persistent dismissal endpoint', (done) => {
|
||||
const spy = jasmine.createSpy('dismiss-endpoint-hit');
|
||||
mock.onPost('/-/callouts/dismiss').reply(spy);
|
||||
|
||||
getSetTimeoutPromise()
|
||||
.then(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('calls hide popover', () => {
|
||||
expect(togglePopover.call).toHaveBeenCalledWith(context, false);
|
||||
});
|
||||
|
||||
it('calls hide', () => {
|
||||
expect(context.hide).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mouseleave', () => {
|
||||
it('calls hide popover if .popover:hover is false', () => {
|
||||
const fakeJquery = {
|
||||
length: 0,
|
||||
};
|
||||
|
||||
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
|
||||
spyOn(togglePopover, 'call');
|
||||
mouseleave();
|
||||
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
|
||||
});
|
||||
|
||||
it('does not call hide popover if .popover:hover is true', () => {
|
||||
const fakeJquery = {
|
||||
length: 1,
|
||||
};
|
||||
|
||||
spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
|
||||
spyOn(togglePopover, 'call');
|
||||
mouseleave();
|
||||
expect(togglePopover.call).not.toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mouseenter', () => {
|
||||
const context = {};
|
||||
|
||||
it('shows popover', () => {
|
||||
spyOn(togglePopover, 'call').and.returnValue(false);
|
||||
mouseenter.call(context);
|
||||
expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
|
||||
});
|
||||
|
||||
it('registers mouseleave event if popover is showed', (done) => {
|
||||
spyOn(togglePopover, 'call').and.returnValue(true);
|
||||
spyOn($.fn, 'on').and.callFake((eventName) => {
|
||||
expect(eventName).toEqual('mouseleave');
|
||||
done();
|
||||
});
|
||||
mouseenter.call(context);
|
||||
});
|
||||
|
||||
it('does not register mouseleave event if popover is not showed', () => {
|
||||
spyOn(togglePopover, 'call').and.returnValue(false);
|
||||
const spy = spyOn($.fn, 'on').and.callFake(() => {});
|
||||
mouseenter.call(context);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('inserted', () => {
|
||||
it('registers click event callback', (done) => {
|
||||
const context = {
|
||||
getAttribute: () => 'popoverId',
|
||||
dataset: {
|
||||
highlight: 'some-feature',
|
||||
},
|
||||
};
|
||||
|
||||
spyOn($.fn, 'on').and.callFake((event) => {
|
||||
expect(event).toEqual('click');
|
||||
done();
|
||||
});
|
||||
inserted.call(context);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import domContentLoaded from '~/feature_highlight/feature_highlight_options';
|
||||
import bp from '~/breakpoints';
|
||||
|
||||
describe('feature highlight options', () => {
|
||||
describe('domContentLoaded', () => {
|
||||
it('should not call highlightFeatures when breakpoint is xs', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call highlightFeatures when breakpoint is sm', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not call highlightFeatures when breakpoint is md', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('md');
|
||||
|
||||
expect(domContentLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call highlightFeatures when breakpoint is lg', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
|
||||
|
||||
expect(domContentLoaded()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
131
spec/javascripts/feature_highlight/feature_highlight_spec.js
Normal file
131
spec/javascripts/feature_highlight/feature_highlight_spec.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
|
||||
import * as featureHighlight from '~/feature_highlight/feature_highlight';
|
||||
|
||||
describe('feature highlight', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div>
|
||||
<div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled>
|
||||
Trigger
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-highlight-popover-content">
|
||||
Content
|
||||
<div class="dismiss-feature-highlight">
|
||||
Dismiss
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
describe('setupFeatureHighlightPopover', () => {
|
||||
const selector = '.js-feature-highlight[data-highlight=test]';
|
||||
beforeEach(() => {
|
||||
spyOn(window, 'addEventListener');
|
||||
spyOn(window, 'removeEventListener');
|
||||
featureHighlight.setupFeatureHighlightPopover('test', 0);
|
||||
});
|
||||
|
||||
it('setup popover content', () => {
|
||||
const $popoverContent = $('.feature-highlight-popover-content');
|
||||
const outerHTML = $popoverContent.prop('outerHTML');
|
||||
|
||||
expect($(selector).data('content')).toEqual(outerHTML);
|
||||
});
|
||||
|
||||
it('setup mouseenter', () => {
|
||||
const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
|
||||
$(selector).trigger('mouseenter');
|
||||
|
||||
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
|
||||
});
|
||||
|
||||
it('setup debounced mouseleave', (done) => {
|
||||
const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
|
||||
$(selector).trigger('mouseleave');
|
||||
|
||||
// Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
|
||||
setTimeout(() => {
|
||||
expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('setup inserted.bs.popover', () => {
|
||||
$(selector).trigger('mouseenter');
|
||||
const popoverId = $(selector).attr('aria-describedby');
|
||||
const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
|
||||
|
||||
$(`#${popoverId} .dismiss-feature-highlight`).click();
|
||||
expect(spyEvent).toHaveBeenTriggered();
|
||||
});
|
||||
|
||||
it('setup show.bs.popover', () => {
|
||||
$(selector).trigger('show.bs.popover');
|
||||
expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('setup hide.bs.popover', () => {
|
||||
$(selector).trigger('hide.bs.popover');
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('removes disabled attribute', () => {
|
||||
expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('displays popover', () => {
|
||||
expect($(selector).attr('aria-describedby')).toBeFalsy();
|
||||
$(selector).trigger('mouseenter');
|
||||
expect($(selector).attr('aria-describedby')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -54,9 +54,9 @@
|
|||
lodash "^4.2.0"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@gitlab-org/gitlab-svgs@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.7.0.tgz#dbb1330a1b1ee478378dddab53fe1a881e810f5d"
|
||||
"@gitlab-org/gitlab-svgs@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.8.0.tgz#95d6afa94395860699ddad60a82bd1bbbc2ba89f"
|
||||
|
||||
"@types/jquery@^2.0.40":
|
||||
version "2.0.48"
|
||||
|
|
Loading…
Reference in a new issue