Commit dbdef82b authored by Angelo Gulina's avatar Angelo Gulina Committed by Nicolò Maria Mezzopera

Subscription Activation: Fix Subscription Form UX

parent f637b48d
...@@ -9,9 +9,16 @@ import { ...@@ -9,9 +9,16 @@ import {
GlLink, GlLink,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { subscriptionActivationForm, subscriptionQueries } from '../constants'; import { helpPagePath } from '~/helpers/help_page_helper';
import validation from '~/vue_shared/directives/validation';
import {
fieldRequiredMessage,
subscriptionActivationForm,
subscriptionQueries,
} from '../constants';
export const SUBSCRIPTION_ACTIVATION_EVENT = 'subscription-activation'; export const SUBSCRIPTION_ACTIVATION_EVENT = 'subscription-activation';
export const adminLicenseUrl = helpPagePath('/user/admin_area/license');
export default { export default {
i18n: { i18n: {
...@@ -21,6 +28,7 @@ export default { ...@@ -21,6 +28,7 @@ export default {
pasteActivationCode: subscriptionActivationForm.pasteActivationCode, pasteActivationCode: subscriptionActivationForm.pasteActivationCode,
acceptTerms: subscriptionActivationForm.acceptTerms, acceptTerms: subscriptionActivationForm.acceptTerms,
activateLabel: subscriptionActivationForm.activateLabel, activateLabel: subscriptionActivationForm.activateLabel,
fieldRequiredMessage,
}, },
name: 'CloudLicenseSubscriptionActivationForm', name: 'CloudLicenseSubscriptionActivationForm',
components: { components: {
...@@ -33,27 +41,58 @@ export default { ...@@ -33,27 +41,58 @@ export default {
GlSprintf, GlSprintf,
GlLink, GlLink,
}, },
links: {
adminLicenseUrl,
},
directives: {
validation: validation(),
},
data() { data() {
const form = {
state: false,
showValidation: false,
fields: {
activationCode: {
required: true,
state: null,
value: '',
},
terms: {
required: true,
state: null,
},
},
};
return { return {
activationCode: null, form,
isLoading: false, isLoading: false,
termsAccepted: false,
}; };
}, },
computed: { computed: {
activateButtonDisabled() { isCheckboxValid() {
return this.isLoading || !this.termsAccepted; if (this.form.showValidation) {
return this.form.fields.terms.state ? null : false;
}
return null;
},
isRequestingActivation() {
return this.isLoading;
}, },
}, },
methods: { methods: {
submit() { submit() {
if (!this.form.state) {
this.form.showValidation = true;
return;
}
this.form.showValidation = false;
this.isLoading = true; this.isLoading = true;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: subscriptionQueries.mutation, mutation: subscriptionQueries.mutation,
variables: { variables: {
gitlabSubscriptionActivateInput: { gitlabSubscriptionActivateInput: {
activationCode: this.activationCode, activationCode: this.form.fields.activationCode.value,
}, },
}, },
}) })
...@@ -88,26 +127,40 @@ export default { ...@@ -88,26 +127,40 @@ export default {
<p> <p>
<gl-sprintf :message="$options.i18n.howToActivateSubscription"> <gl-sprintf :message="$options.i18n.howToActivateSubscription">
<template #link="{ content }"> <template #link="{ content }">
<gl-link href="" target="_blank">{{ content }}</gl-link> <gl-link :href="$options.links.adminLicenseUrl" target="_blank">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<gl-form @submit.stop.prevent="submit"> <gl-form novalidate @submit.prevent="submit">
<gl-form-group class="gl-mb-0"> <div class="gl-display-flex gl-flex-wrap">
<div class="gl-display-flex gl-flex-wrap"> <gl-form-group
class="gl-flex-grow-1"
:invalid-feedback="form.fields.activationCode.feedback"
data-testid="form-group-activation-code"
>
<label class="gl-w-full" for="activation-code-group"> <label class="gl-w-full" for="activation-code-group">
{{ $options.i18n.activationCode }} {{ $options.i18n.activationCode }}
</label> </label>
<gl-form-input <gl-form-input
id="activation-code-group" id="activation-code-group"
v-model="activationCode" v-model="form.fields.activationCode.value"
v-validation:[form.showValidation]
:disabled="isLoading" :disabled="isLoading"
:placeholder="$options.i18n.pasteActivationCode" :placeholder="$options.i18n.pasteActivationCode"
class="gl-w-full gl-mb-4" :state="form.fields.activationCode.state"
name="activationCode"
class="gl-mb-4"
required required
/> />
</gl-form-group>
<gl-form-checkbox v-model="termsAccepted"> <gl-form-group
class="gl-mb-0"
:state="isCheckboxValid"
:invalid-feedback="$options.i18n.fieldRequiredMessage"
data-testid="form-group-terms"
>
<gl-form-checkbox v-model="form.fields.terms.state" :state="isCheckboxValid">
<gl-sprintf :message="$options.i18n.acceptTerms"> <gl-sprintf :message="$options.i18n.acceptTerms">
<template #link="{ content }"> <template #link="{ content }">
<gl-link href="https://about.gitlab.com/terms/" target="_blank" <gl-link href="https://about.gitlab.com/terms/" target="_blank"
...@@ -116,19 +169,19 @@ export default { ...@@ -116,19 +169,19 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</gl-form-checkbox> </gl-form-checkbox>
</gl-form-group>
<gl-button <gl-button
:disabled="activateButtonDisabled" :loading="isRequestingActivation"
category="primary" category="primary"
class="gl-mt-6" class="gl-mt-6 js-no-auto-disable"
data-testid="activate-button" data-testid="activate-button"
type="submit" type="submit"
variant="confirm" variant="confirm"
> >
{{ $options.i18n.activateLabel }} {{ $options.i18n.activateLabel }}
</gl-button> </gl-button>
</div> </div>
</gl-form-group>
</gl-form> </gl-form>
</gl-card> </gl-card>
</template> </template>
...@@ -3,6 +3,7 @@ import activateSubscriptionMutation from './graphql/mutations/activate_subscript ...@@ -3,6 +3,7 @@ import activateSubscriptionMutation from './graphql/mutations/activate_subscript
import getCurrentLicense from './graphql/queries/get_current_license.query.graphql'; import getCurrentLicense from './graphql/queries/get_current_license.query.graphql';
import getLicenseHistory from './graphql/queries/get_license_history.query.graphql'; import getLicenseHistory from './graphql/queries/get_license_history.query.graphql';
export const fieldRequiredMessage = s__('SuperSonics|This field is required.');
export const subscriptionMainTitle = s__('SuperSonics|Your subscription'); export const subscriptionMainTitle = s__('SuperSonics|Your subscription');
export const subscriptionActivationTitle = s__( export const subscriptionActivationTitle = s__(
`SuperSonics|You do not have an active subscription`, `SuperSonics|You do not have an active subscription`,
......
import { GlForm, GlFormInput, GlFormCheckbox } from '@gitlab/ui'; import { GlForm, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import CloudLicenseSubscriptionActivationForm, { import CloudLicenseSubscriptionActivationForm, {
SUBSCRIPTION_ACTIVATION_EVENT, SUBSCRIPTION_ACTIVATION_EVENT,
} from 'ee/pages/admin/cloud_licenses/components/subscription_activation_form.vue'; } from 'ee/pages/admin/cloud_licenses/components/subscription_activation_form.vue';
import { subscriptionQueries } from 'ee/pages/admin/cloud_licenses/constants'; import { fieldRequiredMessage, subscriptionQueries } from 'ee/pages/admin/cloud_licenses/constants';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -26,9 +26,10 @@ describe('CloudLicenseApp', () => { ...@@ -26,9 +26,10 @@ describe('CloudLicenseApp', () => {
const findActivateButton = () => wrapper.findByTestId('activate-button'); const findActivateButton = () => wrapper.findByTestId('activate-button');
const findAgreementCheckbox = () => wrapper.findComponent(GlFormCheckbox); const findAgreementCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findAgreementCheckboxFormGroup = () => wrapper.findByTestId('form-group-terms');
const findActivationCodeFormGroup = () => wrapper.findByTestId('form-group-activation-code');
const findActivationCodeInput = () => wrapper.findComponent(GlFormInput); const findActivationCodeInput = () => wrapper.findComponent(GlFormInput);
const findActivateSubscriptionForm = () => wrapper.findComponent(GlForm); const findActivateSubscriptionForm = () => wrapper.findComponent(GlForm);
const enableAcceptAgreementCheckbox = () => findAgreementCheckbox().vm.$emit('input', true);
const GlFormInputStub = stubComponent(GlFormInput, { const GlFormInputStub = stubComponent(GlFormInput, {
template: `<input />`, template: `<input />`,
...@@ -39,16 +40,17 @@ describe('CloudLicenseApp', () => { ...@@ -39,16 +40,17 @@ describe('CloudLicenseApp', () => {
stopPropagation, stopPropagation,
}); });
const createComponentWithApollo = (props = {}, resolverMock) => { const createComponentWithApollo = ({ props = {}, mutationMock, stubs = {} } = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(CloudLicenseSubscriptionActivationForm, { shallowMount(CloudLicenseSubscriptionActivationForm, {
localVue, localVue,
apolloProvider: createMockApolloProvider(resolverMock), apolloProvider: createMockApolloProvider(mutationMock),
propsData: { propsData: {
...props, ...props,
}, },
stubs: { stubs: {
GlFormInput: GlFormInputStub, GlFormInput: GlFormInputStub,
...stubs,
}, },
}), }),
); );
...@@ -77,25 +79,45 @@ describe('CloudLicenseApp', () => { ...@@ -77,25 +79,45 @@ describe('CloudLicenseApp', () => {
expect(findAgreementCheckbox().exists()).toBe(true); expect(findAgreementCheckbox().exists()).toBe(true);
}); });
it('disables the activate button if the agreement is unaccepted', () => { it('has the activate button enabled', () => {
expect(findActivateButton().props('disabled')).toBe(true); expect(findActivateButton().props('disabled')).toBe(false);
}); });
});
it('enables the activate button if the agreement is accepted', async () => { describe('form errors', () => {
expect(findActivateButton().props('disabled')).toBe(true); const mutationMock = jest.fn();
enableAcceptAgreementCheckbox(); beforeEach(() => {
await wrapper.vm.$nextTick(); createComponentWithApollo({ mutationMock });
});
expect(findActivateButton().props('disabled')).toBe(false); it('shows an error for the text field', async () => {
await findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
expect(findActivationCodeFormGroup().attributes('invalid-feedback')).toBe(
'Please fill out this field.',
);
});
it('shows an error for the checkbox field', async () => {
await findActivationCodeInput().vm.$emit('input', fakeActivationCode);
expect(findAgreementCheckboxFormGroup().attributes('invalid-feedback')).toBe(
fieldRequiredMessage,
);
});
it('does not perform any mutation', () => {
expect(mutationMock).toHaveBeenCalledTimes(0);
}); });
}); });
describe('Activate the subscription', () => { describe('activate the subscription', () => {
describe('when submitting the form', () => { describe('when submitting the form', () => {
const mutationMock = jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS); const mutationMock = jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS);
beforeEach(() => { beforeEach(async () => {
createComponentWithApollo({}, mutationMock); createComponentWithApollo({ mutationMock });
findActivationCodeInput().vm.$emit('input', fakeActivationCode); await findActivationCodeInput().vm.$emit('input', fakeActivationCode);
await findAgreementCheckbox().vm.$emit('input', true);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent()); findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
}); });
...@@ -110,17 +132,6 @@ describe('CloudLicenseApp', () => { ...@@ -110,17 +132,6 @@ describe('CloudLicenseApp', () => {
}, },
}); });
}); });
});
describe('when the mutation is successful', () => {
beforeEach(() => {
createComponentWithApollo(
{},
jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS),
);
findActivationCodeInput().vm.$emit('input', fakeActivationCode);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
});
it('emits a successful event', () => { it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_EVENT)).toEqual([[true]]); expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_EVENT)).toEqual([[true]]);
...@@ -128,11 +139,11 @@ describe('CloudLicenseApp', () => { ...@@ -128,11 +139,11 @@ describe('CloudLicenseApp', () => {
}); });
describe('when the mutation is not successful but looks like it is', () => { describe('when the mutation is not successful but looks like it is', () => {
const mutationMock = jest
.fn()
.mockResolvedValue(activateLicenseMutationResponse.ERRORS_AS_DATA);
beforeEach(() => { beforeEach(() => {
createComponentWithApollo( createComponentWithApollo({ mutationMock });
{},
jest.fn().mockResolvedValue(activateLicenseMutationResponse.FAILURE_IN_DISGUISE),
);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent()); findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
}); });
...@@ -144,11 +155,9 @@ describe('CloudLicenseApp', () => { ...@@ -144,11 +155,9 @@ describe('CloudLicenseApp', () => {
}); });
describe('when the mutation is not successful', () => { describe('when the mutation is not successful', () => {
const mutationMock = jest.fn().mockRejectedValue(activateLicenseMutationResponse.FAILURE);
beforeEach(() => { beforeEach(() => {
createComponentWithApollo( createComponentWithApollo({ mutationMock });
{},
jest.fn().mockRejectedValue(activateLicenseMutationResponse.FAILURE),
);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent()); findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
}); });
......
...@@ -72,7 +72,7 @@ export const activateLicenseMutationResponse = { ...@@ -72,7 +72,7 @@ export const activateLicenseMutationResponse = {
], ],
}, },
], ],
FAILURE_IN_DISGUISE: { ERRORS_AS_DATA: {
data: { data: {
gitlabSubscriptionActivate: { gitlabSubscriptionActivate: {
license: null, license: null,
......
...@@ -30974,6 +30974,9 @@ msgstr "" ...@@ -30974,6 +30974,9 @@ msgstr ""
msgid "SuperSonics|There is a connectivity issue." msgid "SuperSonics|There is a connectivity issue."
msgstr "" msgstr ""
msgid "SuperSonics|This field is required."
msgstr ""
msgid "SuperSonics|Type" msgid "SuperSonics|Type"
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