Commit 0547fd80 authored by Zack Cuddy's avatar Zack Cuddy Committed by Olena Horal-Koretska

Global Search - SearchableDropdownItem Component

parent 2abc6539
...@@ -8,12 +8,10 @@ import { ...@@ -8,12 +8,10 @@ import {
GlButton, GlButton,
GlSkeletonLoader, GlSkeletonLoader,
GlTooltipDirective, GlTooltipDirective,
GlAvatar,
} from '@gitlab/ui'; } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { ANY_OPTION } from '../constants'; import { ANY_OPTION } from '../constants';
import SearchableDropdownItem from './searchable_dropdown_item.vue';
export default { export default {
i18n: { i18n: {
...@@ -28,7 +26,7 @@ export default { ...@@ -28,7 +26,7 @@ export default {
GlIcon, GlIcon,
GlButton, GlButton,
GlSkeletonLoader, GlSkeletonLoader,
GlAvatar, SearchableDropdownItem,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -79,11 +77,8 @@ export default { ...@@ -79,11 +77,8 @@ export default {
resetDropdown() { resetDropdown() {
this.$emit('change', ANY_OPTION); this.$emit('change', ANY_OPTION);
}, },
truncateNamespace(namespace) { updateDropdown(item) {
return truncateNamespace(namespace); this.$emit('change', item);
},
highlightedItemName(name) {
return highlight(name, this.searchText);
}, },
}, },
ANY_OPTION, ANY_OPTION,
...@@ -97,7 +92,7 @@ export default { ...@@ -97,7 +92,7 @@ export default {
toggle-class="gl-text-truncate" toggle-class="gl-text-truncate"
:header-text="headerText" :header-text="headerText"
:right="true" :right="true"
@show="$emit('search', searchText)" @show="openDropdown"
@shown="$refs.searchBox.focusInput()" @shown="$refs.searchBox.focusInput()"
> >
<template #button-content> <template #button-content>
...@@ -126,46 +121,29 @@ export default { ...@@ -126,46 +121,29 @@ export default {
v-model="searchText" v-model="searchText"
class="gl-m-3" class="gl-m-3"
:debounce="500" :debounce="500"
@input="$emit('search', searchText)" @input="openDropdown"
/> />
<gl-dropdown-item <gl-dropdown-item
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2" 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-check-item="true"
:is-checked="isSelected($options.ANY_OPTION)" :is-checked="isSelected($options.ANY_OPTION)"
:is-check-centered="true" :is-check-centered="true"
@click="resetDropdown" @click="updateDropdown($options.ANY_OPTION)"
> >
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span> <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
</gl-dropdown-item> </gl-dropdown-item>
</div> </div>
<div v-if="!loading"> <div v-if="!loading">
<gl-dropdown-item <searchable-dropdown-item
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
:is-check-item="true" :item="item"
:is-checked="isSelected(item)" :selected-item="selectedItem"
:is-check-centered="true" :search-text="searchText"
@click="$emit('change', item)" :name="name"
> :full-name="fullName"
<div class="gl-display-flex gl-align-items-center"> @change="updateDropdown"
<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>
<div v-if="loading" class="gl-mx-4 gl-mt-3"> <div v-if="loading" class="gl-mx-4 gl-mt-3">
<gl-skeleton-loader :height="100"> <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 { import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlSkeletonLoader,
GlAvatar,
} from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; 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 { 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 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'; import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -36,37 +29,27 @@ describe('Global Search Searchable Dropdown', () => { ...@@ -36,37 +29,27 @@ describe('Global Search Searchable Dropdown', () => {
}, },
}); });
wrapper = extendedWrapper( wrapper = mountFn(SearchableDropdown, {
mountFn(SearchableDropdown, { store,
store, propsData: {
propsData: { ...defaultProps,
...defaultProps, ...props,
...props, },
}, });
}),
);
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findGlDropdown = () => wrapper.find(GlDropdown); const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().find(GlSearchBoxByType); const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text'); const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
const findDropdownItems = () => findGlDropdown().findAll(GlDropdownItem); const findSearchableDropdownItems = () =>
const findDropdownItemTitles = () => wrapper.findAllByTestId('item-title'); findGlDropdown().findAllComponents(SearchableDropdownItem);
const findDropdownItemNamespaces = () => wrapper.findAllByTestId('item-namespace'); const findAnyDropdownItem = () => findGlDropdown().findComponent(GlDropdownItem);
const findDropdownAvatars = () => wrapper.findAllComponents(GlAvatar); const findFirstGroupDropdownItem = () => findSearchableDropdownItems().at(0);
const findAnyDropdownItem = () => findDropdownItems().at(0); const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
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', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
...@@ -109,19 +92,12 @@ describe('Global Search Searchable Dropdown', () => { ...@@ -109,19 +92,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(false); expect(findLoader().exists()).toBe(false);
}); });
it('renders titles correctly including Any', () => { it('renders the Any Dropdown', () => {
const resultsIncludeAny = ['Any'].concat(MOCK_GROUPS.map((n) => n[GROUP_DATA.name])); expect(findAnyDropdownItem().exists()).toBe(true);
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', () => { it('renders SearchableDropdownItem for each item', () => {
const avatars = MOCK_GROUPS.map((n) => n.avatar_url); expect(findSearchableDropdownItems()).toHaveLength(MOCK_GROUPS.length);
expect(findDropdownAvatarUrls()).toStrictEqual(avatars);
}); });
}); });
...@@ -134,18 +110,12 @@ describe('Global Search Searchable Dropdown', () => { ...@@ -134,18 +110,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(true); expect(findLoader().exists()).toBe(true);
}); });
it('renders only Any in dropdown', () => { it('renders the Any Dropdown', () => {
expect(findDropdownItemTitlesText()).toStrictEqual(['Any']); expect(findAnyDropdownItem().exists()).toBe(true);
});
});
describe('when item is selected', () => {
beforeEach(() => {
createComponent({}, { items: MOCK_GROUPS, selectedItem: MOCK_GROUPS[0] });
}); });
it('marks the dropdown as checked', () => { it('does not render SearchableDropdownItem', () => {
expect(findFirstGroupDropdownItem().attributes('ischecked')).toBe('true'); expect(findSearchableDropdownItems()).toHaveLength(0);
}); });
}); });
}); });
...@@ -184,8 +154,8 @@ describe('Global Search Searchable Dropdown', () => { ...@@ -184,8 +154,8 @@ describe('Global Search Searchable Dropdown', () => {
expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]); expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]);
}); });
it('clicking result dropdown item $emits @change with result', () => { it('on SearchableDropdownItem @change, the wrapper $emits change with the item', () => {
findFirstGroupDropdownItem().vm.$emit('click'); findFirstGroupDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
expect(wrapper.emitted('change')[0]).toEqual([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