diff options
Diffstat (limited to 'app/assets/javascripts/header_search/components')
3 files changed, 90 insertions, 15 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 36fc48a2ba8..4406cacdf3f 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -11,6 +11,7 @@ import { SEARCH_BOX_INDEX, SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, + SEARCH_SHORTCUTS_MIN_CHARACTERS, } from '../constants'; import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from './header_search_default_items.vue'; @@ -50,7 +51,7 @@ export default { }, computed: { ...mapState(['search', 'loading']), - ...mapGetters(['searchQuery', 'searchOptions']), + ...mapGetters(['searchQuery', 'searchOptions', 'autocompleteGroupedSearchOptions']), searchText: { get() { return this.search; @@ -66,14 +67,20 @@ export default { return this.currentFocusedOption?.html_id; }, isLoggedIn() { - return gon?.current_username; + return Boolean(gon?.current_username); }, showSearchDropdown() { - return this.showDropdown && this.isLoggedIn; + const hasResultsUnderMinCharacters = + this.searchText?.length === 1 ? this?.autocompleteGroupedSearchOptions?.length > 0 : true; + + return this.showDropdown && this.isLoggedIn && hasResultsUnderMinCharacters; }, showDefaultItems() { return !this.searchText; }, + showShortcuts() { + return this.searchText && this.searchText?.length >= SEARCH_SHORTCUTS_MIN_CHARACTERS; + }, defaultIndex() { if (this.showDefaultItems) { return SEARCH_BOX_INDEX; @@ -105,6 +112,9 @@ export default { count: this.searchOptions.length, }); }, + headerSearchActivityDescriptor() { + return this.showDropdown ? 'is-active' : 'is-not-active'; + }, }, methods: { ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), @@ -136,13 +146,15 @@ export default { v-outside="closeDropdown" role="search" :aria-label="$options.i18n.searchGitlab" - class="header-search gl-relative" + class="header-search gl-relative gl-rounded-base" + :class="headerSearchActivityDescriptor" > <gl-search-box-by-type id="search" v-model="searchText" role="searchbox" class="gl-z-index-1" + data-qa-selector="search_term_field" autocomplete="off" :placeholder="$options.i18n.searchGitlab" :aria-activedescendant="currentFocusedId" @@ -182,7 +194,10 @@ export default { :current-focused-option="currentFocusedOption" /> <template v-else> - <header-search-scoped-items :current-focused-option="currentFocusedOption" /> + <header-search-scoped-items + v-if="showShortcuts" + :current-focused-option="currentFocusedOption" + /> <header-search-autocomplete-items :current-focused-option="currentFocusedOption" /> </template> </div> diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue index c0e2c18bece..025c48f355d 100644 --- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue @@ -11,7 +11,18 @@ import { import { mapState, mapGetters } from 'vuex'; import { s__ } from '~/locale'; import highlight from '~/lib/utils/highlight'; -import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants'; +import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/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', @@ -39,7 +50,7 @@ export default { }, }, computed: { - ...mapState(['search', 'loading', 'autocompleteError']), + ...mapState(['search', 'loading', 'autocompleteError', 'searchContext']), ...mapGetters(['autocompleteGroupedSearchOptions']), }, watch: { @@ -52,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); }, @@ -65,15 +83,45 @@ 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, }; </script> <template> <div> <template v-if="!loading"> - <div v-for="option in autocompleteGroupedSearchOptions" :key="option.category"> - <gl-dropdown-divider /> + <div v-for="(option, index) in autocompleteGroupedSearchOptions" :key="option.category"> + <gl-dropdown-divider v-if="index > 0" /> <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header> <gl-dropdown-item v-for="data in option.data" @@ -90,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="square" + :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> diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue index 3aebee71509..34d1bd71399 100644 --- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdownItem } from '@gitlab/ui'; +import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; import { __, sprintf } from '~/locale'; @@ -7,6 +7,7 @@ export default { name: 'HeaderSearchScopedItems', components: { GlDropdownItem, + GlDropdownDivider, }, props: { currentFocusedOption: { @@ -17,7 +18,7 @@ export default { }, computed: { ...mapState(['search']), - ...mapGetters(['scopedSearchOptions']), + ...mapGetters(['scopedSearchOptions', 'autocompleteGroupedSearchOptions']), }, methods: { isOptionFocused(option) { @@ -53,5 +54,6 @@ export default { <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> </span> </gl-dropdown-item> + <gl-dropdown-divider v-if="autocompleteGroupedSearchOptions.length > 0" /> </div> </template> |