Commit 5972f9ad authored by Savas Vedova's avatar Savas Vedova

Merge branch 'ag-328246-activation-modal-components-graphql' into 'master'

Subscription Activation Modal: Modal Component + History Mutation

See merge request gitlab-org/gitlab!60909
parents 0efe495c a1fe6c7c
......@@ -60,12 +60,9 @@ export default {
@subscription-activation-failure="handleFormActivationFailure"
/>
<template #footer>
<gl-link
v-if="licenseUploadPath"
data-testid="upload-license-link"
:href="licenseUploadPath"
>{{ $options.i18n.uploadLegacyLicense }}</gl-link
>
<gl-link v-if="licenseUploadPath" data-testid="upload-license-link" :href="licenseUploadPath"
>{{ $options.i18n.uploadLegacyLicense }}
</gl-link>
</template>
</gl-card>
</template>
......@@ -8,7 +8,6 @@ import {
GlLink,
GlSprintf,
} from '@gitlab/ui';
import produce from 'immer';
import validation from '~/vue_shared/directives/validation';
import {
activateLabel,
......@@ -16,18 +15,7 @@ import {
subscriptionActivationForm,
subscriptionQueries,
} from '../constants';
const getLicenseFromData = ({
data: {
gitlabSubscriptionActivate: { license },
},
}) => license;
const getErrorsAsData = ({
data: {
gitlabSubscriptionActivate: { errors },
},
}) => errors;
import { getErrorsAsData, updateSubscriptionAppCache } from '../graphql/utils';
export const SUBSCRIPTION_ACTIVATION_FAILURE_EVENT = 'subscription-activation-failure';
export const SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT = 'subscription-activation-success';
......@@ -109,17 +97,7 @@ export default {
activationCode: this.form.fields.activationCode.value,
},
},
update: (cache, mutation) => {
const license = getLicenseFromData(mutation);
if (!license) {
return;
}
const { query } = subscriptionQueries;
const data = produce(license, (draftData) => {
draftData.currentLicense = license;
});
cache.writeQuery({ query, data });
},
update: this.updateSubscriptionAppCache,
})
.then((res) => {
const errors = getErrorsAsData(res);
......@@ -136,6 +114,7 @@ export default {
this.isLoading = false;
});
},
updateSubscriptionAppCache,
},
};
</script>
......
<script>
import { GlButton } from '@gitlab/ui';
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { pick, some } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import {
enterActivationCode,
licensedToHeaderText,
manageSubscriptionButtonText,
notificationType,
subscriptionDetailsHeaderText,
subscriptionType,
syncSubscriptionButtonText,
notificationType,
} from '../constants';
import SubscriptionActivationModal from './subscription_activation_modal.vue';
import SubscriptionDetailsCard from './subscription_details_card.vue';
import SubscriptionDetailsHistory from './subscription_details_history.vue';
import SubscriptionDetailsUserInfo from './subscription_details_user_info.vue';
export const subscriptionDetailsFields = ['id', 'plan', 'expiresAt', 'lastSync', 'startsAt'];
export const licensedToFields = ['name', 'email', 'company'];
export const modalId = 'subscription-activation-modal';
export default {
i18n: {
......@@ -23,10 +26,18 @@ export default {
manageSubscriptionButtonText,
subscriptionDetailsHeaderText,
syncSubscriptionButtonText,
enterActivationCode,
},
modal: {
id: modalId,
},
name: 'SubscriptionBreakdown',
directives: {
GlModal: GlModalDirective,
},
components: {
GlButton,
SubscriptionActivationModal,
SubscriptionDetailsCard,
SubscriptionDetailsHistory,
SubscriptionDetailsUserInfo,
......@@ -55,7 +66,7 @@ export default {
canSyncSubscription() {
return this.subscriptionSyncPath && this.subscription.type === subscriptionType.CLOUD;
},
canMangeSubscription() {
canManageSubscription() {
return false;
},
hasSubscription() {
......@@ -65,7 +76,10 @@ export default {
return Boolean(this.subscriptionList.length);
},
shouldShowFooter() {
return some(pick(this, ['canSyncSubscription', 'canMangeSubscription']), Boolean);
return some(
pick(this, ['canSyncSubscription', 'canMangeSubscription', 'hasSubscription']),
Boolean,
);
},
subscriptionHistory() {
return this.hasSubscriptionHistory ? this.subscriptionList : [this.subscription];
......@@ -96,6 +110,7 @@ export default {
<template>
<div>
<subscription-activation-modal v-if="hasSubscription" :modal-id="$options.modal.id" />
<subscription-sync-notifications
v-if="notification"
class="mb-4"
......@@ -120,7 +135,16 @@ export default {
>
{{ $options.i18n.syncSubscriptionButtonText }}
</gl-button>
<gl-button v-if="canMangeSubscription">
<gl-button
v-if="hasSubscription"
v-gl-modal="$options.modal.id"
category="primary"
variant="confirm"
data-testid="subscription-activation-action"
>
{{ $options.i18n.enterActivationCode }}
</gl-button>
<gl-button v-if="canManageSubscription">
{{ $options.i18n.manageSubscriptionButtonText }}
</gl-button>
</template>
......
import produce from 'immer';
import { subscriptionHistoryQueries, subscriptionQueries } from '../constants';
export const getLicenseFromData = ({ data } = {}) => data?.gitlabSubscriptionActivate?.license;
export const getErrorsAsData = ({ data } = {}) => data?.gitlabSubscriptionActivate?.errors || [];
export const updateSubscriptionAppCache = (cache, mutation) => {
const license = getLicenseFromData(mutation);
if (!license) {
return;
}
const { query } = subscriptionQueries;
const { query: historyQuery } = subscriptionHistoryQueries;
const data = produce({}, (draftData) => {
draftData.currentLicense = license;
});
cache.writeQuery({ query, data });
const subscriptionsList = cache.readQuery({ query: historyQuery });
const subscriptionListData = produce(subscriptionsList, (draftData) => {
draftData.licenseHistoryEntries.nodes = [
license,
...subscriptionsList.licenseHistoryEntries.nodes,
];
});
cache.writeQuery({ query: historyQuery, data: subscriptionListData });
};
......@@ -116,10 +116,11 @@ describe('CloudLicenseApp', () => {
});
describe('activate the subscription', () => {
describe('when submitting the form', () => {
describe('when submitting the mutation is successful', () => {
const mutationMock = jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS);
beforeEach(async () => {
createComponentWithApollo({ mutationMock });
jest.spyOn(wrapper.vm, 'updateSubscriptionAppCache').mockImplementation();
await findActivationCodeInput().vm.$emit('input', fakeActivationCode);
await findAgreementCheckbox().vm.$emit('input', true);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
......@@ -140,6 +141,10 @@ describe('CloudLicenseApp', () => {
it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT)).toEqual([[]]);
});
it('calls the method to update the cache', () => {
expect(wrapper.vm.updateSubscriptionAppCache).toHaveBeenCalledTimes(1);
});
});
describe('when the mutation is not successful', () => {
......
......@@ -2,8 +2,10 @@ import { GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import SubscriptionActivationModal from 'ee/pages/admin/cloud_licenses/components/subscription_activation_modal.vue';
import SubscriptionBreakdown, {
licensedToFields,
modalId,
subscriptionDetailsFields,
} from 'ee/pages/admin/cloud_licenses/components/subscription_breakdown.vue';
import SubscriptionDetailsCard from 'ee/pages/admin/cloud_licenses/components/subscription_details_card.vue';
......@@ -25,6 +27,7 @@ import { license, subscriptionHistory } from '../mock_data';
describe('Subscription Breakdown', () => {
let axiosMock;
let wrapper;
let glModalDirective;
const [, legacyLicense] = subscriptionHistory;
const connectivityHelpURL = 'connectivity/help/url';
......@@ -34,13 +37,24 @@ describe('Subscription Breakdown', () => {
const findDetailsCardFooter = () => wrapper.find('.gl-card-footer');
const findDetailsHistory = () => wrapper.findComponent(SubscriptionDetailsHistory);
const findDetailsUserInfo = () => wrapper.findComponent(SubscriptionDetailsUserInfo);
const findSubscriptionActivationAction = () =>
wrapper.findByTestId('subscription-activation-action');
const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action');
const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal);
const findSubscriptionSyncNotifications = () =>
wrapper.findComponent(SubscriptionSyncNotifications);
const createComponent = ({ props, stubs } = {}) => {
glModalDirective = jest.fn();
wrapper = extendedWrapper(
shallowMount(SubscriptionBreakdown, {
directives: {
glModal: {
bind(_, { value }) {
glModalDirective(value);
},
},
},
provide: {
connectivityHelpURL,
subscriptionSyncPath,
......@@ -114,6 +128,20 @@ describe('Subscription Breakdown', () => {
expect(findSubscriptionSyncAction().exists()).toBe(true);
});
it('shows a button to activate a new subscription', () => {
createComponent({ stubs: { GlCard, SubscriptionDetailsCard } });
expect(findSubscriptionActivationAction().exists()).toBe(true);
});
it('presents a subscription activation modal', () => {
expect(findSubscriptionActivationModal().exists()).toBe(true);
});
it('passes the correct modal id', () => {
expect(findSubscriptionActivationModal().attributes('modalid')).toBe(modalId);
});
it.todo('shows a button to manage the subscription');
describe('with a legacy license', () => {
......@@ -128,8 +156,8 @@ describe('Subscription Breakdown', () => {
expect(findSubscriptionSyncAction().exists()).toBe(false);
});
it('does not show the subscription details footer', () => {
expect(findDetailsCardFooter().exists()).toBe(false);
it('shows the subscription details footer', () => {
expect(findDetailsCardFooter().exists()).toBe(true);
});
it('does not show the sync subscription notifications', () => {
......@@ -197,9 +225,11 @@ describe('Subscription Breakdown', () => {
});
describe('with no subscription data', () => {
it('does not show user info', () => {
beforeEach(() => {
createComponent({ props: { subscription: {} } });
});
it('does not show user info', () => {
expect(findDetailsUserInfo().exists()).toBe(false);
});
......@@ -208,6 +238,10 @@ describe('Subscription Breakdown', () => {
expect(findDetailsUserInfo().exists()).toBe(false);
});
it('does not show the subscription details footer', () => {
expect(findDetailsCardFooter().exists()).toBe(false);
});
});
describe('with no subscription history data', () => {
......@@ -220,4 +254,13 @@ describe('Subscription Breakdown', () => {
});
});
});
describe('activating a new subscription', () => {
it('shows a modal', () => {
createComponent({ stubs: { GlCard, SubscriptionDetailsCard } });
findSubscriptionActivationAction().vm.$emit('click');
expect(glModalDirective).toHaveBeenCalledWith(modalId);
});
});
});
import {
getErrorsAsData,
getLicenseFromData,
updateSubscriptionAppCache,
} from 'ee/pages/admin/cloud_licenses/graphql/utils';
import { activateLicenseMutationResponse } from '../mock_data';
describe('graphQl utils', () => {
describe('getLicenseFromData', () => {
const license = { id: 'license-id' };
const gitlabSubscriptionActivate = { license };
it('returns the license data', () => {
const result = getLicenseFromData({ data: { gitlabSubscriptionActivate } });
expect(result).toMatchObject(license);
});
it('returns undefined with no subscription', () => {
const result = getLicenseFromData({ data: { gitlabSubscriptionActivate: null } });
expect(result).toBeUndefined();
});
it('returns undefined with no data', () => {
const result = getLicenseFromData({ data: null });
expect(result).toBeUndefined();
});
it('returns undefined with no params passed', () => {
const result = getLicenseFromData();
expect(result).toBeUndefined();
});
});
describe('getErrorsAsData', () => {
const errors = ['an error'];
const gitlabSubscriptionActivate = { errors };
it('returns the errors data', () => {
const result = getErrorsAsData({ data: { gitlabSubscriptionActivate } });
expect(result).toEqual(errors);
});
it('returns an empty array with no errors', () => {
const result = getErrorsAsData({ data: { gitlabSubscriptionActivate: null } });
expect(result).toEqual([]);
});
it('returns an empty array with no data', () => {
const result = getErrorsAsData({ data: null });
expect(result).toEqual([]);
});
it('returns an empty array with no params passed', () => {
const result = getErrorsAsData();
expect(result).toEqual([]);
});
});
describe('updateSubscriptionAppCache', () => {
const cache = {
readQuery: jest.fn(() => ({ licenseHistoryEntries: { nodes: [] } })),
writeQuery: jest.fn(),
};
it('calls writeQuery the correct number of times', () => {
updateSubscriptionAppCache(cache, activateLicenseMutationResponse.SUCCESS);
expect(cache.writeQuery).toHaveBeenCalledTimes(2);
});
it('calls writeQuery the first time to update the current subscription', () => {
updateSubscriptionAppCache(cache, activateLicenseMutationResponse.SUCCESS);
expect(cache.writeQuery.mock.calls[0][0]).toEqual(
expect.objectContaining({
data: {
currentLicense:
activateLicenseMutationResponse.SUCCESS.data.gitlabSubscriptionActivate.license,
},
}),
);
});
it('calls writeQuery the second time to update the subscription history', () => {
updateSubscriptionAppCache(cache, activateLicenseMutationResponse.SUCCESS);
expect(cache.writeQuery.mock.calls[1][0]).toEqual(
expect.objectContaining({
data: {
licenseHistoryEntries: {
nodes: [
activateLicenseMutationResponse.SUCCESS.data.gitlabSubscriptionActivate.license,
],
},
},
}),
);
});
});
});
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