Commit 6adb4828 authored by David O'Regan's avatar David O'Regan

Merge branch '288015-eng-show-more-info-on-hover-of-trial-status-in-sidebar' into 'master'

[ENG] Show more info on hover of trial status in sidebar

See merge request gitlab-org/gitlab!51003
parents 71f8a6f8 ecf8963a
<script>
import { GlButton, GlPopover, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
const RESIZE_EVENT_DEBOUNCE_MS = 150;
export default {
components: {
GlButton,
GlPopover,
GlSprintf,
},
props: {
containerId: {
type: [String, null],
required: false,
default: null,
},
groupName: {
type: String,
required: true,
},
planName: {
type: String,
required: true,
},
plansHref: {
type: String,
required: true,
},
purchaseHref: {
type: String,
required: true,
},
targetId: {
type: String,
required: true,
},
trialEndDate: {
type: Date,
required: true,
},
},
data: () => ({
disabled: false,
}),
i18n: {
compareAllButtonTitle: s__('Trials|Compare all plans'),
popoverTitle: s__('Trials|Hey there'),
popoverContent: s__(`Trials|Your trial ends on
%{boldStart}%{trialEndDate}%{boldEnd}. We hope you are enjoying GitLab
%{planName}. To continue using GitLab %{planName} after your trial ends,
you will need to buy a subscription. You can also choose GitLab Premium
if its features are sufficient for your needs.`),
upgradeButtonTitle: s__('Trials|Upgrade %{groupName} to %{planName}'),
},
computed: {
formattedTrialEndDate() {
return formatDate(this.trialEndDate, 'yyyy-mm-dd');
},
},
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 = ['xs', 'sm'].includes(bp.getBreakpointSize());
},
},
};
</script>
<template>
<gl-popover
:container="containerId"
:target="targetId"
:disabled="disabled"
triggers="hover focus"
placement="rightbottom"
boundary="viewport"
:delay="{ hide: 400 }"
>
<template #title>
{{ $options.i18n.popoverTitle }}
<gl-emoji class="gl-vertical-align-baseline font-size-inherit gl-ml-1" data-name="wave" />
</template>
<gl-sprintf :message="$options.i18n.popoverContent">
<template #bold="{ content }">
<b>{{ sprintf(content, { trialEndDate: formattedTrialEndDate }) }}</b>
</template>
<template #planName>{{ planName }}</template>
</gl-sprintf>
<div class="gl-mt-5">
<gl-button
:href="purchaseHref"
category="primary"
variant="confirm"
size="small"
class="gl-mb-0"
block
>
<span class="gl-font-sm">
<gl-sprintf :message="$options.i18n.upgradeButtonTitle">
<template #groupName>{{ groupName }}</template>
<template #planName>{{ planName }}</template>
</gl-sprintf>
</span>
</gl-button>
<gl-button
:href="plansHref"
category="secondary"
variant="confirm"
size="small"
class="gl-mb-0"
block
:title="$options.i18n.compareAllButtonTitle"
>
<span class="gl-font-sm">{{ $options.i18n.compareAllButtonTitle }}</span>
</gl-button>
</div>
</gl-popover>
</template>
<script> <script>
import { GlLink, GlProgressBar } from '@gitlab/ui'; import { GlLink, GlProgressBar } from '@gitlab/ui';
import { n__, sprintf } from '~/locale';
export default { export default {
components: { components: {
...@@ -7,8 +8,13 @@ export default { ...@@ -7,8 +8,13 @@ export default {
GlProgressBar, GlProgressBar,
}, },
props: { props: {
href: { containerId: {
type: String, type: [String, null],
required: false,
default: null,
},
daysRemaining: {
type: Number,
required: true, required: true,
}, },
navIconImagePath: { navIconImagePath: {
...@@ -19,23 +25,42 @@ export default { ...@@ -19,23 +25,42 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
title: { planName: {
type: String, type: String,
required: true, required: true,
}, },
plansHref: {
type: String,
required: true,
},
},
computed: {
widgetTitle() {
const i18nWidgetTitle = n__(
'Trials|%{planName} Trial %{enDash} %{num} day left',
'Trials|%{planName} Trial %{enDash} %{num} days left',
this.daysRemaining,
);
return sprintf(i18nWidgetTitle, {
planName: this.planName,
enDash: '',
num: this.daysRemaining,
});
},
}, },
}; };
</script> </script>
<template> <template>
<gl-link :title="title" :href="href"> <gl-link :id="containerId" :title="widgetTitle" :href="plansHref">
<div class="gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full"> <div class="gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full">
<span class="gl-display-flex gl-align-items-center"> <span class="gl-display-flex gl-align-items-center">
<span class="nav-icon-container svg-container"> <span class="nav-icon-container svg-container">
<img :src="navIconImagePath" width="16" class="svg" /> <img :src="navIconImagePath" width="16" class="svg" />
</span> </span>
<span class="nav-item-name gl-white-space-normal"> <span class="nav-item-name gl-white-space-normal">
{{ title }} {{ widgetTitle }}
</span> </span>
</span> </span>
<span class="gl-display-flex gl-align-items-stretch gl-mt-3"> <span class="gl-display-flex gl-align-items-stretch gl-mt-3">
......
import Vue from 'vue'; import Vue from 'vue';
import TrialStatusPopover from './components/trial_status_popover.vue';
import TrialStatusWidget from './components/trial_status_widget.vue'; import TrialStatusWidget from './components/trial_status_widget.vue';
export default () => { export const initTrialStatusWidget = () => {
const el = document.getElementById('js-trial-status-widget'); const el = document.getElementById('js-trial-status-widget');
if (!el) return undefined; if (!el) return undefined;
const { percentageComplete } = el.dataset; const { daysRemaining, percentageComplete, ...props } = el.dataset;
return new Vue({ return new Vue({
el, el,
render: (createElement) => render: (createElement) =>
createElement(TrialStatusWidget, { createElement(TrialStatusWidget, {
props: { props: {
...el.dataset, ...props,
daysRemaining: Number(daysRemaining),
percentageComplete: Number(percentageComplete), percentageComplete: Number(percentageComplete),
}, },
}), }),
}); });
}; };
export const initTrialStatusPopover = () => {
const el = document.getElementById('js-trial-status-popover');
if (!el) return undefined;
const { trialEndDate, ...props } = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(TrialStatusPopover, {
props: {
...props,
trialEndDate: new Date(trialEndDate),
},
}),
});
};
export const initTrialStatusWidgetAndPopover = () => {
return {
widget: initTrialStatusWidget(),
popover: initTrialStatusPopover(),
};
};
import $ from 'jquery'; import $ from 'jquery';
import 'bootstrap/js/dist/modal'; import 'bootstrap/js/dist/modal';
import initTrialStatusWidget from 'ee/contextual_sidebar/group_trial_status_widget';
import initEETrialBanner from 'ee/ee_trial_banner'; import initEETrialBanner from 'ee/ee_trial_banner';
import trackNavbarEvents from 'ee/event_tracking/navbar'; import trackNavbarEvents from 'ee/event_tracking/navbar';
import initNamespaceStorageLimitAlert from 'ee/namespace_storage_limit_alert'; import initNamespaceStorageLimitAlert from 'ee/namespace_storage_limit_alert';
...@@ -16,6 +15,4 @@ $(() => { ...@@ -16,6 +15,4 @@ $(() => {
initNamespaceStorageLimitAlert(); initNamespaceStorageLimitAlert();
trackNavbarEvents(); trackNavbarEvents();
initTrialStatusWidget();
}); });
import { initTrialStatusWidgetAndPopover } from 'ee/contextual_sidebar/group_trial_status_widget_and_popover';
initTrialStatusWidgetAndPopover();
...@@ -8,15 +8,8 @@ module TrialStatusWidgetHelper ...@@ -8,15 +8,8 @@ module TrialStatusWidgetHelper
user_can_administer_group?(group) user_can_administer_group?(group)
end end
def trial_days_remaining_in_words(group) def plan_title_for_group(group)
num_of_days = group.trial_days_remaining group.gitlab_subscription&.plan_title
plan_title = group.gitlab_subscription&.plan_title
ns_(
"Trials|%{plan} Trial %{en_dash} %{num} day left",
"Trials|%{plan} Trial %{en_dash} %{num} days left",
num_of_days
) % { plan: plan_title, num: num_of_days, en_dash: '–' }
end end
private private
......
...@@ -2,7 +2,17 @@ ...@@ -2,7 +2,17 @@
- return unless show_trial_status_widget?(root_group) - return unless show_trial_status_widget?(root_group)
= nav_link do = nav_link do
#js-trial-status-widget{ data: { href: group_billings_path(root_group), #js-trial-status-widget{ data: { container_id: 'trial-status-sidebar-widget',
days_remaining: root_group.trial_days_remaining,
nav_icon_image_path: image_path('illustrations/golden_tanuki.svg'), nav_icon_image_path: image_path('illustrations/golden_tanuki.svg'),
title: trial_days_remaining_in_words(root_group), percentage_complete: root_group.trial_percentage_complete,
percentage_complete: root_group.trial_percentage_complete } } plan_name: plan_title_for_group(root_group),
plans_href: group_billings_path(root_group) } }
#js-trial-status-popover{ data: { container_id: 'trial-status-sidebar-widget',
group_name: root_group.name,
plan_name: plan_title_for_group(root_group),
plans_href: group_billings_path(root_group),
purchase_href: new_subscriptions_path(namespace_id: group.id, plan_id: '2c92a0fc5a83f01d015aa6db83c45aac'),
target_id: 'trial-status-sidebar-widget',
trial_end_date: root_group.trial_ends_on } }
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TrialStatusPopover component matches the snapshot 1`] = `
<gl-popover-stub
boundary="viewport"
cssclasses=""
delay="[object Object]"
placement="rightbottom"
target="target-element-identifier"
triggers="hover focus"
>
<gl-sprintf-stub
message="Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you are enjoying GitLab %{planName}. To continue using GitLab %{planName} after your trial ends, you will need to buy a subscription. You can also choose GitLab Premium if its features are sufficient for your needs."
/>
<div
class="gl-mt-5"
>
<gl-button-stub
block=""
buttontextclasses=""
category="primary"
class="gl-mb-0"
href="transactions/new"
icon=""
size="small"
variant="confirm"
>
<span
class="gl-font-sm"
>
<gl-sprintf-stub
message="Upgrade %{groupName} to %{planName}"
/>
</span>
</gl-button-stub>
<gl-button-stub
block=""
buttontextclasses=""
category="secondary"
class="gl-mb-0"
href="billing/path-for/group"
icon=""
size="small"
title="Compare all plans"
variant="confirm"
>
<span
class="gl-font-sm"
>
Compare all plans
</span>
</gl-button-stub>
</div>
</gl-popover-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TrialStatusWidget component matches the snapshot 1`] = ` exports[`TrialStatusWidget component without the optional containerId prop matches the snapshot 1`] = `
<gl-link-stub <gl-link-stub
href="billing/path-for/group" href="billing/path-for/group"
title="Gold Trial – 27 days left" title="Ultimate Trial – 20 days left"
> >
<div <div
class="gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full" class="gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full"
...@@ -25,7 +25,7 @@ exports[`TrialStatusWidget component matches the snapshot 1`] = ` ...@@ -25,7 +25,7 @@ exports[`TrialStatusWidget component matches the snapshot 1`] = `
class="nav-item-name gl-white-space-normal" class="nav-item-name gl-white-space-normal"
> >
Gold Trial – 27 days left Ultimate Trial – 20 days left
</span> </span>
</span> </span>
......
import { GlPopover } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMount } from '@vue/test-utils';
import TrialStatusPopover from 'ee/contextual_sidebar/components/trial_status_popover.vue';
describe('TrialStatusPopover component', () => {
let wrapper;
const createComponent = () => {
return shallowMount(TrialStatusPopover, {
propsData: {
groupName: 'Some Test Group',
planName: 'Ultimate',
plansHref: 'billing/path-for/group',
purchaseHref: 'transactions/new',
targetId: 'target-element-identifier',
trialEndDate: new Date('2021-02-28'),
},
});
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('methods', () => {
describe('onResize', () => {
const getGlPopover = () => wrapper.findComponent(GlPopover);
it.each`
bp | isDisabled
${'xs'} | ${'true'}
${'sm'} | ${'true'}
${'md'} | ${undefined}
${'lg'} | ${undefined}
${'xl'} | ${undefined}
`(
'sets disabled to `$isDisabled` when the breakpoint is "$bp"',
async ({ bp, isDisabled }) => {
jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(bp);
wrapper.vm.onResize();
await wrapper.vm.$nextTick();
expect(getGlPopover().attributes('disabled')).toBe(isDisabled);
},
);
});
});
});
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_widget.vue'; import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_widget.vue';
...@@ -5,26 +6,47 @@ import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_wid ...@@ -5,26 +6,47 @@ import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_wid
describe('TrialStatusWidget component', () => { describe('TrialStatusWidget component', () => {
let wrapper; let wrapper;
const createComponent = () => { const getGlLink = () => wrapper.findComponent(GlLink);
const createComponent = ({ props } = {}) => {
return shallowMount(TrialStatusWidget, { return shallowMount(TrialStatusWidget, {
propsData: { propsData: {
href: 'billing/path-for/group', daysRemaining: 20,
navIconImagePath: 'illustrations/golden_tanuki.svg', navIconImagePath: 'illustrations/golden_tanuki.svg',
percentageComplete: 10, percentageComplete: 10,
title: 'Gold Trial – 27 days left', planName: 'Ultimate',
plansHref: 'billing/path-for/group',
...props,
}, },
}); });
}; };
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('matches the snapshot', () => { describe('without the optional containerId prop', () => {
expect(wrapper.element).toMatchSnapshot(); beforeEach(() => {
wrapper = createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders without an id', () => {
expect(getGlLink().attributes('id')).toBe(undefined);
});
});
describe('with the optional containerId prop', () => {
beforeEach(() => {
wrapper = createComponent({ props: { containerId: 'some-id' } });
});
it('renders with the given id', () => {
expect(getGlLink().attributes('id')).toBe('some-id');
});
}); });
}); });
...@@ -58,34 +58,25 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -58,34 +58,25 @@ RSpec.describe TrialStatusWidgetHelper do
end end
end end
describe '#trial_days_remaining_in_words' do describe '#plan_title_for_group' do
let_it_be(:group) { build(:group) } using RSpec::Parameterized::TableSyntax
let!(:subscription) { build(:gitlab_subscription, :active_trial, namespace: group) }
subject { helper.trial_days_remaining_in_words(group) } let_it_be(:group) { create(:group) }
context 'when there are 0 days remaining' do subject { helper.plan_title_for_group(group) }
before do
subscription.trial_ends_on = Date.current
end
it { is_expected.to eq('Ultimate Trial – 0 days left') } where(:plan, :title) do
:bronze | 'Bronze'
:silver | 'Silver'
:gold | 'Gold'
:premium | 'Premium'
:ultimate | 'Ultimate'
end end
context 'when there is 1 day remaining' do with_them do
before do let!(:subscription) { build(:gitlab_subscription, plan, namespace: group) }
subscription.trial_ends_on = Date.current.advance(days: 1)
end
it { is_expected.to eq('Ultimate Trial – 1 day left') } it { is_expected.to eq(title) }
end
context 'when there are 2+ days remaining' do
before do
subscription.trial_ends_on = Date.current.advance(days: 13)
end
it { is_expected.to eq('Ultimate Trial – 13 days left') }
end end
end end
end end
...@@ -5,31 +5,43 @@ require 'spec_helper' ...@@ -5,31 +5,43 @@ require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_group' do RSpec.describe 'layouts/nav/sidebar/_group' do
before do before do
assign(:group, group) assign(:group, group)
allow(view).to receive(:show_trial_status_widget?).with(group).and_return(show_trial_status_widget) allow(view).to receive(:show_trial_status_widget?).and_return(false)
end end
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:show_trial_status_widget) { false }
describe 'trial status widget' do describe 'trial status widget', :aggregate_failures do
let!(:gitlab_subscription) { create(:gitlab_subscription, :active_trial, namespace: group) } let!(:gitlab_subscription) { create(:gitlab_subscription, :active_trial, namespace: group) }
let(:show_widget) { false }
context 'when the experiment is off' do before do
it 'is not rendered' do allow(view).to receive(:show_trial_status_widget?).and_return(show_widget)
render render
end
expect(rendered).not_to have_selector '#js-trial-status-widget' subject { rendered }
context 'when the widget should not be shown' do
it 'does not render' do
is_expected.not_to have_selector '#js-trial-status-widget'
is_expected.not_to have_selector '#js-trial-status-popover'
end end
end end
context 'when the experiment is on' do context 'when the widget should be shown' do
let(:show_trial_status_widget) { true } let(:show_widget) { true }
it 'is rendered' do it 'renders both the widget & popover component initialization elements' do
render is_expected.to have_selector '#js-trial-status-widget'
is_expected.to have_selector '#js-trial-status-popover'
end
it 'supplies the same popover-trigger id value to both initialization elements' do
expected_id = 'trial-status-sidebar-widget'
expect(rendered).to have_selector '#js-trial-status-widget' is_expected.to have_selector "[data-container-id=#{expected_id}]"
is_expected.to have_selector "[data-target-id=#{expected_id}]"
end end
end end
end end
......
...@@ -31378,20 +31378,29 @@ msgstr "" ...@@ -31378,20 +31378,29 @@ msgstr ""
msgid "Trending" msgid "Trending"
msgstr "" msgstr ""
msgid "Trials|%{plan} Trial %{en_dash} %{num} day left" msgid "Trials|%{planName} Trial %{enDash} %{num} day left"
msgid_plural "Trials|%{plan} Trial %{en_dash} %{num} days left" msgid_plural "Trials|%{planName} Trial %{enDash} %{num} days left"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Trials|Compare all plans"
msgstr ""
msgid "Trials|Create a new group to start your GitLab Ultimate trial." msgid "Trials|Create a new group to start your GitLab Ultimate trial."
msgstr "" msgstr ""
msgid "Trials|Go back to GitLab" msgid "Trials|Go back to GitLab"
msgstr "" msgstr ""
msgid "Trials|Hey there"
msgstr ""
msgid "Trials|Skip Trial" msgid "Trials|Skip Trial"
msgstr "" msgstr ""
msgid "Trials|Upgrade %{groupName} to %{planName}"
msgstr ""
msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'" msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'"
msgstr "" msgstr ""
...@@ -31407,6 +31416,9 @@ msgstr "" ...@@ -31407,6 +31416,9 @@ msgstr ""
msgid "Trials|You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'" msgid "Trials|You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'"
msgstr "" msgstr ""
msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you are enjoying GitLab %{planName}. To continue using GitLab %{planName} after your trial ends, you will need to buy a subscription. You can also choose GitLab Premium if its features are sufficient for your needs."
msgstr ""
msgid "Trial|Company name" msgid "Trial|Company name"
msgstr "" 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