Commit 85a3bea5 authored by Tomas Bulva's avatar Tomas Bulva Committed by Andrew Fontaine

Search Autocomplete: Review how icons are generated

This change will add subtitle to the autocomplete dropdown result.
I adds namespace path as subtitle to the results. It also fixes
the icon alias generated from the name of the project/group
instead of namespace of project or group.

Changelog: fixed
parent f659545e
......@@ -12,7 +12,17 @@ import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
import { truncateNamespace } from '~/lib/utils/text_utility';
import {
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
MERGE_REQUEST_CATEGORY,
ISSUES_CATEGORY,
RECENT_EPICS_CATEGORY,
LARGE_AVATAR_PX,
SMALL_AVATAR_PX,
} from '../constants';
export default {
name: 'HeaderSearchAutocompleteItems',
......@@ -40,7 +50,7 @@ export default {
},
},
computed: {
...mapState(['search', 'loading', 'autocompleteError']),
...mapState(['search', 'loading', 'autocompleteError', 'searchContext']),
...mapGetters(['autocompleteGroupedSearchOptions']),
},
watch: {
......@@ -53,6 +63,13 @@ export default {
},
},
methods: {
truncateNamespace(string) {
if (string.split(' / ').length > 2) {
return truncateNamespace(string);
}
return string;
},
highlightedName(val) {
return highlight(val, this.search);
},
......@@ -66,6 +83,35 @@ export default {
isOptionFocused(data) {
return this.currentFocusedOption?.html_id === data.html_id;
},
isProjectsCategory(data) {
return data.category === PROJECTS_CATEGORY;
},
getEntityId(data) {
switch (data.category) {
case GROUPS_CATEGORY:
case RECENT_EPICS_CATEGORY:
return data.group_id || data.id || this.searchContext?.group?.id;
case PROJECTS_CATEGORY:
case ISSUES_CATEGORY:
case MERGE_REQUEST_CATEGORY:
return data.project_id || data.id || this.searchContext?.project?.id;
default:
return data.id;
}
},
getEntitytName(data) {
switch (data.category) {
case GROUPS_CATEGORY:
case RECENT_EPICS_CATEGORY:
return data.group_name || data.value || data.label || this.searchContext?.group?.name;
case PROJECTS_CATEGORY:
case ISSUES_CATEGORY:
case MERGE_REQUEST_CATEGORY:
return data.project_name || data.value || data.label || this.searchContext?.project?.name;
default:
return data.label;
}
},
},
AVATAR_SHAPE_OPTION_RECT,
};
......@@ -92,12 +138,22 @@ export default {
<gl-avatar
v-if="data.avatar_url !== undefined"
:src="data.avatar_url"
:entity-id="data.id"
:entity-name="data.label"
:entity-id="getEntityId(data)"
:entity-name="getEntitytName(data)"
:size="avatarSize(data)"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
/>
<span v-safe-html="highlightedName(data.label)"></span>
<span class="gl-display-flex gl-flex-direction-column">
<span
v-safe-html="highlightedName(data.value || data.label)"
class="gl-text-gray-900"
></span>
<span
v-if="data.value"
v-safe-html="truncateNamespace(data.label)"
class="gl-font-sm gl-text-gray-500"
></span>
</span>
</div>
</gl-dropdown-item>
</div>
......
......@@ -20,6 +20,12 @@ export const GROUPS_CATEGORY = 'Groups';
export const PROJECTS_CATEGORY = 'Projects';
export const ISSUES_CATEGORY = 'Recent issues';
export const MERGE_REQUEST_CATEGORY = 'Recent merge requests';
export const RECENT_EPICS_CATEGORY = 'Recent epics';
export const LARGE_AVATAR_PX = 32;
export const SMALL_AVATAR_PX = 16;
......
......@@ -260,6 +260,7 @@ module SearchHelper
{
category: "Groups",
id: group.id,
value: "#{search_result_sanitize(group.name)}",
label: "#{search_result_sanitize(group.full_name)}",
url: group_path(group),
avatar_url: group.avatar_url || ''
......@@ -311,7 +312,9 @@ module SearchHelper
id: mr.id,
label: search_result_sanitize(mr.title),
url: merge_request_path(mr),
avatar_url: mr.project.avatar_url || ''
avatar_url: mr.project.avatar_url || '',
project_id: mr.target_project_id,
project_name: mr.target_project.name
}
end
end
......@@ -325,7 +328,9 @@ module SearchHelper
id: i.id,
label: search_result_sanitize(i.title),
url: issue_path(i),
avatar_url: i.project.avatar_url || ''
avatar_url: i.project.avatar_url || '',
project_id: i.project_id,
project_name: i.project.name
}
end
end
......
......@@ -154,7 +154,9 @@ module EE
id: e.id,
label: search_result_sanitize(e.title),
url: epic_path(e),
avatar_url: e.group.avatar_url || ''
avatar_url: e.group.avatar_url || '',
group_id: e.group_id,
group_name: e.group&.name
}
end
end
......
......@@ -8,6 +8,9 @@ import {
LARGE_AVATAR_PX,
PROJECTS_CATEGORY,
SMALL_AVATAR_PX,
ISSUES_CATEGORY,
MERGE_REQUEST_CATEGORY,
RECENT_EPICS_CATEGORY,
} from '~/header_search/constants';
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
......@@ -50,7 +53,12 @@ describe('HeaderSearchAutocompleteItems', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
const findDropdownItemTitles = () =>
findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
const findDropdownItemSubTitles = () =>
findDropdownItems()
.wrappers.filter((w) => w.findAll('span').length > 2)
.map((w) => w.findAll('span').at(2).text());
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
......@@ -95,10 +103,17 @@ describe('HeaderSearchAutocompleteItems', () => {
});
it('renders titles correctly', () => {
const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.label);
const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
});
it('renders sub-titles correctly', () => {
const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
(o) => o.label,
);
expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
});
it('renders links correctly', () => {
const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
......@@ -106,15 +121,30 @@ describe('HeaderSearchAutocompleteItems', () => {
});
describe.each`
item | showAvatar | avatarSize
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)}
${{ data: [{ category: 'Settings' }] }} | ${false} | ${false}
`('GlAvatar', ({ item, showAvatar, avatarSize }) => {
item | showAvatar | avatarSize | searchContext | entityId | entityName
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
`('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
beforeEach(() => {
createComponent({}, { autocompleteGroupedSearchOptions: () => [item] });
createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
});
it(`should${showAvatar ? '' : ' not'} render`, () => {
......@@ -124,6 +154,16 @@ describe('HeaderSearchAutocompleteItems', () => {
it(`should set avatarSize to ${avatarSize}`, () => {
expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
});
it(`should set avatar entityId to ${entityId}`, () => {
expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
});
it(`should set avatar entityName to ${entityName}`, () => {
expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
entityName,
);
});
});
});
});
......
......@@ -96,19 +96,22 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
{
category: 'Projects',
id: 1,
label: 'MockProject1',
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
id: 1,
label: 'MockGroup1',
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
id: 2,
label: 'MockProject2',
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
},
{
......@@ -123,21 +126,24 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
label: 'MockProject1',
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
label: 'MockGroup1',
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
label: 'MockProject2',
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
},
{
......@@ -157,7 +163,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-0',
id: 1,
label: 'MockProject1',
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
},
{
......@@ -165,7 +172,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-2',
id: 2,
label: 'MockProject2',
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
},
],
......@@ -178,7 +186,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Groups-1',
id: 1,
label: 'MockGroup1',
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
},
],
......@@ -202,21 +211,24 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
label: 'MockProject1',
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
label: 'MockProject2',
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
label: 'MockGroup1',
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
},
{
......
......@@ -71,7 +71,7 @@ RSpec.describe SearchHelper do
create(:group).add_owner(user)
result = search_autocomplete_opts("gro").first
expect(result.keys).to match_array(%i[category id label url avatar_url])
expect(result.keys).to match_array(%i[category id value label url avatar_url])
end
it 'includes the users recently viewed issues', :aggregate_failures do
......
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