Skip to content
Snippets Groups Projects
Unverified Commit 04109f4b authored by Doug Stull's avatar Doug Stull
Browse files

Remove frontend code for onboarding tour

- no longer needed.
parent f3d635d3
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 1202 deletions
Loading
Loading
@@ -100,26 +100,6 @@
}
}
 
.onboarding-popover {
box-shadow: 0 2px 4px $dropdown-shadow-color;
max-width: 280px;
.popover-body {
font-size: $gl-font-size;
line-height: $gl-line-height;
padding: $gl-padding;
}
.popover-header {
display: none;
}
.accept-mr-label {
background-color: $accepting-mr-label-color;
color: $white;
}
}
/**
* user_popover component
*/
Loading
Loading
@@ -132,13 +112,6 @@
}
}
 
.onboarding-welcome-page {
.popover {
min-width: auto;
max-width: 40%;
}
}
.suggest-gitlab-ci-yml {
margin-top: -1em;
 
Loading
Loading
Loading
Loading
@@ -553,41 +553,6 @@ img.emoji {
}
}
 
.onboarding-helper-container {
bottom: 40px;
right: 40px;
font-size: $gl-font-size-small;
background: $gray-50;
width: 200px;
border-radius: 24px;
box-shadow: 0 2px 4px $issue-boards-card-shadow;
z-index: 10000;
.collapsible {
max-height: 0;
transition: max-height 0.5s cubic-bezier(0, 1, 0, 1);
}
&.expanded {
border-bottom-right-radius: $border-radius-default;
border-bottom-left-radius: $border-radius-default;
.collapsible {
max-height: 1000px;
transition: max-height 1s ease-in-out;
}
}
.avatar {
border-color: darken($gray-normal, 10%);
img {
width: 32px;
height: 32px;
}
}
}
.gl-font-sm { font-size: $gl-font-size-small; }
.gl-font-lg { font-size: $gl-font-size-large; }
.gl-font-base { font-size: $gl-font-size-14; }
Loading
Loading
Loading
Loading
@@ -711,7 +711,6 @@ $input-lg-width: 320px;
*/
$document-index-color: #888;
$help-shortcut-header-color: #333;
$accepting-mr-label-color: #69d100;
 
