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