Commit 4856eb00 authored by Savas Vedova's avatar Savas Vedova

Merge branch 'vs/add-sorting-to-seat-usage-table' into 'master'

Add ability to sort billable members

See merge request gitlab-org/gitlab!66374
parents f1ed5229 671a17dc
......@@ -45,9 +45,6 @@ export default {
issueMetricImagesPath: '/api/:version/projects/:id/issues/:issue_iid/metric_images',
issueMetricSingleImagePath:
'/api/:version/projects/:id/issues/:issue_iid/metric_images/:image_id',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
billableGroupMemberMembershipsPath:
'/api/:version/groups/:group_id/billable_members/:member_id/memberships',
userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
......@@ -370,41 +367,6 @@ export default {
return axios.delete(individualMetricImageUrl);
},
fetchBillableGroupMembersList(namespaceId, options = {}, callback = () => {}) {
const url = Api.buildUrl(this.billableGroupMembersPath).replace(':id', namespaceId);
const defaults = {
per_page: DEFAULT_PER_PAGE,
page: 1,
};
const passedOptions = options;
// calling search API with empty string will not return results
if (!passedOptions.search) {
passedOptions.search = undefined;
}
return axios
.get(url, {
params: {
...defaults,
...passedOptions,
},
})
.then(({ data, headers }) => {
callback(data);
return { data, headers };
});
},
fetchBillableGroupMemberMemberships(namespaceId, memberId) {
const url = Api.buildUrl(this.billableGroupMemberMembershipsPath)
.replace(':group_id', namespaceId)
.replace(':member_id', memberId);
return axios.get(url);
},
projectGroups(id, options) {
const url = Api.buildUrl(this.projectGroupsPath).replace(':id', encodeURIComponent(id));
......
import { DEFAULT_PER_PAGE } from '~/api';
import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const GROUPS_BILLABLE_MEMBERS_SINGLE_PATH = '/api/:version/groups/:group_id/billable_members/:id';
const GROUPS_BILLABLE_MEMBERS_PATH = '/api/:version/groups/:id/billable_members';
const GROUPS_BILLABLE_MEMBERS_SINGLE_MEMBERSHIPS_PATH =
'/api/:version/groups/:group_id/billable_members/:member_id/memberships';
export function removeBillableMemberFromGroup(groupId, memberId, options) {
export const fetchBillableGroupMembersList = (namespaceId, options = {}) => {
const url = buildApiUrl(GROUPS_BILLABLE_MEMBERS_PATH).replace(':id', namespaceId);
const defaults = {
per_page: DEFAULT_PER_PAGE,
page: 1,
};
return axios.get(url, {
params: {
...defaults,
...options,
},
});
};
export const fetchBillableGroupMemberMemberships = (namespaceId, memberId) => {
const url = buildApiUrl(GROUPS_BILLABLE_MEMBERS_SINGLE_MEMBERSHIPS_PATH)
.replace(':group_id', namespaceId)
.replace(':member_id', memberId);
return axios.get(url);
};
export const removeBillableMemberFromGroup = (groupId, memberId) => {
const url = buildApiUrl(GROUPS_BILLABLE_MEMBERS_SINGLE_PATH)
.replace(':group_id', groupId)
.replace(':id', memberId);
return axios.delete(url, { params: { ...options } });
}
return axios.delete(url);
};
......@@ -10,22 +10,21 @@ import {
GlModalDirective,
GlIcon,
GlPagination,
GlSearchBoxByType,
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
import { parseInt, debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
FIELDS,
AVATAR_SIZE,
SEARCH_DEBOUNCE_MS,
REMOVE_BILLABLE_MEMBER_MODAL_ID,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_ID,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_TITLE,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT,
SORT_OPTIONS,
} from 'ee/billings/seat_usage/constants';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import FilterSortContainerRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import RemoveBillableMemberModal from './remove_billable_member_modal.vue';
import SubscriptionSeatDetails from './subscription_seat_details.vue';
......@@ -44,15 +43,10 @@ export default {
GlModal,
GlIcon,
GlPagination,
GlSearchBoxByType,
GlTable,
RemoveBillableMemberModal,
SubscriptionSeatDetails,
},
data() {
return {
searchQuery: '',
};
FilterSortContainerRoot,
},
computed: {
...mapState([
......@@ -63,44 +57,26 @@ export default {
'namespaceName',
'namespaceId',
'billableMemberToRemove',
'search',
'sort',
]),
...mapGetters(['tableItems']),
currentPage: {
get() {
return parseInt(this.page, 10);
return this.page;
},
set(val) {
this.fetchBillableMembersList({ page: val, search: this.searchQuery });
this.setCurrentPage(val);
},
},
perPageFormatted() {
return parseInt(this.perPage, 10);
},
totalFormatted() {
return parseInt(this.total, 10);
},
emptyText() {
if (this.searchQuery?.length < 3) {
if (this.search?.length < 3) {
return s__('Billing|Enter at least three characters to search.');
}
return s__('Billing|No users to display.');
},
},
watch: {
searchQuery() {
this.executeQuery();
},
},
created() {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this.debouncedSearch = debounce(function search() {
this.fetchBillableMembersList({ search: this.searchQuery });
}, SEARCH_DEBOUNCE_MS);
this.fetchBillableMembersList();
},
methods: {
......@@ -108,20 +84,22 @@ export default {
'fetchBillableMembersList',
'resetBillableMembers',
'setBillableMemberToRemove',
'setSearchQuery',
'setCurrentPage',
'setSortOption',
]),
onSearchEnter() {
this.debouncedSearch.cancel();
this.executeQuery();
},
executeQuery() {
const queryLength = this.searchQuery?.length;
const MIN_SEARCH_LENGTH = 3;
applyFilter(searchTerms) {
const searchQuery = searchTerms.reduce((terms, searchTerm) => {
if (searchTerm.type !== 'filtered-search-term') {
return '';
}
if (queryLength === 0 || queryLength >= MIN_SEARCH_LENGTH) {
this.debouncedSearch();
} else if (queryLength < MIN_SEARCH_LENGTH) {
this.resetBillableMembers();
}
return `${terms} ${searchTerm.value.data}`;
}, '');
this.setSearchQuery(searchQuery.trim() || null);
},
handleSortOptionChange(sortOption) {
this.setSortOption(sortOption);
},
displayRemoveMemberModal(user) {
if (user.removable) {
......@@ -141,6 +119,7 @@ export default {
emailNotVisibleTooltipText: s__(
'Billing|An email address is only visible for users with public emails.',
),
filterUsersPlaceholder: __('Filter users'),
},
avatarSize: AVATAR_SIZE,
fields: FIELDS,
......@@ -148,6 +127,7 @@ export default {
cannotRemoveModalId: CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_ID,
cannotRemoveModalTitle: CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_TITLE,
cannotRemoveModalText: CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT,
sortOptions: SORT_OPTIONS,
};
</script>
......@@ -166,11 +146,17 @@ export default {
</h4>
<gl-badge>{{ total }}</gl-badge>
</div>
</div>
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('Billing|Type to search')"
@keydown.enter.prevent="onSearchEnter"
<div class="gl-bg-gray-10 gl-p-3">
<filter-sort-container-root
:namespace="namespaceId"
:tokens="[]"
:search-input-placeholder="$options.i18n.filterUsersPlaceholder"
:sort-options="$options.sortOptions"
initial-sort-by="last_activity_on_desc"
@onFilter="applyFilter"
@onSort="handleSortOptionChange"
/>
</div>
......@@ -258,8 +244,8 @@ export default {
<gl-pagination
v-if="currentPage"
v-model="currentPage"
:per-page="perPageFormatted"
:total-items="totalFormatted"
:per-page="perPage"
:total-items="total"
align="center"
class="gl-mt-5"
/>
......
......@@ -47,3 +47,22 @@ group and all its subgroups and projects. This action can't be undone.`,
);
export const AVATAR_SIZE = 32;
export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const SORT_OPTIONS = [
{
id: 10,
title: __('Last Activity'),
sortDirection: {
descending: 'last_activity_on_desc',
ascending: 'last_activity_on_asc',
},
},
{
id: 20,
title: __('Name'),
sortDirection: {
descending: 'name_desc',
ascending: 'name_asc',
},
},
];
import Api from 'ee/api';
import * as GroupsApi from 'ee/api/groups_api';
import createFlash, { FLASH_TYPES } from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
export const fetchBillableMembersList = ({ dispatch, state }, { page, search } = {}) => {
dispatch('requestBillableMembersList');
export const fetchBillableMembersList = ({ commit, dispatch, state }) => {
commit(types.REQUEST_BILLABLE_MEMBERS);
return Api.fetchBillableGroupMembersList(state.namespaceId, { page, search })
.then((data) => dispatch('receiveBillableMembersListSuccess', data))
const { page, search, sort } = state;
return GroupsApi.fetchBillableGroupMembersList(state.namespaceId, { page, search, sort })
.then(({ data, headers }) => dispatch('receiveBillableMembersListSuccess', { data, headers }))
.catch(() => dispatch('receiveBillableMembersListError'));
};
export const requestBillableMembersList = ({ commit }) => commit(types.REQUEST_BILLABLE_MEMBERS);
export const receiveBillableMembersListSuccess = ({ commit }, response) =>
commit(types.RECEIVE_BILLABLE_MEMBERS_SUCCESS, response);
......@@ -68,7 +67,7 @@ export const fetchBillableMemberDetails = ({ dispatch, commit, state }, memberId
commit(types.FETCH_BILLABLE_MEMBER_DETAILS, memberId);
return Api.fetchBillableGroupMemberMemberships(state.namespaceId, memberId)
return GroupsApi.fetchBillableGroupMemberMemberships(state.namespaceId, memberId)
.then(({ data }) =>
commit(types.FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS, { memberId, memberships: data }),
)
......@@ -82,3 +81,21 @@ export const fetchBillableMemberDetailsError = ({ commit }, memberId) => {
message: s__('Billing|An error occurred while getting a billable member details'),
});
};
export const setSearchQuery = ({ commit, dispatch }, searchQuery) => {
commit(types.SET_SEARCH_QUERY, searchQuery);
dispatch('fetchBillableMembersList');
};
export const setCurrentPage = ({ commit, dispatch }, page) => {
commit(types.SET_CURRENT_PAGE, page);
dispatch('fetchBillableMembersList');
};
export const setSortOption = ({ commit, dispatch }, sortOption) => {
commit(types.SET_SORT_OPTION, sortOption);
dispatch('fetchBillableMembersList');
};
......@@ -2,7 +2,9 @@ export const REQUEST_BILLABLE_MEMBERS = 'REQUEST_BILLABLE_MEMBERS';
export const RECEIVE_BILLABLE_MEMBERS_SUCCESS = 'RECEIVE_BILLABLE_MEMBERS_SUCCESS';
export const RECEIVE_BILLABLE_MEMBERS_ERROR = 'RECEIVE_BILLABLE_MEMBERS_ERROR';
export const SET_SEARCH = 'SET_SEARCH';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const SET_SORT_OPTION = 'SET_SORT_OPTION';
export const RESET_BILLABLE_MEMBERS = 'RESET_BILLABLE_MEMBERS';
export const REMOVE_BILLABLE_MEMBER = 'REMOVE_BILLABLE_MEMBER';
......
......@@ -16,9 +16,9 @@ export default {
const { data, headers } = payload;
state.members = data;
state.total = headers[HEADER_TOTAL_ENTRIES];
state.page = headers[HEADER_PAGE_NUMBER];
state.perPage = headers[HEADER_ITEMS_PER_PAGE];
state.total = Number(headers[HEADER_TOTAL_ENTRIES]);
state.page = Number(headers[HEADER_PAGE_NUMBER]);
state.perPage = Number(headers[HEADER_ITEMS_PER_PAGE]);
state.isLoading = false;
},
......@@ -28,8 +28,16 @@ export default {
state.hasError = true;
},
[types.SET_SEARCH](state, searchString) {
state.search = searchString ?? '';
[types.SET_SEARCH_QUERY](state, searchString) {
state.search = searchString ?? null;
},
[types.SET_CURRENT_PAGE](state, pageNumber) {
state.page = pageNumber;
},
[types.SET_SORT_OPTION](state, sortOption) {
state.sort = sortOption;
},
[types.RESET_BILLABLE_MEMBERS](state) {
......
......@@ -9,4 +9,6 @@ export default ({ namespaceId = null, namespaceName = null } = {}) => ({
perPage: null,
billableMemberToRemove: null,
userDetails: {},
search: null,
sort: 'last_activity_on_desc',
});
import MockAdapter from 'axios-mock-adapter';
import * as GroupsApi from 'ee/api/groups_api';
import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
describe('GroupsApi', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
};
let originalGon;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
originalGon = window.gon;
window.gon = { ...dummyGon };
});
afterEach(() => {
mock.restore();
window.gon = originalGon;
});
describe('Billable members list', () => {
const namespaceId = 1000;
describe('fetchBillableGroupMembersList', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members`;
it('GETs the right url', async () => {
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
const { data } = await GroupsApi.fetchBillableGroupMembersList(namespaceId);
expect(data).toEqual([]);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
params: { page: 1, per_page: DEFAULT_PER_PAGE },
});
});
});
describe('fetchBillableGroupMemberMemberships', () => {
const memberId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members/${memberId}/memberships`;
it('fetches memberships for the member', async () => {
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
const { data } = await GroupsApi.fetchBillableGroupMemberMemberships(namespaceId, memberId);
expect(data).toEqual([]);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
});
});
describe('removeBillableMemberFromGroup', () => {
const memberId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members/${memberId}`;
it('removes a billable member from a group', async () => {
jest.spyOn(axios, 'delete');
mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, []);
const { data } = await GroupsApi.removeBillableMemberFromGroup(namespaceId, memberId);
expect(data).toEqual([]);
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
});
});
});
});
......@@ -742,37 +742,6 @@ describe('Api', () => {
});
});
describe('Billable members list', () => {
const namespaceId = 1000;
describe('fetchBillableGroupMembersList', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members`;
it('GETs the right url', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
return Api.fetchBillableGroupMembersList(namespaceId).then(({ data }) => {
expect(data).toEqual([]);
});
});
});
describe('fetchBillableGroupMemberMemberships', () => {
const memberId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members/${memberId}/memberships`;
it('fetches memberships for the member', async () => {
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
const { data } = await Api.fetchBillableGroupMemberMemberships(namespaceId, memberId);
expect(data).toEqual([]);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
});
});
});
describe('Project analytics: deployment frequency', () => {
const projectPath = 'test/project';
const encodedProjectPath = encodeURIComponent(projectPath);
......
......@@ -4,7 +4,6 @@ import {
GlTable,
GlAvatarLink,
GlAvatarLabeled,
GlSearchBoxByType,
GlBadge,
GlModal,
} from '@gitlab/ui';
......@@ -14,6 +13,7 @@ import SubscriptionSeats from 'ee/billings/seat_usage/components/subscription_se
import { CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT } from 'ee/billings/seat_usage/constants';
import { mockDataSeats, mockTableItems } from 'ee_jest/billings/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import FilterSortContainerRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -22,6 +22,7 @@ const actionSpies = {
fetchBillableMembersList: jest.fn(),
resetBillableMembers: jest.fn(),
setBillableMemberToRemove: jest.fn(),
setSearchQuery: jest.fn(),
};
const providedFields = {
......@@ -39,11 +40,12 @@ const fakeStore = ({ initialState, initialGetters }) =>
state: {
isLoading: false,
hasError: false,
namespaceId: null,
namespaceId: 1,
members: [...mockDataSeats.data],
total: 300,
page: 1,
perPage: 5,
sort: 'last_activity_on_desc',
...providedFields,
...initialState,
},
......@@ -65,22 +67,21 @@ describe('Subscription Seats', () => {
);
};
const findTable = () => wrapper.find(GlTable);
const findTableEmptyText = () => findTable().attributes('empty-text');
const findTable = () => wrapper.findComponent(GlTable);
const findPageHeading = () => wrapper.find('[data-testid="heading-info"]');
const findPageHeadingText = () => findPageHeading().find('[data-testid="heading-info-text"]');
const findPageHeadingBadge = () => findPageHeading().find(GlBadge);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findPagination = () => wrapper.find(GlPagination);
const findSearchBox = () => wrapper.findComponent(FilterSortContainerRoot);
const findPagination = () => wrapper.findComponent(GlPagination);
const findAllRemoveUserItems = () => wrapper.findAllByTestId('remove-user');
const findErrorModal = () => wrapper.findComponent(GlModal);
const serializeUser = (rowWrapper) => {
const avatarLink = rowWrapper.find(GlAvatarLink);
const avatarLabeled = rowWrapper.find(GlAvatarLabeled);
const avatarLink = rowWrapper.findComponent(GlAvatarLink);
const avatarLabeled = rowWrapper.findComponent(GlAvatarLabeled);
return {
avatarLink: {
......@@ -102,7 +103,7 @@ describe('Subscription Seats', () => {
user: serializeUser(rowWrapper),
email: emailWrapper.text(),
tooltip: emailWrapper.find('span').attributes('title'),
dropdownExists: rowWrapper.find(GlDropdown).exists(),
dropdownExists: rowWrapper.findComponent(GlDropdown).exists(),
};
};
......@@ -204,22 +205,6 @@ describe('Subscription Seats', () => {
});
});
describe('pagination', () => {
it.each([null, NaN, undefined, 'a string', false])(
'will not render given %s for currentPage',
(value) => {
wrapper = createComponent({
initialState: {
page: value,
},
});
expect(findPagination().exists()).toBe(false);
wrapper.destroy();
},
);
});
describe('is loading', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { isLoading: true } });
......@@ -239,82 +224,17 @@ describe('Subscription Seats', () => {
wrapper = createComponent();
});
it('input event triggers the fetchBillableMembersList action', async () => {
it('input event triggers the setSearchQuery action', async () => {
const SEARCH_STRING = 'search string';
// fetchBillableMembersList is called once on created()
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', SEARCH_STRING);
// fetchBillableMembersList is triggered a second time on input
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(2);
// fetchBillableMembersList is triggered the second time with the correct argument
expect(actionSpies.fetchBillableMembersList.mock.calls[1][1]).toEqual({
search: SEARCH_STRING,
});
});
});
describe('typing inside of the search box', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('causes the empty table text to change based on the number of typed characters', async () => {
const EMPTY_TEXT_TOO_SHORT = 'Enter at least three characters to search.';
const EMPTY_TEXT_NO_USERS = 'No users to display.';
expect(findTableEmptyText()).toBe(EMPTY_TEXT_TOO_SHORT);
await findSearchBox().vm.$emit('input', 'a');
expect(findTableEmptyText()).toBe(EMPTY_TEXT_TOO_SHORT);
await findSearchBox().vm.$emit('input', 'aa');
expect(findTableEmptyText()).toBe(EMPTY_TEXT_TOO_SHORT);
await findSearchBox().vm.$emit('input', 'aaa');
expect(findTableEmptyText()).toBe(EMPTY_TEXT_NO_USERS);
});
it('dispatches the.resetBillableMembers action when 1 or 2 characters have been typed', async () => {
expect(actionSpies.resetBillableMembers).not.toHaveBeenCalled();
await findSearchBox().vm.$emit('input', 'a');
expect(actionSpies.resetBillableMembers).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', 'aa');
expect(actionSpies.resetBillableMembers).toHaveBeenCalledTimes(2);
await findSearchBox().vm.$emit('input', 'aaa');
expect(actionSpies.resetBillableMembers).toHaveBeenCalledTimes(2);
});
it('dispatches fetchBillableMembersList action when search box is emptied out', async () => {
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', 'a');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', '');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(2);
});
it('dispatches fetchBillableMembersList action when more than 2 characters are typed', async () => {
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', 'a');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', 'aa');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(1);
await findSearchBox().vm.$emit('input', 'aaa');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(2);
await findSearchBox().vm.$emit('onFilter', [
{ type: 'filtered-search-term', value: { data: SEARCH_STRING } },
]);
await findSearchBox().vm.$emit('input', 'aaaa');
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledTimes(3);
expect(actionSpies.setSearchQuery).toHaveBeenCalledWith(expect.any(Object), SEARCH_STRING);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import Api from 'ee/api';
import * as GroupsApi from 'ee/api/groups_api';
import * as actions from 'ee/billings/seat_usage/store/actions';
import * as types from 'ee/billings/seat_usage/store/mutation_types';
......@@ -32,8 +31,9 @@ describe('seats actions', () => {
});
it('passes correct arguments to Api call', () => {
const payload = { page: 5, search: 'search string' };
const spy = jest.spyOn(Api, 'fetchBillableGroupMembersList');
const payload = { page: 5, search: 'search string', sort: 'last_activity_on_desc' };
state = Object.assign(state, payload);
const spy = jest.spyOn(GroupsApi, 'fetchBillableGroupMembersList');
testAction({
action: actions.fetchBillableMembersList,
......@@ -58,12 +58,12 @@ describe('seats actions', () => {
action: actions.fetchBillableMembersList,
state,
expectedActions: [
{ type: 'requestBillableMembersList' },
{
type: 'receiveBillableMembersListSuccess',
payload: mockDataSeats,
},
],
expectedMutations: [{ type: types.REQUEST_BILLABLE_MEMBERS }],
});
});
});
......@@ -77,25 +77,13 @@ describe('seats actions', () => {
testAction({
action: actions.fetchBillableMembersList,
state,
expectedActions: [
{ type: 'requestBillableMembersList' },
{ type: 'receiveBillableMembersListError' },
],
expectedActions: [{ type: 'receiveBillableMembersListError' }],
expectedMutations: [{ type: types.REQUEST_BILLABLE_MEMBERS }],
});
});
});
});
describe('requestBillableMembersList', () => {
it('should commit the request mutation', () => {
testAction({
action: actions.requestBillableMembersList,
state,
expectedMutations: [{ type: types.REQUEST_BILLABLE_MEMBERS }],
});
});
});
describe('receiveBillableMembersListSuccess', () => {
it('should commit the success mutation', () => {
testAction({
......@@ -224,7 +212,7 @@ describe('seats actions', () => {
const member = mockDataSeats.data[0];
beforeAll(() => {
Api.fetchBillableGroupMemberMemberships = jest
GroupsApi.fetchBillableGroupMemberMemberships = jest
.fn()
.mockResolvedValue({ data: mockMemberDetails });
});
......@@ -258,7 +246,7 @@ describe('seats actions', () => {
],
});
expect(Api.fetchBillableGroupMemberMemberships).toHaveBeenCalledWith(null, 2);
expect(GroupsApi.fetchBillableGroupMemberMemberships).toHaveBeenCalledWith(null, 2);
});
it('calls fetchBillableGroupMemberMemberships api only once', async () => {
......@@ -289,12 +277,12 @@ describe('seats actions', () => {
],
});
expect(Api.fetchBillableGroupMemberMemberships).toHaveBeenCalledTimes(1);
expect(GroupsApi.fetchBillableGroupMemberMemberships).toHaveBeenCalledTimes(1);
});
describe('on API error', () => {
beforeAll(() => {
Api.fetchBillableGroupMemberMemberships = jest.fn().mockRejectedValue();
GroupsApi.fetchBillableGroupMemberMemberships = jest.fn().mockRejectedValue();
});
it('dispatches fetchBillableMemberDetailsError', async () => {
......
......@@ -32,9 +32,9 @@ describe('EE billings seats module mutations', () => {
it('sets state as expected', () => {
expect(state.members).toMatchObject(mockDataSeats.data);
expect(state.total).toBe('3');
expect(state.page).toBe('1');
expect(state.perPage).toBe('1');
expect(state.total).toBe(3);
expect(state.page).toBe(1);
expect(state.perPage).toBe(1);
});
it('sets isLoading to false', () => {
......@@ -56,16 +56,20 @@ describe('EE billings seats module mutations', () => {
});
});
describe(types.SET_SEARCH, () => {
const SEARCH_STRING = 'a search string';
describe(types.SET_SEARCH_QUERY, () => {
it('sets the search state', () => {
const SEARCH_STRING = 'a search string';
beforeEach(() => {
mutations[types.SET_SEARCH](state, SEARCH_STRING);
});
mutations[types.SET_SEARCH_QUERY](state, SEARCH_STRING);
it('sets the search state', () => {
expect(state.search).toBe(SEARCH_STRING);
});
it('sets the search state item to null', () => {
mutations[types.SET_SEARCH_QUERY](state);
expect(state.search).toBe(null);
});
});
describe(types.RESET_BILLABLE_MEMBERS, () => {
......@@ -176,5 +180,21 @@ describe('EE billings seats module mutations', () => {
expect(state.userDetails[member.id.toString()].isLoading).toBe(false);
});
});
describe(types.SET_CURRENT_PAGE, () => {
it('sets the page state', () => {
mutations[types.SET_CURRENT_PAGE](state, 1);
expect(state.page).toBe(1);
});
});
describe(types.SET_SORT_OPTION, () => {
it('sets the sort state', () => {
mutations[types.SET_SORT_OPTION](state, 'last_activity_on_desc');
expect(state.sort).toBe('last_activity_on_desc');
});
});
});
});
......@@ -5234,9 +5234,6 @@ msgstr ""
msgid "Billing|Type %{username} to confirm"
msgstr ""
msgid "Billing|Type to search"
msgstr ""
msgid "Billing|User was successfully removed"
msgstr ""
......@@ -14074,6 +14071,9 @@ msgstr ""
msgid "Filter results..."
msgstr ""
msgid "Filter users"
msgstr ""
msgid "Filter your repositories by name"
msgstr ""
......@@ -19110,6 +19110,9 @@ msgstr ""
msgid "Last Accessed On"
msgstr ""
msgid "Last Activity"
msgstr ""
msgid "Last Pipeline"
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