Commit 81f692ab authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '239384_01-refactor-searchable-dropdown' into 'master'

Global Search - SearchableDropdownItem Component

See merge request gitlab-org/gitlab!63567
parents 863aa3cb 0547fd80
......@@ -8,12 +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';
import SearchableDropdownItem from './searchable_dropdown_item.vue';
export default {
i18n: {
......@@ -28,7 +26,7 @@ export default {
GlIcon,
GlButton,
GlSkeletonLoader,
GlAvatar,
SearchableDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -79,11 +77,8 @@ export default {
resetDropdown() {
this.$emit('change', ANY_OPTION);
},
truncateNamespace(namespace) {
return truncateNamespace(namespace);
},
highlightedItemName(name) {
return highlight(name, this.searchText);
updateDropdown(item) {
this.$emit('change', item);
},
},
ANY_OPTION,
......@@ -97,7 +92,7 @@ export default {
toggle-class="gl-text-truncate"
:header-text="headerText"
:right="true"
@show="$emit('search', searchText)"
@show="openDropdown"
@shown="$refs.searchBox.focusInput()"
>
<template #button-content>
......@@ -126,46 +121,29 @@ export default {
v-model="searchText"
class="gl-m-3"
:debounce="500"
@input="$emit('search', searchText)"
@input="openDropdown"
/>
<gl-dropdown-item
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"
@click="updateDropdown($options.ANY_OPTION)"
>
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
</gl-dropdown-item>
</div>
<div v-if="!loading">
<gl-dropdown-item
<searchable-dropdown-item
v-for="item in items"
:key="item.id"
:is-check-item="true"
:is-checked="isSelected(item)"
:is-check-centered="true"
@click="$emit('change', item)"
>
<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>
:item="item"
:selected-item="selectedItem"
:search-text="searchText"
:name="name"
:full-name="fullName"
@change="updateDropdown"
/>
</div>
<div v-if="loading" class="gl-mx-4 gl-mt-3">
<gl-skeleton-loader :height="100">
......
<script>
import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
export default {
name: 'SearchableDropdownItem',
components: {
GlDropdownItem,
GlAvatar,
},
props: {
item: {
type: Object,
required: true,
},
selectedItem: {
type: Object,
required: true,
},
searchText: {
type: String,
required: false,
default: '',
},
name: {
type: String,
required: true,
},
fullName: {
type: String,
required: true,
},
},
computed: {
isSelected() {
return this.item.id === this.selectedItem.id;
},
truncatedNamespace() {
return truncateNamespace(this.item[this.fullName]);
},
highlightedItemName() {
return highlight(this.item[this.name], this.searchText);
},
},
};
</script>
<template>
<gl-dropdown-item
:is-check-item="true"
:is-checked="isSelected"
:is-check-centered="true"
@click="$emit('change', item)"
>
<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] }}</span>
<span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{
truncatedNamespace
}}</span>
</div>
</div>
</gl-dropdown-item>
</template>
import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS } from 'jest/search/mock_data';
import { truncateNamespace } from '~/lib/utils/text_utility';
import SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue';
import { GROUP_DATA } from '~/search/topbar/constants';
describe('Global Search Searchable Dropdown Item', () => {
let wrapper;
const defaultProps = {
item: MOCK_GROUPS[0],
selectedItem: MOCK_GROUPS[0],
name: GROUP_DATA.name,
fullName: GROUP_DATA.fullName,
};
const createComponent = (props) => {
wrapper = shallowMountExtended(SearchableDropdownItem, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
const findDropdownTitle = () => wrapper.findByTestId('item-title');
const findDropdownSubtitle = () => wrapper.findByTestId('item-namespace');
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders GlDropdownItem', () => {
expect(findGlDropdownItem().exists()).toBe(true);
});
it('renders GlAvatar', () => {
expect(findGlAvatar().exists()).toBe(true);
});
it('renders Dropdown Title correctly', () => {
const titleEl = findDropdownTitle();
expect(titleEl.exists()).toBe(true);
expect(titleEl.text()).toBe(MOCK_GROUPS[0][GROUP_DATA.name]);
});
it('renders Dropdown Subtitle correctly', () => {
const subtitleEl = findDropdownSubtitle();
expect(subtitleEl.exists()).toBe(true);
expect(subtitleEl.text()).toBe(truncateNamespace(MOCK_GROUPS[0][GROUP_DATA.fullName]));
});
});
describe('when item === selectedItem', () => {
beforeEach(() => {
createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[0] });
});
it('marks the dropdown as checked', () => {
expect(findGlDropdownItem().attributes('ischecked')).toBe('true');
});
});
describe('when item !== selectedItem', () => {
beforeEach(() => {
createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[1] });
});
it('marks the dropdown as not checked', () => {
expect(findGlDropdownItem().attributes('ischecked')).toBeUndefined();
});
});
});
describe('actions', () => {
beforeEach(() => {
createComponent();
});
it('clicking the dropdown item $emits change with the item', () => {
findGlDropdownItem().vm.$emit('click');
expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
});
});
});
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlSkeletonLoader,
GlAvatar,
} from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } 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 SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue';
import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
Vue.use(Vuex);
......@@ -36,37 +29,27 @@ describe('Global Search Searchable Dropdown', () => {
},
});
wrapper = extendedWrapper(
mountFn(SearchableDropdown, {
store,
propsData: {
...defaultProps,
...props,
},
}),
);
wrapper = mountFn(SearchableDropdown, {
store,
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlDropdown = () => wrapper.find(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().find(GlSearchBoxByType);
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
const findDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
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'));
const findSearchableDropdownItems = () =>
findGlDropdown().findAllComponents(SearchableDropdownItem);
const findAnyDropdownItem = () => findGlDropdown().findComponent(GlDropdownItem);
const findFirstGroupDropdownItem = () => findSearchableDropdownItems().at(0);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
describe('template', () => {
beforeEach(() => {
......@@ -109,19 +92,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(false);
});
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 the Any Dropdown', () => {
expect(findAnyDropdownItem().exists()).toBe(true);
});
it('renders GlAvatar for each item', () => {
const avatars = MOCK_GROUPS.map((n) => n.avatar_url);
expect(findDropdownAvatarUrls()).toStrictEqual(avatars);
it('renders SearchableDropdownItem for each item', () => {
expect(findSearchableDropdownItems()).toHaveLength(MOCK_GROUPS.length);
});
});
......@@ -134,18 +110,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(true);
});
it('renders only Any in dropdown', () => {
expect(findDropdownItemTitlesText()).toStrictEqual(['Any']);
});
});
describe('when item is selected', () => {
beforeEach(() => {
createComponent({}, { items: MOCK_GROUPS, selectedItem: MOCK_GROUPS[0] });
it('renders the Any Dropdown', () => {
expect(findAnyDropdownItem().exists()).toBe(true);
});
it('marks the dropdown as checked', () => {
expect(findFirstGroupDropdownItem().attributes('ischecked')).toBe('true');
it('does not render SearchableDropdownItem', () => {
expect(findSearchableDropdownItems()).toHaveLength(0);
});
});
});
......@@ -184,8 +154,8 @@ describe('Global Search Searchable Dropdown', () => {
expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]);
});
it('clicking result dropdown item $emits @change with result', () => {
findFirstGroupDropdownItem().vm.$emit('click');
it('on SearchableDropdownItem @change, the wrapper $emits change with the item', () => {
findFirstGroupDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
});
......
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