Commit 73d63a36 authored by Tim Zallmann's avatar Tim Zallmann Committed by Nathan Friend

All API Instances from api.js for main bundle

Creates new API directory with split files

This splits out all User related API calls to better
tree-shake the big file

Updated the user merge requests + status modal

Refactored + Organized projects API calls

Fixed specs for api tree shaking

Centralized api.js with named exports

Changed to a central rest_api file

Restructured to rest_api import

Needed Prettier Update

Fixed spyOn for UserApi
parent 4262129a
......@@ -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