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 { ...@@ -4,6 +4,7 @@ import {
GlButtonGroup, GlButtonGroup,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
...@@ -20,6 +21,7 @@ export default { ...@@ -20,6 +21,7 @@ export default {
GlButtonGroup, GlButtonGroup,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
...@@ -57,8 +59,20 @@ export default { ...@@ -57,8 +59,20 @@ export default {
userNamespace() { userNamespace() {
return this.currentUser.namespace || {}; 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: { methods: {
focusInput() {
this.$refs.search.focusInput();
},
handleClick({ id, fullPath }) { handleClick({ id, fullPath }) {
this.selectedNamespace = { this.selectedNamespace = {
id: getIdFromGraphQLId(id), id: getIdFromGraphQLId(id),
...@@ -78,19 +92,25 @@ export default { ...@@ -78,19 +92,25 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!" toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
data-qa-selector="select_namespace_dropdown" data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })" @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" /> <gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else> <template v-else>
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)"> <gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }} {{ group.fullPath }}
</gl-dropdown-item> </gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)"> <gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }} {{ userNamespace.fullPath }}
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
<gl-dropdown-text v-if="hasNoMatches">{{ __('No matches found') }}</gl-dropdown-text>
</template>
</gl-dropdown> </gl-dropdown>
<input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" /> <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
......
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 { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -34,9 +40,6 @@ describe('NewProjectUrlSelect component', () => { ...@@ -34,9 +40,6 @@ describe('NewProjectUrlSelect component', () => {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data })]];
const apolloProvider = createMockApollo(requestHandlers);
const provide = { const provide = {
namespaceFullPath: 'h5bp', namespaceFullPath: 'h5bp',
namespaceId: '28', namespaceId: '28',
...@@ -44,11 +47,25 @@ describe('NewProjectUrlSelect component', () => { ...@@ -44,11 +47,25 @@ describe('NewProjectUrlSelect component', () => {
trackLabel: 'blank_project', trackLabel: 'blank_project',
}; };
const mountComponent = ({ mountFn = shallowMount } = {}) => const mountComponent = ({ search = '', queryResponse = data, mountFn = shallowMount } = {}) => {
mountFn(NewProjectUrlSelect, { localVue, apolloProvider, provide }); 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 findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input'); const findHiddenInput = () => wrapper.find('input');
afterEach(() => { afterEach(() => {
...@@ -74,6 +91,19 @@ describe('NewProjectUrlSelect component', () => { ...@@ -74,6 +91,19 @@ describe('NewProjectUrlSelect component', () => {
expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId); 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 () => { it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount }); wrapper = mountComponent({ mountFn: mount });
...@@ -89,6 +119,27 @@ describe('NewProjectUrlSelect component', () => { ...@@ -89,6 +119,27 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath); 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 () => { it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent(); 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