Commit 84bbf25a authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'ss/assignee-dropdown-search' into 'master'

Add assignee search to group issue board sidebar

See merge request gitlab-org/gitlab!47241
parents d9e5c894 9b9e03a5
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { GlDropdownItem, GlDropdownDivider, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui'; import {
GlDropdownItem,
GlDropdownDivider,
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
} from '@gitlab/ui';
import { __, n__ } from '~/locale'; import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
import searchUsers from '~/boards/queries/users_search.query.graphql';
export default { export default {
noSearchDelay: 0,
searchDelay: 250,
i18n: { i18n: {
unassigned: __('Unassigned'), unassigned: __('Unassigned'),
assignee: __('Assignee'), assignee: __('Assignee'),
...@@ -22,23 +31,42 @@ export default { ...@@ -22,23 +31,42 @@ export default {
GlDropdownDivider, GlDropdownDivider,
GlAvatarLabeled, GlAvatarLabeled,
GlAvatarLink, GlAvatarLink,
GlSearchBoxByType,
}, },
data() { data() {
return { return {
search: '',
participants: [], participants: [],
selected: this.$store.getters.activeIssue.assignees, selected: this.$store.getters.activeIssue.assignees,
}; };
}, },
apollo: { apollo: {
participants: { participants: {
query: getIssueParticipants, query() {
return this.isSearchEmpty ? getIssueParticipants : searchUsers;
},
variables() { variables() {
if (this.isSearchEmpty) {
return { return {
id: `gid://gitlab/Issue/${this.activeIssue.iid}`, id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
}; };
}
return {
search: this.search,
};
}, },
update(data) { update(data) {
if (this.isSearchEmpty) {
return data.issue?.participants?.nodes || []; return data.issue?.participants?.nodes || [];
}
return data.users?.nodes || [];
},
debounce() {
const { noSearchDelay, searchDelay } = this.$options;
return this.isSearchEmpty ? noSearchDelay : searchDelay;
}, },
}, },
}, },
...@@ -58,6 +86,9 @@ export default { ...@@ -58,6 +86,9 @@ export default {
selectedUserNames() { selectedUserNames() {
return this.selected.map(({ username }) => username); return this.selected.map(({ username }) => username);
}, },
isSearchEmpty() {
return this.search === '';
},
}, },
methods: { methods: {
...mapActions(['setAssignees']), ...mapActions(['setAssignees']),
...@@ -97,6 +128,9 @@ export default { ...@@ -97,6 +128,9 @@ export default {
:text="$options.i18n.assignees" :text="$options.i18n.assignees"
:header-text="$options.i18n.assignTo" :header-text="$options.i18n.assignTo"
> >
<template #search>
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items> <template #items>
<gl-dropdown-item <gl-dropdown-item
:is-checked="selectedIsEmpty" :is-checked="selectedIsEmpty"
......
query usersSearch($search: String!) {
users(search: $search) {
nodes {
username
name
webUrl
avatarUrl
id
}
}
}
...@@ -21,6 +21,7 @@ export default { ...@@ -21,6 +21,7 @@ export default {
<template> <template>
<gl-dropdown class="show" :text="text" :header-text="headerText"> <gl-dropdown class="show" :text="text" :header-text="headerText">
<slot name="search"></slot>
<gl-dropdown-form> <gl-dropdown-form>
<slot name="items"></slot> <slot name="items"></slot>
</gl-dropdown-form> </gl-dropdown-form>
......
---
title: Add search assignees to group issue boards
merge_request: 47241
author:
type: added
import { mount } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui'; import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import store from '~/boards/stores'; import store from '~/boards/stores';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
import searchUsers from '~/boards/queries/users_search.query.graphql';
import { participants } from '../mock_data'; import { participants } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('BoardCardAssigneeDropdown', () => { describe('BoardCardAssigneeDropdown', () => {
let wrapper; let wrapper;
let fakeApollo;
let getIssueParticipantsSpy;
let getSearchUsersSpy;
const iid = '111'; const iid = '111';
const activeIssueName = 'test'; const activeIssueName = 'test';
const anotherIssueName = 'hello'; const anotherIssueName = 'hello';
const createComponent = () => { const createComponent = (search = '') => {
wrapper = mount(BoardAssigneeDropdown, { wrapper = mount(BoardAssigneeDropdown, {
data() { data() {
return { return {
search,
selected: store.getters.activeIssue.assignees,
participants,
};
},
store,
provide: {
canUpdate: true,
rootPath: '',
},
});
};
const createComponentWithApollo = (search = '') => {
fakeApollo = createMockApollo([
[getIssueParticipants, getIssueParticipantsSpy],
[searchUsers, getSearchUsersSpy],
]);
wrapper = mount(BoardAssigneeDropdown, {
localVue,
apolloProvider: fakeApollo,
data() {
return {
search,
selected: store.getters.activeIssue.assignees, selected: store.getters.activeIssue.assignees,
participants, participants,
}; };
...@@ -43,7 +79,7 @@ describe('BoardCardAssigneeDropdown', () => { ...@@ -43,7 +79,7 @@ describe('BoardCardAssigneeDropdown', () => {
}; };
const findByText = text => { const findByText = text => {
return wrapper.findAll(GlDropdownItem).wrappers.find(x => x.text().indexOf(text) === 0); return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0);
}; };
beforeEach(() => { beforeEach(() => {
...@@ -58,6 +94,10 @@ describe('BoardCardAssigneeDropdown', () => { ...@@ -58,6 +94,10 @@ describe('BoardCardAssigneeDropdown', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue(); jest.spyOn(store, 'dispatch').mockResolvedValue();
}); });
afterEach(() => {
jest.restoreAllMocks();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -203,27 +243,66 @@ describe('BoardCardAssigneeDropdown', () => { ...@@ -203,27 +243,66 @@ describe('BoardCardAssigneeDropdown', () => {
}, },
); );
describe('Apollo Schema', () => { describe('Apollo', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); getIssueParticipantsSpy = jest.fn().mockResolvedValue({
data: {
issue: {
participants: {
nodes: [
{
username: 'participant',
name: 'participant',
webUrl: '',
avatarUrl: '',
id: '',
},
],
},
},
},
});
getSearchUsersSpy = jest.fn().mockResolvedValue({
data: {
users: {
nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }],
},
},
});
}); });
it('returns the correct query', () => { describe('when search is empty', () => {
expect(wrapper.vm.$options.apollo.participants.query).toEqual(getIssueParticipants); beforeEach(() => {
createComponentWithApollo();
}); });
it('contains the correct variables', () => { it('calls getIssueParticipants', async () => {
const { variables } = wrapper.vm.$options.apollo.participants; jest.runOnlyPendingTimers();
const boundVariable = variables.bind(wrapper.vm); await wrapper.vm.$nextTick();
expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' });
});
});
expect(boundVariable()).toEqual({ id: 'gid://gitlab/Issue/111' }); describe('when search is not empty', () => {
beforeEach(() => {
createComponentWithApollo('search term');
}); });
it('returns the correct data from update', () => { it('calls searchUsers', async () => {
const node = { test: 1 }; jest.runOnlyPendingTimers();
const { update } = wrapper.vm.$options.apollo.participants; await wrapper.vm.$nextTick();
expect(update({ issue: { participants: { nodes: [node] } } })).toEqual([node]); expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' });
});
});
}); });
it('finds GlSearchBoxByType', async () => {
createComponent();
await openDropdown();
expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
}); });
}); });
...@@ -15,4 +15,17 @@ describe('MultiSelectDropdown Component', () => { ...@@ -15,4 +15,17 @@ describe('MultiSelectDropdown Component', () => {
}); });
expect(getByText(wrapper.element, 'Test')).toBeDefined(); expect(getByText(wrapper.element, 'Test')).toBeDefined();
}); });
it('renders search slot', () => {
const wrapper = shallowMount(MultiSelectDropdown, {
propsData: {
text: '',
headerText: '',
},
slots: {
search: '<p>Search</p>',
},
});
expect(getByText(wrapper.element, 'Search')).toBeDefined();
});
}); });
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