diff options
Diffstat (limited to 'app/assets/javascripts/search')
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)); |