summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/search
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/search')
-rw-r--r--app/assets/javascripts/search/highlight_blob_search_result.js4
-rw-r--r--app/assets/javascripts/search/index.js15
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue2
-rw-r--r--app/assets/javascripts/search/sort/components/app.vue103
-rw-r--r--app/assets/javascripts/search/sort/constants.js19
-rw-r--r--app/assets/javascripts/search/sort/index.js27
-rw-r--r--app/assets/javascripts/search/store/actions.js2
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue73
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue2
-rw-r--r--app/assets/javascripts/search/topbar/index.js31
13 files changed, 257 insertions, 35 deletions
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
index 3c3ac3582d0..c553d5b14a0 100644
--- a/app/assets/javascripts/search/highlight_blob_search_result.js
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -1,7 +1,7 @@
-export default () => {
+export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
- const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase();
+ const searchTerm = search.toLowerCase();
const blobs = contentBody.querySelectorAll('.blob-result');
blobs.forEach((blob) => {
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index d2bb1ccfc44..10c41315972 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,14 +1,25 @@
+import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { queryToObject } from '~/lib/utils/url_utility';
+import Project from '~/pages/projects/project';
+import refreshCounts from '~/pages/search/show/refresh_counts';
+import { initSidebar } from './sidebar';
+import { initSearchSort } from './sort';
import createStore from './store';
import { initTopbar } from './topbar';
-import { initSidebar } from './sidebar';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
// Our query treats + as %20. This replaces the query + symbols with %20.
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
- const store = createStore({ query: queryToObject(sanitizedSearch) });
+ const query = queryToObject(sanitizedSearch);
+
+ const store = createStore({ query });
initTopbar(store);
initSidebar(store);
+ initSearchSort(store);
+
+ setHighlightClass(query.search); // Code Highlighting
+ refreshCounts(); // Other Scope Tab Counts
+ Project.initRefSwitcher(); // Code Search Branch Picker
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index e233d18b716..4640259314b 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,8 +1,8 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton, GlLink } from '@gitlab/ui';
-import StatusFilter from './status_filter.vue';
+import { mapActions, mapState } from 'vuex';
import ConfidentialityFilter from './confidentiality_filter.vue';
+import StatusFilter from './status_filter.vue';
export default {
name: 'GlobalSearchSidebar',
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index b27c4e26fb5..73911b9d319 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { sprintf, s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/search/sort/components/app.vue b/app/assets/javascripts/search/sort/components/app.vue
new file mode 100644
index 00000000000..e4eba655e39
--- /dev/null
+++ b/app/assets/javascripts/search/sort/components/app.vue
@@ -0,0 +1,103 @@
+<script>
+import {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { SORT_DIRECTION_UI } from '../constants';
+
+export default {
+ name: 'GlobalSearchSort',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ searchSortOptions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ selectedSortOption: {
+ get() {
+ const { sort } = this.query;
+
+ if (!sort) {
+ return this.searchSortOptions[0];
+ }
+
+ const sortOption = this.searchSortOptions.find((option) => {
+ if (!option.sortable) {
+ return option.sortParam === sort;
+ }
+
+ return Object.values(option.sortParam).indexOf(sort) !== -1;
+ });
+
+ // Handle invalid sort param
+ return sortOption || this.searchSortOptions[0];
+ },
+ set(value) {
+ this.setQuery({ key: 'sort', value });
+ this.applyQuery();
+ },
+ },
+ sortDirectionData() {
+ if (!this.selectedSortOption.sortable) {
+ return SORT_DIRECTION_UI.disabled;
+ }
+
+ return this.query?.sort?.includes('asc') ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ handleSortChange(option) {
+ if (!option.sortable) {
+ this.selectedSortOption = option.sortParam;
+ } else {
+ // Default new sort options to desc
+ this.selectedSortOption = option.sortParam.desc;
+ }
+ },
+ handleSortDirectionChange() {
+ this.selectedSortOption =
+ this.sortDirectionData.direction === 'desc'
+ ? this.selectedSortOption.sortParam.asc
+ : this.selectedSortOption.sortParam.desc;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button-group>
+ <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
+ <gl-dropdown-item
+ v-for="sortOption in searchSortOptions"
+ :key="sortOption.title"
+ is-check-item
+ :is-checked="sortOption.title === selectedSortOption.title"
+ @click="handleSortChange(sortOption)"
+ >{{ sortOption.title }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ :disabled="!selectedSortOption.sortable"
+ :title="sortDirectionData.tooltip"
+ :icon="sortDirectionData.icon"
+ @click="handleSortDirectionChange"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/search/sort/constants.js b/app/assets/javascripts/search/sort/constants.js
new file mode 100644
index 00000000000..575fba5873b
--- /dev/null
+++ b/app/assets/javascripts/search/sort/constants.js
@@ -0,0 +1,19 @@
+import { __ } from '~/locale';
+
+export const SORT_DIRECTION_UI = {
+ disabled: {
+ direction: null,
+ tooltip: '',
+ icon: 'sort-highest',
+ },
+ desc: {
+ direction: 'desc',
+ tooltip: __('Sort direction: Descending'),
+ icon: 'sort-highest',
+ },
+ asc: {
+ direction: 'asc',
+ tooltip: __('Sort direction: Ascending'),
+ icon: 'sort-lowest',
+ },
+};
diff --git a/app/assets/javascripts/search/sort/index.js b/app/assets/javascripts/search/sort/index.js
new file mode 100644
index 00000000000..84bb5175b1d
--- /dev/null
+++ b/app/assets/javascripts/search/sort/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import GlobalSearchSort from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSearchSort = (store) => {
+ const el = document.getElementById('js-search-sort');
+
+ if (!el) return false;
+
+ let { searchSortOptions } = el.dataset;
+
+ searchSortOptions = JSON.parse(searchSortOptions);
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(GlobalSearchSort, {
+ props: {
+ searchSortOptions,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index bdfe966d990..0af679644f3 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import createFlash from '~/flash';
-import { __ } from '~/locale';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => {
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
new file mode 100644
index 00000000000..987735ed811
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import GroupFilter from './group_filter.vue';
+import ProjectFilter from './project_filter.vue';
+
+export default {
+ name: 'GlobalSearchTopbar',
+ components: {
+ GlForm,
+ GlSearchBoxByType,
+ GroupFilter,
+ ProjectFilter,
+ GlButton,
+ },
+ props: {
+ groupInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ search: {
+ get() {
+ return this.query ? this.query.search : '';
+ },
+ set(value) {
+ this.setQuery({ key: 'search', value });
+ },
+ },
+ showFilters() {
+ return !this.query.snippets || this.query.snippets === 'false';
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ },
+};
+</script>
+
+<template>
+ <gl-form class="search-page-form" @submit.prevent="applyQuery">
+ <section class="gl-lg-display-flex gl-align-items-flex-end">
+ <div class="gl-flex-fill-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
+ <label>{{ __('What are you searching for?') }}</label>
+ <gl-search-box-by-type
+ id="dashboard_search"
+ v-model="search"
+ name="search"
+ :placeholder="__(`Search for projects, issues, etc.`)"
+ />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Group') }}</label>
+ <group-filter :initial-data="groupInitialData" />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Project') }}</label>
+ <project-filter :initial-data="projectInitialData" />
+ </div>
+ <gl-button class="btn-search gl-lg-ml-2" variant="success" type="submit">{{
+ __('Search')
+ }}</gl-button>
+ </section>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
index fce9ec17d23..2acab4e805d 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -1,9 +1,9 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
+import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'GroupFilter',
@@ -37,6 +37,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="group-filter"
:header-text="$options.GROUP_DATA.headerText"
:selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
:items-display-value="$options.GROUP_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
index 3f1f3848ac7..b2dd79fcfa3 100644
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -1,8 +1,8 @@
<script>
import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'ProjectFilter',
@@ -27,7 +27,7 @@ export default {
handleProjectChange(project) {
// This determines if we need to update the group filter or not
const queryParams = {
- ...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }),
+ ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id,
};
@@ -40,6 +40,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="project-filter"
:header-text="$options.PROJECT_DATA.headerText"
:selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
:items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
index 14577fd7d7a..5fb7217db74 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
@@ -101,7 +101,7 @@ export default {
@keydown.enter.stop="resetDropdown"
@click.stop="resetDropdown"
>
- <gl-icon name="clear" class="gl-text-gray-200! gl-hover-text-blue-800!" />
+ <gl-icon name="clear" />
</gl-button>
<gl-icon name="chevron-down" />
</template>
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index f0308109b32..87316e10e8d 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -1,44 +1,31 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import GroupFilter from './components/group_filter.vue';
-import ProjectFilter from './components/project_filter.vue';
+import GlobalSearchTopbar from './components/app.vue';
Vue.use(Translate);
-const mountSearchableDropdown = (store, { id, component }) => {
- const el = document.getElementById(id);
+export const initTopbar = (store) => {
+ const el = document.getElementById('js-search-topbar');
if (!el) {
return false;
}
- let { initialData } = el.dataset;
+ let { groupInitialData, projectInitialData } = el.dataset;
- initialData = JSON.parse(initialData);
+ groupInitialData = JSON.parse(groupInitialData);
+ projectInitialData = JSON.parse(projectInitialData);
return new Vue({
el,
store,
render(createElement) {
- return createElement(component, {
+ return createElement(GlobalSearchTopbar, {
props: {
- initialData,
+ groupInitialData,
+ projectInitialData,
},
});
},
});
};
-
-const searchableDropdowns = [
- {
- id: 'js-search-group-dropdown',
- component: GroupFilter,
- },
- {
- id: 'js-search-project-dropdown',
- component: ProjectFilter,
- },
-];
-
-export const initTopbar = (store) =>
- searchableDropdowns.map((dropdown) => mountSearchableDropdown(store, dropdown));