diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-21 18:11:18 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-21 18:11:18 +0000 |
commit | 79f759cc144c7020942f09762154c6758ee1d275 (patch) | |
tree | 5d159ea257006957a34e854949662ad67a8c3532 /app/assets/javascripts/header_search | |
parent | 9ef26edc4ae561ba072dee3900fef4408e227b48 (diff) | |
download | gitlab-ce-79f759cc144c7020942f09762154c6758ee1d275.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/header_search')
10 files changed, 171 insertions, 7 deletions
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 580c27f6c61..c6590fd8eb3 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -3,6 +3,7 @@ import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; +import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from './header_search_default_items.vue'; import HeaderSearchScopedItems from './header_search_scoped_items.vue'; @@ -16,6 +17,7 @@ export default { GlSearchBoxByType, HeaderSearchDefaultItems, HeaderSearchScopedItems, + HeaderSearchAutocompleteItems, }, data() { return { @@ -41,7 +43,7 @@ export default { }, }, methods: { - ...mapActions(['setSearch']), + ...mapActions(['setSearch', 'fetchAutocompleteOptions']), openDropdown() { this.showDropdown = true; }, @@ -51,6 +53,13 @@ export default { submitSearch() { return visitUrl(this.searchQuery); }, + getAutocompleteOptions(searchTerm) { + if (!searchTerm) { + return; + } + + this.fetchAutocompleteOptions(); + }, }, }; </script> @@ -64,18 +73,20 @@ export default { :placeholder="$options.i18n.searchPlaceholder" @focus="openDropdown" @click="openDropdown" + @input="getAutocompleteOptions" @keydown.enter="submitSearch" @keydown.esc="closeDropdown" /> <div v-if="showSearchDropdown" data-testid="header-search-dropdown-menu" - class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-left-0 gl-z-index-1 gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0" + class="header-search-dropdown-menu gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0" > <div class="header-search-dropdown-content gl-overflow-y-auto gl-py-2"> <header-search-default-items v-if="showDefaultItems" /> <template v-else> <header-search-scoped-items /> + <header-search-autocomplete-items /> </template> </div> </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 new file mode 100644 index 00000000000..9bea2b280f7 --- /dev/null +++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue @@ -0,0 +1,74 @@ +<script> +import { + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, + GlAvatar, + GlLoadingIcon, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import { mapState, mapGetters } from 'vuex'; +import highlight from '~/lib/utils/highlight'; +import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants'; + +export default { + name: 'HeaderSearchAutocompleteItems', + components: { + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, + GlAvatar, + GlLoadingIcon, + }, + directives: { + SafeHtml, + }, + computed: { + ...mapState(['search', 'loading']), + ...mapGetters(['autocompleteGroupedSearchOptions']), + }, + methods: { + highlightedName(val) { + return highlight(val, this.search); + }, + avatarSize(data) { + if (data.category === GROUPS_CATEGORY || data.category === PROJECTS_CATEGORY) { + return LARGE_AVATAR_PX; + } + + return SMALL_AVATAR_PX; + }, + }, +}; +</script> + +<template> + <div> + <template v-if="!loading"> + <div v-for="option in autocompleteGroupedSearchOptions" :key="option.category"> + <gl-dropdown-divider /> + <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="(data, index) in option.data" + :id="`autocomplete-${option.category}-${index}`" + :key="index" + tabindex="-1" + :href="data.url" + > + <div class="gl-display-flex gl-align-items-center"> + <gl-avatar + v-if="data.avatar_url !== undefined" + :src="data.avatar_url" + :entity-id="data.id" + :entity-name="data.label" + :size="avatarSize(data)" + shape="square" + /> + <span v-safe-html="highlightedName(data.label)"></span> + </div> + </gl-dropdown-item> + </div> + </template> + <gl-loading-icon v-else size="lg" class="my-4" /> + </div> +</template> diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js index fffed7bcbdb..2fadb1bd1ee 100644 --- a/app/assets/javascripts/header_search/constants.js +++ b/app/assets/javascripts/header_search/constants.js @@ -15,3 +15,11 @@ export const MSG_IN_ALL_GITLAB = __('in all GitLab'); export const MSG_IN_GROUP = __('in group'); export const MSG_IN_PROJECT = __('in project'); + +export const GROUPS_CATEGORY = 'Groups'; + +export const PROJECTS_CATEGORY = 'Projects'; + +export const LARGE_AVATAR_PX = 32; + +export const SMALL_AVATAR_PX = 16; diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js index 2d37ee137fc..d7e21f55ea5 100644 --- a/app/assets/javascripts/header_search/index.js +++ b/app/assets/javascripts/header_search/index.js @@ -12,13 +12,13 @@ export const initHeaderSearchApp = () => { return false; } - const { searchPath, issuesPath, mrPath } = el.dataset; + const { searchPath, issuesPath, mrPath, autocompletePath } = el.dataset; let { searchContext } = el.dataset; searchContext = JSON.parse(searchContext); return new Vue({ el, - store: createStore({ searchPath, issuesPath, mrPath, searchContext }), + store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }), render(createElement) { return createElement(HeaderSearchApp); }, diff --git a/app/assets/javascripts/header_search/store/actions.js b/app/assets/javascripts/header_search/store/actions.js index 841aee04029..2c3b1bd4c0f 100644 --- a/app/assets/javascripts/header_search/store/actions.js +++ b/app/assets/javascripts/header_search/store/actions.js @@ -1,5 +1,19 @@ +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; import * as types from './mutation_types'; +export const fetchAutocompleteOptions = ({ commit, getters }) => { + commit(types.REQUEST_AUTOCOMPLETE); + return axios + .get(getters.autocompleteQuery) + .then(({ data }) => commit(types.RECEIVE_AUTOCOMPLETE_SUCCESS, data)) + .catch(() => { + commit(types.RECEIVE_AUTOCOMPLETE_ERROR); + createFlash({ message: __('There was an error fetching search autocomplete suggestions') }); + }); +}; + export const setSearch = ({ commit }, value) => { commit(types.SET_SEARCH, value); }; diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js index d1e1fc8ad73..3f4e231ca55 100644 --- a/app/assets/javascripts/header_search/store/getters.js +++ b/app/assets/javascripts/header_search/store/getters.js @@ -23,6 +23,16 @@ export const searchQuery = (state) => { return `${state.searchPath}?${objectToQuery(query)}`; }; +export const autocompleteQuery = (state) => { + const query = { + term: state.search, + project_id: state.searchContext.project?.id, + project_ref: state.searchContext.ref, + }; + + return `${state.autocompletePath}?${objectToQuery(query)}`; +}; + export const scopedIssuesPath = (state) => { return ( state.searchContext.project_metadata?.issues_path || @@ -133,3 +143,25 @@ export const scopedSearchOptions = (state, getters) => { return options; }; + +export const autocompleteGroupedSearchOptions = (state) => { + const groupedOptions = {}; + const results = []; + + state.autocompleteOptions.forEach((option) => { + const category = groupedOptions[option.category]; + + if (category) { + category.data.push(option); + } else { + groupedOptions[option.category] = { + category: option.category, + data: [option], + }; + + results.push(groupedOptions[option.category]); + } + }); + + return results; +}; diff --git a/app/assets/javascripts/header_search/store/index.js b/app/assets/javascripts/header_search/store/index.js index 8b74f8662a5..06cca4be8a7 100644 --- a/app/assets/javascripts/header_search/store/index.js +++ b/app/assets/javascripts/header_search/store/index.js @@ -7,11 +7,17 @@ import createState from './state'; Vue.use(Vuex); -export const getStoreConfig = ({ searchPath, issuesPath, mrPath, searchContext }) => ({ +export const getStoreConfig = ({ + searchPath, + issuesPath, + mrPath, + autocompletePath, + searchContext, +}) => ({ actions, getters, mutations, - state: createState({ searchPath, issuesPath, mrPath, searchContext }), + state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }), }); const createStore = (config) => new Vuex.Store(getStoreConfig(config)); diff --git a/app/assets/javascripts/header_search/store/mutation_types.js b/app/assets/javascripts/header_search/store/mutation_types.js index 0bc94ae055f..a2358621ce6 100644 --- a/app/assets/javascripts/header_search/store/mutation_types.js +++ b/app/assets/javascripts/header_search/store/mutation_types.js @@ -1 +1,5 @@ +export const REQUEST_AUTOCOMPLETE = 'REQUEST_AUTOCOMPLETE'; +export const RECEIVE_AUTOCOMPLETE_SUCCESS = 'RECEIVE_AUTOCOMPLETE_SUCCESS'; +export const RECEIVE_AUTOCOMPLETE_ERROR = 'RECEIVE_AUTOCOMPLETE_ERROR'; + export const SET_SEARCH = 'SET_SEARCH'; diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js index 5b1438929d4..175b5406540 100644 --- a/app/assets/javascripts/header_search/store/mutations.js +++ b/app/assets/javascripts/header_search/store/mutations.js @@ -1,6 +1,18 @@ import * as types from './mutation_types'; export default { + [types.REQUEST_AUTOCOMPLETE](state) { + state.loading = true; + state.autocompleteOptions = []; + }, + [types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) { + state.loading = false; + state.autocompleteOptions = data; + }, + [types.RECEIVE_AUTOCOMPLETE_ERROR](state) { + state.loading = false; + state.autocompleteOptions = []; + }, [types.SET_SEARCH](state, value) { state.search = value; }, diff --git a/app/assets/javascripts/header_search/store/state.js b/app/assets/javascripts/header_search/store/state.js index fb2c83dbbe3..3d4073f0583 100644 --- a/app/assets/javascripts/header_search/store/state.js +++ b/app/assets/javascripts/header_search/store/state.js @@ -1,8 +1,11 @@ -const createState = ({ searchPath, issuesPath, mrPath, searchContext }) => ({ +const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }) => ({ searchPath, issuesPath, mrPath, + autocompletePath, searchContext, search: '', + autocompleteOptions: [], + loading: false, }); export default createState; |