Commit e9eb7967 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '8838-adapt-subscriptions-page-for-free-plans-and-trials' into 'master'

Resolve "Adapt subscriptions page for free plans and trials"

Closes #8838

See merge request gitlab-org/gitlab-ee!8838
parents 71381976 9feeceba
......@@ -3,7 +3,12 @@ import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import SubscriptionTableRow from './subscription_table_row.vue';
import { CUSTOMER_PORTAL_URL } from '../constants';
import {
CUSTOMER_PORTAL_URL,
TABLE_TYPE_DEFAULT,
TABLE_TYPE_FREE,
TABLE_TYPE_TRIAL,
} from '../constants';
import { s__, sprintf } from '~/locale';
export default {
......@@ -13,12 +18,12 @@ export default {
GlLoadingIcon,
},
computed: {
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'rows', 'endpoint']),
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'tables', 'endpoint']),
...mapGetters('subscription', ['isFreePlan']),
subscriptionHeader() {
let suffix = this.isFreePlan ? '' : s__('SubscriptionTable|subscription');
let suffix = '';
if (!this.isFreePlan && this.plan.trial) {
suffix += ` - ${s__('SubscriptionTable|Trial')}`;
suffix = `${s__('SubscriptionTable|Trial')}`;
}
return sprintf(s__('SubscriptionTable|GitLab.com %{planName} %{suffix}'), {
planName: this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name),
......@@ -28,6 +33,17 @@ export default {
actionButtonText() {
return this.isFreePlan ? s__('SubscriptionTable|Upgrade') : s__('SubscriptionTable|Manage');
},
visibleRows() {
let tableKey = TABLE_TYPE_DEFAULT;
if (this.plan.code === null) {
tableKey = TABLE_TYPE_FREE;
} else if (this.plan.trial) {
tableKey = TABLE_TYPE_TRIAL;
}
return this.tables[tableKey].rows;
},
},
mounted() {
this.fetchSubscription();
......@@ -60,7 +76,7 @@ export default {
</div>
<div class="card-body flex-grid d-flex flex-column flex-sm-row flex-md-row flex-lg-column">
<subscription-table-row
v-for="(row, i) in rows"
v-for="(row, i) in visibleRows"
:key="`subscription-rows-${i}`"
:header="row.header"
:columns="row.columns"
......
......@@ -56,7 +56,11 @@ export default {
</span>
</div>
<template v-for="(col, i) in columns">
<div :key="`subscription-col-${i}`" class="grid-cell" :class="[col.hidden ? 'no-value' : '']">
<div
:key="`subscription-col-${i}`"
class="grid-cell"
:class="[col.hideContent ? 'no-value' : '']"
>
<span class="property-label"> {{ col.label }} </span>
<popover v-if="col.popover" :options="getPopoverOptions(col)" />
<p
......
export const USAGE_ROW_INDEX = 0;
export const BILLING_ROW_INDEX = 1;
export const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions';
export const TABLE_TYPE_DEFAULT = 'default';
export const TABLE_TYPE_FREE = 'free';
export const TABLE_TYPE_TRIAL = 'trial';
import Vue from 'vue';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from '../../../constants';
import * as types from './mutation_types';
import { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from '../../../constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
......@@ -16,22 +16,22 @@ export default {
[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) {
const data = convertObjectPropsToCamelCase(payload, { deep: true });
const { plan, usage, billing } = data;
let tableKey = TABLE_TYPE_DEFAULT;
state.plan = plan;
/*
* Update column values for billing and usage row.
* We iterate over the rows within the state
* and update only the column's value property in the state
* with the data we received from the API for the given column
*/
[USAGE_ROW_INDEX, BILLING_ROW_INDEX].forEach(rowIdx => {
const currentRow = state.rows[rowIdx];
currentRow.columns.forEach(currentCol => {
if (rowIdx === USAGE_ROW_INDEX) {
Vue.set(currentCol, 'value', usage[currentCol.id]);
} else if (rowIdx === BILLING_ROW_INDEX) {
Vue.set(currentCol, 'value', billing[currentCol.id]);
if (state.plan.code === null) {
tableKey = TABLE_TYPE_FREE;
} else if (state.plan.trial) {
tableKey = TABLE_TYPE_TRIAL;
}
state.tables[tableKey].rows.forEach(row => {
row.columns.forEach(col => {
if (Object.prototype.hasOwnProperty.call(usage, col.id)) {
Vue.set(col, 'value', usage[col.id]);
} else if (Object.prototype.hasOwnProperty.call(billing, col.id)) {
Vue.set(col, 'value', billing[col.id]);
}
});
});
......
......@@ -9,6 +9,70 @@ export default () => ({
name: null,
trial: false,
},
tables: {
free: {
rows: [
{
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan.',
),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Subscription start date'),
value: null,
isDate: true,
},
],
},
],
},
trial: {
rows: [
{
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Trial start date'),
value: null,
isDate: true,
},
{
id: 'subscriptionEndDate',
label: s__('SubscriptionTable|Trial end date'),
value: null,
isDate: true,
},
],
},
],
},
default: {
rows: [
{
header: {
......@@ -28,7 +92,7 @@ export default () => ({
value: null,
colClass: 'number',
popover: {
content: s__(`SubscriptionTable|Usage count is performed once a day at 12:00 PM.`),
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
},
},
{
......@@ -83,7 +147,7 @@ export default () => ({
'SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances.',
),
},
hidden: true,
hideContent: true, // temporarily display a blank cell (as we don't have content yet)
},
{
id: 'nextInvoice',
......@@ -95,9 +159,11 @@ export default () => ({
'SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances.',
),
},
hidden: true,
hideContent: true, // temporarily display a blank cell (as we don't have content yet)
},
],
},
],
},
},
});
---
title: Adapt subscriptions page for free plans and trials
merge_request: 8838
author:
type: other
......@@ -37,10 +37,17 @@ describe('Subscription Table Row', () => {
vm = mountComponent(Component, props);
});
it(`should render one header cell and ${columns.length} columns in total`, () => {
it(`should render one header cell and ${columns.length} visible columns in total`, () => {
expect(vm.$el.querySelectorAll('.grid-cell')).toHaveLength(columns.length + 1);
});
it(`should not render a hidden column`, () => {
const hiddenColIdx = columns.find(c => !c.display);
const hiddenCol = vm.$el.querySelectorAll('.grid-cell:not(.header-cell)')[hiddenColIdx];
expect(hiddenCol).toBe(undefined);
});
it('should render a title in the header cell', () => {
expect(vm.$el.querySelector('.header-cell').textContent).toContain(props.header.title);
});
......
......@@ -2,8 +2,8 @@ import Vue from 'vue';
import component from 'ee/billings/components/subscription_table.vue';
import createStore from 'ee/billings/stores';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import mockDataSubscription from 'ee/billings/stores/modules/subscription/mock_data_subscription.json';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import mockDataSubscription from '../mock_data';
import { resetStore } from '../helpers';
describe('Subscription Table', () => {
......@@ -41,14 +41,17 @@ describe('Subscription Table', () => {
beforeEach(done => {
vm.$store.state.subscription.namespaceId = namespaceId;
vm.$store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription);
vm.$store.commit(
`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`,
mockDataSubscription.gold,
);
vm.$store.state.subscription.isLoading = false;
vm.$nextTick(done);
});
it('should render the card title "GitLab.com Gold subscription"', () => {
it('should render the card title "GitLab.com Gold"', () => {
expect(vm.$el.querySelector('.js-subscription-header strong').textContent.trim()).toBe(
'GitLab.com Gold subscription',
'GitLab.com Gold',
);
});
......
export default {
gold: {
plan: {
name: 'Gold',
code: 'gold',
trial: false,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 98,
max_seats_used: 104,
seats_owed: 4,
},
billing: {
subscription_start_date: '2018-07-11',
subscription_end_date: '2019-07-11',
last_invoice: '2018-09-01',
next_invoice: '2018-10-01',
},
},
free: {
plan: {
name: null,
code: null,
trial: null,
},
usage: {
seats_in_subscription: 0,
seats_in_use: 0,
max_seats_used: 5,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-10-30',
subscription_end_date: null,
trial_ends_on: null,
},
},
trial: {
plan: {
name: 'Gold',
code: 'gold',
trial: true,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 1,
max_seats_used: 0,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-12-13',
subscription_end_date: '2019-12-13',
trial_ends_on: '2019-12-13',
},
},
};
......@@ -6,7 +6,7 @@ import state from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import * as actions from 'ee/billings/stores/modules/subscription/actions';
import mockDataSubscription from './data/mock_data_subscription.json';
import mockDataSubscription from '../../../mock_data';
describe('subscription actions', () => {
let mockedState;
......@@ -51,7 +51,7 @@ describe('subscription actions', () => {
beforeEach(() => {
mock
.onGet(/\/api\/v4\/namespaces\/\d+\/gitlab_subscription(.*)$/)
.replyOnce(200, mockDataSubscription);
.replyOnce(200, mockDataSubscription.gold);
});
it('should dispatch the request and success actions', done => {
......@@ -64,7 +64,7 @@ describe('subscription actions', () => {
{ type: 'requestSubscription' },
{
type: 'receiveSubscriptionSuccess',
payload: mockDataSubscription,
payload: mockDataSubscription.gold,
},
],
done,
......@@ -107,12 +107,12 @@ describe('subscription actions', () => {
it('should commit the success mutation', done => {
testAction(
actions.receiveSubscriptionSuccess,
mockDataSubscription,
mockDataSubscription.gold,
mockedState,
[
{
type: types.RECEIVE_SUBSCRIPTION_SUCCESS,
payload: mockDataSubscription,
payload: mockDataSubscription.gold,
},
],
[],
......
{
"plan": {
"name": "Gold",
"code": "gold",
"trial": false
},
"usage": {
"seats_in_subscription": 100,
"seats_in_use": 98,
"max_seats_used": 104,
"seats_owed": 4
},
"billing": {
"subscription_start_date": "2018-07-11",
"subscription_end_date": "2019-07-11",
"last_invoice": "2018-09-01",
"next_invoice": "2018-10-01"
}
}
import createState from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import mutations from 'ee/billings/stores/modules/subscription/mutations';
import { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from 'ee/billings/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockData from './data/mock_data_subscription.json';
import mockData from '../../../mock_data';
describe('subscription module mutations', () => {
describe('SET_PNAMESPACE_ID', () => {
describe('SET_NAMESPACE_ID', () => {
it('should set "namespaceId" to "1"', () => {
const state = createState();
const namespaceId = '1';
......@@ -34,9 +33,10 @@ describe('subscription module mutations', () => {
let payload;
let state;
describe('Gold subscription', () => {
beforeEach(() => {
payload = mockData;
state = createState();
payload = mockData.gold;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
......@@ -51,7 +51,7 @@ describe('subscription module mutations', () => {
});
it('should set the column values on the "Usage" row', () => {
const usageRow = state.rows[USAGE_ROW_INDEX];
const usageRow = state.tables.default.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
expect(column.value).toBe(data.usage[column.id]);
......@@ -59,14 +59,73 @@ describe('subscription module mutations', () => {
});
it('should set the column values on the "Billing" row', () => {
const billingow = state.rows[BILLING_ROW_INDEX];
const billingRow = state.tables.default.rows[1];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
billingow.columns.forEach(column => {
billingRow.columns.forEach(column => {
expect(column.value).toBe(data.billing[column.id]);
});
});
});
describe('Free plan', () => {
beforeEach(() => {
state = createState();
payload = mockData.free;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" from "billings row" correctly', () => {
const usageRow = state.tables.free.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
}
});
});
});
describe('Gold trial', () => {
beforeEach(() => {
state = createState();
payload = mockData.trial;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" and "subscriptionEndDate" from "billings row" correctly', () => {
const usageRow = state.tables.trial.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
} else if (column.id === 'subscriptionEndDate') {
expect(column.value).toBe(data.billing.subscriptionEndDate);
}
});
});
});
});
describe('RECEIVE_SUBSCRIPTION_ERROR', () => {
let state;
......
......@@ -8406,9 +8406,18 @@ msgstr ""
msgid "SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances."
msgstr ""
msgid "SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan."
msgstr ""
msgid "SubscriptionTable|Trial"
msgstr ""
msgid "SubscriptionTable|Trial end date"
msgstr ""
msgid "SubscriptionTable|Trial start date"
msgstr ""
msgid "SubscriptionTable|Upgrade"
msgstr ""
......@@ -8418,9 +8427,6 @@ msgstr ""
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
msgstr ""
msgid "SubscriptionTable|subscription"
msgstr ""
msgid "Suggested change"
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