Commit 905c32b5 authored by Mark Florian's avatar Mark Florian

Merge branch '34640-async-search-epics-dropdown' into 'master'

Add support for async search within Epics dropdown

See merge request gitlab-org/gitlab!26980
parents e25c11f0 d44479f2
...@@ -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,
};
return axios.get(url); if (search) {
params.search = search;
}
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,13 +52,16 @@ export default { ...@@ -52,13 +52,16 @@ export default {
> >
</li> </li>
<li class="divider"></li> <li class="divider"></li>
<li v-for="epic in epics" :key="epic.id"> <template v-if="epics.length">
<gl-link <li v-for="epic in epics" :key="epic.id">
:class="{ 'is-active': isSelected(epic) }" <gl-link
@click.prevent="handleItemClick(epic)" :class="{ 'is-active': isSelected(epic) }"
>{{ epic.title }}</gl-link @click.prevent="handleItemClick(epic)"
> >{{ 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, {
dispatch: () => {}, state,
}); 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