Commit 1d4d8f35 authored by Nathan Friend's avatar Nathan Friend

Merge branch 'tz-extract-main-js-api-calls' into 'master'

Refactor/Reorganize api.js for better tree-shaking

See merge request gitlab-org/gitlab!50038
parents c547a17b 73d63a36
......@@ -5,6 +5,12 @@ import { __ } from '~/locale';
const DEFAULT_PER_PAGE = 20;
/**
* Slow deprecation Notice: Please rather use for new calls
* or during refactors /rest_api as this is doing named exports
* which support treeshaking
*/
const Api = {
DEFAULT_PER_PAGE,
groupsPath: '/api/:version/groups.json',
......@@ -152,7 +158,10 @@ const Api = {
});
},
// Return groups list. Filtered by query
/**
* @deprecated This method will be removed soon. Use the
* `getGroups` method in `~/rest_api` instead.
*/
groups(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.groupsPath);
return axios
......@@ -188,7 +197,10 @@ const Api = {
.then(({ data }) => callback(data));
},
// Return projects list. Filtered by query
/**
* @deprecated This method will be removed soon. Use the
* `getProjects` method in `~/rest_api` instead.
*/
projects(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.projectsPath);
const defaults = {
......@@ -521,6 +533,10 @@ const Api = {
.replace(':namespace_path', namespacePath);
},
/**
* @deprecated This method will be removed soon. Use the
* `getUsers` method in `~/rest_api` instead.
*/
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
......@@ -532,6 +548,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUser` method in `~/rest_api` instead.
*/
user(id, options) {
const url = Api.buildUrl(this.userPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
......@@ -539,11 +559,19 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserCounts` method in `~/rest_api` instead.
*/
userCounts() {
const url = Api.buildUrl(this.userCountsPath);
return axios.get(url);
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserStatus` method in `~/rest_api` instead.
*/
userStatus(id, options) {
const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
......@@ -551,6 +579,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `getUserProjects` method in `~/rest_api` instead.
*/
userProjects(userId, query, options, callback) {
const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
const defaults = {
......@@ -586,6 +618,10 @@ const Api = {
});
},
/**
* @deprecated This method will be removed soon. Use the
* `updateUserStatus` method in `~/rest_api` instead.
*/
postUserStatus({ emoji, message, availability }) {
const url = Api.buildUrl(this.userPostStatusPath);
......
import { joinPaths } from '../lib/utils/url_utility';
export function buildApiUrl(url) {
return joinPaths('/', gon.relative_url_root || '', url.replace(':version', gon.api_version));
}
export const DEFAULT_PER_PAGE = 20;
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
const GROUPS_PATH = '/api/:version/groups.json';
export function getGroups(query, options, callback = () => {}) {
const url = buildApiUrl(GROUPS_PATH);
return axios
.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
})
.then(({ data }) => {
callback(data);
return data;
});
}
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
const PROJECTS_PATH = '/api/:version/projects.json';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
const defaults = {
search: query,
per_page: DEFAULT_PER_PAGE,
simple: true,
};
if (gon.current_user_id) {
defaults.membership = true;
}
return axios
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data, headers }) => {
callback(data);
return { data, headers };
});
}
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
const USER_PATH = '/api/:version/users/:id';
const USER_STATUS_PATH = '/api/:version/users/:id/status';
const USER_PROJECTS_PATH = '/api/:version/users/:id/projects';
const USER_POST_STATUS_PATH = '/api/:version/user/status';
export function getUsers(query, options) {
const url = buildApiUrl(USERS_PATH);
return axios.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
});
}
export function getUser(id, options) {
const url = buildApiUrl(USER_PATH).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: options,
});
}
export function getUserCounts() {
const url = buildApiUrl(USER_COUNTS_PATH);
return axios.get(url);
}
export function getUserStatus(id, options) {
const url = buildApiUrl(USER_STATUS_PATH).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: options,
});
}
export function getUserProjects(userId, query, options, callback) {
const url = buildApiUrl(USER_PROJECTS_PATH).replace(':id', userId);
const defaults = {
search: query,
per_page: DEFAULT_PER_PAGE,
};
return axios
.get(url, {
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
}
export function updateUserStatus({ emoji, message, availability }) {
const url = buildApiUrl(USER_POST_STATUS_PATH);
return axios.put(url, {
emoji,
message,
availability,
});
}
import Api from '~/api';
import { getUserCounts } from '~/rest_api';
let channel;
......@@ -30,7 +30,7 @@ function updateMergeRequestCounts(newCount) {
* Refresh user counts (and broadcast if open)
*/
export function refreshUserMergeRequestCounts() {
return Api.userCounts()
return getUserCounts()
.then(({ data }) => {
const assignedMergeRequests = data.assigned_merge_requests;
const reviewerMergeRequests = data.review_requested_merge_requests;
......
import Api from '~/api';
import AccessorUtilities from '~/lib/utils/accessor';
import * as types from './mutation_types';
import { getTopFrequentItems } from '../utils';
import { getGroups, getProjects } from '~/rest_api';
export const setNamespace = ({ commit }, namespace) => {
commit(types.SET_NAMESPACE, namespace);
......@@ -54,11 +54,15 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
membership: Boolean(gon.current_user_id),
};
let searchFunction;
if (state.namespace === 'projects') {
searchFunction = getProjects;
params.order_by = 'last_activity_at';
} else {
searchFunction = getGroups;
}
return Api[state.namespace](searchQuery, params)
return searchFunction(searchQuery, params)
.then((results) => {
dispatch('receiveSearchedItemsSuccess', results);
})
......
......@@ -3,7 +3,7 @@ import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { USER_SEARCH_DELAY } from '../constants';
import Api from '~/api';
import { getUsers } from '~/rest_api';
export default {
components: {
......@@ -54,7 +54,7 @@ export default {
this.retrieveUsers(query);
},
retrieveUsers: debounce(function debouncedRetrieveUsers() {
return Api.users(this.query, this.$options.queryOptions)
return getUsers(this.query, this.$options.queryOptions)
.then((response) => {
this.users = response.data.map((token) => ({
id: token.id,
......
import Api from '../../api';
import { getUsers, getUser, getUserStatus } from '~/rest_api';
import Cache from './cache';
class UsersCache extends Cache {
......@@ -7,7 +7,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(username));
}
return Api.users('', { username }).then(({ data }) => {
return getUsers('', { username }).then(({ data }) => {
if (!data.length) {
throw new Error(`User "${username}" could not be found!`);
}
......@@ -28,7 +28,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(userId));
}
return Api.user(userId).then(({ data }) => {
return getUser(userId).then(({ data }) => {
this.internalStorage[userId] = data;
return data;
});
......@@ -40,7 +40,7 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(userId).status);
}
return Api.userStatus(userId).then(({ data }) => {
return getUserStatus(userId).then(({ data }) => {
if (!this.hasData(userId)) {
this.internalStorage[userId] = {};
}
......
export * from './api/groups_api';
export * from './api/projects_api';
export * from './api/user_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests. See https://stackoverflow.com/a/53307822/1063392.
// As a workaround, in Jest tests, import the methods from the file
// in which they are defined:
//
// import * as UserApi from '~/api/user_api';
// vs...
// import * as UserApi from '~/rest_api';
//
// // This will only work with option #2 above.
// jest.spyOn(UserApi, 'getUsers')
......@@ -6,7 +6,7 @@ import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import Api from '~/api';
import { updateUserStatus } from '~/rest_api';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy, isValidAvailibility } from './utils';
import * as Emoji from '~/emoji';
......@@ -163,7 +163,7 @@ export default {
setStatus() {
const { emoji, message, availability } = this;
Api.postUserStatus({
updateUserStatus({
emoji,
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
......
<script>
import Api from '~/api';
import { getUser } from '~/rest_api';
import AuditFilterToken from './shared/audit_filter_token.vue';
......@@ -10,7 +11,7 @@ export default {
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return Api.user(id).then((res) => res.data);
return getUser(id).then((res) => res.data);
},
fetchSuggestions(term) {
const { groupId, projectPath } = this.config;
......
<script>
import Api from '~/api';
import { getUsers, getUser } from '~/rest_api';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
......@@ -9,10 +9,10 @@ export default {
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return Api.user(id).then((res) => res.data);
return getUser(id).then((res) => res.data);
},
fetchSuggestions(term) {
return Api.users(term).then((res) => res.data);
return getUsers(term).then((res) => res.data);
},
getItemName({ name }) {
return name;
......
import * as apiUtils from '~/api/api_utils';
describe('~/api/api_utils.js', () => {
describe('buildApiUrl', () => {
beforeEach(() => {
window.gon = {
api_version: 'v7',
};
});
it('returns a URL with the correct API version', () => {
expect(apiUtils.buildApiUrl('/api/:version/users/:id/status')).toEqual(
'/api/v7/users/:id/status',
);
});
it('only replaces the first instance of :version in the URL', () => {
expect(apiUtils.buildApiUrl('/api/:version/projects/:id/packages/:version')).toEqual(
'/api/v7/projects/:id/packages/:version',
);
});
describe('when gon includes a relative_url_root property', () => {
beforeEach(() => {
window.gon.relative_url_root = '/relative/root';
});
it('returns a URL with the correct relative root URL and API version', () => {
expect(apiUtils.buildApiUrl('/api/:version/users/:id/status')).toEqual(
'/relative/root/api/v7/users/:id/status',
);
});
});
});
});
......@@ -3,7 +3,7 @@ import {
closeUserCountsBroadcast,
refreshUserMergeRequestCounts,
} from '~/commons/nav/user_merge_requests';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
jest.mock('~/api');
......@@ -33,14 +33,12 @@ describe('User Merge Requests', () => {
describe('refreshUserMergeRequestCounts', () => {
beforeEach(() => {
Api.userCounts.mockReturnValue(
Promise.resolve({
data: {
assigned_merge_requests: TEST_COUNT,
review_requested_merge_requests: TEST_COUNT,
},
}),
);
jest.spyOn(UserApi, 'getUserCounts').mockResolvedValue({
data: {
assigned_merge_requests: TEST_COUNT,
review_requested_merge_requests: TEST_COUNT,
},
});
});
describe('with open broadcast channel', () => {
......@@ -55,7 +53,7 @@ describe('User Merge Requests', () => {
});
it('calls the API', () => {
expect(Api.userCounts).toHaveBeenCalled();
expect(UserApi.getUserCounts).toHaveBeenCalled();
});
it('posts count to BroadcastChannel', () => {
......
......@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import { GlTokenSelector } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
const label = 'testgroup';
......@@ -28,7 +28,7 @@ describe('MembersTokenSelect', () => {
let wrapper;
beforeEach(() => {
jest.spyOn(Api, 'users').mockResolvedValue({ data: allUsers });
jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers });
wrapper = createComponent();
});
......@@ -57,7 +57,7 @@ describe('MembersTokenSelect', () => {
await waitForPromises();
expect(Api.users).not.toHaveBeenCalled();
expect(UserApi.getUsers).not.toHaveBeenCalled();
});
});
......@@ -90,7 +90,10 @@ describe('MembersTokenSelect', () => {
await waitForPromises();
expect(Api.users).toHaveBeenCalledWith(searchParam, wrapper.vm.$options.queryOptions);
expect(UserApi.getUsers).toHaveBeenCalledWith(
searchParam,
wrapper.vm.$options.queryOptions,
);
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
......
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
......@@ -88,7 +88,9 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'users').mockImplementation((query, options) => apiSpy(query, options));
jest
.spyOn(UserApi, 'getUsers')
.mockImplementation((query, options) => apiSpy(query, options));
});
it('stores and returns data from API call if cache is empty', (done) => {
......@@ -151,7 +153,7 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'user').mockImplementation((id) => apiSpy(id));
jest.spyOn(UserApi, 'getUser').mockImplementation((id) => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', (done) => {
......@@ -208,7 +210,7 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
jest.spyOn(Api, 'userStatus').mockImplementation((id) => apiSpy(id));
jest.spyOn(UserApi, 'getUserStatus').mockImplementation((id) => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', (done) => {
......
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { initEmojiMock } from 'helpers/emoji';
import Api from '~/api';
import * as UserApi from '~/api/user_api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import SetStatusModalWrapper, {
AVAILABILITY_STATUS,
} from '~/set_status_modal/set_status_modal_wrapper.vue';
jest.mock('~/api');
jest.mock('~/flash');
describe('SetStatusModalWrapper', () => {
......@@ -150,7 +149,7 @@ describe('SetStatusModalWrapper', () => {
describe('update status', () => {
describe('succeeds', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
});
it('clicking "removeStatus" clears the emoji and message fields', async () => {
......@@ -173,12 +172,12 @@ describe('SetStatusModalWrapper', () => {
const commonParams = { emoji: defaultEmoji, message: defaultMessage };
expect(Api.postUserStatus).toHaveBeenCalledTimes(2);
expect(Api.postUserStatus).toHaveBeenNthCalledWith(1, {
expect(UserApi.updateUserStatus).toHaveBeenCalledTimes(2);
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(1, {
availability: AVAILABILITY_STATUS.NOT_SET,
...commonParams,
});
expect(Api.postUserStatus).toHaveBeenNthCalledWith(2, {
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(2, {
availability: AVAILABILITY_STATUS.BUSY,
...commonParams,
});
......@@ -196,7 +195,7 @@ describe('SetStatusModalWrapper', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockResolvedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
return initModal({ mockOnUpdateSuccess: false });
});
......@@ -210,7 +209,7 @@ describe('SetStatusModalWrapper', () => {
describe('with errors', () => {
beforeEach(() => {
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
});
it('calls the "onUpdateFail" handler', async () => {
......@@ -225,7 +224,7 @@ describe('SetStatusModalWrapper', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(Api, 'postUserStatus').mockRejectedValue();
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
return initModal({ mockOnUpdateFailure: false });
});
......
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