/*
* Issues
Loading
Loading
Loading
Loading
@@ -13,6 +13,4 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= footer_message
 
= render_if_exists "shared/onboarding_guide"
= yield :scripts_body
Loading
Loading
@@ -35,7 +35,6 @@
= link_to _("Help"), help_path
%li.d-md-none
= link_to _("Support"), support_url
= render_if_exists "shared/learn_gitlab_menu_item"
%li.d-md-none
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
Loading
Loading
Loading
Loading
@@ -9,7 +9,6 @@
%button.js-shortcuts-modal-trigger{ type: "button" }
= _("Keyboard shortcuts")
%span.text-secondary.float-right{ "aria-hidden": true }= '?'.html_safe
= render_if_exists "shared/learn_gitlab_menu_item"
%li.divider
%li
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
Loading
Loading
import $ from 'jquery';
import initEETrialBanner from 'ee/ee_trial_banner';
import trackNavbarEvents from 'ee/event_tracking/navbar';
import initOnboarding from 'ee/onboarding/onboarding_helper';
 
$(() => {
/**
Loading
Loading
@@ -13,6 +12,4 @@ $(() => {
initEETrialBanner();
 
trackNavbarEvents();
initOnboarding();
});
import { s__, sprintf } from '~/locale';
import { glEmojiTag } from '~/emoji';
export const ONBOARDING_DISMISSED_COOKIE_NAME = 'onboarding_dismissed';
export const STORAGE_KEY = 'onboarding_state';
export const AVAILABLE_TOURS = {
GUIDED_GITLAB_TOUR: 1,
CREATE_PROJECT_TOUR: 2,
INVITE_COLLEAGUES_TOUR: 3,
};
export const TOUR_TITLES = [
{ id: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR, title: s__('UserOnboardingTour|Guided GitLab Tour') },
{ id: AVAILABLE_TOURS.CREATE_PROJECT_TOUR, title: s__('UserOnboardingTour|Create a project') },
{
id: AVAILABLE_TOURS.INVITE_COLLEAGUES_TOUR,
title: s__('UserOnboardingTour|Invite colleagues'),
},
];
export const ONBOARDING_PROPS_DEFAULTS = {
tourKey: AVAILABLE_TOURS.GUIDED_GITLAB_TOUR,
lastStepIndex: -1,
createdProjectPath: '',
};
export const ACCEPTING_MR_LABEL_TEXT = 'Accepting merge requests';
export const LABEL_SEARCH_QUERY = `scope=all&state=opened&label_name[]=${encodeURIComponent(
ACCEPTING_MR_LABEL_TEXT,
)}`;
export const FEEDBACK_CONTENT = {
text: sprintf(
s__(
"UserOnboardingTour|Great job! %{clapHands} We hope the tour was helpful and that you learned how to use GitLab.%{lineBreak}%{lineBreak}We'd love to get your feedback on this tour.%{lineBreak}%{lineBreak}%{emphasisStart}How helpful would you say this guided tour was?%{emphasisEnd}%{lineBreak}%{lineBreak}",
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
lineBreak: '<br/>',
clapHands: glEmojiTag('clap'),
},
false,
),
feedbackButtons: true,
feedbackSize: 5,
};
export const EXIT_TOUR_CONTENT = {
text: sprintf(
s__('UserOnboardingTour|Thanks for the feedback! %{thumbsUp}'),
{
thumbsUp: glEmojiTag('thumbsup'),
},
false,
),
buttonText: s__("UserOnboardingTour|Close 'Learn GitLab'"),
exitTour: true,
};
export const DNT_EXIT_TOUR_CONTENT = {
text: sprintf(
s__(
'UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right.',
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
},
false,
),
buttonText: s__('UserOnboardingTour|Got it'),
exitTour: true,
};
import onboardingUtils from './utils';
import { AVAILABLE_TOURS } from './constants';
export const getProjectPath = () => {
let projectPath;
const activeTab = document.querySelector('.js-toggle-container.active');
const projectPathInput = activeTab.querySelector('#project_path');
const select = activeTab.querySelector('select.js-select-namespace');
if (!projectPathInput) {
return '';
}
if (select) {
const selectedOption = select.options[select.selectedIndex];
const { showPath } = selectedOption.dataset;
projectPath = `${showPath}/${projectPathInput.value}`;
} else {
projectPath = projectPathInput.value;
}
return projectPath;
};
/**
* Binds a submit event handler to the form on the "New project" page (for user onboarding only).
* It intercepts form submit and sets the project path of project to be created on the localStorage.
* The project path is used later in the onboarding process.
*
* @param {*} form The form we're going to add the submit event handler to
*/
export const bindOnboardingEvents = form => {
if (!form) {
return;
}
const onboardingState = onboardingUtils.getOnboardingLocalStorageState();
if (
!onboardingUtils.isOnboardingDismissed() &&
onboardingState &&
onboardingState.tourKey === AVAILABLE_TOURS.CREATE_PROJECT_TOUR
) {
form.addEventListener('submit', event => {
event.preventDefault();
event.stopPropagation();
const createdProjectPath = getProjectPath();
onboardingUtils.updateLocalStorage({ createdProjectPath });
form.submit();
});
}
};
import Vue from 'vue';
import ActionPopover from './components/action_popover.vue';
// retry for 10 times (5 seconds in total)
const maxTries = 10;
const timeout = 500;
const mountComponent = (intervalId, el, { target, content, placement, showPopover }) => {
clearInterval(intervalId);
return new Vue({
el,
render(h) {
return h(ActionPopover, {
props: {
target,
content,
placement,
showDefault: showPopover,
},
});
},
});
};
const renderPopover = (popoverSelector, content, placement, showPopover) => {
const popoverContainer = document.getElementById('js-onboarding-action-popover');
let retry = 0;
if (!popoverContainer) {
return false;
}
// continuously check if target element already exists (might be delayed to to dynamic component creation)
const intervalId = setInterval(() => {
if (retry >= maxTries) {
clearInterval(intervalId);
}
retry += 1;
const target = document.querySelector(popoverSelector);
if (!target) {
return false;
}
return mountComponent(intervalId, popoverContainer, {
target,
content,
placement,
showPopover,
});
}, timeout);
return intervalId;
};
const actionPopoverUtils = {
renderPopover,
};
export default actionPopoverUtils;
<script>
import { GlPopover } from '@gitlab/ui';
import eventHub from '../event_hub';
export default {
name: 'ActionPopover',
components: {
GlPopover,
},
props: {
target: {
type: HTMLElement,
required: true,
},
content: {
type: String,
required: false,
default: '',
},
placement: {
type: String,
required: false,
default: 'top',
},
showDefault: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showPopover: this.showDefault,
};
},
mounted() {
eventHub.$on('onboardingHelper.showActionPopover', () => this.toggleShowPopover(true));
eventHub.$on('onboardingHelper.hideActionPopover', () => this.toggleShowPopover(false));
eventHub.$on('onboardingHelper.destroyActionPopover', () =>
this.$root.$off('bv::popover::show'),
);
},
beforeDestroy() {
eventHub.$off('onboardingHelper.showActionPopover');
eventHub.$off('onboardingHelper.hideActionPopover');
eventHub.$off('onboardingHelper.destroyActionPopover');
},
methods: {
toggleShowPopover(show) {
this.showPopover = show;
},
},
};
</script>
<template>
<gl-popover
v-bind="$attrs"
:target="target"
boundary="viewport"
:placement="placement"
:show="showPopover"
:css-classes="['blue', 'onboarding-popover']"
>
<div v-html="content"></div>
</gl-popover>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { redirectTo } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import OnboardingHelper from './onboarding_helper.vue';
import actionPopoverUtils from '../action_popover_utils';
import eventHub from '../event_hub';
const TRACKING_CATEGORY = 'onboarding';
export default {
components: {
OnboardingHelper,
},
props: {
tourTitles: {
type: Array,
required: true,
},
feedbackContent: {
type: Object,
required: true,
},
dntExitTourContent: {
type: Object,
required: true,
},
exitTourContent: {
type: Object,
required: true,
},
goldenTanukiSvgPath: {
type: String,
required: true,
},
},
data() {
return {
showStepContent: false,
initialShowPopover: false,
dismissPopover: false,
};
},
computed: {
...mapState([
'projectName',
'tourKey',
'tourData',
'lastStepIndex',
'helpContentIndex',
'tourFeedback',
'exitTour',
'dntExitTour',
'dismissed',
]),
...mapGetters([
'stepIndex',
'stepContent',
'helpContent',
'totalTourPartSteps',
'percentageCompleted',
'actionPopover',
]),
helpContentData() {
if (!this.showStepContent) return null;
if (this.exitTour) return this.exitTourContent;
if (this.tourFeedback) return this.feedbackContent;
if (this.dntExitTour) return this.dntExitTourContent;
return this.helpContent;
},
completedSteps() {
return Math.max(this.lastStepIndex, 0);
},
},
mounted() {
this.init();
},
methods: {
...mapActions([
'setTourKey',
'setLastStepIndex',
'setHelpContentIndex',
'switchTourPart',
'setExitTour',
'setTourFeedback',
'setDntExitTour',
'setDismissed',
]),
init() {
// ensure we show help content on consecutive pages only
if (this.tourKey) {
const nextStepIndex = this.lastStepIndex + 1;
// show help content when the current was the last visited page (e.g., user navigates away and comes back to current page)
if (this.lastStepIndex === this.stepIndex) {
this.showStepContent = true;
this.initActionPopover();
// show help content when this is the upcoming page in the content list (otherwise don't show the help content)
// and update the lastStepIndex
} else if (nextStepIndex === this.stepIndex) {
this.setLastStepIndex(nextStepIndex);
this.showStepContent = true;
this.initActionPopover();
}
}
},
initActionPopover() {
if (this.actionPopover) {
const { selector, text, placement } = this.actionPopover;
// immediately show the action popover if there's not helpContent for this step
const showPopover = !this.helpContent && selector !== undefined;
actionPopoverUtils.renderPopover(selector, text, placement, showPopover);
}
},
showActionPopover() {
eventHub.$emit('onboardingHelper.showActionPopover');
},
hideActionPopover() {
eventHub.$emit('onboardingHelper.hideActionPopover');
},
handleRestartStep() {
this.showExitTourContent(false);
this.handleFeedbackTourContent(false);
Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: this.getTrackingLabel(),
property: 'restart_this_step',
});
eventHub.$emit('onboardingHelper.hideActionPopover');
},
handleSkipStep() {
if (this.actionPopover) {
const { selector } = this.actionPopover;
const popoverEl = selector ? document.querySelector(selector) : null;
if (popoverEl) {
Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: this.getTrackingLabel(),
property: 'skip_this_step',
});
popoverEl.click();
}
}
},
handleStepContentButton(button) {
const { showExitTourContent, redirectPath, nextPart, dismissPopover } = button;
const helpContentItems = this.stepContent
? this.stepContent.getHelpContent({ projectName: this.projectName })
: null;
const showNextContentItem =
helpContentItems &&
helpContentItems.length > 1 &&
this.helpContentIndex < helpContentItems.length - 1;
// display exit tour content
if (showExitTourContent) {
this.handleShowExitTourContent(true);
return;
}
// dismiss popover if necessary
if (dismissPopover === undefined || dismissPopover === true) {
this.dismissPopover = true;
}
// redirect to redirectPath
if (redirectPath) {
redirectTo(redirectPath);
return;
}
// switch to the next tour part
if (nextPart !== undefined) {
this.switchTourPart(nextPart);
this.initActionPopover();
return;
}
// switch to next content item
if (showNextContentItem) {
this.setHelpContentIndex(this.helpContentIndex + 1);
return;
}
Tracking.event(TRACKING_CATEGORY, 'click_button', {
label: this.getTrackingLabel(),
property: 'got_it',
});
this.showActionPopover();
},
handleFeedbackButton(button) {
const { feedbackResult } = button;
// track feedback
if (feedbackResult) this.trackFeedback(feedbackResult);
// display exit tour content
this.handleShowExitTourContent(true);
},
trackFeedback(feedbackResult) {
Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: 'feedback',
property: 'feedback_result',
value: feedbackResult,
});
},
handleShowExitTourContent(showExitTour) {
Tracking.event(TRACKING_CATEGORY, 'click_link', {
label: this.getTrackingLabel(),
property: 'exit_learn_gitlab',
});
this.showExitTourContent(showExitTour);
},
handleFeedbackTourContent(showTourFeedback) {
this.configureEndingTourPopup();
this.setTourFeedback(showTourFeedback);
},
handleDntExitTourContent(showExitTour) {
this.configureEndingTourPopup();
this.setDntExitTour(showExitTour);
},
showExitTourContent(showExitTour) {
this.configureEndingTourPopup();
this.setExitTour(showExitTour);
},
configureEndingTourPopup() {
this.dismissPopover = false;
this.showStepContent = true;
},
handleExitTourButton() {
this.hideActionPopover();
this.setDismissed(true);
// remove popover event handlers
eventHub.$emit('onboardingHelper.destroyActionPopover');
},
afterAppearHook() {
this.initialShowPopover = true;
},
getTrackingLabel() {
const step = this.stepIndex + 1;
return `part_${this.tourKey}_step_${step}`;
},
},
};
</script>
<template>
<transition appear name="slide-in-fwd-bottom" @after-appear="afterAppearHook">
<onboarding-helper
v-if="!dismissed"
:tour-titles="tourTitles"
:active-tour="tourKey"
:completed-steps="completedSteps"
:help-content="helpContentData"
:percentage-completed="percentageCompleted"
:total-steps-for-tour="totalTourPartSteps"
:initial-show="initialShowPopover"
:dismiss-popover="dismissPopover"
:golden-tanuki-svg-path="goldenTanukiSvgPath"
@clickStepContentButton="handleStepContentButton"
@clickExitTourButton="handleExitTourButton"
@clickFeedbackButton="handleFeedbackButton"
@restartStep="handleRestartStep"
@skipStep="handleSkipStep"
@showFeedbackContent="handleFeedbackTourContent"
@showDntExitContent="handleDntExitTourContent"
@showExitTourContent="handleShowExitTourContent"
/>
</transition>
</template>
<script>
import { GlPopover, GlDeprecatedButton, GlButtonGroup } from '@gitlab/ui';
export default {
name: 'HelpContentPopover',
components: {
GlPopover,
GlDeprecatedButton,
GlButtonGroup,
},
props: {
target: {
type: HTMLElement,
required: true,
},
helpContent: {
type: Object,
required: false,
default: null,
},
placement: {
type: String,
required: false,
default: 'top',
},
show: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
callStepContentButton(button) {
this.$emit('clickStepContentButton', button);
},
callExitTour() {
this.$emit('clickExitTourButton');
},
submitFeedback(button) {
this.$emit('clickFeedbackButton', button);
},
},
};
</script>
<template>
<gl-popover
v-bind="$attrs"
:target="target"
:placement="placement"
:show="show"
:disabled="disabled"
:css-classes="['onboarding-popover']"
>
<div>
<p v-html="helpContent.text"></p>
<template v-if="helpContent.buttons">
<template v-for="(button, index) in helpContent.buttons">
<gl-deprecated-button
v-if="!button.readOnly"
:key="index"
:class="button.btnClass"
class="btn btn-sm mr-2"
@click="callStepContentButton(button)"
>
{{ button.text }}
</gl-deprecated-button>
<span v-else :key="index" :class="button.btnClass" class="btn btn-sm mr-2">
{{ button.text }}
</span>
</template>
</template>
<template v-if="helpContent.exitTour">
<gl-deprecated-button class="btn btn-sm btn-primary mr-2" @click="callExitTour">
{{ helpContent.buttonText }}
</gl-deprecated-button>
</template>
<template v-if="helpContent.feedbackButtons">
<gl-button-group>
<gl-deprecated-button
v-for="feedbackValue in helpContent.feedbackSize"
:key="feedbackValue"
@click="
submitFeedback({
feedbackResult: feedbackValue,
})
"
>
{{ feedbackValue }}
</gl-deprecated-button>
</gl-button-group>
<div class="pt-1">
<small>{{ __('Not helpful') }}</small>
<small class="ml-4">{{ __('Very helpful') }}</small>
</div>
</template>
</div>
</gl-popover>
</template>
<script>
import { GlLink, GlProgressBar, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import HelpContentPopover from './help_content_popover.vue';
import TourPartsList from './tour_parts_list.vue';
import Tracking from '~/tracking';
export default {
name: 'OnboardingHelper',
components: {
Icon,
GlLink,
GlProgressBar,
GlDeprecatedButton,
GlLoadingIcon,
HelpContentPopover,
TourPartsList,
},
props: {
tourTitles: {
type: Array,
required: true,
},
activeTour: {
type: Number,
required: false,
default: null,
},
totalStepsForTour: {
type: Number,
required: false,
default: 0,
},
helpContent: {
type: Object,
required: false,
default: null,
},
percentageCompleted: {
type: Number,
required: false,
default: 0,
},
completedSteps: {
type: Number,
required: false,
default: 0,
},
initialShow: {
type: Boolean,
required: false,
default: false,
},
dismissPopover: {
type: Boolean,
required: false,
default: false,
},
goldenTanukiSvgPath: {
type: String,
required: true,
},
},
data() {
return {
expanded: false,
showPopover: false,
popoverDismissed: false,
helpContentTrigger: null,
showLoadingIcon: false,
};
},
computed: {
totalTours() {
return this.tourTitles.length;
},
tourInfo() {
return sprintf(s__('UserOnboardingTour|%{activeTour}/%{totalTours}'), {
activeTour: this.activeTour,
totalTours: this.totalTours,
});
},
hasTourTitles() {
return this.totalTours > 0;
},
toggleButtonLabel() {
return this.expanded ? __('Close') : __('More');
},
toggleButtonIcon() {
return this.expanded ? 'close' : 'ellipsis_h';
},
showLink() {
return this.activeTour && Boolean(this.helpContent);
},
},
watch: {
initialShow(newVal) {
if (newVal) {
this.showPopover = newVal;
}
},
dismissPopover(newVal) {
this.popoverDismissed = newVal;
if (newVal) {
this.showPopover = false;
}
},
},
mounted() {
this.helpContentTrigger = this.$refs.onboardingHelper;
},
methods: {
transitionEndCallback() {
if (!this.popoverDismissed && !this.expanded) {
this.showPopover = true;
}
},
toggleMenu() {
this.expanded = !this.expanded;
if (!this.popoverDismissed && this.expanded) {
this.showPopover = false;
}
},
skipStep() {
this.showLoadingIcon = true;
this.$emit('skipStep');
},
restartStep() {
this.$emit('restartStep');
},
beginExitTourProcess() {
if (Tracking.enabled()) {
this.$emit('showFeedbackContent', true);
} else {
this.$emit('showDntExitContent', true);
}
},
callStepContentButton(button) {
this.$emit('clickStepContentButton', button);
},
callExitTour() {
this.$emit('clickExitTourButton');
},
submitFeedback(button) {
this.$emit('clickFeedbackButton', button);
},
},
};
</script>
<template>
<div
id="js-onboarding-helper"
ref="onboardingHelper"
class="onboarding-helper-container d-none d-lg-block position-fixed"
:class="{ expanded: expanded }"
@click="toggleMenu"
@transitionend="transitionEndCallback"
>
<help-content-popover
v-if="helpContent && helpContentTrigger"
:help-content="helpContent"
:target="helpContentTrigger"
:show="showPopover"
:disabled="popoverDismissed"
@clickStepContentButton="callStepContentButton"
@clickExitTourButton="callExitTour"
@clickFeedbackButton="submitFeedback"
/>
<div class="d-flex align-items-center cursor-pointer">
<div class="avatar s48 mr-1 d-flex">
<img
v-if="!showLoadingIcon"
:src="goldenTanukiSvgPath"
:alt="s__('Golden Tanuki')"
class="m-auto"
/>
<gl-loading-icon v-else :inline="true" class="m-auto" />
</div>
<div class="d-flex flex-grow justify-content-between">
<div class="qa-headline">
<strong class="title">{{ s__('UserOnboardingTour|Learn GitLab') }}</strong>
<strong v-if="activeTour">{{ tourInfo }}</strong>
<gl-progress-bar class="mt-1" :value="percentageCompleted" variant="info" />
</div>
<gl-deprecated-button
class="qa-toggle-btn btn btn-transparent mr-1"
type="button"
:aria-label="toggleButtonLabel"
>
<icon :size="14" :name="toggleButtonIcon" />
</gl-deprecated-button>
</div>
</div>
<div class="collapsible overflow-hidden">
<div v-if="hasTourTitles" class="qa-tour-parts-list">
<tour-parts-list
:tour-titles="tourTitles"
:active-tour="activeTour"
:total-steps-for-tour="totalStepsForTour"
:completed-steps="completedSteps"
/>
</div>
<hr class="my-2" />
<ul class="list-unstyled mx-2 mb-2">
<li v-if="showLink">
<gl-link class="qa-skip-step-link d-inline-flex" @click="skipStep">
<icon name="collapse-right" class="mr-1" />
<span>{{ s__('UserOnboardingTour|Skip this step') }}</span>
</gl-link>
</li>
<li v-if="showLink">
<gl-link class="qa-restart-step-link d-inline-flex" @click="restartStep">
<icon name="repeat" class="mr-1" />
<span>{{ s__('UserOnboardingTour|Restart this step') }}</span>
</gl-link>
</li>
<li>
<gl-link class="qa-exit-tour-link d-inline-flex" @click="beginExitTourProcess">
<icon name="leave" class="mr-1" />
<span>{{ s__("UserOnboardingTour|Exit 'Learn GitLab'") }}</span>
</gl-link>
</li>
</ul>
</div>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
export default {
name: 'TourPartsList',
props: {
tourTitles: {
type: Array,
required: true,
},
activeTour: {
type: Number,
required: false,
default: null,
},
totalStepsForTour: {
type: Number,
required: false,
default: 0,
},
completedSteps: {
type: Number,
required: false,
default: 0,
},
},
computed: {
stepsCompletedInfo() {
return sprintf(s__('UserOnboardingTour|%{completed}/%{total} steps completed'), {
completed: this.completedSteps,
total: this.totalStepsForTour,
});
},
},
methods: {
isActiveTour(tourNo) {
return tourNo === this.activeTour;
},
},
};
</script>
<template>
<ul class="list-unstyled">
<li
v-for="tour in tourTitles"
:key="tour.id"
class="tour-item my-2 px-2"
:class="{ active: isActiveTour(tour.id), 'py-2': isActiveTour(tour.id) }"
>
<span class="tour-title" :class="{ 'text-info': isActiveTour(tour.id) }"
><strong>{{ tour.id }}</strong> {{ tour.title }}</span
>
<div v-if="isActiveTour(tour.id)" class="text-secondary">{{ stepsCompletedInfo }}</div>
</li>
</ul>
</template>
<style scoped>
.tour-item.active {
background: #f6fafe;
}
.tour-item.active .tour-title {
font-weight: bold;
}
</style>
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import Vue from 'vue';
import { mapActions } from 'vuex';
import OnboardingApp from './components/app.vue';
import createStore from './store';
import onboardingUtils from '../utils';
import {
TOUR_TITLES,
FEEDBACK_CONTENT,
EXIT_TOUR_CONTENT,
DNT_EXIT_TOUR_CONTENT,
} from '../constants';
import TOUR_PARTS from '../tour_parts';
export default function() {
const el = document.getElementById('js-onboarding-helper');
if (!el) {
return false;
}
const tourData = onboardingUtils.getOnboardingLocalStorageState();
if (!tourData || onboardingUtils.isOnboardingDismissed()) {
return false;
}
const { projectFullPath, projectName, goldenTanukiSvgPath } = el.dataset;
const url = window.location.href;
const { tourKey, lastStepIndex, createdProjectPath } = tourData;
const store = createStore();
return new Vue({
el,
store,
components: {
OnboardingApp,
},
created() {
if (tourKey) {
this.setInitialData({
url,
projectFullPath,
projectName,
tourData: TOUR_PARTS,
tourKey,
lastStepIndex,
createdProjectPath,
});
}
},
methods: {
...mapActions(['setInitialData']),
},
render(h) {
return h(OnboardingApp, {
props: {
tourTitles: TOUR_TITLES,
exitTourContent: EXIT_TOUR_CONTENT,
feedbackContent: FEEDBACK_CONTENT,
dntExitTourContent: DNT_EXIT_TOUR_CONTENT,
goldenTanukiSvgPath,
},
});
},
});
}
import Cookies from 'js-cookie';
import * as types from './mutation_types';
import { ONBOARDING_DISMISSED_COOKIE_NAME } from '../../constants';
import onboardingUtils from '../../utils';
export const setInitialData = ({ commit }, data) => {
commit(types.SET_INITIAL_DATA, data);
};
export const setTourKey = ({ commit }, tourKey) => {
commit(types.SET_TOUR_KEY, tourKey);
onboardingUtils.updateLocalStorage({ tourKey });
};
export const setLastStepIndex = ({ commit }, lastStepIndex) => {
commit(types.SET_LAST_STEP_INDEX, lastStepIndex);
onboardingUtils.updateLocalStorage({ lastStepIndex });
};
export const setHelpContentIndex = ({ commit }, helpContentIndex) => {
commit(types.SET_HELP_CONTENT_INDEX, helpContentIndex);
};
export const switchTourPart = ({ dispatch }, tourKey) => {
dispatch('setTourKey', tourKey);
dispatch('setLastStepIndex', 0);
dispatch('setHelpContentIndex', 0);
};
export const setTourFeedback = ({ commit }, tourFeedback) => {
commit(types.SET_FEEDBACK, tourFeedback);
};
export const setExitTour = ({ commit }, exitTour) => {
commit(types.SET_EXIT_TOUR, exitTour);
};
export const setDntExitTour = ({ commit }, dntExitTour) => {
commit(types.SET_DNT_EXIT_TOUR, dntExitTour);
};
export const setDismissed = ({ commit }, dismissed) => {
commit(types.SET_DISMISSED, dismissed);
Cookies.set(ONBOARDING_DISMISSED_COOKIE_NAME, dismissed);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const stepIndex = state => {
const { tourData, tourKey, url, projectFullPath, createdProjectPath } = state;
let idx = -1;
if (tourData && tourData[tourKey] && url !== '') {
idx = tourData[tourKey].findIndex(item =>
item.forUrl({ projectFullPath, createdProjectPath }).test(state.url),
);
}
return idx !== -1 ? idx : null;
};
export const stepContent = (state, getters) => {
const { tourData, tourKey } = state;
if (!tourData || !tourData[tourKey] || getters.stepIndex === null) {
return null;
}
return tourData[tourKey][getters.stepIndex] ? tourData[tourKey][getters.stepIndex] : null;
};
export const helpContent = (state, getters) => {
const { projectName, helpContentIndex } = state;
if (getters.stepContent === null) {
return null;
}
return getters.stepContent.getHelpContent
? getters.stepContent.getHelpContent({ projectName })[helpContentIndex]
: null;
};
export const totalTourPartSteps = state => {
if (state.tourData && state.tourKey && state.tourData[state.tourKey]) {
return state.tourData[state.tourKey].length;
}
return 0;
};
export const percentageCompleted = state => {
const { tourData, tourKey, lastStepIndex } = state;
if (lastStepIndex === -1 || !tourData || !tourData[tourKey]) {
return 0;
}
return Math.floor((100 * lastStepIndex) / tourData[tourKey].length);
};
export const actionPopover = (state, getters) =>
getters.stepContent !== null && getters.stepContent.actionPopover
? getters.stepContent.actionPopover
: null;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state: state(),
});
export default createStore;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment