Commit f9fa5e85 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '223107-remove-suggest-popup' into 'master'

Replace suggest pipeline popup

See merge request gitlab-org/gitlab!35069
parents f6b00316 ab6bb811
<script> <script>
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue'; import MrWidgetIcon from './mr_widget_icon.vue';
import PipelineTourState from './states/mr_widget_pipeline_tour.vue'; import Tracking from '~/tracking';
import { s__ } from '~/locale';
const trackingMixin = Tracking.mixin();
const TRACK_LABEL = 'no_pipeline_noticed';
export default { export default {
name: 'MRWidgetSuggestPipeline', name: 'MRWidgetSuggestPipeline',
iconName: 'status_notfound', iconName: 'status_notfound',
popoverTarget: 'suggest-popover', trackLabel: TRACK_LABEL,
popoverContainer: 'suggest-pipeline',
trackLabel: 'no_pipeline_noticed',
linkTrackValue: 30, linkTrackValue: 30,
linkTrackEvent: 'click_link', linkTrackEvent: 'click_link',
showTrackValue: 10,
showTrackEvent: 'click_button',
helpContent: s__(
`mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
),
helpURL: 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/',
components: { components: {
GlLink, GlLink,
GlSprintf, GlSprintf,
GlButton,
MrWidgetIcon, MrWidgetIcon,
PipelineTourState,
}, },
mixins: [trackingMixin],
props: { props: {
pipelinePath: { pipelinePath: {
type: String, type: String,
...@@ -31,12 +40,24 @@ export default { ...@@ -31,12 +40,24 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
tracking() {
return {
label: TRACK_LABEL,
property: this.humanAccess,
};
},
},
mounted() {
this.track();
},
}; };
</script> </script>
<template> <template>
<div :id="$options.popoverContainer" class="d-flex mr-pipeline-suggest gl-mb-3"> <div class="mr-widget-body mr-pipeline-suggest gl-mb-3">
<div class="gl-display-flex gl-align-items-center">
<mr-widget-icon :name="$options.iconName" /> <mr-widget-icon :name="$options.iconName" />
<div :id="$options.popoverTarget"> <div>
<gl-sprintf <gl-sprintf
:message=" :message="
s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
...@@ -52,7 +73,8 @@ export default { ...@@ -52,7 +73,8 @@ export default {
<template #addPipelineLink="{content}"> <template #addPipelineLink="{content}">
<gl-link <gl-link
:href="pipelinePath" :href="pipelinePath"
class="ml-2 js-add-pipeline-path" class="gl-ml-1"
data-testid="add-pipeline-link"
:data-track-property="humanAccess" :data-track-property="humanAccess"
:data-track-value="$options.linkTrackValue" :data-track-value="$options.linkTrackValue"
:data-track-event="$options.linkTrackEvent" :data-track-event="$options.linkTrackEvent"
...@@ -62,14 +84,45 @@ export default { ...@@ -62,14 +84,45 @@ export default {
</gl-link> </gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
<pipeline-tour-state </div>
:pipeline-path="pipelinePath" </div>
:pipeline-svg-path="pipelineSvgPath" <div class="row">
:human-access="humanAccess" <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n3 svg-content svg-225">
:popover-target="$options.popoverTarget" <img data-testid="pipeline-image" :src="pipelineSvgPath" />
:popover-container="$options.popoverContainer" </div>
:track-label="$options.trackLabel" <div class="col-md-7 order-md-first col-12">
/> <div class="ml-6 gl-pt-5">
<strong>
{{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
</strong>
<p class="gl-mt-2">
<gl-sprintf :message="$options.helpContent">
<template #link="{ content }">
<gl-link
data-testid="help"
:href="$options.helpURL"
target="_blank"
class="font-size-inherit"
>{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<gl-button
data-testid="ok"
category="primary"
class="gl-mt-2"
variant="info"
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.showTrackValue"
:data-track-event="$options.showTrackEvent"
:data-track-label="$options.trackLabel"
>
{{ __('Show me how to add a pipeline') }}
</gl-button>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script>
import { GlPopover, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import { s__ } from '~/locale';
const trackingMixin = Tracking.mixin();
const cookieKey = 'suggest_pipeline_dismissed';
export default {
name: 'MRWidgetPipelineTour',
dismissTrackValue: 20,
showTrackValue: 10,
trackEvent: 'click_button',
helpContent: s__(
`mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd}, simply add a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
),
helpURL: 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/',
components: {
GlPopover,
GlDeprecatedButton,
Icon,
GlSprintf,
GlLink,
},
mixins: [trackingMixin],
props: {
pipelinePath: {
type: String,
required: true,
},
pipelineSvgPath: {
type: String,
required: true,
},
humanAccess: {
type: String,
required: true,
},
popoverTarget: {
type: String,
required: true,
},
popoverContainer: {
type: String,
required: true,
},
trackLabel: {
type: String,
required: true,
},
},
data() {
return {
popoverDismissed: parseBoolean(Cookies.get(cookieKey)),
tracking: {
label: this.trackLabel,
property: this.humanAccess,
},
};
},
mounted() {
this.trackOnShow();
},
methods: {
trackOnShow() {
if (!this.popoverDismissed) {
this.track();
}
},
dismissPopover() {
this.popoverDismissed = true;
Cookies.set(cookieKey, this.popoverDismissed, { expires: 365 });
},
},
};
</script>
<template>
<gl-popover
v-if="!popoverDismissed"
show
:target="popoverTarget"
:container="popoverContainer"
placement="rightbottom"
>
<template #title>
<button
class="btn-blank float-right mt-1"
type="button"
:aria-label="__('Close')"
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
@click="dismissPopover"
>
<icon name="close" aria-hidden="true" />
</button>
{{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
</template>
<div class="svg-content svg-150 pt-1">
<img :src="pipelineSvgPath" />
</div>
<gl-sprintf :message="$options.helpContent">
<template #link="{ content }">
<gl-link :href="$options.helpURL" target="_blank" class="font-size-inherit">{{
content
}}</gl-link>
</template>
</gl-sprintf>
<gl-deprecated-button
ref="ok"
category="primary"
class="mt-2 mb-0"
variant="info"
block
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.showTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
>
{{ __('Show me how to add a pipeline') }}
</gl-deprecated-button>
<gl-deprecated-button
ref="no-thanks"
category="secondary"
class="mt-2 mb-0"
variant="info"
block
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
@click="dismissPopover"
>
{{ __('No thanks') }}
</gl-deprecated-button>
</gl-popover>
</template>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
width: 100%; width: 100%;
} }
$image-widths: 80 130 150 250 306 394 430; $image-widths: 80 130 150 225 250 306 394 430;
@each $width in $image-widths { @each $width in $image-widths {
&.svg-#{$width} { &.svg-#{$width} {
img, img,
......
...@@ -397,6 +397,16 @@ $mr-widget-min-height: 69px; ...@@ -397,6 +397,16 @@ $mr-widget-min-height: 69px;
} }
} }
} }
&.mr-pipeline-suggest {
border-radius: $border-radius-default;
line-height: 20px;
border: 1px solid $border-color;
.circle-icon-container {
color: $gl-text-color-quaternary;
}
}
} }
.mr-widget-help { .mr-widget-help {
...@@ -596,26 +606,6 @@ $mr-widget-min-height: 69px; ...@@ -596,26 +606,6 @@ $mr-widget-min-height: 69px;
} }
} }
.mr-pipeline-suggest {
flex-wrap: wrap;
border-radius: $border-radius-default;
padding: $gl-padding;
border: 1px solid $border-color;
min-height: $mr-widget-min-height;
@include media-breakpoint-up(md) {
align-items: center;
}
.circle-icon-container {
color: $gl-text-color-quaternary;
}
.popover {
z-index: 240;
}
}
.card-new-merge-request { .card-new-merge-request {
.card-header { .card-header {
padding: 5px 10px; padding: 5px 10px;
......
...@@ -15311,9 +15311,6 @@ msgstr "" ...@@ -15311,9 +15311,6 @@ msgstr ""
msgid "No test coverage" msgid "No test coverage"
msgstr "" msgstr ""
msgid "No thanks"
msgstr ""
msgid "No vulnerabilities present" msgid "No vulnerabilities present"
msgstr "" msgstr ""
...@@ -27554,7 +27551,7 @@ msgstr "" ...@@ -27554,7 +27551,7 @@ msgstr ""
msgid "mrWidget|To approve this merge request, please enter your password. This project requires all approvals to be authenticated." msgid "mrWidget|To approve this merge request, please enter your password. This project requires all approvals to be authenticated."
msgstr "" msgstr ""
msgid "mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd}, simply add a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust." msgid "mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust."
msgstr "" msgstr ""
msgid "mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged" msgid "mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue'; import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import stubChildren from 'helpers/stub_children';
import PipelineTourState from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue'; import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import { popoverProps, iconName } from './pipeline_tour_mock_data';
describe('MRWidgetHeader', () => { describe('MRWidgetSuggestPipeline', () => {
let wrapper; let wrapper;
const pipelinePath = '/foo/bar/add/pipeline/path'; let trackingSpy;
const pipelineSvgPath = '/foo/bar/pipeline/svg/path';
const humanAccess = 'maintainer'; const mockTrackingOnWrapper = () => {
const iconName = 'status_notfound'; trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
};
beforeEach(() => { beforeEach(() => {
document.body.dataset.page = 'projects:merge_requests:show';
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
wrapper = mount(suggestPipelineComponent, { wrapper = mount(suggestPipelineComponent, {
propsData: { pipelinePath, pipelineSvgPath, humanAccess }, propsData: popoverProps,
stubs: { stubs: {
...stubChildren(PipelineTourState),
GlSprintf, GlSprintf,
}, },
}); });
...@@ -25,14 +27,17 @@ describe('MRWidgetHeader', () => { ...@@ -25,14 +27,17 @@ describe('MRWidgetHeader', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
unmockTracking();
}); });
describe('template', () => { describe('template', () => {
const findOkBtn = () => wrapper.find('[data-testid="ok"]');
it('renders add pipeline file link', () => { it('renders add pipeline file link', () => {
const link = wrapper.find(GlLink); const link = wrapper.find(GlLink);
expect(link.exists()).toBe(true); expect(link.exists()).toBe(true);
expect(link.attributes().href).toBe(pipelinePath); expect(link.attributes().href).toBe(popoverProps.pipelinePath);
}); });
it('renders the expected text', () => { it('renders the expected text', () => {
...@@ -52,27 +57,62 @@ describe('MRWidgetHeader', () => { ...@@ -52,27 +57,62 @@ describe('MRWidgetHeader', () => {
); );
}); });
describe('tracking', () => { it('renders the show me how button', () => {
let spy; const button = findOkBtn();
beforeEach(() => { expect(button.exists()).toBe(true);
spy = mockTracking('_category_', wrapper.element, jest.spyOn); expect(button.classes('btn-info')).toEqual(true);
expect(button.attributes('href')).toBe(popoverProps.pipelinePath);
}); });
afterEach(() => { it('renders the help link', () => {
unmockTracking(); const link = wrapper.find('[data-testid="help"]');
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(wrapper.vm.$options.helpURL);
}); });
it('send an event when ok button is clicked', () => { it('renders the empty pipelines image', () => {
const link = wrapper.find(GlLink); const image = wrapper.find('[data-testid="pipeline-image"]');
expect(image.exists()).toBe(true);
expect(image.attributes().src).toBe(popoverProps.pipelineSvgPath);
});
describe('tracking', () => {
it('send event for basic view of the suggest pipeline widget', () => {
const expectedCategory = undefined;
const expectedAction = undefined;
expect(trackingSpy).toHaveBeenCalledWith(expectedCategory, expectedAction, {
label: wrapper.vm.$options.trackLabel,
property: popoverProps.humanAccess,
});
});
it('send an event when add pipeline link is clicked', () => {
mockTrackingOnWrapper();
const link = wrapper.find('[data-testid="add-pipeline-link"]');
triggerEvent(link.element); triggerEvent(link.element);
expect(spy).toHaveBeenCalledWith('_category_', 'click_link', { expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
label: 'no_pipeline_noticed', label: wrapper.vm.$options.trackLabel,
property: humanAccess, property: popoverProps.humanAccess,
value: '30', value: '30',
}); });
}); });
it('send an event when ok button is clicked', () => {
mockTrackingOnWrapper();
const okBtn = findOkBtn();
triggerEvent(okBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: wrapper.vm.$options.trackLabel,
property: popoverProps.humanAccess,
value: '10',
});
});
}); });
}); });
}); });
...@@ -2,9 +2,6 @@ export const popoverProps = { ...@@ -2,9 +2,6 @@ export const popoverProps = {
pipelinePath: '/foo/bar/add/pipeline/path', pipelinePath: '/foo/bar/add/pipeline/path',
pipelineSvgPath: 'assets/illustrations/something.svg', pipelineSvgPath: 'assets/illustrations/something.svg',
humanAccess: 'maintainer', humanAccess: 'maintainer',
popoverTarget: 'suggest-popover',
popoverContainer: 'suggest-pipeline',
trackLabel: 'some_tracking_label',
}; };
export const cookieKey = 'suggest_pipeline_dismissed'; export const iconName = 'status_notfound';
import { shallowMount } from '@vue/test-utils';
import { GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import pipelineTourState from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue';
import { popoverProps, cookieKey } from './pipeline_tour_mock_data';
describe('MRWidgetPipelineTour', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
describe(`when ${cookieKey} cookie is set`, () => {
beforeEach(() => {
Cookies.set(cookieKey, true);
wrapper = shallowMount(pipelineTourState, {
propsData: popoverProps,
});
});
it('does not render the popover', () => {
const popover = wrapper.find(GlPopover);
expect(popover.exists()).toBe(false);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('does not call tracking', () => {
expect(trackingSpy).not.toHaveBeenCalled();
});
});
});
describe(`when ${cookieKey} cookie is not set`, () => {
const findOkBtn = () => wrapper.find({ ref: 'ok' });
const findDismissBtn = () => wrapper.find({ ref: 'no-thanks' });
beforeEach(() => {
Cookies.remove(cookieKey);
wrapper = shallowMount(pipelineTourState, {
propsData: popoverProps,
stubs: {
GlSprintf,
},
});
});
it('renders the popover', () => {
const popover = wrapper.find(GlPopover);
expect(popover.exists()).toBe(true);
});
it('renders the help link', () => {
const link = wrapper.find(GlLink);
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(wrapper.vm.$options.helpURL);
});
it('renders the show me how button', () => {
const button = findOkBtn();
expect(button.exists()).toBe(true);
expect(button.attributes().category).toBe('primary');
});
it('renders the dismiss button', () => {
const button = findDismissBtn();
expect(button.exists()).toBe(true);
expect(button.attributes().category).toBe('secondary');
});
it('renders the empty pipelines image', () => {
const image = wrapper.find('img');
expect(image.exists()).toBe(true);
expect(image.attributes().src).toBe(popoverProps.pipelineSvgPath);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('send event for basic view of popover', () => {
document.body.dataset.page = 'projects:merge_requests:show';
wrapper.vm.trackOnShow();
expect(trackingSpy).toHaveBeenCalledWith(undefined, undefined, {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
});
});
it('send an event when ok button is clicked', () => {
const okBtn = findOkBtn();
triggerEvent(okBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
value: '10',
});
});
it('send an event when dismiss button is clicked', () => {
const dismissBtn = findDismissBtn();
triggerEvent(dismissBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
value: '20',
});
});
});
describe('dismissPopover', () => {
it('updates popoverDismissed', () => {
const button = findDismissBtn();
const popover = wrapper.find(GlPopover);
button.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(Cookies.get(cookieKey)).toBe('true');
expect(popover.exists()).toBe(false);
});
});
});
});
});
});
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