Commit 3157a8cc authored by Zack Cuddy's avatar Zack Cuddy Committed by Enrique Alcántara

Global Search - Refactor Group/Project Filters

parent 631c7c83
......@@ -39,8 +39,8 @@ export default {
<searchable-dropdown
data-testid="group-filter"
:header-text="$options.GROUP_DATA.headerText"
:selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
:items-display-value="$options.GROUP_DATA.itemsDisplayValue"
:name="$options.GROUP_DATA.name"
:full-name="$options.GROUP_DATA.fullName"
:loading="fetchingGroups"
:selected-item="selectedGroup"
:items="groups"
......
......@@ -42,8 +42,8 @@ export default {
<searchable-dropdown
data-testid="project-filter"
:header-text="$options.PROJECT_DATA.headerText"
:selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
:items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
:name="$options.PROJECT_DATA.name"
:full-name="$options.PROJECT_DATA.fullName"
:loading="fetchingProjects"
:selected-item="selectedProject"
:items="projects"
......
......@@ -8,7 +8,10 @@ import {
GlButton,
GlSkeletonLoader,
GlTooltipDirective,
GlAvatar,
} from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { ANY_OPTION } from '../constants';
......@@ -25,6 +28,7 @@ export default {
GlIcon,
GlButton,
GlSkeletonLoader,
GlAvatar,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -35,12 +39,12 @@ export default {
required: false,
default: "__('Filter')",
},
selectedDisplayValue: {
name: {
type: String,
required: false,
default: 'name',
},
itemsDisplayValue: {
fullName: {
type: String,
required: false,
default: 'name',
......@@ -75,6 +79,12 @@ export default {
resetDropdown() {
this.$emit('change', ANY_OPTION);
},
truncateNamespace(namespace) {
return truncateNamespace(namespace);
},
highlightedItemName(name) {
return highlight(name, this.searchText);
},
},
ANY_OPTION,
};
......@@ -83,15 +93,16 @@ export default {
<template>
<gl-dropdown
class="gl-w-full"
menu-class="gl-w-full!"
menu-class="global-search-dropdown-menu"
toggle-class="gl-text-truncate"
:header-text="headerText"
:right="true"
@show="$emit('search', searchText)"
@shown="$refs.searchBox.focusInput()"
>
<template #button-content>
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedItem[selectedDisplayValue] }}
{{ selectedItem[name] }}
</span>
<gl-loading-icon v-if="loading" inline class="gl-mr-3" />
<gl-button
......@@ -121,9 +132,10 @@ export default {
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
:is-check-item="true"
:is-checked="isSelected($options.ANY_OPTION)"
:is-check-centered="true"
@click="resetDropdown"
>
{{ $options.ANY_OPTION.name }}
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
</gl-dropdown-item>
</div>
<div v-if="!loading">
......@@ -132,9 +144,27 @@ export default {
:key="item.id"
:is-check-item="true"
:is-checked="isSelected(item)"
:is-check-centered="true"
@click="$emit('change', item)"
>
{{ item[itemsDisplayValue] }}
<div class="gl-display-flex gl-align-items-center">
<gl-avatar
:src="item.avatar_url"
:entity-id="item.id"
:entity-name="item[name]"
shape="rect"
:size="32"
/>
<div class="gl-display-flex gl-flex-direction-column">
<!-- eslint-disable-next-line vue/no-v-html -->
<span data-testid="item-title" v-html="highlightedItemName(item[name])">{{
item[name]
}}</span>
<span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{
truncateNamespace(item[fullName])
}}</span>
</div>
</div>
</gl-dropdown-item>
</div>
<div v-if="loading" class="gl-mx-4 gl-mt-3">
......
......@@ -9,13 +9,13 @@ export const ANY_OPTION = Object.freeze({
export const GROUP_DATA = {
headerText: __('Filter results by group'),
queryParam: 'group_id',
selectedDisplayValue: 'name',
itemsDisplayValue: 'full_name',
name: 'name',
fullName: 'full_name',
};
export const PROJECT_DATA = {
headerText: __('Filter results by project'),
queryParam: 'project_id',
selectedDisplayValue: 'name_with_namespace',
itemsDisplayValue: 'name_with_namespace',
name: 'name',
fullName: 'name_with_namespace',
};
......@@ -295,6 +295,16 @@ input[type='checkbox']:hover {
@include str-truncated(10em);
}
.global-search-dropdown-menu {
width: 100% !important;
max-width: 400px;
@include media-breakpoint-up(md) {
// This is larger than the container so width: 100% doesn't work.
width: 400px !important;
}
}
// Disable webkit input icons, link to solution: https://stackoverflow.com/questions/9421551/how-do-i-remove-all-default-webkit-search-field-styling
/* stylelint-disable property-no-vendor-prefix */
input[type='search']::-webkit-search-decoration,
......
......@@ -32,7 +32,7 @@ RSpec.describe 'User searches for code' do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
end
......
......@@ -90,7 +90,7 @@ RSpec.describe 'User searches for issues', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
search_for_issue(issue1.title)
......
......@@ -55,7 +55,7 @@ RSpec.describe 'User searches for merge requests', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
search_for_mr(merge_request1.title)
......
......@@ -35,7 +35,7 @@ RSpec.describe 'User searches for milestones', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
fill_in('dashboard_search', with: milestone1.title)
......
......@@ -23,7 +23,7 @@ RSpec.describe 'User searches for wiki pages', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
fill_in('dashboard_search', with: search_term)
......
......@@ -33,10 +33,10 @@ RSpec.describe 'User uses search filters', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(group_project.full_name)
click_on(group_project.name)
end
expect(find('[data-testid="project-filter"]')).to have_content(group_project.full_name)
expect(find('[data-testid="project-filter"]')).to have_content(group_project.name)
end
context 'when the group filter is set' do
......@@ -65,10 +65,10 @@ RSpec.describe 'User uses search filters', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
click_on(project.full_name)
click_on(project.name)
end
expect(find('[data-testid="project-filter"]')).to have_content(project.full_name)
expect(find('[data-testid="project-filter"]')).to have_content(project.name)
end
context 'when the project filter is set' do
......
......@@ -2,47 +2,49 @@ export const MOCK_QUERY = {
scope: 'issues',
state: 'all',
confidential: null,
group_id: 'test_1',
group_id: 1,
};
export const MOCK_GROUP = {
name: 'test group',
full_name: 'full name test group',
id: 'test_1',
full_name: 'full name / test group',
id: 1,
};
export const MOCK_GROUPS = [
{
avatar_url: null,
name: 'test group',
full_name: 'full name test group',
id: 'test_1',
full_name: 'full name / test group',
id: 1,
},
{
avatar_url: 'https://avatar.com',
name: 'test group 2',
full_name: 'full name test group 2',
id: 'test_2',
full_name: 'full name / test group 2',
id: 2,
},
];
export const MOCK_PROJECT = {
name: 'test project',
namespace: MOCK_GROUP,
nameWithNamespace: 'test group test project',
id: 'test_1',
nameWithNamespace: 'test group / test project',
id: 1,
};
export const MOCK_PROJECTS = [
{
name: 'test project',
namespace: MOCK_GROUP,
name_with_namespace: 'test group test project',
id: 'test_1',
name_with_namespace: 'test group / test project',
id: 1,
},
{
name: 'test project 2',
namespace: MOCK_GROUP,
name_with_namespace: 'test group test project 2',
id: 'test_2',
name_with_namespace: 'test group / test project 2',
id: 2,
},
];
......
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlSkeletonLoader,
GlAvatar,
} from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import { truncateNamespace } from '~/lib/utils/text_utility';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('Global Search Searchable Dropdown', () => {
let wrapper;
const defaultProps = {
headerText: GROUP_DATA.headerText,
selectedDisplayValue: GROUP_DATA.selectedDisplayValue,
itemsDisplayValue: GROUP_DATA.itemsDisplayValue,
name: GROUP_DATA.name,
fullName: GROUP_DATA.fullName,
loading: false,
selectedItem: ANY_OPTION,
items: [],
......@@ -28,14 +36,15 @@ describe('Global Search Searchable Dropdown', () => {
},
});
wrapper = mountFn(SearchableDropdown, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
});
wrapper = extendedWrapper(
mountFn(SearchableDropdown, {
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
......@@ -47,11 +56,18 @@ describe('Global Search Searchable Dropdown', () => {
const findGlDropdownSearch = () => findGlDropdown().find(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
const findDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
const findDropdownItemTitles = () => wrapper.findAllByTestId('item-title');
const findDropdownItemNamespaces = () => wrapper.findAllByTestId('item-namespace');
const findDropdownAvatars = () => wrapper.findAllComponents(GlAvatar);
const findAnyDropdownItem = () => findDropdownItems().at(0);
const findFirstGroupDropdownItem = () => findDropdownItems().at(1);
const findLoader = () => wrapper.find(GlSkeletonLoader);
const findDropdownItemTitlesText = () => findDropdownItemTitles().wrappers.map((w) => w.text());
const findDropdownItemNamespacesText = () =>
findDropdownItemNamespaces().wrappers.map((w) => w.text());
const findDropdownAvatarUrls = () => findDropdownAvatars().wrappers.map((w) => w.props('src'));
describe('template', () => {
beforeEach(() => {
createComponent();
......@@ -93,9 +109,19 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders an instance for each namespace', () => {
const resultsIncludeAny = ['Any'].concat(MOCK_GROUPS.map((n) => n.full_name));
expect(findDropdownItemsText()).toStrictEqual(resultsIncludeAny);
it('renders titles correctly including Any', () => {
const resultsIncludeAny = ['Any'].concat(MOCK_GROUPS.map((n) => n[GROUP_DATA.name]));
expect(findDropdownItemTitlesText()).toStrictEqual(resultsIncludeAny);
});
it('renders namespaces truncated correctly', () => {
const namespaces = MOCK_GROUPS.map((n) => truncateNamespace(n[GROUP_DATA.fullName]));
expect(findDropdownItemNamespacesText()).toStrictEqual(namespaces);
});
it('renders GlAvatar for each item', () => {
const avatars = MOCK_GROUPS.map((n) => n.avatar_url);
expect(findDropdownAvatarUrls()).toStrictEqual(avatars);
});
});
......@@ -109,7 +135,7 @@ describe('Global Search Searchable Dropdown', () => {
});
it('renders only Any in dropdown', () => {
expect(findDropdownItemsText()).toStrictEqual(['Any']);
expect(findDropdownItemTitlesText()).toStrictEqual(['Any']);
});
});
......@@ -140,8 +166,8 @@ describe('Global Search Searchable Dropdown', () => {
createComponent({}, { selectedItem: MOCK_GROUP }, mount);
});
it('sets dropdown text to the selectedItem selectedDisplayValue', () => {
expect(findDropdownText().text()).toBe(MOCK_GROUP[GROUP_DATA.selectedDisplayValue]);
it('sets dropdown text to the selectedItem name', () => {
expect(findDropdownText().text()).toBe(MOCK_GROUP[GROUP_DATA.name]);
});
});
});
......
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