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>
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 IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
import searchUsers from '~/boards/queries/users_search.query.graphql';
export default {
noSearchDelay: 0,
searchDelay: 250,
i18n: {
unassigned: __('Unassigned'),
assignee: __('Assignee'),
......@@ -22,23 +31,42 @@ export default {
GlDropdownDivider,
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
},
data() {
return {
search: '',
participants: [],
selected: this.$store.getters.activeIssue.assignees,
};
},
apollo: {
participants: {
query: getIssueParticipants,
query() {
return this.isSearchEmpty ? getIssueParticipants : searchUsers;
},
variables() {
if (this.isSearchEmpty) {
return {
id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
};
}
return {
search: this.search,
};
},
update(data) {
if (this.isSearchEmpty) {
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 {
selectedUserNames() {
return this.selected.map(({ username }) => username);
},
isSearchEmpty() {
return this.search === '';
},
},
methods: {
...mapActions(['setAssignees']),
......@@ -97,6 +128,9 @@ export default {
:text="$options.i18n.assignees"
:header-text="$options.i18n.assignTo"
>
<template #search>
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items>
<gl-dropdown-item
:is-checked="selectedIsEmpty"
......
query usersSearch($search: String!) {
users(search: $search) {
nodes {
username
name
webUrl
avatarUrl
id
}
}
}
......@@ -21,6 +21,7 @@ export default {
<template>
<gl-dropdown class="show" :text="text" :header-text="headerText">
<slot name="search"></slot>
<gl-dropdown-form>
<slot name="items"></slot>
</gl-dropdown-form>
......
---
title: Add search assignees to group issue boards
merge_request: 47241
author:
type: added
import { mount } from '@vue/test-utils';
import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
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 IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import store from '~/boards/stores';
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';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('BoardCardAssigneeDropdown', () => {
let wrapper;
let fakeApollo;
let getIssueParticipantsSpy;
let getSearchUsersSpy;
const iid = '111';
const activeIssueName = 'test';
const anotherIssueName = 'hello';
const createComponent = () => {
const createComponent = (search = '') => {
wrapper = mount(BoardAssigneeDropdown, {
data() {
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,
participants,
};
......@@ -43,7 +79,7 @@ describe('BoardCardAssigneeDropdown', () => {
};
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(() => {
......@@ -58,6 +94,10 @@ describe('BoardCardAssigneeDropdown', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
afterEach(() => {
jest.restoreAllMocks();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -203,27 +243,66 @@ describe('BoardCardAssigneeDropdown', () => {
},
);
describe('Apollo Schema', () => {
describe('Apollo', () => {
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', () => {
expect(wrapper.vm.$options.apollo.participants.query).toEqual(getIssueParticipants);
describe('when search is empty', () => {
beforeEach(() => {
createComponentWithApollo();
});
it('contains the correct variables', () => {
const { variables } = wrapper.vm.$options.apollo.participants;
const boundVariable = variables.bind(wrapper.vm);
it('calls getIssueParticipants', async () => {
jest.runOnlyPendingTimers();
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', () => {
const node = { test: 1 };
const { update } = wrapper.vm.$options.apollo.participants;
it('calls searchUsers', async () => {
jest.runOnlyPendingTimers();
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', () => {
});
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