Commit 1500fc7d authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'add-vuex-store-for-milestone-combobox' into 'master'

Add Vuex store for milestone combobox

See merge request gitlab-org/gitlab!45287
parents 29d2717c bcab8914
import Api from '~/api';
import * as types from './mutation_types';
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setSelectedMilestones = ({ commit }, selectedMilestones) =>
commit(types.SET_SELECTED_MILESTONES, selectedMilestones);
export const toggleMilestones = ({ commit, state }, selectedMilestone) => {
const removeMilestone = state.selectedMilestones.includes(selectedMilestone);
if (removeMilestone) {
commit(types.REMOVE_SELECTED_MILESTONE, selectedMilestone);
} else {
commit(types.ADD_SELECTED_MILESTONE, selectedMilestone);
}
};
export const search = ({ dispatch, commit }, query) => {
commit(types.SET_QUERY, query);
dispatch('searchMilestones');
};
export const fetchMilestones = ({ commit, state }) => {
commit(types.REQUEST_START);
Api.projectMilestones(state.projectId)
.then(response => {
commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
export const searchMilestones = ({ commit, state }) => {
commit(types.REQUEST_START);
const options = {
search: state.query,
scope: 'milestones',
};
Api.projectSearch(state.projectId, options)
.then(response => {
commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
/** Returns `true` if there is at least one in-progress request */
export const isLoading = ({ requestCount }) => requestCount > 0;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
});
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES';
export const ADD_SELECTED_MILESTONE = 'ADD_SELECTED_MILESTONE';
export const REMOVE_SELECTED_MILESTONE = 'REMOVE_SELECTED_MILESTONE';
export const SET_QUERY = 'SET_QUERY';
export const REQUEST_START = 'REQUEST_START';
export const REQUEST_FINISH = 'REQUEST_FINISH';
export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS';
export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR';
import Vue from 'vue';
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
[types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId;
},
[types.SET_SELECTED_MILESTONES](state, selectedMilestones) {
Vue.set(state, 'selectedMilestones', selectedMilestones);
},
[types.ADD_SELECTED_MILESTONE](state, selectedMilestone) {
state.selectedMilestones.push(selectedMilestone);
},
[types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone) {
const filteredMilestones = state.selectedMilestones.filter(
milestone => milestone !== selectedMilestone,
);
Vue.set(state, 'selectedMilestones', filteredMilestones);
},
[types.SET_QUERY](state, query) {
state.query = query;
},
[types.REQUEST_START](state) {
state.requestCount += 1;
},
[types.REQUEST_FINISH](state) {
state.requestCount -= 1;
},
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = {
list: convertObjectPropsToCamelCase(response.data).map(({ title }) => ({ title })),
totalCount: parseInt(response.headers['x-total'], 10),
error: null,
};
},
[types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error) {
state.matches.projectMilestones = {
list: [],
totalCount: 0,
error,
};
},
};
export default () => ({
projectId: null,
groupId: null,
query: '',
matches: {
projectMilestones: {
list: [],
totalCount: 0,
error: null,
},
},
selectedMilestones: [],
requestCount: 0,
});
---
title: Add vuex stores for milestone comboxbox
merge_request: 45287
author:
type: added
import testAction from 'helpers/vuex_action_helper';
import createState from '~/milestones/stores/state';
import * as actions from '~/milestones/stores/actions';
import * as types from '~/milestones/stores/mutation_types';
let mockProjectMilestonesReturnValue;
let mockProjectSearchReturnValue;
jest.mock('~/api', () => ({
// `__esModule: true` is required when mocking modules with default exports:
// https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options
__esModule: true,
default: {
projectMilestones: () => mockProjectMilestonesReturnValue,
projectSearch: () => mockProjectSearchReturnValue,
},
}));
describe('Milestone combobox Vuex store actions', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('setProjectId', () => {
it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => {
const projectId = '4';
testAction(actions.setProjectId, projectId, state, [
{ type: types.SET_PROJECT_ID, payload: projectId },
]);
});
});
describe('setSelectedMilestones', () => {
it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => {
const selectedMilestones = ['v1.2.3'];
testAction(actions.setSelectedMilestones, selectedMilestones, state, [
{ type: types.SET_SELECTED_MILESTONES, payload: selectedMilestones },
]);
});
});
describe('toggleMilestones', () => {
const selectedMilestone = 'v1.2.3';
it(`commits ${types.ADD_SELECTED_MILESTONE} with the new selected milestone name`, () => {
testAction(actions.toggleMilestones, selectedMilestone, state, [
{ type: types.ADD_SELECTED_MILESTONE, payload: selectedMilestone },
]);
});
it(`commits ${types.REMOVE_SELECTED_MILESTONE} with the new selected milestone name`, () => {
state.selectedMilestones = [selectedMilestone];
testAction(actions.toggleMilestones, selectedMilestone, state, [
{ type: types.REMOVE_SELECTED_MILESTONE, payload: selectedMilestone },
]);
});
});
describe('search', () => {
it(`commits ${types.SET_QUERY} with the new search query`, () => {
const query = 'v1.0';
testAction(
actions.search,
query,
state,
[{ type: types.SET_QUERY, payload: query }],
[{ type: 'searchMilestones' }],
);
});
});
describe('searchMilestones', () => {
describe('when the search is successful', () => {
const projectSearchApiResponse = { data: [{ title: 'v1.0' }] };
beforeEach(() => {
mockProjectSearchReturnValue = Promise.resolve(projectSearchApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the search fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockProjectSearchReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
describe('fetchMilestones', () => {
describe('when the fetch is successful', () => {
const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] };
beforeEach(() => {
mockProjectMilestonesReturnValue = Promise.resolve(projectMilestonesApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the fetch fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockProjectMilestonesReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
});
import * as getters from '~/milestones/stores/getters';
describe('Milestone comboxbox Vuex store getters', () => {
describe('isLoading', () => {
it.each`
requestCount | isLoading
${2} | ${true}
${1} | ${true}
${0} | ${false}
${-1} | ${false}
`('returns true when at least one request is in progress', ({ requestCount, isLoading }) => {
expect(getters.isLoading({ requestCount })).toBe(isLoading);
});
});
});
import createState from '~/milestones/stores/state';
import mutations from '~/milestones/stores/mutations';
import * as types from '~/milestones/stores/mutation_types';
describe('Milestones combobox Vuex store mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('initial state', () => {
it('is created with the correct structure and initial values', () => {
expect(state).toEqual({
projectId: null,
groupId: null,
query: '',
matches: {
projectMilestones: {
list: [],
totalCount: 0,
error: null,
},
},
selectedMilestones: [],
requestCount: 0,
});
});
});
describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => {
const newProjectId = '4';
mutations[types.SET_PROJECT_ID](state, newProjectId);
expect(state.projectId).toBe(newProjectId);
});
});
describe(`${types.SET_SELECTED_MILESTONES}`, () => {
it('sets the selected milestones', () => {
const selectedMilestones = ['v1.2.3'];
mutations[types.SET_SELECTED_MILESTONES](state, selectedMilestones);
expect(state.selectedMilestones).toEqual(['v1.2.3']);
});
});
describe(`${types.ADD_SELECTED_MILESTONESs}`, () => {
it('adds the selected milestones', () => {
const selectedMilestone = 'v1.2.3';
mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone);
expect(state.selectedMilestones).toEqual(['v1.2.3']);
});
});
describe(`${types.REMOVE_SELECTED_MILESTONES}`, () => {
it('removes the selected milestones', () => {
const selectedMilestone = 'v1.2.3';
mutations[types.SET_SELECTED_MILESTONES](state, [selectedMilestone]);
expect(state.selectedMilestones).toEqual(['v1.2.3']);
mutations[types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone);
expect(state.selectedMilestones).toEqual([]);
});
});
describe(`${types.SET_QUERY}`, () => {
it('updates the search query', () => {
const newQuery = 'hello';
mutations[types.SET_QUERY](state, newQuery);
expect(state.query).toBe(newQuery);
});
});
describe(`${types.REQUEST_START}`, () => {
it('increments requestCount by 1', () => {
mutations[types.REQUEST_START](state);
expect(state.requestCount).toBe(1);
mutations[types.REQUEST_START](state);
expect(state.requestCount).toBe(2);
mutations[types.REQUEST_START](state);
expect(state.requestCount).toBe(3);
});
});
describe(`${types.REQUEST_FINISH}`, () => {
it('decrements requestCount by 1', () => {
state.requestCount = 3;
mutations[types.REQUEST_FINISH](state);
expect(state.requestCount).toBe(2);
mutations[types.REQUEST_FINISH](state);
expect(state.requestCount).toBe(1);
mutations[types.REQUEST_FINISH](state);
expect(state.requestCount).toBe(0);
});
});
describe(`${types.RECEIVE_PROJECT_MILESTONES_SUCCESS}`, () => {
it('updates state.matches.projectMilestones based on the provided API response', () => {
const response = {
data: [
{
title: 'v0.1',
},
{
title: 'v0.2',
},
],
headers: {
'x-total': 2,
},
};
mutations[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response);
expect(state.matches.projectMilestones).toEqual({
list: [
{
title: 'v0.1',
},
{
title: 'v0.2',
},
],
error: null,
totalCount: 2,
});
});
describe(`${types.RECEIVE_PROJECT_MILESTONES_ERROR}`, () => {
it('updates state.matches.projectMilestones to an empty state with the error object', () => {
const error = new Error('Something went wrong!');
state.matches.projectMilestones = {
list: [{ title: 'v0.1' }],
totalCount: 1,
error: null,
};
mutations[types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error);
expect(state.matches.projectMilestones).toEqual({
list: [],
totalCount: 0,
error,
});
});
});
});
});
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