Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/header_search')
-rw-r--r--app/assets/javascripts/header_search/components/app.vue15
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue74
-rw-r--r--app/assets/javascripts/header_search/constants.js8
-rw-r--r--app/assets/javascripts/header_search/index.js4
-rw-r--r--app/assets/javascripts/header_search/store/actions.js14
-rw-r--r--app/assets/javascripts/header_search/store/getters.js32
-rw-r--r--app/assets/javascripts/header_search/store/index.js10
-rw-r--r--app/assets/javascripts/header_search/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/header_search/store/mutations.js12
-rw-r--r--app/assets/javascripts/header_search/store/state.js5
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;