Commit fc70e982 authored by Coung Ngo's avatar Coung Ngo Committed by Vitaly Slobodin

Improve Vue dropdown filter results on New Project page

parent 59e8d9a6
......@@ -4,6 +4,7 @@ import {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
......@@ -20,6 +21,7 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
......@@ -57,8 +59,20 @@ export default {
userNamespace() {
return this.currentUser.namespace || {};
},
hasGroupMatches() {
return this.userGroups.length;
},
hasNamespaceMatches() {
return this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase());
},
hasNoMatches() {
return !this.hasGroupMatches && !this.hasNamespaceMatches;
},
},
methods: {
focusInput() {
this.$refs.search.focusInput();
},
handleClick({ id, fullPath }) {
this.selectedNamespace = {
id: getIdFromGraphQLId(id),
......@@ -78,18 +92,24 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput"
>
<gl-search-box-by-type v-model.trim="search" />
<gl-search-box-by-type ref="search" v-model.trim="search" />
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }}
</gl-dropdown-item>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
<gl-dropdown-text v-if="hasNoMatches">{{ __('No matches found') }}</gl-dropdown-text>
</template>
</gl-dropdown>
......
import { GlButton, GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -34,9 +40,6 @@ describe('NewProjectUrlSelect component', () => {
const localVue = createLocalVue();
localVue.use(VueApollo);
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data })]];
const apolloProvider = createMockApollo(requestHandlers);
const provide = {
namespaceFullPath: 'h5bp',
namespaceId: '28',
......@@ -44,11 +47,25 @@ describe('NewProjectUrlSelect component', () => {
trackLabel: 'blank_project',
};
const mountComponent = ({ mountFn = shallowMount } = {}) =>
mountFn(NewProjectUrlSelect, { localVue, apolloProvider, provide });
const mountComponent = ({ search = '', queryResponse = data, mountFn = shallowMount } = {}) => {
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]];
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(NewProjectUrlSelect, {
localVue,
apolloProvider,
provide,
data() {
return {
search,
};
},
});
};
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
afterEach(() => {
......@@ -74,6 +91,19 @@ describe('NewProjectUrlSelect component', () => {
expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId);
});
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
const spy = jest.spyOn(findInput().vm, 'focusInput');
findDropdown().vm.$emit('shown');
expect(spy).toHaveBeenCalledTimes(1);
});
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
......@@ -89,6 +119,27 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
});
it('renders `No matches found` when there are no matching dropdown items', async () => {
const queryResponse = {
currentUser: {
groups: {
nodes: [],
},
namespace: {
id: 'gid://gitlab/Namespace/1',
fullPath: 'root',
},
},
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.find('li').text()).toBe('No matches found');
});
it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent();
......
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