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/super_sidebar/components/global_search')
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue154
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js14
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue106
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/constants.js2
6 files changed, 193 insertions, 100 deletions
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index 96e6c9bab9e..a1d0e400b5f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -2,21 +2,25 @@
import { debounce } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { getFormattedItem } from '../utils';
+
import {
COMMON_HANDLES,
COMMAND_HANDLE,
USER_HANDLE,
PROJECT_HANDLE,
ISSUE_HANDLE,
- GLOBAL_COMMANDS_GROUP_TITLE,
+ PATH_HANDLE,
PAGES_GROUP_TITLE,
+ PATH_GROUP_TITLE,
GROUP_TITLES,
+ MAX_ROWS,
} from './constants';
import SearchItem from './search_item.vue';
-import { commandMapper, linksReducer, autocompleteQuery } from './utils';
+import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
export default {
name: 'CommandPaletteItems',
@@ -25,7 +29,14 @@ export default {
GlLoadingIcon,
SearchItem,
},
- inject: ['commandPaletteCommands', 'commandPaletteLinks', 'autocompletePath', 'searchContext'],
+ inject: [
+ 'commandPaletteCommands',
+ 'commandPaletteLinks',
+ 'autocompletePath',
+ 'searchContext',
+ 'projectFilesPath',
+ 'projectBlobPath',
+ ],
props: {
searchQuery: {
type: String,
@@ -35,21 +46,45 @@ export default {
type: String,
required: true,
validator: (value) => {
- return COMMON_HANDLES.includes(value);
+ return [...COMMON_HANDLES, PATH_HANDLE].includes(value);
},
},
},
data: () => ({
groups: [],
- error: null,
loading: false,
+ projectFiles: [],
+ debouncedSearch: debounce(function debouncedSearch() {
+ switch (this.handle) {
+ case COMMAND_HANDLE:
+ this.getCommandsAndPages();
+ break;
+ /* TODO: Search for recent issues initiated by #(ISSUE_HANDLE) from the command palette scope
+ was removed as using the # in command palette conflicted
+ with the existing global search functionality to search for issue by its id.
+ The code that performs the Recent issues search was not removed from the code base
+ as it would be nice to bring it back when we decide how to combine both search by id and text.
+ In scope of https://gitlab.com/gitlab-org/gitlab/-/issues/417434
+ we either bring back the search by #issue_text or remove the related code completely */
+ case USER_HANDLE:
+ case PROJECT_HANDLE:
+ case ISSUE_HANDLE:
+ this.getScopedItems();
+ break;
+ case PATH_HANDLE:
+ this.getProjectFiles();
+ break;
+ default:
+ break;
+ }
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
}),
computed: {
isCommandMode() {
return this.handle === COMMAND_HANDLE;
},
- isUserMode() {
- return this.handle === USER_HANDLE;
+ isPathMode() {
+ return this.handle === PATH_HANDLE;
},
commands() {
return this.commandPaletteCommands.map(commandMapper);
@@ -62,7 +97,7 @@ export default {
? this.commands
.map(({ name, items }) => {
return {
- name: name || GLOBAL_COMMANDS_GROUP_TITLE,
+ name,
items: this.filterBySearchQuery(items, 'text'),
};
})
@@ -73,7 +108,7 @@ export default {
return this.groups?.length && this.groups.some((group) => group.items?.length);
},
hasSearchQuery() {
- if (this.isCommandMode) {
+ if (this.isCommandMode || this.isPathMode) {
return this.searchQuery?.length > 0;
}
return this.searchQuery?.length > 2;
@@ -84,44 +119,58 @@ export default {
}
return this.searchQuery;
},
+ filteredProjectFiles() {
+ if (!this.searchQuery) {
+ return this.projectFiles.slice(0, MAX_ROWS);
+ }
+ return this.filterBySearchQuery(this.projectFiles, 'text').slice(0, MAX_ROWS);
+ },
},
watch: {
searchQuery: {
handler() {
- switch (this.handle) {
- case COMMAND_HANDLE:
- this.getCommandsAndPages();
- break;
- case USER_HANDLE:
- case PROJECT_HANDLE:
- case ISSUE_HANDLE:
- this.getScopedItems();
- break;
- default:
- break;
- }
+ this.debouncedSearch();
},
immediate: true,
},
},
+ updated() {
+ this.$emit('updated');
+ },
methods: {
filterBySearchQuery(items, key = 'keywords') {
return fuzzaldrinPlus.filter(items, this.searchQuery, { key });
},
+ async getProjectFiles() {
+ if (!this.projectFiles.length) {
+ this.loading = true;
+
+ try {
+ const response = await axios.get(this.projectFilesPath);
+ this.projectFiles = response?.data.map(fileMapper.bind(null, this.projectBlobPath));
+ } catch (error) {
+ Sentry.captureException(error);
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ this.groups = [
+ {
+ name: PATH_GROUP_TITLE,
+ items: this.filteredProjectFiles,
+ },
+ ];
+ },
getCommandsAndPages() {
if (!this.searchQuery) {
this.groups = [...this.commands];
return;
}
- const matchedLinks = this.filterBySearchQuery(this.links);
- if (this.filteredCommands.length || matchedLinks.length) {
- this.groups = [];
- }
+ this.groups = [...this.filteredCommands];
- if (this.filteredCommands.length) {
- this.groups = [...this.filteredCommands];
- }
+ const matchedLinks = this.filterBySearchQuery(this.links);
if (matchedLinks.length) {
this.groups.push({
@@ -130,62 +179,57 @@ export default {
});
}
},
- getScopedItems: debounce(function debouncedSearch() {
- if (this.searchQuery && this.searchQuery.length < 3) return null;
+ async getScopedItems() {
+ if (this.searchQuery && this.searchQuery.length < 3) return;
this.loading = true;
- return axios
- .get(
+ try {
+ const response = await axios.get(
autocompleteQuery({
path: this.autocompletePath,
searchTerm: this.searchTerm,
handle: this.handle,
projectId: this.searchContext.project?.id,
}),
- )
- .then(({ data }) => {
- this.groups = this.getGroups(data);
- })
- .catch((error) => {
- this.error = error;
- })
- .finally(() => {
- this.loading = false;
- });
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- getGroups(data) {
- return [
- {
- name: GROUP_TITLES[this.handle],
- items: data.map(getFormattedItem),
- },
- ];
+ );
+
+ this.groups = [
+ {
+ name: GROUP_TITLES[this.handle],
+ items: response.data.map(getFormattedItem),
+ },
+ ];
+ } catch (error) {
+ Sentry.captureException(error);
+ } finally {
+ this.loading = false;
+ }
},
},
};
</script>
<template>
- <ul class="gl-p-0 gl-m-0 gl-list-style-none">
+ <div>
<gl-loading-icon v-if="loading" size="lg" class="gl-my-5" />
- <template v-else-if="hasResults">
+ <ul v-else-if="hasResults" class="gl-p-0 gl-m-0 gl-list-style-none">
<gl-disclosure-dropdown-group
v-for="(group, index) in groups"
:key="index"
:group="group"
bordered
- class="{'gl-mt-0!': index===0}"
+ :class="{ 'gl-mt-0!': index === 0 }"
>
<template #list-item="{ item }">
<search-item :item="item" :search-query="searchQuery" />
</template>
</gl-disclosure-dropdown-group>
- </template>
+ </ul>
<div v-else-if="hasSearchQuery && !hasResults" class="gl-text-gray-700 gl-pl-5 gl-py-3">
{{ __('No results found') }}
</div>
- </ul>
+ </div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index 9dab16984f5..a43e621da44 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -2,19 +2,21 @@ import { s__, sprintf } from '~/locale';
export const COMMAND_HANDLE = '>';
export const USER_HANDLE = '@';
-export const PROJECT_HANDLE = '&';
+export const PROJECT_HANDLE = ':';
export const ISSUE_HANDLE = '#';
+export const PATH_HANDLE = '/';
-export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, ISSUE_HANDLE];
+export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
- 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search...',
+ 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{pathHandle} for project file, or perform generic search...',
),
{
commandHandle: COMMAND_HANDLE,
userHandle: USER_HANDLE,
issueHandle: ISSUE_HANDLE,
projectHandle: PROJECT_HANDLE,
+ pathHandle: PATH_HANDLE,
},
false,
);
@@ -24,6 +26,7 @@ export const SEARCH_SCOPE_PLACEHOLDER = {
[USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'),
[PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'),
[ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'),
+ [PATH_HANDLE]: s__('CommandPalette|go to project file'),
};
export const SEARCH_SCOPE = {
@@ -37,9 +40,13 @@ export const USERS_GROUP_TITLE = s__('GlobalSearch|Users');
export const PAGES_GROUP_TITLE = s__('CommandPalette|Pages');
export const PROJECTS_GROUP_TITLE = s__('GlobalSearch|Projects');
export const ISSUE_GROUP_TITLE = s__('GlobalSearch|Recent issues');
+export const PATH_GROUP_TITLE = s__('CommandPalette|Project files');
export const GROUP_TITLES = {
[USER_HANDLE]: USERS_GROUP_TITLE,
[PROJECT_HANDLE]: PROJECTS_GROUP_TITLE,
[ISSUE_HANDLE]: ISSUE_GROUP_TITLE,
+ [PATH_HANDLE]: PATH_GROUP_TITLE,
};
+
+export const MAX_ROWS = 20;
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
index dce2b24f551..efd93e88fa9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
@@ -1,5 +1,5 @@
<script>
-import { COMMON_HANDLES, SEARCH_SCOPE_PLACEHOLDER } from './constants';
+import { COMMON_HANDLES, PATH_HANDLE, SEARCH_SCOPE_PLACEHOLDER } from './constants';
export default {
name: 'FakeSearchInput',
@@ -11,7 +11,7 @@ export default {
scope: {
type: String,
required: true,
- validator: (value) => COMMON_HANDLES.includes(value),
+ validator: (value) => [...COMMON_HANDLES, PATH_HANDLE].includes(value),
},
},
computed: {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 5c8c0e59eaf..347a8ffb0b4 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,12 +1,12 @@
import { isNil, omitBy } from 'lodash';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE } from './constants';
+import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
+import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
export const commandMapper = ({ name, items }) => {
// TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
// and is out of scope for the basic command palette items. If it proves to be useful, we can add it later.
return {
- name,
+ name: name || GLOBAL_COMMANDS_GROUP_TITLE,
items: items.filter(({ component }) => component !== 'invite_members'),
};
};
@@ -32,6 +32,14 @@ export const linksReducer = (acc, menuItem) => {
return acc;
};
+export const fileMapper = (projectBlobPath, file) => {
+ return {
+ icon: 'doc-code',
+ text: file,
+ href: joinPaths(projectBlobPath, file),
+ };
+};
+
export const autocompleteQuery = ({ path, searchTerm, handle, projectId }) => {
const query = omitBy(
{
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index cb34f2b8c26..bec8c191b31 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -25,20 +25,24 @@ import {
SEARCH_RESULTS_SCOPE,
} from '~/vue_shared/global_search/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { darkModeEnabled } from '~/lib/utils/color_utils';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
SCOPE_TOKEN_MAX_LENGTH,
INPUT_FIELD_PADDING,
- IS_SEARCHING,
SEARCH_MODAL_ID,
SEARCH_INPUT_SELECTOR,
SEARCH_RESULTS_ITEM_SELECTOR,
} from '../constants';
import CommandPaletteItems from '../command_palette/command_palette_items.vue';
import FakeSearchInput from '../command_palette/fake_search_input.vue';
-import { COMMON_HANDLES, SEARCH_OR_COMMAND_MODE_PLACEHOLDER } from '../command_palette/constants';
+import {
+ COMMON_HANDLES,
+ PATH_HANDLE,
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+} from '../command_palette/constants';
import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue';
import GlobalSearchDefaultItems from './global_search_default_items.vue';
import GlobalSearchScopedItems from './global_search_scoped_items.vue';
@@ -68,6 +72,11 @@ export default {
FakeSearchInput,
},
mixins: [glFeatureFlagMixin()],
+ data() {
+ return {
+ nextFocusedItemIndex: null,
+ };
+ },
computed: {
...mapState(['search', 'loading', 'searchContext']),
...mapGetters(['searchQuery', 'searchOptions', 'scopedSearchOptions']),
@@ -108,34 +117,38 @@ export default {
count: this.searchOptions.length,
});
},
- searchBarClasses() {
- return {
- [IS_SEARCHING]: this.searchTermOverMin,
- };
- },
- showScopeHelp() {
+ showScopeToken() {
return this.searchTermOverMin && !this.isCommandMode;
},
searchBarItem() {
return this.searchOptions?.[0];
},
- infieldHelpContent() {
+ scopeTokenText() {
return this.searchBarItem?.scope || this.searchBarItem?.description;
},
- infieldHelpIcon() {
- return this.searchBarItem?.icon;
+ scopeTokenIcon() {
+ if (!this.isCommandMode) {
+ return this.searchBarItem?.icon;
+ }
+ return null;
},
- scopeTokenTitle() {
+ searchScope() {
return sprintf(this.$options.i18n.SEARCH_RESULTS_SCOPE, {
- scope: this.infieldHelpContent,
+ scope: this.scopeTokenText,
});
},
-
+ truncatedSearchScope() {
+ return truncate(this.searchScope, SCOPE_TOKEN_MAX_LENGTH);
+ },
searchTextFirstChar() {
return this.searchText?.trim().charAt(0);
},
isCommandMode() {
- return this.glFeatures?.commandPalette && COMMON_HANDLES.includes(this.searchTextFirstChar);
+ return (
+ this.glFeatures?.commandPalette &&
+ (COMMON_HANDLES.includes(this.searchTextFirstChar) ||
+ (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE))
+ );
},
commandPaletteQuery() {
if (this.isCommandMode) {
@@ -143,6 +156,14 @@ export default {
}
return '';
},
+ commandHighlightClass() {
+ return darkModeEnabled() ? 'gl-bg-gray-10!' : 'gl-bg-gray-50!';
+ },
+ },
+ watch: {
+ nextFocusedItemIndex() {
+ this.highlightFirstCommand();
+ },
},
methods: {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
@@ -156,9 +177,6 @@ export default {
this.fetchAutocompleteOptions();
}
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- getTruncatedScope(scope) {
- return truncate(scope, SCOPE_TOKEN_MAX_LENGTH);
- },
observeTokenWidth({ contentRect: { width } }) {
const inputField = this.$refs?.searchInputBox?.$el?.querySelector('input');
if (!inputField) {
@@ -206,7 +224,7 @@ export default {
}
},
focusSearchInput() {
- this.$refs.searchInputBox.$el.querySelector('input').focus();
+ this.$refs.searchInput.$el.querySelector('input').focus();
},
focusNextItem(event, elements, offset) {
const { target } = event;
@@ -221,11 +239,34 @@ export default {
elements[index]?.focus();
},
submitSearch() {
+ if (this.isCommandMode) {
+ this.runFirstCommand();
+ return;
+ }
if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS) {
return;
}
visitUrl(this.searchQuery);
},
+ runFirstCommand() {
+ this.getFocusableOptions()[0]?.firstChild.click();
+ },
+ onSearchModalShown() {
+ this.$emit('shown');
+ },
+ onSearchModalHidden() {
+ this.searchText = '';
+ this.$emit('hidden');
+ },
+ highlightFirstCommand() {
+ if (this.isCommandMode) {
+ const activeCommand = this.getFocusableOptions()[0]?.firstChild;
+ activeCommand?.classList.toggle(
+ this.commandHighlightClass,
+ Boolean(!this.nextFocusedItemIndex),
+ );
+ }
+ },
},
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
@@ -243,24 +284,22 @@ export default {
body-class="gl-p-0!"
modal-class="global-search-modal"
:centered="false"
- @hidden="$emit('hidden')"
- @shown="$emit('shown')"
+ @shown="onSearchModalShown"
+ @hide="onSearchModalHidden"
>
<form
role="search"
:aria-label="searchPlaceholder"
class="gl-relative gl-rounded-base gl-w-full"
- :class="searchBarClasses"
data-testid="global-search-form"
>
<div class="gl-p-1 gl-relative">
<gl-search-box-by-type
id="search"
- ref="searchInputBox"
+ ref="searchInput"
v-model="searchText"
role="searchbox"
data-testid="global-search-input"
- data-qa-selector="global_search_input"
autocomplete="off"
:placeholder="searchPlaceholder"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
@@ -270,24 +309,20 @@ export default {
@keydown="onKeydown"
/>
<gl-token
- v-if="showScopeHelp"
+ v-if="showScopeToken"
v-gl-resize-observer-directive="observeTokenWidth"
- class="in-search-scope-help gl-sm-display-block gl-display-none"
+ class="search-scope-help gl-absolute gl-sm-display-block gl-display-none"
view-only
- :title="scopeTokenTitle"
+ :title="searchScope"
>
<gl-icon
- v-if="infieldHelpIcon"
+ v-if="scopeTokenIcon"
class="gl-mr-2"
- :aria-label="infieldHelpContent"
- :name="infieldHelpIcon"
+ :aria-label="scopeTokenText"
+ :name="scopeTokenIcon"
:size="16"
/>
- {{
- getTruncatedScope(
- sprintf($options.i18n.SEARCH_RESULTS_SCOPE, { scope: infieldHelpContent }),
- )
- }}
+ {{ truncatedSearchScope }}
</gl-token>
<span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">
{{ $options.i18n.SEARCH_DESCRIBED_BY_WITH_RESULTS }}
@@ -319,6 +354,7 @@ export default {
v-if="isCommandMode"
:search-query="commandPaletteQuery"
:handle="searchTextFirstChar"
+ @updated="highlightFirstCommand"
/>
<template v-else>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/constants.js
index cb267df6122..5a860fcd1ab 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/constants.js
@@ -18,8 +18,6 @@ export const SCOPE_TOKEN_MAX_LENGTH = 36;
export const INPUT_FIELD_PADDING = 84;
-export const IS_SEARCHING = 'is-searching';
-
export const FETCH_TYPES = ['generic', 'search'];
export const SEARCH_MODAL_ID = 'super-sidebar-search-modal';