Commit 46e9bc93 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'mw-remove-paid-feature-callout-badge' into 'master'

Remove "paid feature callout" badge

See merge request gitlab-org/gitlab!78954
parents 037bba6b 78d50cd4
import '~/pages/projects/merge_requests/creations/new/index';
import { initPaidFeatureCalloutBadgeAndPopover } from 'ee/paid_feature_callouts/index';
import UserCallout from '~/user_callout';
import initForm from '../../shared/init_form';
initForm();
initPaidFeatureCalloutBadgeAndPopover();
// eslint-disable-next-line no-new
new UserCallout();
import '~/pages/projects/merge_requests/edit/index';
import { initPaidFeatureCalloutBadgeAndPopover } from 'ee/paid_feature_callouts/index';
import initForm from '../shared/init_form';
initForm();
initPaidFeatureCalloutBadgeAndPopover();
<script>
import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { sprintf } from '~/locale';
import Tracking from '~/tracking';
import {
BADGE,
EXPERIMENT_KEY,
POPOVER_OR_TOOLTIP_BREAKPOINT,
RESIZE_EVENT_DEBOUNCE_MS,
} from '../constants';
const { i18n, trackingEvents } = BADGE;
const trackingMixin = Tracking.mixin({ experiment: EXPERIMENT_KEY });
export default {
components: {
GlBadge,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin],
props: {
featureName: {
type: String,
required: false,
default: '',
},
},
data() {
return {
tooltipDisabled: false,
};
},
i18n,
trackingEvents,
computed: {
title() {
if (this.featureName === '') return this.$options.i18n.title.generic;
return sprintf(this.$options.i18n.title.specific, { featureName: this.featureName });
},
},
created() {
this.debouncedResize = debounce(() => this.onResize(), RESIZE_EVENT_DEBOUNCE_MS);
window.addEventListener('resize', this.debouncedResize);
},
mounted() {
this.trackBadgeDisplayedForExperiment();
this.onResize();
},
beforeDestroy() {
window.removeEventListener('resize', this.debouncedResize);
},
methods: {
onResize() {
this.updateTooltipDisabledState();
},
trackBadgeDisplayedForExperiment() {
const { action, ...options } = this.$options.trackingEvents.displayBadge;
this.track(action, options);
},
updateTooltipDisabledState() {
this.tooltipDisabled = bp.getBreakpointSize() !== POPOVER_OR_TOOLTIP_BREAKPOINT;
},
},
};
</script>
<template>
<gl-badge
v-gl-tooltip="{ disabled: tooltipDisabled }"
:title="title"
tabindex="0"
size="sm"
class="feature-highlight-badge"
>
<gl-icon name="license" :size="14" />
</gl-badge>
</template>
<script>
import { GlButton, GlPopover } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { sprintf } from '~/locale';
import Tracking from '~/tracking';
import {
POPOVER,
EXPERIMENT_KEY,
POPOVER_OR_TOOLTIP_BREAKPOINT,
RESIZE_EVENT_DEBOUNCE_MS,
} from '../constants';
const { i18n, trackingEvents } = POPOVER;
const trackingMixin = Tracking.mixin({ experiment: EXPERIMENT_KEY });
export default {
components: {
GlButton,
GlPopover,
},
mixins: [trackingMixin],
props: {
containerId: {
type: String,
required: false,
default: undefined,
},
daysRemaining: {
type: Number,
required: true,
},
featureName: {
type: String,
required: true,
},
hrefComparePlans: {
type: String,
required: true,
},
hrefUpgradeToPaid: {
type: String,
required: true,
},
planNameForTrial: {
type: String,
required: true,
},
planNameForUpgrade: {
type: String,
required: true,
},
promoImageAltText: {
type: String,
required: false,
default: i18n.defaultImgAltText,
},
promoImagePath: {
type: String,
required: false,
default: undefined,
},
targetId: {
type: String,
required: true,
},
},
data() {
return {
disabled: false,
};
},
i18n,
trackingEvents,
computed: {
title() {
return sprintf(this.$options.i18n.title.countableTranslator(this.daysRemaining), {
daysRemaining: this.daysRemaining,
featureName: this.featureName,
});
},
content() {
return sprintf(this.$options.i18n.content, {
featureName: this.featureName,
planNameForTrial: this.planNameForTrial,
planNameForUpgrade: this.planNameForUpgrade,
});
},
upgradeButtonLabel() {
return sprintf(this.$options.i18n.buttons.upgrade, {
planNameForUpgrade: this.planNameForUpgrade,
});
},
comparePlansButtonLabel() {
return this.$options.i18n.buttons.comparePlans;
},
},
created() {
this.debouncedResize = debounce(() => this.onResize(), RESIZE_EVENT_DEBOUNCE_MS);
window.addEventListener('resize', this.debouncedResize);
},
mounted() {
this.onResize();
},
beforeDestroy() {
window.removeEventListener('resize', this.debouncedResize);
},
methods: {
onResize() {
this.updateDisabledState();
},
updateDisabledState() {
this.disabled = bp.getBreakpointSize() === POPOVER_OR_TOOLTIP_BREAKPOINT;
},
onShown() {
const { action, ...options } = this.$options.trackingEvents.popoverShown;
this.track(action, { ...options, label: `${options.label}:${this.featureName}` });
},
onUpgradeBtnClick() {
const { action, ...options } = this.$options.trackingEvents.upgradeBtnClick;
this.track(action, options);
},
onCompareBtnClick() {
const { action, ...options } = this.$options.trackingEvents.compareBtnClick;
this.track(action, options);
},
},
};
</script>
<template>
<gl-popover
:container="containerId"
:target="targetId"
:disabled="disabled"
placement="top"
boundary="viewport"
:delay="{ hide: 400 }"
@shown="onShown"
>
<template #title>{{ title }}</template>
<div v-if="promoImagePath" class="gl-display-flex gl-justify-content-center gl-mt-n3 gl-mb-4">
<img
:src="promoImagePath"
:alt="promoImageAltText"
height="40"
width="40"
data-testid="promo-img"
/>
</div>
{{ content }}
<div class="gl-mt-5">
<gl-button
:href="hrefUpgradeToPaid"
target="_blank"
category="primary"
variant="confirm"
size="small"
class="gl-mb-0"
block
data-testid="upgradeBtn"
@click="onUpgradeBtnClick"
>
<span class="gl-font-sm">{{ upgradeButtonLabel }}</span>
</gl-button>
<gl-button
:href="hrefComparePlans"
target="_blank"
category="secondary"
variant="confirm"
size="small"
class="gl-mb-0"
block
data-testid="compareBtn"
@click="onCompareBtnClick"
>
<span class="gl-font-sm">{{ comparePlansButtonLabel }}</span>
</gl-button>
</div>
</gl-popover>
</template>
import { __, n__, s__ } from '~/locale';
const CLICK_BUTTON = 'click_button';
export const EXPERIMENT_KEY = 'highlight_paid_features_during_active_trial';
export const RESIZE_EVENT_DEBOUNCE_MS = 150;
export const POPOVER_OR_TOOLTIP_BREAKPOINT = 'xs';
export const BADGE = {
i18n: {
title: {
generic: __('This feature is part of your GitLab Ultimate trial.'),
specific: __('The %{featureName} feature is part of your GitLab Ultimate trial.'),
},
},
trackingEvents: {
displayBadge: { action: 'display_badge', label: 'feature_highlight_badge' },
},
};
export const POPOVER = {
i18n: {
buttons: {
comparePlans: s__('BillingPlans|Compare all plans'),
upgrade: s__('BillingPlans|Upgrade to GitLab %{planNameForUpgrade}'),
},
content: s__(`FeatureHighlight|Enjoying your GitLab %{planNameForTrial} trial? To continue
using %{featureName} after your trial ends, upgrade to GitLab %{planNameForUpgrade}.`),
defaultImgAltText: __('SVG illustration'),
title: {
countableTranslator(count) {
return n__(
'FeatureHighlight|%{daysRemaining} day remaining to enjoy %{featureName}',
'FeatureHighlight|%{daysRemaining} days remaining to enjoy %{featureName}',
count,
);
},
},
},
trackingEvents: {
popoverShown: { action: 'popover_shown', label: 'feature_highlight_popover' },
upgradeBtnClick: { action: CLICK_BUTTON, label: 'upgrade_to_ultimate' },
compareBtnClick: { action: CLICK_BUTTON, label: 'compare_all_plans' },
},
};
import Vue from 'vue';
import PaidFeatureCalloutBadge from './components/paid_feature_callout_badge.vue';
import PaidFeatureCalloutPopover from './components/paid_feature_callout_popover.vue';
export const initPaidFeatureCalloutBadge = () => {
const el = document.getElementById('js-paid-feature-badge');
if (!el) return undefined;
const { featureName, id } = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(PaidFeatureCalloutBadge, { props: { featureName }, attrs: { id } }),
});
};
export const initPaidFeatureCalloutPopover = () => {
const el = document.getElementById('js-paid-feature-popover');
if (!el) return undefined;
const {
containerId,
daysRemaining,
featureName,
hrefComparePlans,
hrefUpgradeToPaid,
planNameForTrial,
planNameForUpgrade,
promoImageAltText,
promoImagePath,
targetId,
} = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(PaidFeatureCalloutPopover, {
props: {
containerId,
daysRemaining: Number(daysRemaining),
featureName,
hrefComparePlans,
hrefUpgradeToPaid,
planNameForTrial,
planNameForUpgrade,
promoImageAltText,
promoImagePath,
targetId,
},
}),
});
};
export const initPaidFeatureCalloutBadgeAndPopover = () => {
return {
badge: initPaidFeatureCalloutBadge(),
popover: initPaidFeatureCalloutPopover(),
};
};
import { GlBadge, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMount } from '@vue/test-utils';
import PaidFeatureCalloutBadge from 'ee/paid_feature_callouts/components/paid_feature_callout_badge.vue';
import { BADGE } from 'ee/paid_feature_callouts/constants';
import { mockTracking } from 'helpers/tracking_helper';
import { sprintf } from '~/locale';
describe('PaidFeatureCalloutBadge component', () => {
let trackingSpy;
let wrapper;
const { i18n, trackingEvents } = BADGE;
const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlIcon = () => wrapper.findComponent(GlIcon);
const createComponent = (props = {}) => {
return shallowMount(PaidFeatureCalloutBadge, { propsData: props });
};
afterEach(() => {
wrapper.destroy();
});
describe('default rendering', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('sets attributes on the GlBadge component', () => {
expect(findGlBadge().attributes()).toMatchObject({
title: i18n.title.generic,
tabindex: '0',
size: 'sm',
class: 'feature-highlight-badge',
});
});
it('sets attributes on the GlIcon component', () => {
expect(findGlIcon().attributes()).toEqual({
name: 'license',
size: '14',
});
});
});
describe('title', () => {
describe('when no featureName is provided', () => {
it('sets the title to a sensible default', () => {
wrapper = createComponent();
expect(findGlBadge().attributes('title')).toBe(i18n.title.generic);
});
});
describe('when an optional featureName is provided', () => {
it('sets the title using the given feature name', () => {
const props = { featureName: 'fantastical thing' };
wrapper = createComponent(props);
expect(findGlBadge().attributes('title')).toBe(sprintf(i18n.title.specific, props));
});
});
});
describe('tracking', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = createComponent();
});
it('tracks that the badge has been displayed when mounted', () => {
const { action, ...trackingOpts } = trackingEvents.displayBadge;
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
action,
expect.objectContaining(trackingOpts),
);
});
});
describe('onResize', () => {
beforeEach(() => {
wrapper = createComponent();
});
it.each`
bp | tooltipDisabled
${'xs'} | ${false}
${'sm'} | ${true}
${'md'} | ${true}
${'lg'} | ${true}
${'xl'} | ${true}
`(
'sets tooltipDisabled to `$tooltipDisabled` when the breakpoint is "$bp"',
async ({ bp, tooltipDisabled }) => {
jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(bp);
wrapper.vm.onResize();
await wrapper.vm.$nextTick();
expect(wrapper.vm.tooltipDisabled).toBe(tooltipDisabled);
},
);
});
});
import { GlPopover } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { mount, shallowMount } from '@vue/test-utils';
import PaidFeatureCalloutPopover from 'ee/paid_feature_callouts/components/paid_feature_callout_popover.vue';
import { POPOVER } from 'ee/paid_feature_callouts/constants';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
const { i18n, trackingEvents } = POPOVER;
describe('PaidFeatureCalloutPopover', () => {
let trackingSpy;
let wrapper;
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findUpgradeBtn = () => wrapper.findByTestId('upgradeBtn');
const findCompareBtn = () => wrapper.findByTestId('compareBtn');
const defaultProps = {
daysRemaining: 12,
featureName: 'some feature',
hrefComparePlans: '/group/test-group/-/billings',
hrefUpgradeToPaid: '/-/subscriptions/new?namespace_id=123&plan_id=abc456',
planNameForTrial: 'Awesomesauce',
planNameForUpgrade: 'Amazing',
targetId: 'some-feature-callout-target',
};
const createComponent = (extraProps = {}, mountFn = shallowMount) => {
return extendedWrapper(
mountFn(PaidFeatureCalloutPopover, {
propsData: {
...defaultProps,
...extraProps,
},
}),
);
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = createComponent();
});
afterEach(() => {
unmockTracking();
wrapper.destroy();
});
describe('interpolated strings', () => {
it('correctly interpolates them all', () => {
wrapper = createComponent({}, mount);
expect(wrapper.text()).not.toMatch(/%{\w+}/);
});
});
describe('GlPopover attributes', () => {
const sharedAttrs = {
boundary: 'viewport',
placement: 'top',
target: defaultProps.targetId,
};
describe('with some default props', () => {
it('sets attributes on the GlPopover component', () => {
const attributes = findGlPopover().attributes();
expect(attributes).toMatchObject(sharedAttrs);
expect(attributes.containerId).toBeUndefined();
});
});
describe('with additional, optional props', () => {
beforeEach(() => {
wrapper = createComponent({ containerId: 'some-container-id' });
});
it('sets more attributes on the GlPopover component', () => {
expect(findGlPopover().attributes()).toMatchObject({
...sharedAttrs,
container: 'some-container-id',
});
});
});
});
describe('promo image', () => {
const promoImagePathForTest = 'path/to/some/image.svg';
const findPromoImage = () => wrapper.findByTestId('promo-img');
describe('with the optional promoImagePath prop', () => {
beforeEach(() => {
wrapper = createComponent({ promoImagePath: promoImagePathForTest });
});
it('renders the promo image', () => {
expect(findPromoImage().exists()).toBe(true);
});
describe('with the optional promoImageAltText prop', () => {
const promoImageAltText = 'My fancy alt text';
beforeEach(() => {
wrapper = createComponent({
promoImagePath: promoImagePathForTest,
promoImageAltText,
});
});
it('renders the promo image with the given alt text', () => {
expect(findPromoImage().attributes('alt')).toBe(promoImageAltText);
});
});
describe('without the optional promoImageAltText prop', () => {
it('renders the promo image with default alt text', () => {
expect(findPromoImage().attributes('alt')).toBe(i18n.defaultImgAltText);
});
});
});
describe('without the optional promoImagePath prop', () => {
it('does not render a promo image', () => {
expect(findPromoImage().exists()).toBe(false);
});
});
});
describe('title', () => {
const expectTitleToMatch = (daysRemaining) => {
expect(wrapper.text()).toContain(
sprintf(i18n.title.countableTranslator(daysRemaining), {
daysRemaining,
featureName: defaultProps.featureName,
}),
);
};
describe('singularized form', () => {
it('renders the title text with "1 day"', () => {
wrapper = createComponent({ daysRemaining: 1 }, mount);
expectTitleToMatch(1);
});
});
describe('pluralized form', () => {
it('renders the title text with "5 days"', () => {
wrapper = createComponent({ daysRemaining: 5 }, mount);
expectTitleToMatch(5);
});
it('renders the title text with "0 days"', () => {
wrapper = createComponent({ daysRemaining: 0 }, mount);
expectTitleToMatch(0);
});
});
});
describe('content', () => {
it('renders the content text', () => {
expect(findGlPopover().text()).toMatch(
sprintf(i18n.content, {
featureName: defaultProps.featureName,
planNameForTrial: defaultProps.planNameForTrial,
planNameForUpgrade: defaultProps.planNameForUpgrade,
}),
);
});
});
describe('call-to-action buttons', () => {
const sharedAttrs = {
target: '_blank',
variant: 'confirm',
size: 'small',
block: '',
};
describe('upgrade plan button', () => {
it('correctly renders an Upgrade button', () => {
const upgradeBtn = findUpgradeBtn();
expect(upgradeBtn.text()).toEqual(
sprintf(i18n.buttons.upgrade, { planNameForUpgrade: defaultProps.planNameForUpgrade }),
);
expect(upgradeBtn.attributes()).toMatchObject({
...sharedAttrs,
href: defaultProps.hrefUpgradeToPaid,
category: 'primary',
});
});
it('tracks on click', () => {
const { action, ...trackingOpts } = trackingEvents.upgradeBtnClick;
findUpgradeBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
action,
expect.objectContaining(trackingOpts),
);
});
});
describe('compare plans button', () => {
it('correctly renders a Compare button', () => {
const compareBtn = findCompareBtn();
expect(compareBtn.text()).toEqual(i18n.buttons.comparePlans);
expect(compareBtn.attributes()).toMatchObject({
...sharedAttrs,
href: defaultProps.hrefComparePlans,
category: 'secondary',
});
});
it('tracks on click', () => {
const { action, ...trackingOpts } = trackingEvents.compareBtnClick;
findCompareBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
action,
expect.objectContaining(trackingOpts),
);
});
});
});
describe('onShown', () => {
it('tracks that the popover has been shown', () => {
const { action, label } = trackingEvents.popoverShown;
findGlPopover().vm.$emit('shown');
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
action,
expect.objectContaining({
label: `${label}:${defaultProps.featureName}`,
}),
);
});
});
describe('onResize', () => {
it.each`
bp | disabled
${'xs'} | ${'true'}
${'sm'} | ${undefined}
${'md'} | ${undefined}
${'lg'} | ${undefined}
${'xl'} | ${undefined}
`(
'sets the GlPopover’s disabled attribute to `$disabled` when the breakpoint is "$bp"',
async ({ bp, disabled }) => {
jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(bp);
wrapper.vm.onResize();
await wrapper.vm.$nextTick();
expect(findGlPopover().attributes('disabled')).toBe(disabled);
},
);
});
});
......@@ -5421,9 +5421,6 @@ msgstr ""
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name}."
msgstr ""
msgid "BillingPlans|Compare all plans"
msgstr ""
msgid "BillingPlans|Congratulations, your free trial is activated."
msgstr ""
......@@ -5460,9 +5457,6 @@ msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Upgrade to GitLab %{planNameForUpgrade}"
msgstr ""
msgid "BillingPlans|While GitLab is ending availability of the Bronze plan, you can still renew your Bronze subscription one additional time before %{eoa_bronze_plan_end_date}. We are also offering a limited time free upgrade to our Premium Plan (up to 25 users)! Learn more about the changes and offers in our %{announcement_link}."
msgstr ""
......@@ -15141,14 +15135,6 @@ msgstr ""
msgid "FeatureFlag|User List"
msgstr ""
msgid "FeatureHighlight|%{daysRemaining} day remaining to enjoy %{featureName}"
msgid_plural "FeatureHighlight|%{daysRemaining} days remaining to enjoy %{featureName}"
msgstr[0] ""
msgstr[1] ""
msgid "FeatureHighlight|Enjoying your GitLab %{planNameForTrial} trial? To continue using %{featureName} after your trial ends, upgrade to GitLab %{planNameForUpgrade}."
msgstr ""
msgid "Feb"
msgstr ""
......@@ -31127,9 +31113,6 @@ msgstr ""
msgid "SSL verification"
msgstr ""
msgid "SVG illustration"
msgstr ""
msgid "Satisfied"
msgstr ""
......@@ -35475,9 +35458,6 @@ msgstr ""
msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
msgstr ""
msgid "The %{featureName} feature is part of your GitLab Ultimate trial."
msgstr ""
msgid "The %{link_start}true-up model%{link_end} allows having more users, and additional users will incur a retroactive charge on renewal."
msgstr ""
......@@ -36481,9 +36461,6 @@ msgstr ""
msgid "This epic does not exist or you don't have sufficient permission."
msgstr ""
msgid "This feature is part of your GitLab Ultimate trial."
msgstr ""
msgid "This feature requires local storage to be enabled"
msgstr ""
......
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