Commit 61c3777c authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'refactor-assignee-select-use-dropdown-widget' into 'master'

Use dropdown widget in board scope assignee select

See merge request gitlab-org/gitlab!68022
parents 6c9cb080 fd29d841
......@@ -68,7 +68,11 @@ export default {
this.$emit('set-option', option || null);
},
isSelected(option) {
return this.selected && this.selected.title === option.title;
return (
this.selected &&
((option.name && this.selected.name === option.name) ||
(option.title && this.selected.title === option.title))
);
},
showDropdown() {
this.$refs.dropdown.show();
......@@ -79,6 +83,13 @@ export default {
setSearchTerm(search) {
this.$emit('set-search', search);
},
avatarUrl(option) {
return option.avatar_url || option.avatarUrl || null;
},
secondaryText(option) {
// TODO: this has some knowledge of the context where the component is used. We could later rework it.
return option.username || null;
},
},
i18n: {
noMatchingResults: __('No matching results'),
......@@ -121,7 +132,9 @@ export default {
:is-check-item="true"
@click="selectOption(option)"
>
<slot name="preset-item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
......@@ -131,10 +144,14 @@ export default {
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
:avatar-url="avatarUrl(option)"
:secondary-text="secondaryText(option)"
data-testid="unselected-option"
@click="selectOption(option)"
>
<slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
......
<script>
import {
GlButton,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import searchGroupUsers from '~/graphql_shared/queries/group_users_search.query.graphql';
import searchProjectUsers from '~/graphql_shared/queries/users_search.query.graphql';
import { s__ } from '~/locale';
import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { AssigneesPreset, ANY_ASSIGNEE } from '../constants';
export default {
AssigneesPreset,
components: {
UserAvatarImage,
DropdownWidget,
GlButton,
GlDropdown,
GlDropdownForm,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
inject: ['fullPath'],
props: {
......@@ -86,18 +77,12 @@ export default {
},
computed: {
...mapGetters(['isProjectBoard']),
anyAssignee() {
return this.selected.name === ANY_ASSIGNEE.name;
},
isLoading() {
return this.$apollo.queries.searchUsers.loading;
},
isSearchEmpty() {
return this.search === '' && !this.isLoading;
},
selectedIsEmpty() {
return isEmpty(this.selected);
},
noUsersFound() {
return !this.isSearchEmpty && this.users.length === 0;
},
users() {
const filteredUsers = this.searchUsers.filter(
(user) => user.name.includes(this.search) || user.username.includes(this.search),
......@@ -113,6 +98,11 @@ export default {
);
},
},
created() {
if (isEmpty(this.board.assignee)) {
this.selected = ANY_ASSIGNEE;
}
},
methods: {
...mapActions(['setError']),
selectAssignee(user) {
......@@ -129,25 +119,21 @@ export default {
this.isDropdownShowing = false;
}
},
isSelected(user) {
return this.selected?.username === user.username;
},
showDropdown() {
this.$refs.editDropdown.show();
this.$refs.editDropdown.showDropdown();
this.isDropdownShowing = true;
},
setFocus() {
this.$refs.search.focusInput();
},
hideDropdown() {
this.isEditing = false;
},
setSearch(search) {
this.search = search;
},
},
i18n: {
label: s__('BoardScope|Assignee'),
anyAssignee: s__('BoardScope|Any assignee'),
selectAssignee: s__('BoardScope|Select assignee'),
noMatchingResults: s__('BoardScope|No matching results'),
errorSearchingUsers: s__(
'BoardScope|An error occurred while searching for users, please try again.',
),
......@@ -171,7 +157,7 @@ export default {
</gl-button>
</div>
<div v-if="!isEditing" data-testid="selected-assignee">
<div v-if="!selectedIsEmpty" class="gl-display-flex gl-align-items-center">
<div v-if="!anyAssignee" class="gl-display-flex gl-align-items-center">
<user-avatar-image :img-src="selected.avatarUrl || selected.avatar_url" :size="32" />
<div>
<div class="gl-font-weight-bold">{{ selected.name }}</div>
......@@ -181,58 +167,25 @@ export default {
<div v-else class="gl-text-gray-500">{{ $options.i18n.anyAssignee }}</div>
</div>
<gl-dropdown
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:text="$options.i18n.selectAssignee"
lazy
menu-class="gl-w-full!"
class="gl-w-full"
@shown="setFocus"
:select-text="$options.i18n.selectAssignee"
:preset-options="$options.AssigneesPreset"
:options="users"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
@hide="hideDropdown"
@set-option="selectAssignee"
@set-search="setSearch"
>
<template #header>
<gl-search-box-by-type ref="search" v-model.trim="search" class="js-dropdown-input-field" />
</template>
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
v-if="isLoading"
size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
<gl-dropdown-item
v-if="isSearchEmpty"
:is-checked="selectedIsEmpty"
:is-check-centered="true"
@click="selectAssignee(null)"
>
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">
{{ $options.i18n.anyAssignee }}
</span>
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="user in users"
:key="user.id"
:is-checked="isSelected(user)"
:is-check-centered="true"
:is-check-item="true"
:avatar-url="user.avatar_url || user.avatarUrl"
:secondary-text="user.username"
data-testid="unselected-user"
@click="selectAssignee(user)"
>
{{ user.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="noUsersFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
<template #preset-item="{ item }">
{{ item.name }}
</template>
</gl-dropdown-form>
<template #footer>
<slot name="footer"></slot>
<template #item="{ item }">
{{ item.name }}
</template>
</gl-dropdown>
</dropdown-widget>
</div>
</template>
......@@ -79,6 +79,12 @@ export const MilestonesPreset = [
STARTED_MILESTONE,
];
export const ANY_ASSIGNEE = {
id: 'gid://gitlab/User/-1',
name: s__('BoardScope|Any assignee'),
};
export const AssigneesPreset = [ANY_ASSIGNEE];
export const WeightFilterType = {
none: 'None',
};
......
import { GlButton, GlDropdown } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
......@@ -15,6 +15,7 @@ import defaultStore from '~/boards/stores';
import searchGroupUsersQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
import searchProjectUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
......@@ -26,7 +27,7 @@ describe('Assignee select component', () => {
const selectedText = () => wrapper.find('[data-testid="selected-assignee"]').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdown = () => wrapper.findComponent(DropdownWidget);
const usersQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const groupUsersQueryHandlerSuccess = jest.fn().mockResolvedValue(groupMembersResponse);
......@@ -85,33 +86,28 @@ describe('Assignee select component', () => {
expect(usersQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
});
describe('when editing', () => {
it('trigger query and renders dropdown with returned users', async () => {
it('renders selected assignee', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
expect(usersQueryHandlerSuccess).toHaveBeenCalled();
findDropdown().vm.$emit('set-option', mockUser2);
expect(findDropdown().isVisible()).toBe(true);
expect(wrapper.findAll('[data-testid="unselected-user"]')).toHaveLength(3); // 2 users + Any assignee item
await nextTick();
expect(selectedText()).toContain(mockUser2.username);
});
});
it('renders selected assignee', async () => {
describe('when editing', () => {
it('trigger query and renders dropdown with returned users', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
await nextTick();
expect(usersQueryHandlerSuccess).toHaveBeenCalled();
wrapper
.findAll('[data-testid="unselected-user"]')
.at(1)
.vm.$emit('click', new Event('click'));
await waitForPromises();
expect(selectedText()).toContain(mockUser2.username);
expect(findDropdown().isVisible()).toBe(true);
expect(findDropdown().props('options')).toHaveLength(3);
expect(findDropdown().props('presetOptions')).toHaveLength(1);
});
});
......
......@@ -9,7 +9,11 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { boardObj } from 'jest/boards/mock_data';
import { mockProjectMilestonesResponse, mockGroupMilestonesResponse } from 'jest/sidebar/mock_data';
import {
mockProjectMilestonesResponse,
mockGroupMilestonesResponse,
mockMilestone1,
} from 'jest/sidebar/mock_data';
import defaultStore from '~/boards/stores';
import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
......@@ -92,6 +96,15 @@ describe('Milestone select component', () => {
expect(milestonesQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
it('renders selected milestone', async () => {
findEditButton().vm.$emit('click');
await waitForPromises();
findDropdown().vm.$emit('set-option', mockMilestone1);
await waitForPromises();
expect(selectedText()).toContain(mockMilestone1.title);
});
});
describe('when editing', () => {
......
......@@ -5425,9 +5425,6 @@ msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
msgid "BoardScope|No matching results"
msgstr ""
msgid "BoardScope|No milestone"
msgstr ""
......
......@@ -13,7 +13,6 @@ describe('DropdownWidget component', () => {
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(DropdownWidget, {
propsData: {
...props,
options: [
{
id: '1',
......@@ -24,6 +23,7 @@ describe('DropdownWidget component', () => {
title: 'Option 2',
},
],
...props,
},
stubs: {
GlDropdown,
......@@ -76,4 +76,22 @@ describe('DropdownWidget component', () => {
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
});
});
describe('when options are users', () => {
const mockUser = {
id: 1,
name: 'User name',
username: 'username',
avatarUrl: 'foo/bar',
};
beforeEach(() => {
createComponent({ props: { options: [mockUser] } });
});
it('passes user related props to dropdown item', () => {
expect(findDropdownItems().at(0).props('avatarUrl')).toBe(mockUser.avatarUrl);
expect(findDropdownItems().at(0).props('secondaryText')).toBe(mockUser.username);
});
});
});
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