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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-08 21:14:31 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-08 21:14:31 +0300
commitbb0d99269b1bee11939e6a3ddfcefed8c6fd4874 (patch)
tree58f5d3f64251e1847a1bfb77d76ead2abb16c899 /app/assets/javascripts/search/sidebar
parentf1ce233e6ab6535afef76f10528e104672426710 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/search/sidebar')
-rw-r--r--app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue19
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue16
-rw-r--r--app/assets/javascripts/search/sidebar/components/group_filter.vue87
-rw-r--r--app/assets/javascripts/search/sidebar/components/project_filter.vue94
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue3
-rw-r--r--app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue222
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js22
-rw-r--r--app/assets/javascripts/search/sidebar/index.js20
8 files changed, 471 insertions, 12 deletions
diff --git a/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue b/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue
new file mode 100644
index 00000000000..cb017b6898b
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue
@@ -0,0 +1,19 @@
+<script>
+import GroupFilter from './group_filter.vue';
+import ProjectFilter from './project_filter.vue';
+
+export default {
+ name: 'AllScopesStartFilters',
+ components: {
+ GroupFilter,
+ ProjectFilter,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-px-5 gl-pt-6">
+ <group-filter class="gl-mb-5" />
+ <project-filter class="gl-mb-5" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 307be0b0aa0..bbee0e441cc 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -25,6 +25,7 @@ import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
import WikiBlobsFilters from './wiki_blobs_filters.vue';
+import AllScopesStartFilters from './all_scopes_start_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -40,8 +41,16 @@ export default {
DomElementListener,
CommitsFilters,
MilestonesFilters,
+ AllScopesStartFilters,
},
mixins: [glFeatureFlagsMixin()],
+ props: {
+ headerText: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
computed: {
...mapState(['searchType']),
...mapGetters(['currentScope']),
@@ -82,6 +91,13 @@ export default {
<section>
<dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" />
<sidebar-portal>
+ <all-scopes-start-filters />
+ <div
+ v-if="headerText"
+ class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
+ >
+ {{ headerText }}
+ </div>
<scope-sidebar-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
diff --git a/app/assets/javascripts/search/sidebar/components/group_filter.vue b/app/assets/javascripts/search/sidebar/components/group_filter.vue
new file mode 100644
index 00000000000..20231cdda6a
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/group_filter.vue
@@ -0,0 +1,87 @@
+<script>
+import { isEmpty } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
+
+export default {
+ name: 'GroupFilter',
+ i18n: {
+ groupFieldLabel: s__('GlobalSearch|Group'),
+ },
+ components: {
+ SearchableDropdown,
+ },
+ data() {
+ return {
+ search: '',
+ labelId: 'group-filter-dropdown-id',
+ };
+ },
+ computed: {
+ ...mapState(['query', 'groups', 'fetchingGroups', 'groupInitialJson', 'useSidebarNavigation']),
+ ...mapGetters(['frequentGroups', 'currentScope']),
+ selectedGroup() {
+ return isEmpty(this.groupInitialJson) ? ANY_OPTION : this.groupInitialJson;
+ },
+ },
+ watch: {
+ search() {
+ this.debounceSearch();
+ },
+ },
+ created() {
+ // This tracks groups searched via the top nav search bar
+ if (this.query.nav_source === 'navbar' && this.groupInitialJson?.id) {
+ this.setFrequentGroup(this.groupInitialJson);
+ }
+ },
+ methods: {
+ ...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
+ firstLoad() {
+ this.loadFrequentGroups();
+ this.fetchGroups();
+ },
+ handleGroupChange(group) {
+ // If group.id is null we are clearing the filter and don't need to store that in LS.
+ if (group.id) {
+ this.setFrequentGroup(group);
+ }
+
+ visitUrl(
+ setUrlParams({
+ [GROUP_DATA.queryParam]: group.id,
+ [PROJECT_DATA.queryParam]: null,
+ nav_source: null,
+ scope: this.currentScope,
+ }),
+ );
+ },
+ },
+ GROUP_DATA,
+};
+</script>
+
+<template>
+ <div>
+ <h5 :id="labelId" class="gl-mt-0 gl-mb-5 gl-font-sm">
+ {{ $options.i18n.groupFieldLabel }}
+ </h5>
+ <searchable-dropdown
+ data-testid="group-filter"
+ :header-text="$options.GROUP_DATA.headerText"
+ :name="$options.GROUP_DATA.name"
+ :loading="fetchingGroups"
+ :selected-item="selectedGroup"
+ :items="groups"
+ :frequent-items="frequentGroups"
+ :search-handler="fetchGroups"
+ :label-id="labelId"
+ @first-open="firstLoad"
+ @change="handleGroupChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/project_filter.vue b/app/assets/javascripts/search/sidebar/components/project_filter.vue
new file mode 100644
index 00000000000..76983644e60
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/project_filter.vue
@@ -0,0 +1,94 @@
+<script>
+import { isEmpty } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '~/search/sidebar/constants';
+import SearchableDropdown from './searchable_dropdown.vue';
+
+export default {
+ name: 'ProjectFilter',
+ i18n: {
+ projectFieldLabel: s__('GlobalSearch|Project'),
+ },
+ components: {
+ SearchableDropdown,
+ },
+ data() {
+ return {
+ search: '',
+ labelId: 'projects-filter-dropdown-id',
+ };
+ },
+ computed: {
+ ...mapState([
+ 'query',
+ 'projects',
+ 'fetchingProjects',
+ 'projectInitialJson',
+ 'useSidebarNavigation',
+ ]),
+ ...mapGetters(['frequentProjects', 'currentScope']),
+ selectedProject() {
+ return isEmpty(this.projectInitialJson) ? ANY_OPTION : this.projectInitialJson;
+ },
+ },
+ watch: {
+ search() {
+ this.debounceSearch();
+ },
+ },
+ created() {
+ // This tracks projects searched via the top nav search bar
+ if (this.query.nav_source === 'navbar' && this.projectInitialJson?.id) {
+ this.setFrequentProject(this.projectInitialJson);
+ }
+ },
+ methods: {
+ ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
+ firstLoad() {
+ this.loadFrequentProjects();
+ this.fetchProjects();
+ },
+ handleProjectChange(project) {
+ // If project.id is null we are clearing the filter and don't need to store that in LS.
+ if (project.id) {
+ this.setFrequentProject(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_DATA.queryParam]: project.id,
+ nav_source: null,
+ scope: this.currentScope,
+ };
+
+ visitUrl(setUrlParams(queryParams));
+ },
+ },
+ PROJECT_DATA,
+};
+</script>
+
+<template>
+ <div>
+ <h5 :id="labelId" class="gl-mt-0 gl-mb-5 gl-font-sm">
+ {{ $options.i18n.projectFieldLabel }}
+ </h5>
+ <searchable-dropdown
+ data-testid="project-filter"
+ :header-text="$options.PROJECT_DATA.headerText"
+ :name="$options.PROJECT_DATA.name"
+ :loading="fetchingProjects"
+ :selected-item="selectedProject"
+ :items="projects"
+ :frequent-items="frequentProjects"
+ :search-handler="fetchProjects"
+ :label-id="labelId"
+ @first-open="firstLoad"
+ @change="handleProjectChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
index f30618ad9b7..874803a720d 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
@@ -2,6 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import eventHub from '~/super_sidebar/event_hub';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants';
@@ -18,6 +19,8 @@ export default {
...mapGetters(['navigationItems']),
},
created() {
+ eventHub.$emit('toggle-menu-header', false);
+
if (this.urlQuery?.search) {
this.fetchSidebarCount();
}
diff --git a/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
new file mode 100644
index 00000000000..c1f0bfc59f3
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
@@ -0,0 +1,222 @@
+<script>
+import { GlCollapsibleListbox, GlAvatar } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import highlight from '~/lib/utils/highlight';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { __, s__, n__ } from '~/locale';
+import { ANY_OPTION } from '../constants';
+
+export default {
+ name: 'SearchableDropdown',
+ components: {
+ GlAvatar,
+ GlCollapsibleListbox,
+ },
+ directives: {
+ SafeHtml,
+ },
+ i18n: {
+ frequentlySearched: __('Frequently searched'),
+ availableGroups: s__('GlobalSearch|All available groups'),
+ nothingFound: s__('GlobalSearch|Nothing found…'),
+ reset: s__('GlobalSearch|Reset'),
+ itemsFound(count) {
+ return n__('%d item found', '%d items found', count);
+ },
+ },
+ props: {
+ headerText: {
+ type: String,
+ required: false,
+ default: "__('Filter')",
+ },
+ name: {
+ type: String,
+ required: false,
+ default: 'name',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedItem: {
+ type: Object,
+ required: true,
+ },
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ frequentItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ searchHandler: {
+ type: Function,
+ required: true,
+ },
+ labelId: {
+ type: String,
+ required: false,
+ default: 'labelId',
+ },
+ },
+ data() {
+ return {
+ searchText: '',
+ hasBeenOpened: false,
+ showableItems: [],
+ searchInProgress: false,
+ };
+ },
+ watch: {
+ items() {
+ if (this.searchText === '') {
+ this.showableItems = this.defaultItems();
+ }
+ },
+ },
+ created() {
+ this.showableItems = this.defaultItems();
+ },
+ methods: {
+ defaultItems() {
+ const frequentItems = this.convertItemsFormat([...this.frequentItems]);
+ const nonFrequentItems = this.convertItemsFormat([
+ ...this.uniqueItems(this.items, this.frequentItems),
+ ]);
+
+ return [
+ {
+ text: '',
+ options: [
+ {
+ value: ANY_OPTION.name,
+ text: ANY_OPTION.name,
+ ...ANY_OPTION,
+ },
+ ],
+ },
+ {
+ text: this.$options.i18n.frequentlySearched,
+ options: frequentItems,
+ },
+ {
+ text: this.$options.i18n.availableGroups,
+ options: nonFrequentItems,
+ },
+ ].filter((group) => {
+ return group.options.length > 0;
+ });
+ },
+ search(search) {
+ this.searchText = search;
+ this.searchInProgress = true;
+
+ if (search !== '') {
+ debounce(() => {
+ this.searchHandler(this.searchText);
+ this.showableItems = this.convertItemsFormat([...this.items]);
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS)();
+
+ return;
+ }
+
+ this.showableItems = this.defaultItems();
+ },
+ openDropdown() {
+ if (!this.hasBeenOpened) {
+ this.hasBeenOpened = true;
+ this.$emit('first-open');
+ }
+ },
+ resetDropdown() {
+ this.$emit('change', ANY_OPTION);
+ },
+ convertItemsFormat(items) {
+ return items.map((item) => ({ value: item.id, text: item.full_name, ...item }));
+ },
+ truncatedNamespace(item) {
+ const itemDuplicat = { ...item };
+ const namespaceWithFallback = itemDuplicat.name_with_namespace
+ ? itemDuplicat.name_with_namespace
+ : itemDuplicat.full_name;
+
+ return truncateNamespace(namespaceWithFallback);
+ },
+ highlightedItemName(item) {
+ return highlight(item.name, item.searchText);
+ },
+ onSelectGroup(selected) {
+ if (selected === ANY_OPTION.name) {
+ this.$emit('change', ANY_OPTION);
+ return;
+ }
+
+ const flatShowableItems = [...this.frequentItems, ...this.items];
+ const newSelectedItem = flatShowableItems.find((item) => item.id === selected);
+ this.$emit('change', newSelectedItem);
+ },
+ uniqueItems(allItems, frequentItems) {
+ return allItems.filter((item) => {
+ const itemNotIdentical = frequentItems.some((fitem) => fitem.id === item.id);
+ return Boolean(!itemNotIdentical);
+ });
+ },
+ },
+ ANY_OPTION,
+ AVATAR_SHAPE_OPTION_RECT,
+};
+</script>
+
+<template>
+ <gl-collapsible-listbox
+ :items="showableItems"
+ :header-text="headerText"
+ :toggle-text="selectedItem[name]"
+ :no-results-text="$options.i18n.nothingFound"
+ :selected="selectedItem.id"
+ :searching="loading"
+ :reset-button-label="$options.i18n.reset"
+ :toggle-aria-labelled-by="labelId"
+ searchable
+ block
+ @shown="openDropdown"
+ @search="search"
+ @select="onSelectGroup"
+ @reset="resetDropdown"
+ >
+ <template #search-summary-sr-only>
+ {{ $options.i18n.itemsFound(showableItems.length) }}
+ </template>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-avatar
+ :src="item.avatar_url"
+ :entity-id="item.id"
+ :entity-name="item.name"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="32"
+ class="gl-mr-3"
+ aria-hidden="true"
+ />
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span
+ v-safe-html="highlightedItemName(item)"
+ class="gl-font-weight-bold gl-white-space-nowrap"
+ data-testid="item-title"
+ ></span>
+ <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">
+ {{ truncatedNamespace(item) }}</span
+ >
+ </div>
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 95906c840d7..e3b0db670b5 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const SCOPE_ISSUES = 'issues';
export const SCOPE_MERGE_REQUESTS = 'merge_requests';
export const SCOPE_BLOB = 'blobs';
@@ -26,3 +28,23 @@ export const TRACKING_LABEL_RESET = 'Reset Filters';
export const SEARCH_TYPE_BASIC = 'basic';
export const SEARCH_TYPE_ADVANCED = 'advanced';
export const SEARCH_TYPE_ZOEKT = 'zoekt';
+
+export const ANY_OPTION = {
+ id: null,
+ name: __('Any'),
+ name_with_namespace: __('Any'),
+};
+
+export const GROUP_DATA = {
+ headerText: __('Filter results by group'),
+ queryParam: 'group_id',
+ name: 'name',
+ fullName: 'full_name',
+};
+
+export const PROJECT_DATA = {
+ headerText: __('Filter results by project'),
+ queryParam: 'project_id',
+ name: 'name',
+ fullName: 'name_with_namespace',
+};
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 3a699355dc9..9a7472ccad3 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -4,27 +4,23 @@ import GlobalSearchSidebar from './components/app.vue';
Vue.use(Translate);
-export const sidebarInitState = () => {
- const el = document.getElementById('js-search-sidebar');
- if (!el) return {};
-
- const { navigationJson, searchType } = el.dataset;
-
- const navigationJsonParsed = JSON.parse(navigationJson);
-
- return { navigationJsonParsed, searchType };
-};
-
export const initSidebar = (store) => {
const el = document.getElementById('js-search-sidebar');
+ const hederEl = document.getElementById('super-sidebar-context-header');
+ const headerText = hederEl.innerText;
if (!el) return false;
return new Vue({
el,
+ name: 'GlobalSearchSidebar',
store,
render(createElement) {
- return createElement(GlobalSearchSidebar);
+ return createElement(GlobalSearchSidebar, {
+ props: {
+ headerText,
+ },
+ });
},
});
};