Commit dbdb7f6f authored by Alex Buijs's avatar Alex Buijs

Allow selecting a group to apply license to

When an exisiting user is buying a license, allow
the user to select a group if there is one.
parent a29c2a71
......@@ -16,7 +16,7 @@ export default {
};
},
computed: {
...mapState(['newUser']),
...mapState(['isNewUser']),
},
i18n: {
checkout: s__('Checkout|Checkout'),
......@@ -26,7 +26,7 @@ export default {
<template>
<div class="checkout d-flex flex-column justify-content-between w-100">
<div class="full-width">
<progress-bar v-if="newUser" :step="step" />
<progress-bar v-if="isNewUser" :step="step" />
<div class="flash-container"></div>
<h2 class="mt-4 mb-3 mb-lg-5">{{ $options.i18n.checkout }}</h2>
<subscription-details />
......
......@@ -2,6 +2,7 @@
import _ from 'underscore';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { mapState, mapGetters, mapActions } from 'vuex';
import { NEW_GROUP } from 'ee/subscriptions/new/constants';
import { GlFormGroup, GlFormSelect, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import Step from './step.vue';
......@@ -22,11 +23,19 @@ export default {
...mapState([
'availablePlans',
'selectedPlan',
'isNewUser',
'groupData',
'selectedGroup',
'isSetupForCompany',
'organizationName',
'numberOfUsers',
]),
...mapGetters(['selectedPlanText']),
...mapGetters([
'selectedPlanText',
'isGroupSelected',
'selectedGroupUsers',
'selectedGroupName',
]),
selectedPlanModel: {
get() {
return this.selectedPlan;
......@@ -35,6 +44,14 @@ export default {
this.updateSelectedPlan(selectedPlan);
},
},
selectedGroupModel: {
get() {
return this.selectedGroup;
},
set(selectedGroup) {
this.updateSelectedGroup(selectedGroup);
},
},
numberOfUsersModel: {
get() {
return this.numberOfUsers;
......@@ -58,16 +75,42 @@ export default {
if (this.isSetupForCompany) {
return (
!_.isEmpty(this.selectedPlan) &&
!_.isEmpty(this.organizationName) &&
this.numberOfUsers > 0
(!_.isEmpty(this.organizationName) || this.isGroupSelected) &&
this.numberOfUsers > 0 &&
this.numberOfUsers >= this.selectedGroupUsers
);
}
return !_.isEmpty(this.selectedPlan) && this.numberOfUsers === 1;
},
isShowingGroupSelector() {
return !this.isNewUser && this.groupData.length;
},
isShowingNameOfCompanyInput() {
return this.isSetupForCompany && (!this.groupData.length || this.selectedGroup === NEW_GROUP);
},
groupOptionsWithDefault() {
return [
{
text: this.$options.i18n.groupSelectPrompt,
value: null,
},
...this.groupData,
{
text: this.$options.i18n.groupSelectCreateNewOption,
value: NEW_GROUP,
},
];
},
groupSelectDescription() {
return this.selectedGroup === NEW_GROUP
? this.$options.i18n.createNewGroupDescription
: this.$options.i18n.selectedGroupDescription;
},
},
methods: {
...mapActions([
'updateSelectedPlan',
'updateSelectedGroup',
'toggleIsSetupForCompany',
'updateNumberOfUsers',
'updateOrganizationName',
......@@ -77,6 +120,11 @@ export default {
stepTitle: s__('Checkout|Subscription details'),
nextStepButtonText: s__('Checkout|Continue to billing'),
selectedPlanLabel: s__('Checkout|GitLab plan'),
selectedGroupLabel: s__('Checkout|GitLab group'),
groupSelectPrompt: s__('Checkout|Select'),
groupSelectCreateNewOption: s__('Checkout|Create a new group'),
selectedGroupDescription: s__('Checkout|Your subscription will be applied to this group'),
createNewGroupDescription: s__("Checkout|You'll create your new group after checkout"),
organizationNameLabel: s__('Checkout|Name of company or organization using GitLab'),
numberOfUsersLabel: s__('Checkout|Number of users'),
needMoreUsersLink: s__('Checkout|Need more users? Purchase GitLab for your %{company}.'),
......@@ -95,46 +143,44 @@ export default {
:next-step-button-text="$options.i18n.nextStepButtonText"
>
<template #body>
<gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3">
<gl-form-select v-model="selectedPlanModel" v-autofocusonshow :options="availablePlans" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.selectedPlanLabel"
v-if="isShowingGroupSelector"
:label="$options.i18n.selectedGroupLabel"
:description="groupSelectDescription"
label-size="sm"
label-for="selectedPlan"
class="append-bottom-default"
class="mb-3"
>
<gl-form-select
id="selectedPlan"
v-model="selectedPlanModel"
v-autofocusonshow
:options="availablePlans"
ref="group-select"
v-model="selectedGroupModel"
:options="groupOptionsWithDefault"
/>
</gl-form-group>
<gl-form-group
v-if="isSetupForCompany"
v-if="isShowingNameOfCompanyInput"
:label="$options.i18n.organizationNameLabel"
label-size="sm"
label-for="organizationName"
class="append-bottom-default"
class="mb-3"
>
<gl-form-input id="organizationName" v-model="organizationNameModel" type="text" />
<gl-form-input ref="organization-name" v-model="organizationNameModel" type="text" />
</gl-form-group>
<div class="combined d-flex">
<gl-form-group
:label="$options.i18n.numberOfUsersLabel"
label-size="sm"
label-for="numberOfUsers"
class="number"
>
<gl-form-group :label="$options.i18n.numberOfUsersLabel" label-size="sm" class="number">
<gl-form-input
id="numberOfUsers"
ref="number-of-users"
v-model.number="numberOfUsersModel"
type="number"
min="0"
:min="selectedGroupUsers"
:disabled="!isSetupForCompany"
/>
</gl-form-group>
<gl-form-group
v-if="!isSetupForCompany"
class="label prepend-left-default align-self-end company-link"
ref="company-link"
class="label ml-3 align-self-end"
>
<gl-sprintf :message="$options.i18n.needMoreUsersLink">
<template #company>
......@@ -145,13 +191,13 @@ export default {
</div>
</template>
<template #summary>
<strong class="js-summary-line-1">
<strong ref="summary-line-1">
{{ selectedPlanTextLine }}
</strong>
<div v-if="isSetupForCompany" class="js-summary-line-2">
{{ $options.i18n.group }}: {{ organizationName }}
<div v-if="isSetupForCompany" ref="summary-line-2">
{{ $options.i18n.group }}: {{ organizationName || selectedGroupName }}
</div>
<div class="js-summary-line-3">{{ $options.i18n.users }}: {{ numberOfUsers }}</div>
<div ref="summary-line-3">{{ $options.i18n.users }}: {{ numberOfUsers }}</div>
</template>
</step>
</template>
......@@ -18,3 +18,5 @@ export const PROGRESS_STEPS = {
};
export const TAX_RATE = 0;
export const NEW_GROUP = 'new_group';
......@@ -25,6 +25,12 @@ export const updateSelectedPlan = ({ commit }, selectedPlan) => {
commit(types.UPDATE_SELECTED_PLAN, selectedPlan);
};
export const updateSelectedGroup = ({ commit, getters }, selectedGroup) => {
commit(types.UPDATE_SELECTED_GROUP, selectedGroup);
commit(types.UPDATE_ORGANIZATION_NAME, null);
commit(types.UPDATE_NUMBER_OF_USERS, getters.selectedGroupUsers);
};
export const toggleIsSetupForCompany = ({ state, commit }) => {
commit(types.UPDATE_IS_SETUP_FOR_COMPANY, !state.isSetupForCompany);
};
......
import { STEPS } from '../constants';
import { STEPS, NEW_GROUP } from '../constants';
import { s__ } from '~/locale';
export const currentStep = state => state.currentStep;
......@@ -15,8 +15,10 @@ export const selectedPlanPrice = (state, getters) =>
export const selectedPlanDetails = state =>
state.availablePlans.find(plan => plan.value === state.selectedPlan);
export const confirmOrderParams = state => ({
export const confirmOrderParams = (state, getters) => ({
setup_for_company: state.isSetupForCompany,
selected_group: getters.selectedGroupId,
new_user: state.isNewUser,
customer: {
country: state.country,
address_1: state.streetAddressLine1,
......@@ -42,10 +44,27 @@ export const vat = (state, getters) => state.taxRate * getters.totalExVat;
export const totalAmount = (_, getters) => getters.totalExVat + getters.vat;
export const name = state => {
export const name = (state, getters) => {
if (state.isSetupForCompany && state.organizationName) return state.organizationName;
else if (getters.isGroupSelected) return getters.selectedGroupName;
else if (state.isSetupForCompany) return s__('Checkout|Your organization');
return state.fullName;
};
export const usersPresent = state => state.numberOfUsers > 0;
export const isGroupSelected = state =>
state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP;
export const selectedGroupUsers = (state, getters) => {
if (!getters.isGroupSelected) return 1;
return state.groupData.find(group => group.value === state.selectedGroup).numberOfUsers;
};
export const selectedGroupName = (state, getters) => {
if (!getters.isGroupSelected) return null;
return state.groupData.find(group => group.value === state.selectedGroup).text;
};
export const selectedGroupId = (state, getters) =>
getters.isGroupSelected ? state.selectedGroup : null;
......@@ -2,6 +2,8 @@ export const UPDATE_CURRENT_STEP = 'UPDATE_CURRENT_STEP';
export const UPDATE_SELECTED_PLAN = 'UPDATE_SELECTED_PLAN';
export const UPDATE_SELECTED_GROUP = 'UPDATE_SELECTED_GROUP';
export const UPDATE_IS_SETUP_FOR_COMPANY = 'UPDATE_IS_SETUP_FOR_COMPANY';
export const UPDATE_NUMBER_OF_USERS = 'UPDATE_NUMBER_OF_USERS';
......
......@@ -9,6 +9,10 @@ export default {
state.selectedPlan = selectedPlan;
},
[types.UPDATE_SELECTED_GROUP](state, selectedGroup) {
state.selectedGroup = selectedGroup;
},
[types.UPDATE_IS_SETUP_FOR_COMPANY](state, isSetupForCompany) {
state.isSetupForCompany = isSetupForCompany;
},
......
......@@ -9,6 +9,13 @@ const parsePlanData = planData =>
pricePerUserPerYear: plan.price_per_year,
}));
const parseGroupData = groupData =>
JSON.parse(groupData).map(group => ({
value: group.id,
text: group.name,
numberOfUsers: group.users,
}));
const determineSelectedPlan = (planId, plans) => {
if (planId && plans.find(plan => plan.value === planId)) {
return planId;
......@@ -16,18 +23,28 @@ const determineSelectedPlan = (planId, plans) => {
return plans[0] && plans[0].value;
};
export default ({ planData = '[]', planId, setupForCompany, fullName, newUser }) => {
export default ({
planData = '[]',
planId,
setupForCompany,
fullName,
newUser,
groupData = '[]',
}) => {
const availablePlans = parsePlanData(planData);
const isNewUser = parseBoolean(newUser);
return {
currentStep: STEPS[0],
isSetupForCompany: parseBoolean(setupForCompany),
isSetupForCompany: parseBoolean(setupForCompany) || !isNewUser,
availablePlans,
selectedPlan: determineSelectedPlan(planId, availablePlans),
newUser: parseBoolean(newUser),
isNewUser,
fullName,
groupData: parseGroupData(groupData),
selectedGroup: null,
organizationName: null,
numberOfUsers: parseBoolean(setupForCompany) ? 0 : 1,
numberOfUsers: 1,
country: null,
streetAddressLine1: null,
streetAddressLine2: null,
......
......@@ -28,9 +28,9 @@ describe('Checkout', () => {
wrapper.destroy();
});
describe.each([[true, true], [false, false]])('when newUser=%s', (newUser, visible) => {
describe.each([[true, true], [false, false]])('when isNewUser=%s', (isNewUser, visible) => {
beforeEach(() => {
store.state.newUser = newUser;
store.state.isNewUser = isNewUser;
});
it(`progress bar visibility is ${visible}`, () => {
......
......@@ -77,6 +77,23 @@ describe('Subscriptions Actions', () => {
});
});
describe('updateSelectedGroup', () => {
it('updates the selected group, resets the organization name and updates the number of users', done => {
testAction(
actions.updateSelectedGroup,
'groupId',
{ selectedGroupUsers: 3 },
[
{ type: 'UPDATE_SELECTED_GROUP', payload: 'groupId' },
{ type: 'UPDATE_ORGANIZATION_NAME', payload: null },
{ type: 'UPDATE_NUMBER_OF_USERS', payload: 3 },
],
[],
done,
);
});
});
describe('toggleIsSetupForCompany', () => {
it('toggles the isSetupForCompany value', done => {
testAction(
......
......@@ -6,6 +6,7 @@ constants.STEPS = ['firstStep', 'secondStep'];
const state = {
currentStep: 'secondStep',
isSetupForCompany: true,
isNewUser: true,
availablePlans: [
{
value: 'firstPlan',
......@@ -99,17 +100,30 @@ describe('Subscriptions Getters', () => {
describe('name', () => {
it('returns the organization name when setting up for a company and when it is present', () => {
expect(getters.name({ isSetupForCompany: true, organizationName: 'My organization' })).toBe(
'My organization',
);
expect(
getters.name({ isSetupForCompany: true, organizationName: 'My organization' }, getters),
).toBe('My organization');
});
it('returns the selected group name a group is selected', () => {
expect(
getters.name(
{ isSetupForCompany: true },
{ isGroupSelected: true, selectedGroupName: 'Selected group' },
),
).toBe('Selected group');
});
it('returns the default text when setting up for a company and the organization name is not present', () => {
expect(getters.name({ isSetupForCompany: true })).toBe('Your organization');
expect(getters.name({ isSetupForCompany: true }, { isGroupSelected: false })).toBe(
'Your organization',
);
});
it('returns the full name when not setting up for a company', () => {
expect(getters.name({ isSetupForCompany: false, fullName: 'My name' })).toBe('My name');
expect(
getters.name({ isSetupForCompany: false, fullName: 'My name' }, { isGroupSelected: false }),
).toBe('My name');
});
});
......@@ -123,10 +137,78 @@ describe('Subscriptions Getters', () => {
});
});
describe('isGroupSelected', () => {
it('returns true when the selectedGroup is not null and does not equal "true"', () => {
expect(getters.isGroupSelected({ selectedGroup: 1 })).toBe(true);
});
it('returns false when the selectedGroup is null', () => {
expect(getters.isGroupSelected({ selectedGroup: null })).toBe(false);
});
it('returns false when the selectedGroup equals "new"', () => {
expect(getters.isGroupSelected({ selectedGroup: constants.NEW_GROUP })).toBe(false);
});
});
describe('selectedGroupUsers', () => {
it('returns 1 when no group is selected', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: false },
),
).toBe(1);
});
it('returns the number of users of the selected group when a group is selected', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true },
),
).toBe(3);
});
});
describe('selectedGroupName', () => {
it('returns null when no group is selected', () => {
expect(
getters.selectedGroupName(
{ groupData: [{ text: 'Selected group', value: 123 }], selectedGroup: 123 },
{ isGroupSelected: false },
),
).toBe(null);
});
it('returns the text attribute of the selected group when a group is selected', () => {
expect(
getters.selectedGroupName(
{ groupData: [{ text: 'Selected group', value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true },
),
).toBe('Selected group');
});
});
describe('selectedGroupId', () => {
it('returns null when no group is selected', () => {
expect(getters.selectedGroupId({ selectedGroup: 123 }, { isGroupSelected: false })).toBe(
null,
);
});
it('returns the id of the selected group when a group is selected', () => {
expect(getters.selectedGroupId({ selectedGroup: 123 }, { isGroupSelected: true })).toBe(123);
});
});
describe('confirmOrderParams', () => {
it('returns the params to confirm the order', () => {
expect(getters.confirmOrderParams(state)).toEqual({
expect(getters.confirmOrderParams(state, { selectedGroupId: 11 })).toEqual({
setup_for_company: true,
selected_group: 11,
new_user: true,
customer: {
country: 'Country',
address_1: 'Street address line 1',
......
......@@ -24,6 +24,7 @@ describe('ee/subscriptions/new/store/mutation', () => {
mutation | value | stateProp
${types.UPDATE_CURRENT_STEP} | ${'secondStep'} | ${'currentStep'}
${types.UPDATE_SELECTED_PLAN} | ${'secondPlan'} | ${'selectedPlan'}
${types.UPDATE_SELECTED_GROUP} | ${'selectedGroup'} | ${'selectedGroup'}
${types.UPDATE_IS_SETUP_FOR_COMPANY} | ${false} | ${'isSetupForCompany'}
${types.UPDATE_NUMBER_OF_USERS} | ${2} | ${'numberOfUsers'}
${types.UPDATE_ORGANIZATION_NAME} | ${'new name'} | ${'organizationName'}
......
......@@ -10,11 +10,18 @@ describe('projectsSelector default state', () => {
{ id: 'secondPlanId', code: 'silver', price_per_year: 228 },
];
const groupData = [
{ id: 132, name: 'My first group', users: 3 },
{ id: 483, name: 'My second group', users: 12 },
];
const initialData = {
planData: JSON.stringify(planData),
groupData: JSON.stringify(groupData),
planId: 'secondPlanId',
setupForCompany: 'true',
fullName: 'Full Name',
newUser: 'true',
};
const currentDate = new Date('2020-01-07T12:44:08.135Z');
......@@ -67,12 +74,24 @@ describe('projectsSelector default state', () => {
});
describe('isSetupForCompany', () => {
it('sets the isSetupForCompany to true if provided setupForCompany is "true"', () => {
it('sets the isSetupForCompany to true if provided setupForCompany is "true" and the provided newUser is "true"', () => {
expect(state.isSetupForCompany).toEqual(true);
});
it('sets the isSetupForCompany to true if provided newUser is "false"', () => {
const modifiedState = createState({
...initialData,
...{ newUser: 'false' },
});
expect(modifiedState.isSetupForCompany).toEqual(true);
});
it('sets the isSetupForCompany to false if provided setupForCompany is "false"', () => {
const modifiedState = createState({ ...initialData, ...{ setupForCompany: 'false' } });
const modifiedState = createState({
...initialData,
...{ setupForCompany: 'false' },
});
expect(modifiedState.isSetupForCompany).toEqual(false);
});
......@@ -82,22 +101,33 @@ describe('projectsSelector default state', () => {
expect(state.fullName).toEqual('Full Name');
});
it('sets the organizationName to null', () => {
expect(state.organizationName).toBeNull();
});
describe('numberOfUsers', () => {
it('sets the numberOfUsers to 0 when setupForCompany is true', () => {
expect(state.numberOfUsers).toEqual(0);
describe('groupData', () => {
it('sets the groupData to the provided parsed groupData', () => {
expect(state.groupData).toEqual([
{ value: 132, text: 'My first group', numberOfUsers: 3 },
{ value: 483, text: 'My second group', numberOfUsers: 12 },
]);
});
it('sets the numberOfUsers to 1 when setupForCompany is false', () => {
const modifiedState = createState({ ...initialData, ...{ setupForCompany: 'false' } });
it('sets the availablePlans to an empty array when no groupData is provided', () => {
const modifiedState = createState({ ...initialData, ...{ groupData: undefined } });
expect(modifiedState.numberOfUsers).toEqual(1);
expect(modifiedState.groupData).toEqual([]);
});
});
it('sets the selectedGroup to null', () => {
expect(state.selectedGroup).toBeNull();
});
it('sets the organizationName to null', () => {
expect(state.organizationName).toBeNull();
});
it('sets the numberOfUsers to 1', () => {
expect(state.numberOfUsers).toEqual(1);
});
it('sets the country to null', () => {
expect(state.country).toBeNull();
});
......
......@@ -3480,6 +3480,9 @@ msgstr ""
msgid "Checkout|Country"
msgstr ""
msgid "Checkout|Create a new group"
msgstr ""
msgid "Checkout|Credit card form failed to load. Please try again."
msgstr ""
......@@ -3507,6 +3510,9 @@ msgstr ""
msgid "Checkout|Failed to register credit card. Please try again."
msgstr ""
msgid "Checkout|GitLab group"
msgstr ""
msgid "Checkout|GitLab plan"
msgstr ""
......@@ -3531,6 +3537,9 @@ msgstr ""
msgid "Checkout|Please select a state"
msgstr ""
msgid "Checkout|Select"
msgstr ""
msgid "Checkout|State"
msgstr ""
......@@ -3555,9 +3564,15 @@ msgstr ""
msgid "Checkout|Users"
msgstr ""
msgid "Checkout|You'll create your new group after checkout"
msgstr ""
msgid "Checkout|Your organization"
msgstr ""
msgid "Checkout|Your subscription will be applied to this group"
msgstr ""
msgid "Checkout|Zip code"
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