diff --git a/app/assets/images/feature_highlight/graphic-boards.png b/app/assets/images/feature_highlight/graphic-boards.png new file mode 100644 index 0000000000000000000000000000000000000000..f0550a383746036811565d58329dbeb9f75a3750 Binary files /dev/null and b/app/assets/images/feature_highlight/graphic-boards.png differ diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js new file mode 100644 index 0000000000000000000000000000000000000000..f65cb3211a609e3d3d6f859e13684e68322aa356 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -0,0 +1,44 @@ +import Cookies from 'js-cookie'; + +export const getCookieName = cookieId => `feature-highlighted-${cookieId}`; +export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; + +export const showPopover = function showPopover() { + this.popover('show'); + this.addClass('disable-animation'); +}; + +export const hidePopover = function hidePopover() { + this.popover('hide'); + this.removeClass('disable-animation'); +}; + +export const dismiss = function dismiss(cookieId) { + Cookies.set(getCookieName(cookieId), true); + hidePopover.call(this); + this.hide(); +}; + +export const mouseenter = function mouseenter() { + const $featureHighlight = $(this); + showPopover.call($featureHighlight); + + document.querySelector('.popover') + .addEventListener('mouseleave', hidePopover.bind($featureHighlight)); +}; + +export const mouseleave = function mouseleave() { + if (!document.querySelector('.popover:hover')) { + const $featureHighlight = $(this); + hidePopover.call($featureHighlight); + } +}; + +export const setupDismissButton = function setupDismissButton() { + const popoverId = this.getAttribute('aria-describedby'); + const cookieId = this.dataset.highlight; + const $popover = $(this); + + document.querySelector(`#${popoverId} .dismiss-feature-highlight`) + .addEventListener('click', dismiss.bind($popover, cookieId)); +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_manager.js b/app/assets/javascripts/feature_highlight/feature_highlight_manager.js new file mode 100644 index 0000000000000000000000000000000000000000..d5588d2c52b6c2d8abca538544f47f7cdb8ae221 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_manager.js @@ -0,0 +1,60 @@ +import Cookies from 'js-cookie'; +import { + getCookieName, + getSelector, + hidePopover, + setupDismissButton, + mouseenter, + mouseleave, +} from './feature_highlight'; + +export default class FeatureHighlightManager { + constructor(highlightOrder) { + this.highlightOrder = highlightOrder; + } + + init() { + const featureId = this.highlightOrder.find(FeatureHighlightManager.shouldHighlightFeature); + + if (featureId) { + FeatureHighlightManager.highlightFeature(featureId); + window.addEventListener('scroll', () => { + const $featureHighlight = $(getSelector(featureId)); + hidePopover.call($featureHighlight); + }); + } + } + + static shouldHighlightFeature(id) { + const element = document.querySelector(getSelector(id)); + const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true'; + + return element && !previouslyDismissed; + } + + static highlightFeature(id) { + const $selector = $(getSelector(id)); + const $parent = $selector.parent(); + const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); + + // Setup popover + $selector.data('content', $popoverContent[0].outerHTML); + $selector.popover({ + html: true, + // Override the existing template to add custom CSS classes + template: ` + + `, + }); + + $selector.on('mouseenter', mouseenter); + $selector.on('mouseleave', mouseleave); + $selector.on('inserted.bs.popover', setupDismissButton); + + // Display feature highlight + $selector.removeAttr('disabled'); + } +} diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js new file mode 100644 index 0000000000000000000000000000000000000000..51e2eec2165feba1355a5308a66f9b57531a412b --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js @@ -0,0 +1,8 @@ +import FeatureHighlightManager from './feature_highlight_manager'; + +const highlightOrder = ['issue-boards']; + +document.addEventListener('DOMContentLoaded', () => { + const featureHighlight = new FeatureHighlightManager(highlightOrder); + featureHighlight.init(); +}); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 97ce2803ee6ae36c9a1f36a4baa723bc6e2a9747..e74f2a8eb1c7fa9d49ac80e15882c4f8712ee921 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -102,6 +102,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 31eb1c54519b71886dc7e5e8f67f5cd9e6b1dc1d..f16201c9d528162f31619e1455a547b05433d70d 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -51,3 +51,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; +@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss new file mode 100644 index 0000000000000000000000000000000000000000..07579a69f1876a73282333f4a1bec7d324359037 --- /dev/null +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -0,0 +1,102 @@ +.feature-highlight { + @include webkit-prefix(animation, pulse-highlight 2s infinite); + + margin-left: 16px; + width: 8px; + height: 8px; + background-color: $blue-500; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 0 rgba(31, 120, 209, 0.4); + + &:hover, + &.disable-animation { + @include webkit-prefix(animation, none); + } + + &[disabled] { + display: none; + } +} + +.is-showing-fly-out { + .feature-highlight { + display: none; + } +} + +.feature-highlight-popover-content { + display: none; + + hr { + margin: $gl-padding * 0.5 0; + } + + // Extend btn-link because it is being overriden by default + .btn-link { + @extend .btn-link; + + &:hover { + @extend .btn-link:hover; + } + + &:focus { + @extend .btn-link:focus; + } + + &:active { + @extend .btn-link:active; + } + + svg path { + fill: currentColor; + } + } + + .dismiss-feature-highlight { + padding: 0; + } + + img { + width: 100%; + padding: 25px 14px 0; + background-color: rgb(239, 237, 249); + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid #e1e1e1; + } +} + +.popover .feature-highlight-popover-content { + display: block; +} + +.feature-highlight-popover { + padding: 0; + + .popover-content { + padding: 0; + } + + &.popover > .arrow::after { + border-right-color: rgb(239, 237, 249); + } +} + +.feature-highlight-popover-sub-content { + padding: 9px 14px; +} + +@include keyframes(pulse-highlight) { + 0% { + @include webkit-prefix(box-shadow, 0 0 0 0 rgba(31, 120, 209, 0.4)); + } + + 70% { + @include webkit-prefix(box-shadow, 0 0 0 10px rgba(31, 120, 209, 0)); + } + + 100% { + @include webkit-prefix(box-shadow, 0 0 0 0 rgba(31, 120, 209, 0)); + } +} diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 115d08230235f82ee30164e744cb699ba23b2271..ec061ff6cdb5a1acd69ce4ad5b4a02eb3ed8bda8 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -97,6 +97,23 @@ = link_to project_boards_path(@project), title: 'Boards' do %span Boards + .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', animation: 'false', placement: 'right', highlight: 'issue-boards' } } + .feature-highlight-popover-content + %img{ :alt => "Issue Boards", :src => image_path('feature_highlight/graphic-boards.png') } + .feature-highlight-popover-sub-content + Use + = link_to 'Issue Boards', project_boards_path(@project) + to create customized software development workflows like + %strong + Scrum + or + %strong + Kanban + %hr + %button.btn.btn-link.dismiss-feature-highlight{ type: 'button'} + Got it! Don't show this again + = render 'shared/icons/icon_thumbs_up.svg' + = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do diff --git a/app/views/shared/icons/_icon_thumbs_up.svg b/app/views/shared/icons/_icon_thumbs_up.svg new file mode 100644 index 0000000000000000000000000000000000000000..7267462418eb5334dc1f7b44856dfed818bf8bec --- /dev/null +++ b/app/views/shared/icons/_icon_thumbs_up.svg @@ -0,0 +1 @@ +