Commit d44479f2 authored by Kushal Pandya's avatar Kushal Pandya

Add support for async search of Epics

Search for Epics asynchronously when no matches are
present in locally loaded list.
parent 97dc741e
...@@ -8,8 +8,7 @@ export default { ...@@ -8,8 +8,7 @@ export default {
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json', ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription', subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics', childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics',
groupEpicsPath: groupEpicsPath: '/api/:version/groups/:id/epics',
'/api/:version/groups/:id/epics?include_ancestor_groups=:includeAncestorGroups&include_descendant_groups=:includeDescendantGroups',
epicIssuePath: '/api/:version/groups/:id/epics/:epic_iid/issues/:issue_id', epicIssuePath: '/api/:version/groups/:id/epics/:epic_iid/issues/:issue_id',
groupPackagesPath: '/api/:version/groups/:id/packages', groupPackagesPath: '/api/:version/groups/:id/packages',
projectPackagesPath: '/api/:version/projects/:id/packages', projectPackagesPath: '/api/:version/projects/:id/packages',
...@@ -64,13 +63,25 @@ export default { ...@@ -64,13 +63,25 @@ export default {
}); });
}, },
groupEpics({ groupId, includeAncestorGroups = false, includeDescendantGroups = true }) { groupEpics({
const url = Api.buildUrl(this.groupEpicsPath) groupId,
.replace(':id', groupId) includeAncestorGroups = false,
.replace(':includeAncestorGroups', includeAncestorGroups) includeDescendantGroups = true,
.replace(':includeDescendantGroups', includeDescendantGroups); search = '',
}) {
const url = Api.buildUrl(this.groupEpicsPath).replace(':id', groupId);
const params = {
include_ancestor_groups: includeAncestorGroups,
include_descendant_groups: includeDescendantGroups,
};
if (search) {
params.search = search;
}
return axios.get(url); return axios.get(url, {
params,
});
}, },
addEpicIssue({ groupId, epicIid, issueId }) { addEpicIssue({ groupId, epicIid, issueId }) {
......
...@@ -65,7 +65,7 @@ export default { ...@@ -65,7 +65,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic']), ...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']),
...mapGetters(['groupEpics']), ...mapGetters(['groupEpics']),
dropdownSelectInProgress() { dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress; return this.initialEpicLoading || this.epicSelectInProgress;
...@@ -95,6 +95,17 @@ export default { ...@@ -95,6 +95,17 @@ export default {
initialEpicLoading() { initialEpicLoading() {
this.setSelectedEpic(this.initialEpic); this.setSelectedEpic(this.initialEpic);
}, },
/**
* Check if `searchQuery` presence has yielded any matching
* epics, if not, dispatch `fetchEpics` with search query.
*/
searchQuery(value) {
if (value) {
if (!this.groupEpics.length) this.fetchEpics(this.searchQuery);
} else {
this.fetchEpics();
}
},
}, },
mounted() { mounted() {
this.setInitialData({ this.setInitialData({
...@@ -103,7 +114,7 @@ export default { ...@@ -103,7 +114,7 @@ export default {
selectedEpic: this.selectedEpic, selectedEpic: this.selectedEpic,
selectedEpicIssueId: this.epicIssueId, selectedEpicIssueId: this.epicIssueId,
}); });
$(this.$refs.dropdown).on('shown.bs.dropdown', this.handleDropdownShown); $(this.$refs.dropdown).on('shown.bs.dropdown', () => this.fetchEpics());
$(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden); $(this.$refs.dropdown).on('hidden.bs.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
...@@ -131,9 +142,6 @@ export default { ...@@ -131,9 +142,6 @@ export default {
}); });
}); });
}, },
handleDropdownShown() {
if (this.groupEpics.length === 0) this.fetchEpics();
},
handleDropdownHidden() { handleDropdownHidden() {
this.showDropdown = false; this.showDropdown = false;
}, },
......
...@@ -52,6 +52,7 @@ export default { ...@@ -52,6 +52,7 @@ export default {
> >
</li> </li>
<li class="divider"></li> <li class="divider"></li>
<template v-if="epics.length">
<li v-for="epic in epics" :key="epic.id"> <li v-for="epic in epics" :key="epic.id">
<gl-link <gl-link
:class="{ 'is-active': isSelected(epic) }" :class="{ 'is-active': isSelected(epic) }"
...@@ -59,6 +60,8 @@ export default { ...@@ -59,6 +60,8 @@ export default {
>{{ epic.title }}</gl-link >{{ epic.title }}</gl-link
> >
</li> </li>
</template>
<li v-else class="d-block text-center p-2">{{ __('No matches found') }}</li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script>
import { debounce } from 'lodash';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -18,9 +19,9 @@ export default { ...@@ -18,9 +19,9 @@ export default {
}; };
}, },
methods: { methods: {
handleKeyUp() { handleKeyUp: debounce(function debouncedKeyUp() {
this.$emit('onSearchInput', this.query); this.$emit('onSearchInput', this.query);
}, }, 300),
handleInputClear() { handleInputClear() {
this.query = ''; this.query = '';
this.handleKeyUp(); this.handleKeyUp();
......
...@@ -30,13 +30,14 @@ export const receiveEpicsFailure = ({ commit }) => { ...@@ -30,13 +30,14 @@ export const receiveEpicsFailure = ({ commit }) => {
flash(s__('Epics|Something went wrong while fetching group epics.')); flash(s__('Epics|Something went wrong while fetching group epics.'));
commit(types.RECEIVE_EPICS_FAILURE); commit(types.RECEIVE_EPICS_FAILURE);
}; };
export const fetchEpics = ({ state, dispatch }) => { export const fetchEpics = ({ state, dispatch }, search = '') => {
dispatch('requestEpics'); dispatch('requestEpics');
Api.groupEpics({ Api.groupEpics({
groupId: state.groupId, groupId: state.groupId,
includeDescendantGroups: false, includeDescendantGroups: false,
includeAncestorGroups: true, includeAncestorGroups: true,
search,
}) })
.then(({ data }) => { .then(({ data }) => {
dispatch('receiveEpicsSuccess', data); dispatch('receiveEpicsSuccess', data);
......
---
title: Add support for async search within Epics dropdown
merge_request: 26980
author:
type: fixed
...@@ -46,7 +46,9 @@ describe 'Epic in issue sidebar', :js do ...@@ -46,7 +46,9 @@ describe 'Epic in issue sidebar', :js do
page.find('.dropdown-input-field').send_keys('Foo') page.find('.dropdown-input-field').send_keys('Foo')
expect(page.all('.dropdown-content li a').length).to eq(2) # `No Epic` + 1 matching epic wait_for_requests
expect(page).to have_selector('.dropdown-content li a', count: 2) # `No Epic` + 1 matching epic
end end
end end
......
...@@ -90,9 +90,16 @@ describe('Api', () => { ...@@ -90,9 +90,16 @@ describe('Api', () => {
describe('groupEpics', () => { describe('groupEpics', () => {
it('calls `axios.get` using param `groupId`', done => { it('calls `axios.get` using param `groupId`', done => {
const groupId = 2; const groupId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/epics?include_ancestor_groups=false&include_descendant_groups=true`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/epics`;
mock.onGet(expectedUrl).reply(200, mockEpics); mock
.onGet(expectedUrl, {
params: {
include_ancestor_groups: false,
include_descendant_groups: true,
},
})
.reply(200, mockEpics);
Api.groupEpics({ groupId }) Api.groupEpics({ groupId })
.then(({ data }) => { .then(({ data }) => {
...@@ -106,6 +113,33 @@ describe('Api', () => { ...@@ -106,6 +113,33 @@ describe('Api', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls `axios.get` using param `search` when it is provided', done => {
const groupId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/epics`;
mock
.onGet(expectedUrl, {
params: {
include_ancestor_groups: false,
include_descendant_groups: true,
search: 'foo',
},
})
.reply(200, mockEpics);
Api.groupEpics({ groupId, search: 'foo' })
.then(({ data }) => {
data.forEach((epic, index) => {
expect(epic.id).toBe(mockEpics[index].id);
expect(epic.iid).toBe(mockEpics[index].iid);
expect(epic.group_id).toBe(mockEpics[index].group_id);
expect(epic.title).toBe(mockEpics[index].title);
});
})
.then(done)
.catch(done.fail);
});
}); });
describe('addEpicIssue', () => { describe('addEpicIssue', () => {
......
...@@ -13,14 +13,7 @@ import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/drop ...@@ -13,14 +13,7 @@ import DropdownContents from 'ee/vue_shared/components/sidebar/epics_select/drop
import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store'; import createDefaultStore from 'ee/vue_shared/components/sidebar/epics_select/store';
import { import { mockEpic1, mockEpic2, mockAssignRemoveRes, mockIssue, noneEpic } from '../mock_data';
mockEpic1,
mockEpic2,
mockEpics,
mockAssignRemoveRes,
mockIssue,
noneEpic,
} from '../mock_data';
describe('EpicsSelect', () => { describe('EpicsSelect', () => {
describe('Base', () => { describe('Base', () => {
...@@ -82,29 +75,32 @@ describe('EpicsSelect', () => { ...@@ -82,29 +75,32 @@ describe('EpicsSelect', () => {
expect(wrapper.vm.$store.state.selectedEpic).toBe(mockEpic2); expect(wrapper.vm.$store.state.selectedEpic).toBe(mockEpic2);
}); });
}); });
});
describe('methods', () => { describe('searchQuery', () => {
describe('handleDropdownShown', () => { beforeEach(() => {
it('should call `fetchEpics` when `groupEpics` does not return any epics', done => { jest.spyOn(wrapper.vm, 'fetchEpics').mockImplementation(jest.fn());
jest.spyOn(wrapper.vm, 'fetchEpics').mockReturnValue( });
Promise.resolve({
data: mockEpics,
}),
);
store.dispatch('receiveEpicsSuccess', []); it('should call action `fetchEpics` with `searchQuery` when value is set and `groupEpics` is empty', () => {
wrapper.vm.$store.dispatch('receiveEpicsSuccess', []);
wrapper.vm.$store.dispatch('setSearchQuery', 'foo');
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
wrapper.vm.handleDropdownShown(); expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo');
});
});
expect(wrapper.vm.fetchEpics).toHaveBeenCalled(); it('should call action `fetchEpics` without any params when value is empty', () => {
wrapper.vm.$store.dispatch('setSearchQuery', '');
done(); return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith();
});
}); });
}); });
}); });
describe('methods', () => {
describe('handleDropdownHidden', () => { describe('handleDropdownHidden', () => {
it('should set `showDropdown` to false', () => { it('should set `showDropdown` to false', () => {
wrapper.vm.handleDropdownHidden(); wrapper.vm.handleDropdownHidden();
......
...@@ -111,6 +111,16 @@ describe('EpicsSelect', () => { ...@@ -111,6 +111,16 @@ describe('EpicsSelect', () => {
.classes(), .classes(),
).toContain('is-active'); ).toContain('is-active');
}); });
it('should render string "No matches found" when `epics` array is empty', () => {
wrapper.setProps({
epics: [],
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.text()).toContain('No matches found');
});
});
}); });
}); });
}); });
...@@ -4,6 +4,8 @@ import { GlButton } from '@gitlab/ui'; ...@@ -4,6 +4,8 @@ import { GlButton } from '@gitlab/ui';
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 Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
jest.mock('lodash/debounce', () => jest.fn(fn => fn));
const createComponent = () => const createComponent = () =>
shallowMount(DropdownSearchInput, { shallowMount(DropdownSearchInput, {
directives: { directives: {
......
...@@ -193,15 +193,19 @@ describe('EpicsSelect', () => { ...@@ -193,15 +193,19 @@ describe('EpicsSelect', () => {
}), }),
); );
actions.fetchEpics({ actions.fetchEpics(
{
state, state,
dispatch: () => {}, dispatch: () => {},
}); },
'foo',
);
expect(Api.groupEpics).toHaveBeenCalledWith({ expect(Api.groupEpics).toHaveBeenCalledWith({
groupId: state.groupId, groupId: state.groupId,
includeDescendantGroups: false, includeDescendantGroups: false,
includeAncestorGroups: true, includeAncestorGroups: true,
search: 'foo',
}); });
}); });
}); });
......
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