Commit 6fb4ce1d authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Kushal Pandya

Improve loading UX in the License Management list

- Submit button to add a policy now uses the success variant
- When adding a new policy, the cancel button is disabled and the
loading icon is shown in the button itself, rather than replacing
the whole list while it is being updated
- When updating or removing a policy, the loading icon is shown next to
the policy's dropdown
parent db0d1c9b
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants'; import { LICENSE_APPROVAL_STATUS } from '../constants';
import AddLicenseFormDropdown from './add_license_form_dropdown.vue'; import AddLicenseFormDropdown from './add_license_form_dropdown.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -9,6 +10,7 @@ export default { ...@@ -9,6 +10,7 @@ export default {
components: { components: {
AddLicenseFormDropdown, AddLicenseFormDropdown,
GlButton, GlButton,
LoadingButton,
}, },
LICENSE_APPROVAL_STATUS, LICENSE_APPROVAL_STATUS,
approvalStatusOptions: [ approvalStatusOptions: [
...@@ -21,6 +23,11 @@ export default { ...@@ -21,6 +23,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -42,7 +49,6 @@ export default { ...@@ -42,7 +49,6 @@ export default {
newStatus: this.approvalStatus, newStatus: this.approvalStatus,
license: { name: this.licenseName }, license: { name: this.licenseName },
}); });
this.closeForm();
}, },
closeForm() { closeForm() {
this.$emit('closeForm'); this.$emit('closeForm');
...@@ -80,16 +86,16 @@ export default { ...@@ -80,16 +86,16 @@ export default {
</label> </label>
</div> </div>
</div> </div>
<gl-button <loading-button
class="js-submit" class="js-submit"
variant="default"
:disabled="submitDisabled" :disabled="submitDisabled"
:loading="loading"
container-class="btn btn-success btn-align-content d-inline-flex"
:label="s__('LicenseCompliance|Submit')"
data-qa-selector="add_license_submit_button" data-qa-selector="add_license_submit_button"
@click="addLicense" @click="addLicense"
> />
{{ s__('LicenseCompliance|Submit') }} <gl-button class="js-cancel" variant="default" :disabled="loading" @click="closeForm">
</gl-button>
<gl-button class="js-cancel" variant="default" @click="closeForm">
{{ s__('LicenseCompliance|Cancel') }} {{ s__('LicenseCompliance|Cancel') }}
</gl-button> </gl-button>
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { getIssueStatusFromLicenseStatus } from 'ee/vue_shared/license_management/store/utils'; import { getIssueStatusFromLicenseStatus } from 'ee/vue_shared/license_management/store/utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -17,6 +17,7 @@ export default { ...@@ -17,6 +17,7 @@ export default {
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlLoadingIcon,
Icon, Icon,
IssueStatusIcon, IssueStatusIcon,
}, },
...@@ -28,6 +29,11 @@ export default { ...@@ -28,6 +29,11 @@ export default {
Boolean(license.name) && Boolean(license.name) &&
Object.values(LICENSE_APPROVAL_STATUS).includes(license.approvalStatus), Object.values(LICENSE_APPROVAL_STATUS).includes(license.approvalStatus),
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
LICENSE_APPROVAL_STATUS, LICENSE_APPROVAL_STATUS,
[LICENSE_APPROVAL_STATUS.APPROVED]: s__('LicenseCompliance|Allowed'), [LICENSE_APPROVAL_STATUS.APPROVED]: s__('LicenseCompliance|Allowed'),
...@@ -61,8 +67,10 @@ export default { ...@@ -61,8 +67,10 @@ export default {
<span class="js-license-name" data-qa-selector="license_name_content">{{ license.name }}</span> <span class="js-license-name" data-qa-selector="license_name_content">{{ license.name }}</span>
<div class="float-right"> <div class="float-right">
<div class="d-flex"> <div class="d-flex">
<gl-loading-icon v-if="loading" class="js-loading-icon d-flex align-items-center mr-2" />
<gl-dropdown <gl-dropdown
:text="dropdownText" :text="dropdownText"
:disabled="loading"
toggle-class="d-flex justify-content-between align-items-center" toggle-class="d-flex justify-content-between align-items-center"
right right
> >
...@@ -76,6 +84,7 @@ export default { ...@@ -76,6 +84,7 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
<button <button
:disabled="loading"
class="btn btn-blank js-remove-button" class="btn btn-blank js-remove-button"
type="button" type="button"
data-toggle="modal" data-toggle="modal"
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import AddLicenseForm from './components/add_license_form.vue'; import AddLicenseForm from './components/add_license_form.vue';
...@@ -38,6 +38,21 @@ export default { ...@@ -38,6 +38,21 @@ export default {
}, },
computed: { computed: {
...mapState(LICENSE_MANAGEMENT, ['managedLicenses', 'isLoadingManagedLicenses', 'isAdmin']), ...mapState(LICENSE_MANAGEMENT, ['managedLicenses', 'isLoadingManagedLicenses', 'isAdmin']),
...mapGetters(LICENSE_MANAGEMENT, [
'isLicenseBeingUpdated',
'hasPendingLicenses',
'isAddingNewLicense',
]),
showLoadingSpinner() {
return this.isLoadingManagedLicenses && !this.hasPendingLicenses;
},
},
watch: {
isAddingNewLicense(isAddingNewLicense) {
if (!isAddingNewLicense) {
this.closeAddLicenseForm();
}
},
}, },
mounted() { mounted() {
this.setAPISettings({ this.setAPISettings({
...@@ -67,7 +82,7 @@ export default { ...@@ -67,7 +82,7 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoadingManagedLicenses" /> <gl-loading-icon v-if="showLoadingSpinner" />
<div v-else class="license-management"> <div v-else class="license-management">
<delete-confirmation-modal v-if="isAdmin" /> <delete-confirmation-modal v-if="isAdmin" />
...@@ -108,6 +123,7 @@ export default { ...@@ -108,6 +123,7 @@ export default {
<div v-if="formIsOpen" class="prepend-top-default append-bottom-default"> <div v-if="formIsOpen" class="prepend-top-default append-bottom-default">
<add-license-form <add-license-form
:managed-licenses="managedLicenses" :managed-licenses="managedLicenses"
:loading="isAddingNewLicense"
@addLicense="setLicenseApproval" @addLicense="setLicenseApproval"
@closeForm="closeAddLicenseForm" @closeForm="closeAddLicenseForm"
/> />
...@@ -115,7 +131,11 @@ export default { ...@@ -115,7 +131,11 @@ export default {
</template> </template>
<template #default="{ listItem }"> <template #default="{ listItem }">
<admin-license-management-row v-if="isAdmin" :license="listItem" /> <admin-license-management-row
v-if="isAdmin"
:license="listItem"
:loading="isLicenseBeingUpdated(listItem.id)"
/>
<license-management-row v-else :license="listItem" /> <license-management-row v-else :license="listItem" />
</template> </template>
</paginated-list> </paginated-list>
......
...@@ -18,9 +18,11 @@ export const resetLicenseInModal = ({ commit }) => { ...@@ -18,9 +18,11 @@ export const resetLicenseInModal = ({ commit }) => {
export const requestDeleteLicense = ({ commit }) => { export const requestDeleteLicense = ({ commit }) => {
commit(types.REQUEST_DELETE_LICENSE); commit(types.REQUEST_DELETE_LICENSE);
}; };
export const receiveDeleteLicense = ({ commit, dispatch }) => { export const receiveDeleteLicense = ({ commit, dispatch }, id) => {
commit(types.RECEIVE_DELETE_LICENSE); commit(types.RECEIVE_DELETE_LICENSE);
dispatch('fetchManagedLicenses'); return dispatch('fetchManagedLicenses').then(() => {
dispatch('removePendingLicense', id);
});
}; };
export const receiveDeleteLicenseError = ({ commit }, error) => { export const receiveDeleteLicenseError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_LICENSE_ERROR, error); commit(types.RECEIVE_DELETE_LICENSE_ERROR, error);
...@@ -28,14 +30,16 @@ export const receiveDeleteLicenseError = ({ commit }, error) => { ...@@ -28,14 +30,16 @@ export const receiveDeleteLicenseError = ({ commit }, error) => {
export const deleteLicense = ({ dispatch, state }) => { export const deleteLicense = ({ dispatch, state }) => {
const licenseId = state.currentLicenseInModal.id; const licenseId = state.currentLicenseInModal.id;
dispatch('requestDeleteLicense'); dispatch('requestDeleteLicense');
dispatch('addPendingLicense', licenseId);
const endpoint = `${state.apiUrlManageLicenses}/${licenseId}`; const endpoint = `${state.apiUrlManageLicenses}/${licenseId}`;
return axios return axios
.delete(endpoint) .delete(endpoint)
.then(() => { .then(() => {
dispatch('receiveDeleteLicense'); dispatch('receiveDeleteLicense', licenseId);
}) })
.catch(error => { .catch(error => {
dispatch('receiveDeleteLicenseError', error); dispatch('receiveDeleteLicenseError', error);
dispatch('removePendingLicense', licenseId);
}); });
}; };
...@@ -89,7 +93,7 @@ export const fetchParsedLicenseReport = ({ dispatch, state }) => { ...@@ -89,7 +93,7 @@ export const fetchParsedLicenseReport = ({ dispatch, state }) => {
export const requestSetLicenseApproval = ({ commit }) => { export const requestSetLicenseApproval = ({ commit }) => {
commit(types.REQUEST_SET_LICENSE_APPROVAL); commit(types.REQUEST_SET_LICENSE_APPROVAL);
}; };
export const receiveSetLicenseApproval = ({ commit, dispatch, state }) => { export const receiveSetLicenseApproval = ({ commit, dispatch, state }, id) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL); commit(types.RECEIVE_SET_LICENSE_APPROVAL);
// If we have the licenses API endpoint, fetch from there. This corresponds // If we have the licenses API endpoint, fetch from there. This corresponds
// to the cases that we're viewing the merge request or pipeline pages. // to the cases that we're viewing the merge request or pipeline pages.
...@@ -97,10 +101,11 @@ export const receiveSetLicenseApproval = ({ commit, dispatch, state }) => { ...@@ -97,10 +101,11 @@ export const receiveSetLicenseApproval = ({ commit, dispatch, state }) => {
// the project settings page. // the project settings page.
// https://gitlab.com/gitlab-org/gitlab/issues/201867 // https://gitlab.com/gitlab-org/gitlab/issues/201867
if (state.licensesApiPath) { if (state.licensesApiPath) {
dispatch('fetchParsedLicenseReport'); return dispatch('fetchParsedLicenseReport');
} else {
dispatch('fetchManagedLicenses');
} }
return dispatch('fetchManagedLicenses').then(() => {
dispatch('removePendingLicense', id);
});
}; };
export const receiveSetLicenseApprovalError = ({ commit }, error) => { export const receiveSetLicenseApprovalError = ({ commit }, error) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR, error); commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR, error);
...@@ -110,12 +115,23 @@ export const setIsAdmin = ({ commit }, payload) => { ...@@ -110,12 +115,23 @@ export const setIsAdmin = ({ commit }, payload) => {
commit(types.SET_IS_ADMIN, payload); commit(types.SET_IS_ADMIN, payload);
}; };
export const addPendingLicense = ({ state, commit }, id = null) => {
if (!state.pendingLicenses.includes(id)) {
commit(types.ADD_PENDING_LICENSE, id);
}
};
export const removePendingLicense = ({ commit }, id = null) => {
commit(types.REMOVE_PENDING_LICENSE, id);
};
export const setLicenseApproval = ({ dispatch, state }, payload) => { export const setLicenseApproval = ({ dispatch, state }, payload) => {
const { apiUrlManageLicenses } = state; const { apiUrlManageLicenses } = state;
const { license, newStatus } = payload; const { license, newStatus } = payload;
const { id, name } = license; const { id, name } = license;
dispatch('requestSetLicenseApproval'); dispatch('requestSetLicenseApproval');
dispatch('addPendingLicense', id);
let request; let request;
...@@ -131,10 +147,11 @@ export const setLicenseApproval = ({ dispatch, state }, payload) => { ...@@ -131,10 +147,11 @@ export const setLicenseApproval = ({ dispatch, state }, payload) => {
return request return request
.then(() => { .then(() => {
dispatch('receiveSetLicenseApproval'); dispatch('receiveSetLicenseApproval', id);
}) })
.catch(error => { .catch(error => {
dispatch('receiveSetLicenseApprovalError', error); dispatch('receiveSetLicenseApprovalError', error);
dispatch('removePendingLicense', id);
}); });
}; };
export const approveLicense = ({ dispatch }, license) => { export const approveLicense = ({ dispatch }, license) => {
......
...@@ -3,6 +3,12 @@ import { LICENSE_APPROVAL_STATUS } from '../constants'; ...@@ -3,6 +3,12 @@ import { LICENSE_APPROVAL_STATUS } from '../constants';
export const isLoading = state => state.isLoadingManagedLicenses || state.isLoadingLicenseReport; export const isLoading = state => state.isLoadingManagedLicenses || state.isLoadingLicenseReport;
export const isLicenseBeingUpdated = state => (id = null) => state.pendingLicenses.includes(id);
export const isAddingNewLicense = (_, getters) => getters.isLicenseBeingUpdated();
export const hasPendingLicenses = state => state.pendingLicenses.length > 0;
export const licenseReport = state => state.newLicenses; export const licenseReport = state => state.newLicenses;
export const licenseSummaryText = (state, getters) => { export const licenseSummaryText = (state, getters) => {
......
...@@ -14,6 +14,8 @@ export const RESET_LICENSE_IN_MODAL = 'RESET_LICENSE_IN_MODAL'; ...@@ -14,6 +14,8 @@ export const RESET_LICENSE_IN_MODAL = 'RESET_LICENSE_IN_MODAL';
export const SET_API_SETTINGS = 'SET_API_SETTINGS'; export const SET_API_SETTINGS = 'SET_API_SETTINGS';
export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL'; export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL';
export const SET_IS_ADMIN = 'SET_IS_ADMIN'; export const SET_IS_ADMIN = 'SET_IS_ADMIN';
export const ADD_PENDING_LICENSE = 'ADD_PENDING_LICENSE';
export const REMOVE_PENDING_LICENSE = 'REMOVE_PENDING_LICENSE';
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -96,4 +96,10 @@ export default { ...@@ -96,4 +96,10 @@ export default {
currentLicenseInModal: null, currentLicenseInModal: null,
}); });
}, },
[types.ADD_PENDING_LICENSE](state, id) {
state.pendingLicenses.push(id);
},
[types.REMOVE_PENDING_LICENSE](state, id) {
state.pendingLicenses = state.pendingLicenses.filter(pendingLicense => pendingLicense !== id);
},
}; };
...@@ -7,6 +7,7 @@ export default () => ({ ...@@ -7,6 +7,7 @@ export default () => ({
isDeleting: false, isDeleting: false,
isLoadingLicenseReport: false, isLoadingLicenseReport: false,
isLoadingManagedLicenses: false, isLoadingManagedLicenses: false,
pendingLicenses: [],
isSaving: false, isSaving: false,
loadLicenseReportError: false, loadLicenseReportError: false,
loadManagedLicensesError: false, loadManagedLicensesError: false,
......
---
title: Improve loading UX in the License Management list
merge_request: 25620
author:
type: changed
...@@ -7,6 +7,9 @@ describe('AddLicenseForm', () => { ...@@ -7,6 +7,9 @@ describe('AddLicenseForm', () => {
const Component = Vue.extend(LicenseIssueBody); const Component = Vue.extend(LicenseIssueBody);
let vm; let vm;
const findSubmitButton = () => vm.$el.querySelector('.js-submit');
const findCancelButton = () => vm.$el.querySelector('.js-cancel');
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
...@@ -23,7 +26,7 @@ describe('AddLicenseForm', () => { ...@@ -23,7 +26,7 @@ describe('AddLicenseForm', () => {
vm.licenseName = name; vm.licenseName = name;
Vue.nextTick(() => { Vue.nextTick(() => {
const linkEl = vm.$el.querySelector('.js-submit'); const linkEl = findSubmitButton();
linkEl.click(); linkEl.click();
expect(vm.$emit).toHaveBeenCalledWith('addLicense', { expect(vm.$emit).toHaveBeenCalledWith('addLicense', {
...@@ -31,13 +34,12 @@ describe('AddLicenseForm', () => { ...@@ -31,13 +34,12 @@ describe('AddLicenseForm', () => {
license: { name }, license: { name },
}); });
expect(vm.$emit).toHaveBeenCalledWith('closeForm');
done(); done();
}); });
}); });
it('clicking the Cancel button closes the form', () => { it('clicking the Cancel button closes the form', () => {
const linkEl = vm.$el.querySelector('.js-cancel'); const linkEl = findCancelButton();
jest.spyOn(vm, '$emit').mockImplementation(() => {}); jest.spyOn(vm, '$emit').mockImplementation(() => {});
linkEl.click(); linkEl.click();
...@@ -124,10 +126,24 @@ describe('AddLicenseForm', () => { ...@@ -124,10 +126,24 @@ describe('AddLicenseForm', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.submitDisabled).toBe(true); expect(vm.submitDisabled).toBe(true);
const submitButton = vm.$el.querySelector('.js-submit'); const submitButton = findSubmitButton();
expect(submitButton).not.toBeNull();
expect(submitButton.disabled).toBe(true);
done();
});
});
it('disables submit and cancel while a new license is being added', done => {
vm.loading = true;
Vue.nextTick(() => {
const submitButton = findSubmitButton();
const cancelButton = findCancelButton();
expect(submitButton).not.toBeNull(); expect(submitButton).not.toBeNull();
expect(submitButton.disabled).toBe(true); expect(submitButton.disabled).toBe(true);
expect(cancelButton).not.toBeNull();
expect(cancelButton.disabled).toBe(true);
done(); done();
}); });
}); });
......
...@@ -19,8 +19,15 @@ describe('AdminLicenseManagementRow', () => { ...@@ -19,8 +19,15 @@ describe('AdminLicenseManagementRow', () => {
let store; let store;
let actions; let actions;
const createComponent = (props = { license: approvedLicense }) => {
vm = mountComponentWithStore(Component, { props, store });
};
const findNthDropdown = num => [...vm.$el.querySelectorAll('.dropdown-item')][num]; const findNthDropdown = num => [...vm.$el.querySelectorAll('.dropdown-item')][num];
const findNthDropdownIcon = num => findNthDropdown(num).querySelector('svg'); const findNthDropdownIcon = num => findNthDropdown(num).querySelector('svg');
const findLoadingIcon = () => vm.$el.querySelector('.js-loading-icon');
const findDropdownToggle = () => vm.$el.querySelector('.dropdown > button');
const findRemoveButton = () => vm.$el.querySelector('.js-remove-button');
beforeEach(() => { beforeEach(() => {
actions = { actions = {
...@@ -39,9 +46,7 @@ describe('AdminLicenseManagementRow', () => { ...@@ -39,9 +46,7 @@ describe('AdminLicenseManagementRow', () => {
}, },
}); });
const props = { license: approvedLicense }; createComponent();
vm = mountComponentWithStore(Component, { props, store });
}); });
afterEach(() => { afterEach(() => {
...@@ -120,7 +125,7 @@ describe('AdminLicenseManagementRow', () => { ...@@ -120,7 +125,7 @@ describe('AdminLicenseManagementRow', () => {
describe('interaction', () => { describe('interaction', () => {
it('triggering setLicenseInModal by clicking the cancel button', () => { it('triggering setLicenseInModal by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-remove-button'); const linkEl = findRemoveButton();
linkEl.click(); linkEl.click();
expect(actions.setLicenseInModal).toHaveBeenCalled(); expect(actions.setLicenseInModal).toHaveBeenCalled();
...@@ -159,7 +164,7 @@ describe('AdminLicenseManagementRow', () => { ...@@ -159,7 +164,7 @@ describe('AdminLicenseManagementRow', () => {
}); });
it('renders the removal button', () => { it('renders the removal button', () => {
const buttonEl = vm.$el.querySelector('.js-remove-button'); const buttonEl = findRemoveButton();
expect(buttonEl).not.toBeNull(); expect(buttonEl).not.toBeNull();
expect(buttonEl.querySelector('.ic-remove')).not.toBeNull(); expect(buttonEl.querySelector('.ic-remove')).not.toBeNull();
...@@ -186,5 +191,18 @@ describe('AdminLicenseManagementRow', () => { ...@@ -186,5 +191,18 @@ describe('AdminLicenseManagementRow', () => {
expect(secondOption).not.toBeNull(); expect(secondOption).not.toBeNull();
expect(secondOption.innerText.trim()).toBe('Denied'); expect(secondOption.innerText.trim()).toBe('Denied');
}); });
it('does not show a loading icon and enables both the dropdown and the remove button by default', () => {
expect(findLoadingIcon()).toBeNull();
expect(findDropdownToggle().disabled).toBe(false);
expect(findRemoveButton().disabled).toBe(false);
});
it('shows a loading icon and disables both the dropdown and the remove button while loading', () => {
createComponent({ license: approvedLicense, loading: true });
expect(findLoadingIcon()).not.toBeNull();
expect(findDropdownToggle().disabled).toBe(true);
expect(findRemoveButton().disabled).toBe(true);
});
}); });
}); });
...@@ -31,11 +31,17 @@ const PaginatedListMock = { ...@@ -31,11 +31,17 @@ const PaginatedListMock = {
const noop = () => {}; const noop = () => {};
const createComponent = ({ state, props, actionMocks, isAdmin }) => { const createComponent = ({ state, getters, props, actionMocks, isAdmin }) => {
const fakeStore = new Vuex.Store({ const fakeStore = new Vuex.Store({
modules: { modules: {
licenseManagement: { licenseManagement: {
namespaced: true, namespaced: true,
getters: {
isAddingNewLicense: () => false,
hasPendingLicenses: () => false,
isLicenseBeingUpdated: () => () => false,
...getters,
},
state: { state: {
managedLicenses, managedLicenses,
isLoadingManagedLicenses: true, isLoadingManagedLicenses: true,
...@@ -76,11 +82,20 @@ describe('License Management', () => { ...@@ -76,11 +82,20 @@ describe('License Management', () => {
${'when admin'} | ${true} ${'when admin'} | ${true}
${'when developer'} | ${false} ${'when developer'} | ${false}
`('$desc', ({ isAdmin }) => { `('$desc', ({ isAdmin }) => {
it('when loading should render loading icon', () => { it('should render loading icon during initial loading', () => {
createComponent({ state: { isLoadingManagedLicenses: true }, isAdmin }); createComponent({ state: { isLoadingManagedLicenses: true }, isAdmin });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
it('should render list of managed licenses while updating a license', () => {
createComponent({
state: { isLoadingManagedLicenses: true },
getters: { hasPendingLicenses: () => true },
isAdmin,
});
expect(wrapper.find({ name: 'PaginatedList' }).props('list')).toBe(managedLicenses);
});
describe('when not loading', () => { describe('when not loading', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ state: { isLoadingManagedLicenses: false }, isAdmin }); createComponent({ state: { isLoadingManagedLicenses: false }, isAdmin });
......
...@@ -15,6 +15,11 @@ describe('License store actions', () => { ...@@ -15,6 +15,11 @@ describe('License store actions', () => {
let axiosMock; let axiosMock;
let licenseId; let licenseId;
let state; let state;
let mockDispatch;
let mockCommit;
let store;
const expectDispatched = (...args) => expect(mockDispatch).toHaveBeenCalledWith(...args);
beforeEach(() => { beforeEach(() => {
axiosMock = new MockAdapter(axios); axiosMock = new MockAdapter(axios);
...@@ -24,6 +29,13 @@ describe('License store actions', () => { ...@@ -24,6 +29,13 @@ describe('License store actions', () => {
currentLicenseInModal: approvedLicense, currentLicenseInModal: approvedLicense,
}; };
licenseId = approvedLicense.id; licenseId = approvedLicense.id;
mockDispatch = jest.fn(() => Promise.resolve());
mockCommit = jest.fn();
store = {
state,
commit: mockCommit,
dispatch: mockDispatch,
};
}); });
afterEach(() => { afterEach(() => {
...@@ -102,16 +114,12 @@ describe('License store actions', () => { ...@@ -102,16 +114,12 @@ describe('License store actions', () => {
}); });
describe('receiveDeleteLicense', () => { describe('receiveDeleteLicense', () => {
it('commits RECEIVE_DELETE_LICENSE and dispatches fetchManagedLicenses', done => { it('commits RECEIVE_DELETE_LICENSE and dispatches fetchManagedLicenses and removePendingLicense', () => {
testAction( return actions.receiveDeleteLicense(store, licenseId).then(() => {
actions.receiveDeleteLicense, expect(mockCommit).toHaveBeenCalledWith(mutationTypes.RECEIVE_DELETE_LICENSE);
null, expectDispatched('fetchManagedLicenses');
state, expectDispatched('removePendingLicense', licenseId);
[{ type: mutationTypes.RECEIVE_DELETE_LICENSE }], });
[{ type: 'fetchManagedLicenses' }],
)
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -139,41 +147,31 @@ describe('License store actions', () => { ...@@ -139,41 +147,31 @@ describe('License store actions', () => {
endpointMock = axiosMock.onDelete(deleteUrl); endpointMock = axiosMock.onDelete(deleteUrl);
}); });
it('dispatches requestDeleteLicense and receiveDeleteLicense for successful response', done => { it('dispatches requestDeleteLicense, addPendingLicense and receiveDeleteLicense for successful response', () => {
endpointMock.replyOnce(req => { endpointMock.replyOnce(req => {
expect(req.url).toBe(deleteUrl); expect(req.url).toBe(deleteUrl);
return [200, '']; return [200, ''];
}); });
testAction( return actions.deleteLicense(store).then(() => {
actions.deleteLicense, expectDispatched('requestDeleteLicense');
null, expectDispatched('addPendingLicense', licenseId);
state, expectDispatched('receiveDeleteLicense', licenseId);
[], });
[{ type: 'requestDeleteLicense' }, { type: 'receiveDeleteLicense' }],
)
.then(done)
.catch(done.fail);
}); });
it('dispatches requestDeleteLicense and receiveDeleteLicenseError for error response', done => { it('dispatches requestDeleteLicense, addPendingLicense, receiveDeleteLicenseError and removePendingLicense for error response', () => {
endpointMock.replyOnce(req => { endpointMock.replyOnce(req => {
expect(req.url).toBe(deleteUrl); expect(req.url).toBe(deleteUrl);
return [500, '']; return [500, ''];
}); });
testAction( return actions.deleteLicense(store).then(() => {
actions.deleteLicense, expectDispatched('requestDeleteLicense');
null, expectDispatched('addPendingLicense', licenseId);
state, expectDispatched('receiveDeleteLicenseError', expect.any(Error));
[], expectDispatched('removePendingLicense', licenseId);
[ });
{ type: 'requestDeleteLicense' },
{ type: 'receiveDeleteLicenseError', payload: expect.any(Error) },
],
)
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -207,16 +205,12 @@ describe('License store actions', () => { ...@@ -207,16 +205,12 @@ describe('License store actions', () => {
}); });
describe('given the licensesApiPath is not provided', () => { describe('given the licensesApiPath is not provided', () => {
it('commits RECEIVE_SET_LICENSE_APPROVAL and dispatches fetchManagedLicenses', done => { it('commits RECEIVE_SET_LICENSE_APPROVAL and dispatches fetchManagedLicenses and removePendingLicense', () => {
testAction( return actions.receiveSetLicenseApproval(store, licenseId).then(() => {
actions.receiveSetLicenseApproval, expect(mockCommit).toHaveBeenCalledWith(mutationTypes.RECEIVE_SET_LICENSE_APPROVAL);
null, expectDispatched('fetchManagedLicenses');
state, expectDispatched('removePendingLicense', licenseId);
[{ type: mutationTypes.RECEIVE_SET_LICENSE_APPROVAL }], });
[{ type: 'fetchManagedLicenses' }],
)
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -248,7 +242,7 @@ describe('License store actions', () => { ...@@ -248,7 +242,7 @@ describe('License store actions', () => {
putEndpointMock = axiosMock.onPost(apiUrlManageLicenses); putEndpointMock = axiosMock.onPost(apiUrlManageLicenses);
}); });
it('dispatches requestSetLicenseApproval and receiveSetLicenseApproval for successful response', done => { it('dispatches requestSetLicenseApproval, addPendingLicense and receiveSetLicenseApproval for successful response', () => {
putEndpointMock.replyOnce(req => { putEndpointMock.replyOnce(req => {
const { approval_status, name } = JSON.parse(req.data); const { approval_status, name } = JSON.parse(req.data);
...@@ -258,35 +252,25 @@ describe('License store actions', () => { ...@@ -258,35 +252,25 @@ describe('License store actions', () => {
return [200, '']; return [200, ''];
}); });
testAction( return actions.setLicenseApproval(store, { license: newLicense, newStatus }).then(() => {
actions.setLicenseApproval, expectDispatched('requestSetLicenseApproval');
{ license: newLicense, newStatus }, expectDispatched('addPendingLicense', undefined);
state, expectDispatched('receiveSetLicenseApproval', undefined);
[], });
[{ type: 'requestSetLicenseApproval' }, { type: 'receiveSetLicenseApproval' }],
)
.then(done)
.catch(done.fail);
}); });
it('dispatches requestSetLicenseApproval and receiveSetLicenseApprovalError for error response', done => { it('dispatches requestSetLicenseApproval, addPendingLicense, receiveSetLicenseApprovalError and removePendingLicense for error response', () => {
putEndpointMock.replyOnce(req => { putEndpointMock.replyOnce(req => {
expect(req.url).toBe(apiUrlManageLicenses); expect(req.url).toBe(apiUrlManageLicenses);
return [500, '']; return [500, ''];
}); });
testAction( return actions.setLicenseApproval(store, { license: newLicense, newStatus }).then(() => {
actions.setLicenseApproval, expectDispatched('requestSetLicenseApproval');
{ license: newLicense, newStatus }, expectDispatched('addPendingLicense', undefined);
state, expectDispatched('receiveSetLicenseApprovalError', expect.any(Error));
[], expectDispatched('removePendingLicense', undefined);
[ });
{ type: 'requestSetLicenseApproval' },
{ type: 'receiveSetLicenseApprovalError', payload: expect.any(Error) },
],
)
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -299,7 +283,7 @@ describe('License store actions', () => { ...@@ -299,7 +283,7 @@ describe('License store actions', () => {
patchEndpointMock = axiosMock.onPatch(licenseUrl); patchEndpointMock = axiosMock.onPatch(licenseUrl);
}); });
it('dispatches requestSetLicenseApproval and receiveSetLicenseApproval for successful response', done => { it('dispatches requestSetLicenseApproval, addPendingLicense and receiveSetLicenseApproval for successful response', () => {
patchEndpointMock.replyOnce(req => { patchEndpointMock.replyOnce(req => {
expect(req.url).toBe(licenseUrl); expect(req.url).toBe(licenseUrl);
const { approval_status, name } = JSON.parse(req.data); const { approval_status, name } = JSON.parse(req.data);
...@@ -309,35 +293,29 @@ describe('License store actions', () => { ...@@ -309,35 +293,29 @@ describe('License store actions', () => {
return [200, '']; return [200, ''];
}); });
testAction( return actions
actions.setLicenseApproval, .setLicenseApproval(store, { license: approvedLicense, newStatus })
{ license: approvedLicense, newStatus }, .then(() => {
state, expectDispatched('requestSetLicenseApproval');
[], expectDispatched('addPendingLicense', approvedLicense.id);
[{ type: 'requestSetLicenseApproval' }, { type: 'receiveSetLicenseApproval' }], expectDispatched('receiveSetLicenseApproval', approvedLicense.id);
) });
.then(done)
.catch(done.fail);
}); });
it('dispatches requestSetLicenseApproval and receiveSetLicenseApprovalError for error response', done => { it('dispatches requestSetLicenseApproval, addPendingLicense, receiveSetLicenseApprovalError and removePendingLicense for error response', () => {
patchEndpointMock.replyOnce(req => { patchEndpointMock.replyOnce(req => {
expect(req.url).toBe(licenseUrl); expect(req.url).toBe(licenseUrl);
return [500, '']; return [500, ''];
}); });
testAction( return actions
actions.setLicenseApproval, .setLicenseApproval(store, { license: approvedLicense, newStatus })
{ license: approvedLicense, newStatus }, .then(() => {
state, expectDispatched('requestSetLicenseApproval');
[], expectDispatched('addPendingLicense', approvedLicense.id);
[ expectDispatched('receiveSetLicenseApprovalError', expect.any(Error));
{ type: 'requestSetLicenseApproval' }, expectDispatched('removePendingLicense', approvedLicense.id);
{ type: 'receiveSetLicenseApprovalError', payload: expect.any(Error) }, });
],
)
.then(done)
.catch(done.fail);
}); });
}); });
}); });
......
...@@ -28,6 +28,60 @@ describe('getters', () => { ...@@ -28,6 +28,60 @@ describe('getters', () => {
}); });
}); });
describe('isLicenseBeingUpdated', () => {
beforeEach(() => {
state = createState();
});
it.each([5, null])('returns true if given license is being updated', licenseId => {
state.pendingLicenses = [licenseId];
expect(getters.isLicenseBeingUpdated(state)(licenseId)).toBe(true);
});
it('returns true if a new license is being added and no param is passed to the getter', () => {
state.pendingLicenses = [null];
expect(getters.isLicenseBeingUpdated(state)()).toBe(true);
});
it.each`
pendingLicenses | queriedLicense
${[null]} | ${5}
${[5]} | ${null}
${[5]} | ${undefined}
`(
'returns false if given license is not being updated',
({ pendingLicenses, queriedLicense }) => {
state.pendingLicenses = pendingLicenses;
expect(getters.isLicenseBeingUpdated(state)(queriedLicense)).toBe(false);
},
);
});
describe('isAddingNewLicense', () => {
it.each([true, false])('calls isLicenseBeingUpdated internally', returnValue => {
const isLicenseBeingUpdatedMock = jest.fn().mockImplementation(() => returnValue);
expect(
getters.isAddingNewLicense({}, { isLicenseBeingUpdated: isLicenseBeingUpdatedMock }),
).toBe(returnValue);
});
});
describe('hasPendingLicenses', () => {
it('returns true if there are some pending licenses', () => {
state = createState();
state.pendingLicenses = [null];
expect(getters.hasPendingLicenses(state)).toBe(true);
});
it('returns false if there are no pending licenses', () => {
state = createState();
state.pendingLicenses = [];
expect(getters.hasPendingLicenses(state)).toBe(false);
});
});
describe('licenseReport', () => { describe('licenseReport', () => {
it('should return the new licenses from the state', () => { it('should return the new licenses from the state', () => {
const newLicenses = { test: 'foo' }; const newLicenses = { test: 'foo' };
......
...@@ -276,4 +276,31 @@ describe('License store mutations', () => { ...@@ -276,4 +276,31 @@ describe('License store mutations', () => {
expect(store.state.licenseManagement.isLoadingLicenseReport).toBe(false); expect(store.state.licenseManagement.isLoadingLicenseReport).toBe(false);
}); });
}); });
describe('ADD_PENDING_LICENSE', () => {
it('appends given id to pendingLicenses', () => {
store.commit(`licenseManagement/${types.ADD_PENDING_LICENSE}`, 5);
expect(store.state.licenseManagement.pendingLicenses).toEqual([5]);
store.commit(`licenseManagement/${types.ADD_PENDING_LICENSE}`, null);
expect(store.state.licenseManagement.pendingLicenses).toEqual([5, null]);
});
});
describe('REMOVE_PENDING_LICENSE', () => {
beforeEach(() => {
store.replaceState({
...store.state,
licenseManagement: {
pendingLicenses: [5, null],
},
});
});
it('appends given id to pendingLicenses', () => {
store.commit(`licenseManagement/${types.REMOVE_PENDING_LICENSE}`, null);
expect(store.state.licenseManagement.pendingLicenses).toEqual([5]);
store.commit(`licenseManagement/${types.REMOVE_PENDING_LICENSE}`, 5);
expect(store.state.licenseManagement.pendingLicenses).toEqual([]);
});
});
}); });
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