Commit 66be6eb1 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '13714-epics-select-vuex' into 'master'

Update EpicsSelect dropdown to use Vuex

Closes #13714

See merge request gitlab-org/gitlab-ee!15639
parents 3c23fb4a 107cc2a7
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex';
import $ from 'jquery'; import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import EpicsSelectService from './service/epics_select_service'; import createStore from './store';
import EpicsSelectStore from './store/epics_select_store';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
...@@ -19,6 +18,7 @@ import DropdownSearchInput from './dropdown_search_input.vue'; ...@@ -19,6 +18,7 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
export default { export default {
store: createStore(),
components: { components: {
GlLoadingIcon, GlLoadingIcon,
DropdownTitle, DropdownTitle,
...@@ -61,25 +61,14 @@ export default { ...@@ -61,25 +61,14 @@ export default {
}, },
data() { data() {
return { return {
service: new EpicsSelectService({
groupId: this.groupId,
}),
store: new EpicsSelectStore({
selectedEpic: this.initialEpic,
groupId: this.groupId,
selectedEpicIssueId: this.epicIssueId,
}),
showDropdown: false, showDropdown: false,
isEpicSelectLoading: false,
isEpicsLoading: false,
}; };
}, },
computed: { computed: {
epics() { ...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic']),
return this.store.getEpics(); ...mapGetters(['groupEpics']),
}, dropdownSelectInProgress() {
selectedEpic() { return this.initialEpicLoading || this.epicSelectInProgress;
return this.store.getSelectedEpic();
}, },
}, },
watch: { watch: {
...@@ -88,77 +77,28 @@ export default { ...@@ -88,77 +77,28 @@ export default {
* So we need to watch for updates before updating local store. * So we need to watch for updates before updating local store.
*/ */
initialEpicLoading() { initialEpicLoading() {
this.store.setSelectedEpic(this.initialEpic); this.setSelectedEpic(this.initialEpic);
}, },
}, },
mounted() { mounted() {
this.setInitialData({
groupId: this.groupId,
issueId: this.issueId,
selectedEpic: this.selectedEpic,
selectedEpicIssueId: this.epicIssueId,
});
$(this.$refs.dropdown).on('shown.bs.dropdown', this.handleDropdownShown); $(this.$refs.dropdown).on('shown.bs.dropdown', this.handleDropdownShown);
$(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden); $(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
fetchGroupEpics() { ...mapActions([
this.isEpicsLoading = true; 'setInitialData',
return this.service 'setSearchQuery',
.getGroupEpics() 'setSelectedEpic',
.then(({ data }) => { 'fetchEpics',
this.isEpicsLoading = false; 'assignIssueToEpic',
this.store.setEpics(data); 'removeIssueFromEpic',
}) ]),
.catch(() => {
this.isEpicsLoading = false;
createFlash(s__('Epics|Something went wrong while fetching group epics.'));
});
},
handleSelectSuccess({ data, epic, originalSelectedEpic }) {
// Verify if attachment was successful
this.isEpicSelectLoading = false;
if (data.epic.id === epic.id && data.issue.id === this.issueId) {
this.store.setSelectedEpicIssueId(data.id);
} else {
// Revert back to originally selected epic.
this.store.setSelectedEpic(originalSelectedEpic);
}
},
handleSelectFailure(errorMessage, originalSelectedEpic) {
this.isEpicSelectLoading = false;
// Revert back to originally selected epic in case of failure.
this.store.setSelectedEpic(originalSelectedEpic);
createFlash(errorMessage);
},
assignIssueToEpic(epic) {
const originalSelectedEpic = this.store.getSelectedEpic();
this.isEpicSelectLoading = true;
this.store.setSelectedEpic(epic);
return this.service
.assignIssueToEpic(this.issueId, epic)
.then(({ data }) => {
this.handleSelectSuccess({ data, epic, originalSelectedEpic });
})
.catch(() => {
this.handleSelectFailure(
s__('Epics|Something went wrong while assigning issue to epic.'),
originalSelectedEpic,
);
});
},
removeIssueFromEpic(epic) {
const originalSelectedEpic = this.store.getSelectedEpic();
this.isEpicSelectLoading = true;
this.store.setSelectedEpic(noneEpic);
return this.service
.removeIssueFromEpic(this.store.getSelectedEpicIssueId(), epic)
.then(({ data }) => {
this.handleSelectSuccess({ data, epic, originalSelectedEpic });
})
.catch(() => {
this.handleSelectFailure(
s__('Epics|Something went wrong while removing issue from epic.'),
originalSelectedEpic,
);
});
},
handleEditClick() { handleEditClick() {
this.showDropdown = true; this.showDropdown = true;
...@@ -175,7 +115,7 @@ export default { ...@@ -175,7 +115,7 @@ export default {
}); });
}, },
handleDropdownShown() { handleDropdownShown() {
if (this.epics.length === 0) this.fetchGroupEpics(); if (this.groupEpics.length === 0) this.fetchEpics();
}, },
handleDropdownHidden() { handleDropdownHidden() {
this.showDropdown = false; this.showDropdown = false;
...@@ -187,9 +127,6 @@ export default { ...@@ -187,9 +127,6 @@ export default {
this.assignIssueToEpic(epic); this.assignIssueToEpic(epic);
} }
}, },
handleSearchInput(query) {
this.store.filterEpics(query);
},
}, },
}; };
</script> </script>
...@@ -200,7 +137,7 @@ export default { ...@@ -200,7 +137,7 @@ export default {
<dropdown-title <dropdown-title
:can-edit="canEdit" :can-edit="canEdit"
:block-title="blockTitle" :block-title="blockTitle"
:is-loading="initialEpicLoading || isEpicSelectLoading" :is-loading="dropdownSelectInProgress"
@onClickEdit="handleEditClick" @onClickEdit="handleEditClick"
/> />
<dropdown-value v-show="!showDropdown" :epic="selectedEpic"> <dropdown-value v-show="!showDropdown" :epic="selectedEpic">
...@@ -214,14 +151,18 @@ export default { ...@@ -214,14 +151,18 @@ export default {
dropdown-menu-epics dropdown-menu-selectable" dropdown-menu-epics dropdown-menu-selectable"
> >
<dropdown-header /> <dropdown-header />
<dropdown-search-input @onSearchInput="handleSearchInput" /> <dropdown-search-input @onSearchInput="setSearchQuery" />
<dropdown-contents <dropdown-contents
v-if="!isEpicsLoading" v-if="!epicsFetchInProgress"
:epics="epics" :epics="groupEpics"
:selected-epic="selectedEpic" :selected-epic="selectedEpic"
@onItemSelect="handleItemSelect" @onItemSelect="handleItemSelect"
/> />
<gl-loading-icon v-if="isEpicsLoading" class="dropdown-contents-loading" size="md" /> <gl-loading-icon
v-if="epicsFetchInProgress"
class="dropdown-contents-loading"
size="md"
/>
</div> </div>
</div> </div>
</div> </div>
......
import Api from 'ee/api';
export default class EpicsSelectService {
constructor({ groupId }) {
this.groupId = groupId;
}
getGroupEpics() {
return Api.groupEpics({
groupId: this.groupId,
});
}
// eslint-disable-next-line class-methods-use-this
assignIssueToEpic(issueId, epic) {
return Api.addEpicIssue({
issueId,
groupId: epic.groupId,
epicIid: epic.iid,
});
}
// eslint-disable-next-line class-methods-use-this
removeIssueFromEpic(epicIssueId, epic) {
return Api.removeEpicIssue({
epicIssueId,
groupId: epic.groupId,
epicIid: epic.iid,
});
}
}
import flash from '~/flash';
import { s__ } from '~/locale';
import Api from 'ee/api';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { noneEpic } from 'ee/vue_shared/constants';
import * as types from './mutation_types';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const setSearchQuery = ({ commit }, searchQuery) =>
commit(types.SET_SEARCH_QUERY, searchQuery);
export const setSelectedEpic = ({ commit }, selectedEpic) =>
commit(types.SET_SELECTED_EPIC, selectedEpic);
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const receiveEpicsSuccess = ({ state, commit }, data) => {
const epics = data
.filter(rawEpic => rawEpic.group_id === state.groupId)
.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign(rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
commit(types.RECEIVE_EPICS_SUCCESS, { epics });
};
export const receiveEpicsFailure = ({ commit }) => {
flash(s__('Epics|Something went wrong while fetching group epics.'));
commit(types.RECEIVE_EPICS_FAILURE);
};
export const fetchEpics = ({ state, dispatch }) => {
dispatch('requestEpics');
Api.groupEpics({
groupId: state.groupId,
})
.then(({ data }) => {
dispatch('receiveEpicsSuccess', data);
})
.catch(() => {
dispatch('receiveEpicsFailure');
});
};
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => {
// Verify if update was successful
if (data.epic.id === epic.id && data.issue.id === state.issueId) {
commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, {
selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id,
});
}
};
/**
* Shows provided errorMessage in flash banner and
* fires `RECEIVE_ISSUE_UPDATE_FAILURE` mutation
*
* @param {string} errorMessage
*/
export const receiveIssueUpdateFailure = ({ commit }, errorMessage) => {
flash(errorMessage);
commit(types.RECEIVE_ISSUE_UPDATE_FAILURE);
};
export const assignIssueToEpic = ({ state, dispatch }, epic) => {
dispatch('requestIssueUpdate');
Api.addEpicIssue({
issueId: state.issueId,
groupId: epic.groupId,
epicIid: epic.iid,
})
.then(({ data }) => {
dispatch('receiveIssueUpdateSuccess', {
data,
epic,
});
})
.catch(() => {
// Shows flash error for Epic change failure
dispatch(
'receiveIssueUpdateFailure',
s__('Epics|Something went wrong while assigning issue to epic.'),
);
});
};
export const removeIssueFromEpic = ({ state, dispatch }, epic) => {
dispatch('requestIssueUpdate');
Api.removeEpicIssue({
epicIssueId: state.selectedEpicIssueId,
groupId: epic.groupId,
epicIid: epic.iid,
})
.then(({ data }) => {
dispatch('receiveIssueUpdateSuccess', {
data,
epic,
isRemoval: true,
});
})
.catch(() => {
// Shows flash error for Epic remove failure
dispatch(
'receiveIssueUpdateFailure',
s__('Epics|Something went wrong while removing issue from epic.'),
);
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { convertObjectPropsToCamelCase, searchBy } from '~/lib/utils/common_utils';
export default class EpicsSelectStore {
constructor({ groupId, selectedEpic, selectedEpicIssueId }) {
this.groupId = groupId;
this.state = {};
this.state.epics = [];
this.state.allEpics = [];
this.state.selectedEpic = selectedEpic;
this.state.selectedEpicIssueId = selectedEpicIssueId;
}
setEpics(rawEpics) {
// Cache all Epics so that
// during search, we only work with `state.epics`
this.state.allEpics = rawEpics
.filter(epic => epic.group_id === this.groupId)
.map(epic =>
convertObjectPropsToCamelCase(Object.assign(epic, { url: epic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
this.state.epics = this.state.allEpics;
}
getEpics() {
return this.state.epics;
}
filterEpics(query) {
if (query) {
this.state.epics = this.state.allEpics.filter(epic => {
const { title, reference, url, iid } = epic;
// In case user has just pasted ID
// We need to be specific with the search
if (Number(query)) {
return query.includes(iid);
}
return searchBy(query, {
title,
reference,
url,
});
});
} else {
this.state.epics = this.state.allEpics;
}
}
setSelectedEpic(selectedEpic) {
this.state.selectedEpic = selectedEpic;
}
setSelectedEpicIssueId(selectedEpicIssueId) {
this.state.selectedEpicIssueId = selectedEpicIssueId;
}
getSelectedEpic() {
return this.state.selectedEpic;
}
getSelectedEpicIssueId() {
return this.state.selectedEpicIssueId;
}
}
import { searchBy } from '~/lib/utils/common_utils';
/**
* Returns array of Epics
*
* 1. When state.searchQuery is empty, all Epics are returned.
* 2. When state.searchQuery has value, Epics list is filtered
* using the searchQuery against `iid`, `title`, `reference`
* and `url` props of Epic object.
*
* @param {object} state
*/
export const groupEpics = state => {
if (state.searchQuery) {
return state.epics.filter(epic => {
const { title, reference, url, iid } = epic;
// In case user has just pasted ID
// We need to be specific with the search
if (Number(state.searchQuery)) {
return state.searchQuery.includes(iid);
}
return searchBy(state.searchQuery, {
title,
reference,
url,
});
});
}
return state.epics;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import getDefaultState from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
state: getDefaultState(),
actions,
getters,
mutations,
});
export default createStore;
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_SELECTED_EPIC = 'SET_SELECTED_EPIC';
export const REQUEST_EPICS = 'REQUEST_EPICS';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const RECEIVE_EPICS_FAILURE = 'RECEIVE_EPICS_FAILURE';
export const REQUEST_ISSUE_UPDATE = 'REQUEST_ISSUE_UPDATE';
export const RECEIVE_ISSUE_UPDATE_SUCCESS = 'RECEIVE_ISSUE_UPDATE_SUCCESS';
export const RECEIVE_ISSUE_UPDATE_FAILURE = 'RECEIVE_ISSUE_UPDATE_FAILURE';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, { groupId, issueId, selectedEpic, selectedEpicIssueId }) {
state.groupId = groupId;
state.issueId = issueId;
state.selectedEpic = selectedEpic;
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.SET_SEARCH_QUERY](state, searchQuery) {
state.searchQuery = searchQuery;
},
[types.SET_SELECTED_EPIC](state, selectedEpic) {
state.selectedEpic = selectedEpic;
},
[types.REQUEST_EPICS](state) {
state.epicsFetchInProgress = true;
},
[types.RECEIVE_EPICS_SUCCESS](state, { epics }) {
state.epicsFetchInProgress = false;
state.epics = epics;
},
[types.RECEIVE_EPICS_FAILURE](state) {
state.epicsFetchInProgress = false;
},
[types.REQUEST_ISSUE_UPDATE](state) {
state.epicSelectInProgress = true;
},
[types.RECEIVE_ISSUE_UPDATE_SUCCESS](state, { selectedEpic, selectedEpicIssueId }) {
state.epicSelectInProgress = false;
state.selectedEpic = selectedEpic;
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.RECEIVE_ISSUE_UPDATE_FAILURE](state) {
state.epicSelectInProgress = false;
},
};
export default () => ({
// Initial Data
groupId: null,
issueId: null,
selectedEpic: {},
selectedEpicIssueId: null,
// Store
searchQuery: '',
epics: [],
// UI Flags
epicSelectInProgress: false,
epicsFetchInProgress: false,
});
...@@ -2,7 +2,11 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,11 @@ import { shallowMount } from '@vue/test-utils';
import SidebarItemEpicsSelect from 'ee/sidebar/components/sidebar_item_epics_select.vue'; import SidebarItemEpicsSelect from 'ee/sidebar/components/sidebar_item_epics_select.vue';
import { mockSidebarStore, mockEpic1, mockIssue } from '../mock_data'; import {
mockSidebarStore,
mockEpic1,
mockIssue,
} from '../../vue_shared/components/sidebar/mock_data';
describe('SidebarItemEpicsSelect', () => { describe('SidebarItemEpicsSelect', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import EpicsSelectBase from 'ee/vue_shared/components/sidebar/epics_select/base.vue'; import EpicsSelectBase from 'ee/vue_shared/components/sidebar/epics_select/base.vue';
...@@ -11,26 +11,21 @@ import DropdownHeader from 'ee/vue_shared/components/sidebar/epics_select/dropdo ...@@ -11,26 +11,21 @@ import DropdownHeader from 'ee/vue_shared/components/sidebar/epics_select/dropdo
import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/dropdown_search_input.vue'; import DropdownSearchInput from 'ee/vue_shared/components/sidebar/epics_select/dropdown_search_input.vue';
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue'; import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import EpicsSelectService from 'ee/vue_shared/components/sidebar/epics_select/service/epics_select_service'; import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store';
import EpicsSelectStore from 'ee/vue_shared/components/sidebar/epics_select/store/epics_select_store';
import { import { mockEpic1, mockEpic2, mockIssue, noneEpic } from '../mock_data';
mockEpic1,
mockEpic2,
mockIssue,
mockEpics,
mockAssignRemoveRes,
noneEpic,
} from '../../../../sidebar/mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('Base', () => { describe('Base', () => {
const errorMessage = 'Something went wrong while fetching group epics.';
let wrapper; let wrapper;
// const errorMessage = 'Something went wrong while fetching group epics.';
const store = createDefaultStore();
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
wrapper = shallowMount(EpicsSelectBase, { wrapper = shallowMount(EpicsSelectBase, {
store,
localVue: createLocalVue(),
propsData: { propsData: {
canEdit: true, canEdit: true,
blockTitle: 'Epic', blockTitle: 'Epic',
...@@ -47,14 +42,8 @@ describe('EpicsSelect', () => { ...@@ -47,14 +42,8 @@ describe('EpicsSelect', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('data', () => {
it('should have `service` & `store` props initialized', () => {
expect(wrapper.vm.service instanceof EpicsSelectService).toBe(true);
expect(wrapper.vm.store instanceof EpicsSelectStore).toBe(true);
});
});
describe('methods', () => { describe('methods', () => {
/*
describe('fetchGroupEpics', () => { describe('fetchGroupEpics', () => {
it('should call `service.getGroupEpics` and set response to store on request success', done => { it('should call `service.getGroupEpics` and set response to store on request success', done => {
jest.spyOn(wrapper.vm.service, 'getGroupEpics').mockResolvedValue({ data: mockEpics }); jest.spyOn(wrapper.vm.service, 'getGroupEpics').mockResolvedValue({ data: mockEpics });
...@@ -312,17 +301,18 @@ describe('EpicsSelect', () => { ...@@ -312,17 +301,18 @@ describe('EpicsSelect', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
*/
describe('handleDropdownShown', () => { describe('handleDropdownShown', () => {
it('should call `fetchGroupEpics` when store does not have any epics loaded yet', done => { it('should call `fetchEpics` when `groupEpics` does not return any epics', done => {
jest.spyOn(wrapper.vm, 'fetchGroupEpics'); jest.spyOn(wrapper.vm, 'fetchEpics');
wrapper.vm.store.setEpics([]); store.dispatch('receiveEpicsSuccess', []);
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
wrapper.vm.handleDropdownShown(); wrapper.vm.handleDropdownShown();
expect(wrapper.vm.fetchGroupEpics).toHaveBeenCalled(); expect(wrapper.vm.fetchEpics).toHaveBeenCalled();
done(); done();
}); });
...@@ -340,6 +330,7 @@ describe('EpicsSelect', () => { ...@@ -340,6 +330,7 @@ describe('EpicsSelect', () => {
describe('handleItemSelect', () => { describe('handleItemSelect', () => {
it('should call `removeIssueFromEpic` with selected epic when `epic` param represents `No Epic`', () => { it('should call `removeIssueFromEpic` with selected epic when `epic` param represents `No Epic`', () => {
jest.spyOn(wrapper.vm, 'removeIssueFromEpic'); jest.spyOn(wrapper.vm, 'removeIssueFromEpic');
store.dispatch('setSelectedEpic', mockEpic1);
wrapper.vm.handleItemSelect(noneEpic); wrapper.vm.handleItemSelect(noneEpic);
...@@ -354,16 +345,6 @@ describe('EpicsSelect', () => { ...@@ -354,16 +345,6 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.assignIssueToEpic).toHaveBeenCalledWith(mockEpic2); expect(wrapper.vm.assignIssueToEpic).toHaveBeenCalledWith(mockEpic2);
}); });
}); });
describe('handleSearchInput', () => {
it('should call `store.filterEpics` with passed `query` param', () => {
jest.spyOn(wrapper.vm.store, 'filterEpics');
wrapper.vm.handleSearchInput('foo');
expect(wrapper.vm.store.filterEpics).toHaveBeenCalledWith('foo');
});
});
}); });
describe('template', () => { describe('template', () => {
...@@ -445,9 +426,7 @@ describe('EpicsSelect', () => { ...@@ -445,9 +426,7 @@ describe('EpicsSelect', () => {
it('should render DropdownContents component when props `canEdit` & `showDropdown` are true and `isEpicsLoading` is false', done => { it('should render DropdownContents component when props `canEdit` & `showDropdown` are true and `isEpicsLoading` is false', done => {
showDropdown(); showDropdown();
wrapper.setData({ store.dispatch('receiveEpicsSuccess', []);
isEpicsLoading: false,
});
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(wrapper.find(DropdownContents).exists()).toBe(true); expect(wrapper.find(DropdownContents).exists()).toBe(true);
...@@ -457,9 +436,7 @@ describe('EpicsSelect', () => { ...@@ -457,9 +436,7 @@ describe('EpicsSelect', () => {
it('should render GlLoadingIcon component when props `canEdit` & `showDropdown` and `isEpicsLoading` are true', done => { it('should render GlLoadingIcon component when props `canEdit` & `showDropdown` and `isEpicsLoading` are true', done => {
showDropdown(); showDropdown();
wrapper.setData({ store.dispatch('requestEpics');
isEpicsLoading: true,
});
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
......
...@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui'; ...@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue'; import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/dropdown_contents.vue';
import { mockEpic1, mockEpic2, mockEpics, noneEpic } from '../../../../sidebar/mock_data'; import { mockEpic1, mockEpic2, mockEpics, noneEpic } from '../mock_data';
const epics = mockEpics.map(epic => convertObjectPropsToCamelCase(epic)); const epics = mockEpics.map(epic => convertObjectPropsToCamelCase(epic));
......
...@@ -4,7 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -4,7 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import DropdownValueCollapsed from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value_collapsed.vue'; import DropdownValueCollapsed from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value_collapsed.vue';
import { mockEpic1 } from '../../../../sidebar/mock_data'; import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('DropdownValueCollapsed', () => { describe('DropdownValueCollapsed', () => {
......
...@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui'; ...@@ -4,7 +4,7 @@ import { GlLink } from '@gitlab/ui';
import DropdownValue from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value.vue'; import DropdownValue from 'ee/vue_shared/components/sidebar/epics_select/dropdown_value.vue';
import { mockEpic1 } from '../../../../sidebar/mock_data'; import { mockEpic1 } from '../mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('DropdownValue', () => { describe('DropdownValue', () => {
......
import Api from 'ee/api';
import EpicsSelectService from 'ee/vue_shared/components/sidebar/epics_select/service/epics_select_service';
import {
mockEpic1,
mockIssue,
mockEpics,
mockAssignRemoveRes,
} from '../../../../../sidebar/mock_data';
describe('EpicsSelect', () => {
describe('Service', () => {
const service = new EpicsSelectService({ groupId: mockEpic1.group_id });
describe('getGroupEpics', () => {
it('calls `Api.groupEpics` with `groupId`', () => {
jest.spyOn(Api, 'groupEpics').mockResolvedValue({ data: mockEpics });
service.getGroupEpics();
expect(Api.groupEpics).toHaveBeenCalledWith(
expect.objectContaining({
groupId: mockEpic1.group_id,
}),
);
});
});
describe('assignIssueToEpic', () => {
it('calls `Api.addEpicIssue` with `issueId`, `groupId` & `epicIid`', () => {
jest.spyOn(Api, 'addEpicIssue').mockResolvedValue({ data: mockAssignRemoveRes });
service.assignIssueToEpic(mockIssue.id, {
groupId: mockEpic1.group_id,
iid: mockEpic1.iid,
});
expect(Api.addEpicIssue).toHaveBeenCalledWith(
expect.objectContaining({
issueId: mockIssue.id,
groupId: mockEpic1.group_id,
epicIid: mockEpic1.iid,
}),
);
});
});
describe('removeIssueFromEpic', () => {
it('calls `Api.removeEpicIssue` with `epicIssueId`, `groupId` & `epicIid`', () => {
jest.spyOn(Api, 'removeEpicIssue').mockResolvedValue({ data: mockAssignRemoveRes });
service.removeIssueFromEpic(mockIssue.epic_issue_id, {
groupId: mockEpic1.group_id,
iid: mockEpic1.iid,
});
expect(Api.removeEpicIssue).toHaveBeenCalledWith(
expect.objectContaining({
epicIssueId: mockIssue.epic_issue_id,
groupId: mockEpic1.group_id,
epicIid: mockEpic1.iid,
}),
);
});
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EpicsSelectStore from 'ee/vue_shared/components/sidebar/epics_select/store/epics_select_store';
import { mockIssue, mockEpics } from '../../../../../sidebar/mock_data';
describe('EpicsSelect', () => {
describe('Store', () => {
const normalizedEpics = mockEpics.map(epic =>
convertObjectPropsToCamelCase(Object.assign(epic, { url: epic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
let store;
beforeEach(() => {
store = new EpicsSelectStore({
groupId: normalizedEpics[0].groupId,
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockIssue.epic_issue_id,
});
});
describe('constructor', () => {
it('should initialize `state` with all the required properties', () => {
expect(store.groupId).toBe(normalizedEpics[0].groupId);
expect(store.state).toEqual(
expect.objectContaining({
epics: [],
allEpics: [],
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockIssue.epic_issue_id,
}),
);
});
});
describe('setEpics', () => {
it('should set passed `rawEpics` into the store state by normalizing it', () => {
store.setEpics(mockEpics);
expect(store.state.epics.length).toBe(mockEpics.length);
expect(store.state.allEpics.length).toBe(mockEpics.length);
expect(store.state.epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
expect(store.state.allEpics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
});
describe('getEpics', () => {
it('should return value of `state.epics`', () => {
store.setEpics(mockEpics);
const epics = store.getEpics();
expect(epics.length).toBe(mockEpics.length);
});
});
describe('filterEpics', () => {
beforeEach(() => {
store.setEpics(mockEpics);
});
it('should return `state.epics` filtered Epic Title', () => {
store.filterEpics('consequatur');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic Reference', () => {
store.filterEpics('gitlab-org&1');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic URL', () => {
store.filterEpics('http://gitlab.example.com/groups/gitlab-org/-/epics/2');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` filtered Epic Iid', () => {
store.filterEpics('2');
const epics = store.getEpics();
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` without any filters when query is empty', () => {
store.filterEpics('');
const epics = store.getEpics();
expect(epics.length).toBe(normalizedEpics.length);
epics.forEach((epic, index) => {
expect.objectContaining({
...normalizedEpics[index],
});
});
});
});
describe('setSelectedEpic', () => {
it('should set provided `selectedEpic` param to store state', () => {
store.setSelectedEpic(normalizedEpics[1]);
expect(store.state.selectedEpic).toBe(normalizedEpics[1]);
});
});
describe('setSelectedEpicIssueId', () => {
it('should set provided `selectedEpicIssueId` param to store state', () => {
store.setSelectedEpicIssueId(7);
expect(store.state.selectedEpicIssueId).toBe(7);
});
});
describe('getSelectedEpic', () => {
it('should return value of `selectedEpic` from store state', () => {
store.setSelectedEpic(normalizedEpics[1]);
expect(store.getSelectedEpic()).toBe(normalizedEpics[1]);
});
});
describe('getSelectedEpicIssueId', () => {
it('should return value of `selectedEpicIssueId` from store state', () => {
store.setSelectedEpicIssueId(7);
expect(store.getSelectedEpicIssueId()).toBe(7);
});
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as getters from 'ee/vue_shared/components/sidebar/epics_select/store/getters';
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import { mockEpics } from '../../mock_data';
describe('EpicsSelect', () => {
describe('store', () => {
describe('getters', () => {
let state;
const normalizedEpics = mockEpics.map(rawEpic =>
convertObjectPropsToCamelCase(Object.assign(rawEpic, { url: rawEpic.web_edit_url }), {
dropKeys: ['web_edit_url'],
}),
);
beforeEach(() => {
state = createDefaultState();
state.epics = normalizedEpics;
});
describe('groupEpics', () => {
it('should return `state.epics` without any filters when `state.searchQuery` is empty', () => {
state.searchQuery = '';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(normalizedEpics.length);
epics.forEach((epic, index) => {
expect.objectContaining({
...normalizedEpics[index],
});
});
});
it('should return `state.epics` filtered by Epic Title', () => {
state.searchQuery = 'consequatur';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered by Epic Reference', () => {
state.searchQuery = 'gitlab-org&1';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[0],
}),
);
});
it('should return `state.epics` filtered Epic URL', () => {
state.searchQuery = 'http://gitlab.example.com/groups/gitlab-org/-/epics/2';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
it('should return `state.epics` filtered by Epic Iid', () => {
state.searchQuery = '2';
const epics = getters.groupEpics(state);
expect(epics.length).toBe(1);
expect(epics[0]).toEqual(
expect.objectContaining({
...normalizedEpics[1],
}),
);
});
});
});
});
});
import mutations from 'ee/vue_shared/components/sidebar/epics_select/store/mutations';
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types';
import { mockEpic1, mockIssue } from '../../mock_data';
describe('EpicsSelect', () => {
describe('store', () => {
describe('mutations', () => {
let state;
beforeEach(() => {
state = createDefaultState();
});
describe(types.SET_INITIAL_DATA, () => {
it('should set provided `data` param props to state', () => {
const data = {
groupId: mockEpic1.group_id,
issueId: mockIssue.id,
selectedEpic: mockEpic1,
selectedEpicIssueId: mockIssue.epic_issue_id,
};
mutations[types.SET_INITIAL_DATA](state, data);
expect(state).toHaveProperty('groupId', data.groupId);
expect(state).toHaveProperty('issueId', data.issueId);
expect(state).toHaveProperty('selectedEpic', data.selectedEpic);
expect(state).toHaveProperty('selectedEpicIssueId', data.selectedEpicIssueId);
});
});
describe(types.SET_SEARCH_QUERY, () => {
it('should set `searchQuery` param to state', () => {
const searchQuery = 'foo';
mutations[types.SET_SEARCH_QUERY](state, searchQuery);
expect(state).toHaveProperty('searchQuery', searchQuery);
});
});
describe(types.SET_SELECTED_EPIC, () => {
it('should set `selectedEpic` param to state', () => {
mutations[types.SET_SELECTED_EPIC](state, mockEpic1);
expect(state).toHaveProperty('selectedEpic', mockEpic1);
});
});
describe(types.REQUEST_EPICS, () => {
it('should set `state.epicsFetchInProgress` to true', () => {
mutations[types.REQUEST_EPICS](state);
expect(state.epicsFetchInProgress).toBe(true);
});
});
describe(types.RECEIVE_EPICS_SUCCESS, () => {
it('should set `state.epicsFetchInProgress` to false `epics` param to state', () => {
mutations[types.RECEIVE_EPICS_SUCCESS](state, { epics: [mockEpic1] });
expect(state.epicsFetchInProgress).toBe(false);
expect(state.epics).toEqual(expect.arrayContaining([mockEpic1]));
});
});
describe(types.RECEIVE_EPICS_FAILURE, () => {
it('should set `state.epicsFetchInProgress` to false', () => {
mutations[types.RECEIVE_EPICS_FAILURE](state);
expect(state.epicsFetchInProgress).toBe(false);
});
});
describe(types.REQUEST_ISSUE_UPDATE, () => {
it('should set `state.epicSelectInProgress` to true', () => {
mutations[types.REQUEST_ISSUE_UPDATE](state);
expect(state.epicSelectInProgress).toBe(true);
});
});
describe(types.RECEIVE_ISSUE_UPDATE_SUCCESS, () => {
it('should set `state.epicSelectInProgress` to false and `selectedEpic` & `selectedEpicIssueId` params to state', () => {
mutations[types.RECEIVE_ISSUE_UPDATE_SUCCESS](state, {
selectedEpic: mockEpic1,
selectedEpicIssueId: mockIssue.epic_issue_id,
});
expect(state.epicSelectInProgress).toBe(false);
expect(state.selectedEpic).toBe(mockEpic1);
expect(state.selectedEpicIssueId).toBe(mockIssue.epic_issue_id);
});
});
describe(types.RECEIVE_ISSUE_UPDATE_FAILURE, () => {
it('should set `state.epicSelectInProgress` to false', () => {
mutations[types.RECEIVE_ISSUE_UPDATE_FAILURE](state);
expect(state.epicSelectInProgress).toBe(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