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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-16 12:09:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-16 12:09:20 +0300
commit8ae36d93f1a63874b584f0488fde88c1fee999c4 (patch)
treef1788ba1a7fb00248ff008f817f6feea89304339 /app
parentb394e58cc2e52e17e7991e3d7b705aa383c362ee (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue14
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue291
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue43
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/tracking.js21
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/store/getters.js7
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_autocomplete_item.vue37
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue70
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js29
-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/search_item.vue (renamed from app/assets/javascripts/super_sidebar/components/global_search/command_palette/user_autocomplete_item.vue)26
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js22
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue6
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss1
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/finders/namespaces/projects_finder.rb2
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb6
-rw-r--r--app/helpers/search_helper.rb20
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/views/shared/runners/_runner_details.html.haml2
-rw-r--r--app/views/shared/runners/_runner_type_badge.html.haml14
22 files changed, 521 insertions, 106 deletions
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 2ab5dfb8dea..8928f80d83a 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -2,6 +2,7 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import Tracking from '~/tracking';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
HR_DEFAULT_CLASSES,
TRACKING_ACTION_CLICK,
@@ -12,6 +13,8 @@ import {
import { confidentialFilterData } from '../constants/confidential_filter_data';
import { stateFilterData } from '../constants/state_filter_data';
import ConfidentialityFilter from './confidentiality_filter.vue';
+import { labelFilterData } from './label_filter/data';
+import LabelFilter from './label_filter/index.vue';
import StatusFilter from './status_filter.vue';
export default {
@@ -21,7 +24,9 @@ export default {
GlLink,
StatusFilter,
ConfidentialityFilter,
+ LabelFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
...mapGetters(['currentScope']),
@@ -34,6 +39,12 @@ export default {
showStatusFilter() {
return Object.values(stateFilterData.scopes).includes(this.currentScope);
},
+ showLabelFilter() {
+ return (
+ Object.values(labelFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchIssueLabelAggregation
+ );
+ },
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
},
@@ -61,7 +72,8 @@ export default {
<hr v-if="!useNewNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
- <div class="gl-display-flex gl-align-items-center gl-mt-5">
+ <label-filter v-if="showLabelFilter" />
+ <div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
new file mode 100644
index 00000000000..74855482b5d
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -0,0 +1,291 @@
+<script>
+import {
+ GlSearchBoxByType,
+ GlLabel,
+ GlLoadingIcon,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlFormCheckboxGroup,
+ GlDropdownForm,
+ GlAlert,
+ GlOutsideDirective as Outside,
+} from '@gitlab/ui';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { uniq } from 'lodash';
+import { rgbFromHex } from '@gitlab/ui/dist/utils/utils';
+import { slugify } from '~/lib/utils/text_utility';
+import { s__, sprintf } from '~/locale';
+
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+
+import {
+ SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
+ SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_DESCRIBED_BY_DEFAULT,
+ SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_RESULTS_LOADING,
+} from '~/vue_shared/global_search/constants';
+
+import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants';
+import LabelDropdownItems from './label_dropdown_items.vue';
+
+import {
+ FIRST_DROPDOWN_INDEX,
+ SEARCH_BOX_INDEX,
+ SEARCH_RESULTS_DESCRIPTION,
+ SEARCH_INPUT_DESCRIPTION,
+ labelFilterData,
+} from './data';
+
+import { trackSelectCheckbox, trackOpenDropdown } from './tracking';
+
+export default {
+ name: 'LabelFilter',
+ directives: { Outside },
+ components: {
+ DropdownKeyboardNavigation,
+ GlSearchBoxByType,
+ LabelDropdownItems,
+ GlLabel,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlFormCheckboxGroup,
+ GlDropdownForm,
+ GlLoadingIcon,
+ GlAlert,
+ },
+ data() {
+ return {
+ currentFocusIndex: SEARCH_BOX_INDEX,
+ isFocused: false,
+ };
+ },
+ i18n: {
+ SEARCH_LABELS: s__('GlobalSearch|Search labels'),
+ DROPDOWN_HEADER: s__('GlobalSearch|Label(s)'),
+ AGGREGATIONS_ERROR_MESSAGE: s__('GlobalSearch|Fetching aggregations error.'),
+ SEARCH_DESCRIBED_BY_DEFAULT,
+ SEARCH_RESULTS_LOADING,
+ SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
+ },
+ computed: {
+ ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'aggregations']),
+ ...mapGetters([
+ 'filteredLabels',
+ 'filteredUnselectedLabels',
+ 'filteredAppliedSelectedLabels',
+ 'appliedSelectedLabels',
+ 'filteredUnappliedSelectedLabels',
+ ]),
+ searchInputDescribeBy() {
+ if (this.isLoggedIn) {
+ return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN;
+ }
+ return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN;
+ },
+ dropdownResultsDescription() {
+ if (!this.showSearchDropdown) {
+ return ''; // This allows aria-live to see register an update when the dropdown is shown
+ }
+
+ if (this.showDefaultItems) {
+ return sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_DEFAULT, {
+ count: this.filteredLabels.length,
+ });
+ }
+
+ return this.loading
+ ? this.$options.i18n.SEARCH_RESULTS_LOADING
+ : sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_UPDATED, {
+ count: this.filteredLabels.length,
+ });
+ },
+ currentFocusedOption() {
+ return this.filteredLabels[this.currentFocusIndex] || null;
+ },
+ currentFocusedId() {
+ return `${slugify(this.currentFocusedOption?.parent_full_name || 'undefined-name')}_${slugify(
+ this.currentFocusedOption?.title || 'undefined-title',
+ )}`;
+ },
+ defaultIndex() {
+ if (this.showDefaultItems) {
+ return SEARCH_BOX_INDEX;
+ }
+ return FIRST_DROPDOWN_INDEX;
+ },
+ hasSelectedLabels() {
+ return this.filteredAppliedSelectedLabels.length > 0;
+ },
+ hasUnselectedLabels() {
+ return this.filteredUnselectedLabels.length > 0;
+ },
+ dividerClasses() {
+ return [...HR_DEFAULT_CLASSES, ...ONLY_SHOW_MD];
+ },
+ labelSearchBox() {
+ return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
+ },
+ combinedSelectedFilters() {
+ const appliedSelectedLabelKeys = this.appliedSelectedLabels.map((label) => label.key);
+ const { labels = [] } = this.query;
+
+ return uniq([...appliedSelectedLabelKeys, ...labels]);
+ },
+ searchLabels: {
+ get() {
+ return this.searchLabelString;
+ },
+ set(value) {
+ this.setLabelFilterSearch({ value });
+ },
+ },
+ selectedFilters: {
+ get() {
+ return this.combinedSelectedFilters;
+ },
+ set(value) {
+ this.setQuery({ key: this.$options.labelFilterData?.filterParam, value });
+
+ trackSelectCheckbox(value);
+ },
+ },
+ },
+ async created() {
+ await this.fetchAllAggregation();
+ },
+ methods: {
+ ...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
+ openDropdown() {
+ this.isFocused = true;
+
+ trackOpenDropdown();
+ },
+ closeDropdown(event) {
+ const { target } = event;
+
+ if (this.labelSearchBox !== target) {
+ this.isFocused = false;
+ }
+ },
+ onLabelClose(event) {
+ if (!event?.target?.closest('.gl-label')?.dataset) {
+ return;
+ }
+
+ const { key } = event.target.closest('.gl-label').dataset;
+ this.closeLabel({ key });
+ },
+ reactiveLabelColor(label) {
+ const { color, key } = label;
+
+ return this.query?.labels?.some((labelKey) => labelKey === key)
+ ? color
+ : `rgba(${rgbFromHex(color)}, 0.3)`;
+ },
+ isLabelClosable(label) {
+ const { key } = label;
+ return this.query?.labels?.some((labelKey) => labelKey === key);
+ },
+ },
+ FIRST_DROPDOWN_INDEX,
+ SEARCH_RESULTS_DESCRIPTION,
+ SEARCH_INPUT_DESCRIPTION,
+ labelFilterData,
+};
+</script>
+
+<template>
+ <div class="gl-pb-0 gl-md-pt-0 label-filter gl-relative">
+ <h5
+ class="gl-my-0"
+ data-testid="label-filter-title"
+ :class="{ 'gl-font-sm': useSidebarNavigation }"
+ >
+ {{ $options.labelFilterData.header }}
+ </h5>
+ <div class="gl-my-5">
+ <gl-label
+ v-for="label in appliedSelectedLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="reactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="isLabelClosable(label)"
+ @close="onLabelClose"
+ />
+ </div>
+ <gl-search-box-by-type
+ ref="searchLabelInputBox"
+ v-model="searchLabels"
+ role="searchbox"
+ autocomplete="off"
+ :placeholder="$options.i18n.SEARCH_LABELS"
+ :aria-activedescendant="currentFocusedId"
+ :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
+ @focusin="openDropdown"
+ @keydown.esc="closeDropdown"
+ />
+ <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
+ searchInputDescribeBy
+ }}</span>
+ <span
+ role="region"
+ :data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
+ class="gl-sr-only"
+ aria-live="polite"
+ aria-atomic="true"
+ >
+ {{ dropdownResultsDescription }}
+ </span>
+ <div
+ v-if="isFocused"
+ v-outside="closeDropdown"
+ data-testid="header-search-dropdown-menu"
+ class="header-search-dropdown-menu gl-overflow-y-auto 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 gl-mt-3 gl-z-index-1"
+ :class="{
+ 'gl-max-w-none!': useSidebarNavigation,
+ 'gl-min-w-full!': useSidebarNavigation,
+ 'gl-w-full!': useSidebarNavigation,
+ }"
+ >
+ <div class="header-search-dropdown-content gl-py-2">
+ <dropdown-keyboard-navigation
+ v-model="currentFocusIndex"
+ :max="filteredLabels.length - 1"
+ :min="$options.FIRST_DROPDOWN_INDEX"
+ :default-index="defaultIndex"
+ :enable-cycle="true"
+ />
+ <div v-if="!aggregations.error">
+ <gl-dropdown-section-header v-if="hasSelectedLabels || hasUnselectedLabels">{{
+ $options.i18n.DROPDOWN_HEADER
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-form>
+ <gl-form-checkbox-group v-model="selectedFilters">
+ <label-dropdown-items
+ v-if="hasSelectedLabels"
+ data-testid="selected-lavel-items"
+ :labels="filteredAppliedSelectedLabels"
+ />
+ <gl-dropdown-divider v-if="hasSelectedLabels && hasUnselectedLabels" />
+ <label-dropdown-items
+ v-if="hasUnselectedLabels"
+ data-testid="unselected-lavel-items"
+ :labels="filteredUnselectedLabels"
+ />
+ </gl-form-checkbox-group>
+ </gl-dropdown-form>
+ </div>
+ <gl-alert v-else :dismissible="false" variant="danger">
+ {{ $options.i18n.AGGREGATIONS_ERROR_MESSAGE }}
+ </gl-alert>
+ <gl-loading-icon v-if="aggregations.fetching" size="lg" class="my-4" />
+ </div>
+ </div>
+ <hr v-if="!useSidebarNavigation" :class="dividerClasses" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue b/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue
new file mode 100644
index 00000000000..7a9e6a2e4fc
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlFormCheckbox } from '@gitlab/ui';
+
+export default {
+ name: 'LabelDropdownItems',
+ components: {
+ GlFormCheckbox,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <ul class="gl-list-style-none gl-px-0">
+ <li
+ v-for="label in labels"
+ :id="label.key"
+ :ref="label.key"
+ :key="label.key"
+ :aria-label="label.title"
+ tabindex="-1"
+ class="gl-px-5 gl-py-3 label-filter-menu-item"
+ >
+ <gl-form-checkbox
+ class="label-with-color-checkbox gl-display-inline-flex gl-h-5 gl-min-h-5"
+ :value="label.key"
+ >
+ <span
+ data-testid="label-color-indicator"
+ class="gl-rounded-base gl-w-5 gl-h-5 gl-display-inline-block gl-vertical-align-bottom gl-mr-3"
+ :style="{ 'background-color': label.color }"
+ ></span>
+ <span class="gl-reset-text-align gl-m-0 gl-p-0 label-title">{{
+ label.title
+ }}</span></gl-form-checkbox
+ >
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js b/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js
new file mode 100644
index 00000000000..c38922a559c
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/tracking.js
@@ -0,0 +1,21 @@
+import Tracking from '~/tracking';
+
+export const TRACKING_CATEGORY = 'Language filters';
+export const TRACKING_LABEL_FILTER = 'Label Key';
+
+export const TRACKING_LABEL_DROPDOWN = 'Dropdown';
+export const TRACKING_LABEL_CHECKBOX = 'Label Checkbox';
+
+export const TRACKING_ACTION_SELECT = 'search:agreggations:label:select';
+export const TRACKING_ACTION_SHOW = 'search:agreggations:label:show';
+
+export const trackSelectCheckbox = (value) =>
+ Tracking.event(TRACKING_ACTION_SELECT, TRACKING_LABEL_CHECKBOX, {
+ label: TRACKING_LABEL_FILTER,
+ property: value,
+ });
+
+export const trackOpenDropdown = () =>
+ Tracking.event(TRACKING_ACTION_SHOW, TRACKING_LABEL_DROPDOWN, {
+ label: TRACKING_LABEL_DROPDOWN,
+ });
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index e531abf523b..c10b14bd116 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -2,7 +2,6 @@
import { GlButton, GlAlert, GlForm } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { __, s__, sprintf } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants';
import { convertFiltersData } from '../../utils';
import CheckboxFilter from './checkbox_filter.vue';
@@ -24,7 +23,6 @@ export default {
GlAlert,
GlForm,
},
- mixins: [glFeatureFlagsMixin()],
data() {
return {
showAll: false,
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index d31d2b5ae11..c7cb595f42f 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -41,8 +41,11 @@ export const filteredLabels = (state) => {
export const filteredAppliedSelectedLabels = (state) =>
filteredLabels(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
-export const appliedSelectedLabels = (state) =>
- labelAggregationBuckets(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
+export const appliedSelectedLabels = (state) => {
+ return labelAggregationBuckets(state)?.filter((label) =>
+ state?.urlQuery?.labels?.includes(label.key),
+ );
+};
export const filteredUnappliedSelectedLabels = (state) =>
filteredLabels(state)?.filter((label) => state?.query?.labels?.includes(label.key));
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_autocomplete_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_autocomplete_item.vue
deleted file mode 100644
index 6ca1e40f1f6..00000000000
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_autocomplete_item.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import highlight from '~/lib/utils/highlight';
-
-export default {
- name: 'CommandPaletteCommandAutocompleteItem',
- components: {
- GlIcon,
- },
- directives: {
- SafeHtml,
- },
- props: {
- command: {
- type: Object,
- required: true,
- },
- searchQuery: {
- type: String,
- required: true,
- },
- },
- computed: {
- highlightedName() {
- return highlight(this.command.text, this.searchQuery);
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-align-items-center">
- <gl-icon v-if="command.icon" class="gl-mr-3" :name="command.icon" />
- <span v-safe-html="highlightedName" class="gl-text-gray-900"></span>
- </div>
-</template>
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 4c1f3d3e0eb..96e6c9bab9e 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
@@ -3,30 +3,29 @@ import { debounce } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { getFormattedItem } from '../utils';
import {
- USERS_ENDPOINT,
COMMON_HANDLES,
COMMAND_HANDLE,
USER_HANDLE,
+ PROJECT_HANDLE,
+ ISSUE_HANDLE,
GLOBAL_COMMANDS_GROUP_TITLE,
- USERS_GROUP_TITLE,
PAGES_GROUP_TITLE,
+ GROUP_TITLES,
} from './constants';
-import { userMapper, commandMapper, linksReducer } from './utils';
-import UserAutocompleteItem from './user_autocomplete_item.vue';
-import CommandAutocompleteItem from './command_autocomplete_item.vue';
+import SearchItem from './search_item.vue';
+import { commandMapper, linksReducer, autocompleteQuery } from './utils';
export default {
name: 'CommandPaletteItems',
components: {
GlDisclosureDropdownGroup,
GlLoadingIcon,
- UserAutocompleteItem,
- CommandAutocompleteItem,
+ SearchItem,
},
- inject: ['commandPaletteCommands', 'commandPaletteLinks'],
+ inject: ['commandPaletteCommands', 'commandPaletteLinks', 'autocompletePath', 'searchContext'],
props: {
searchQuery: {
type: String,
@@ -74,7 +73,16 @@ export default {
return this.groups?.length && this.groups.some((group) => group.items?.length);
},
hasSearchQuery() {
- return this.searchQuery?.length;
+ if (this.isCommandMode) {
+ return this.searchQuery?.length > 0;
+ }
+ return this.searchQuery?.length > 2;
+ },
+ searchTerm() {
+ if (this.handle === ISSUE_HANDLE) {
+ return `${ISSUE_HANDLE}${this.searchQuery}`;
+ }
+ return this.searchQuery;
},
},
watch: {
@@ -85,7 +93,9 @@ export default {
this.getCommandsAndPages();
break;
case USER_HANDLE:
- this.getUsers();
+ case PROJECT_HANDLE:
+ case ISSUE_HANDLE:
+ this.getScopedItems();
break;
default:
break;
@@ -103,7 +113,6 @@ export default {
this.groups = [...this.commands];
return;
}
-
const matchedLinks = this.filterBySearchQuery(this.links);
if (this.filteredCommands.length || matchedLinks.length) {
@@ -121,24 +130,22 @@ export default {
});
}
},
- getUsers: debounce(function debouncedUserSearch() {
+ getScopedItems: debounce(function debouncedSearch() {
if (this.searchQuery && this.searchQuery.length < 3) return null;
this.loading = true;
return axios
- .get(joinPaths(gon.relative_url_root || '', USERS_ENDPOINT), {
- params: {
- search: this.searchQuery,
- },
- })
+ .get(
+ autocompleteQuery({
+ path: this.autocompletePath,
+ searchTerm: this.searchTerm,
+ handle: this.handle,
+ projectId: this.searchContext.project?.id,
+ }),
+ )
.then(({ data }) => {
- this.groups = [
- {
- name: USERS_GROUP_TITLE,
- items: data.map(userMapper),
- },
- ];
+ this.groups = this.getGroups(data);
})
.catch((error) => {
this.error = error;
@@ -147,6 +154,14 @@ export default {
this.loading = false;
});
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ getGroups(data) {
+ return [
+ {
+ name: GROUP_TITLES[this.handle],
+ items: data.map(getFormattedItem),
+ },
+ ];
+ },
},
};
</script>
@@ -164,12 +179,7 @@ export default {
class="{'gl-mt-0!': index===0}"
>
<template #list-item="{ item }">
- <user-autocomplete-item v-if="isUserMode" :user="item" :search-query="searchQuery" />
- <command-autocomplete-item
- v-if="isCommandMode"
- :command="item"
- :search-query="searchQuery"
- />
+ <search-item :item="item" :search-query="searchQuery" />
</template>
</gl-disclosure-dropdown-group>
</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 a3bb102f6f9..9dab16984f5 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
@@ -1,26 +1,45 @@
import { s__, sprintf } from '~/locale';
-export const USERS_ENDPOINT = '/-/autocomplete/users.json';
export const COMMAND_HANDLE = '>';
export const USER_HANDLE = '@';
+export const PROJECT_HANDLE = '&';
+export const ISSUE_HANDLE = '#';
-export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE];
+export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, ISSUE_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
- 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user or perform generic search...',
+ 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search...',
),
{
commandHandle: COMMAND_HANDLE,
userHandle: USER_HANDLE,
+ issueHandle: ISSUE_HANDLE,
+ projectHandle: PROJECT_HANDLE,
},
false,
);
-export const SEARCH_SCOPE = {
+export const SEARCH_SCOPE_PLACEHOLDER = {
[COMMAND_HANDLE]: s__('CommandPalette|command'),
[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)'),
+};
+
+export const SEARCH_SCOPE = {
+ [USER_HANDLE]: 'user',
+ [PROJECT_HANDLE]: 'project',
+ [ISSUE_HANDLE]: 'issue',
};
export const GLOBAL_COMMANDS_GROUP_TITLE = s__('CommandPalette|Global Commands');
-export const USERS_GROUP_TITLE = s__('CommandPalette|Users');
+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 GROUP_TITLES = {
+ [USER_HANDLE]: USERS_GROUP_TITLE,
+ [PROJECT_HANDLE]: PROJECTS_GROUP_TITLE,
+ [ISSUE_HANDLE]: ISSUE_GROUP_TITLE,
+};
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 201d21f56fe..dce2b24f551 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 } from './constants';
+import { COMMON_HANDLES, SEARCH_SCOPE_PLACEHOLDER } from './constants';
export default {
name: 'FakeSearchInput',
@@ -16,7 +16,7 @@ export default {
},
computed: {
placeholder() {
- return SEARCH_SCOPE[this.scope];
+ return SEARCH_SCOPE_PLACEHOLDER[this.scope];
},
},
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/user_autocomplete_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/search_item.vue
index 6a22df79c0f..b940c7c24c6 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/user_autocomplete_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/search_item.vue
@@ -1,19 +1,20 @@
<script>
-import { GlAvatar } from '@gitlab/ui';
+import { GlAvatar, GlIcon } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
export default {
- name: 'CommandPaletteUserAutocompleteItem',
+ name: 'CommandPaletteSearchItem',
components: {
GlAvatar,
+ GlIcon,
},
directives: {
SafeHtml,
},
props: {
- user: {
+ item: {
type: Object,
required: true,
},
@@ -24,7 +25,7 @@ export default {
},
computed: {
highlightedName() {
- return highlight(this.user.text, this.searchQuery);
+ return highlight(this.item.text, this.searchQuery);
},
},
AVATAR_SHAPE_OPTION_RECT,
@@ -34,18 +35,23 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
<gl-avatar
- v-if="user.avatar_url"
+ v-if="item.avatar_url !== undefined"
class="gl-mr-3"
- :src="user.avatar_url"
- :entity-id="user.id"
- :entity-name="user.text"
- :size="16"
+ :src="item.avatar_url"
+ :entity-id="item.entity_id"
+ :entity-name="item.entity_name"
+ :size="item.avatar_size"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
aria-hidden="true"
/>
+ <gl-icon v-if="item.icon" class="gl-mr-3" :name="item.icon" />
<span class="gl-display-flex gl-flex-direction-column">
<span v-safe-html="highlightedName" class="gl-text-gray-900"></span>
- <span v-safe-html="user.username" class="gl-font-sm gl-text-gray-500"></span>
+ <span
+ v-if="item.namespace"
+ v-safe-html="item.namespace"
+ class="gl-font-sm gl-text-gray-500"
+ ></span>
</span>
</div>
</template>
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 be1edcc6e16..5c8c0e59eaf 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,8 +1,6 @@
-export const userMapper = ({ name: text, web_url: href, ...user } = {}) => ({
- text,
- href,
- ...user,
-});
+import { isNil, omitBy } from 'lodash';
+import { objectToQuery } from '~/lib/utils/url_utility';
+import { SEARCH_SCOPE } 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
@@ -33,3 +31,17 @@ export const linksReducer = (acc, menuItem) => {
}
return acc;
};
+
+export const autocompleteQuery = ({ path, searchTerm, handle, projectId }) => {
+ const query = omitBy(
+ {
+ term: searchTerm,
+ project_id: projectId,
+ filter: 'search',
+ scope: SEARCH_SCOPE[handle],
+ },
+ isNil,
+ );
+
+ return `${path}?${objectToQuery(query)}`;
+};
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 7f49d261bcb..f6afde02fa5 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -89,6 +89,8 @@ export const initSuperSidebar = () => {
...getTrialStatusWidgetData(sidebarData),
commandPaletteCommands,
commandPaletteLinks,
+ autocompletePath,
+ searchContext,
},
store: createStore({
searchPath,
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
index 04dc3b20dff..6764ad4ce73 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
@@ -9,6 +9,11 @@ export default {
GlCollapsibleListbox,
},
props: {
+ headerText: {
+ type: String,
+ required: false,
+ default: '',
+ },
value: {
type: String,
required: true,
@@ -98,6 +103,7 @@ export default {
type="hidden"
/>
<gl-collapsible-listbox
+ :header-text="headerText"
:items="filteredListboxItems"
:toggle-text="selectedTimezoneLabel"
:toggle-class="additionalClass"
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index e57dad9e4cb..5fdab7891ec 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -105,6 +105,7 @@
padding: 5px;
box-shadow: none;
width: 100%;
+ resize: none !important;
}
.md-suggestion-diff {
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index a3c6499bc54..45aefe48538 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -34,6 +34,7 @@ class SearchController < ApplicationController
before_action only: :show do
update_scope_for_code_search
end
+
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
@@ -111,11 +112,12 @@ class SearchController < ApplicationController
@project = search_service.project
@ref = params[:project_ref] if params[:project_ref].present?
@filter = params[:filter]
+ @scope = params[:scope]
# Cache the response on the frontend
expires_in 1.minute
- render json: Gitlab::Json.dump(search_autocomplete_opts(term, filter: @filter))
+ render json: Gitlab::Json.dump(search_autocomplete_opts(term, filter: @filter, scope: @scope))
end
def opensearch
diff --git a/app/finders/namespaces/projects_finder.rb b/app/finders/namespaces/projects_finder.rb
index c96f9527dd8..0194ee40801 100644
--- a/app/finders/namespaces/projects_finder.rb
+++ b/app/finders/namespaces/projects_finder.rb
@@ -32,6 +32,8 @@ module Namespaces
namespace.projects.with_route
end
+ collection = collection.not_aimed_for_deletion if params[:not_aimed_for_deletion].present?
+
collection = filter_projects(collection)
sort(collection)
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
index 726e78f9971..f0781058bea 100644
--- a/app/graphql/resolvers/namespace_projects_resolver.rb
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -7,6 +7,11 @@ module Resolvers
default_value: false,
description: 'Include also subgroup projects.'
+ argument :not_aimed_for_deletion, GraphQL::Types::Boolean,
+ required: false,
+ default_value: false,
+ description: 'Include projects that are not aimed for deletion.'
+
argument :search, GraphQL::Types::String,
required: false,
default_value: nil,
@@ -60,6 +65,7 @@ module Resolvers
def finder_params(args)
{
include_subgroups: args.dig(:include_subgroups),
+ not_aimed_for_deletion: args.dig(:not_aimed_for_deletion),
sort: args.dig(:sort),
search: args.dig(:search),
ids: parse_gids(args.dig(:ids)),
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 1d58239aea9..ecaa65dd129 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -14,12 +14,12 @@ module SearchHelper
:project_ids
].freeze
- def search_autocomplete_opts(term, filter: nil)
+ def search_autocomplete_opts(term, filter: nil, scope: nil)
return unless current_user
results = case filter&.to_sym
when :search
- resource_results(term)
+ resource_results(term, scope: scope)
when :generic
[
recent_items_autocomplete(term),
@@ -36,8 +36,9 @@ module SearchHelper
results.flatten { |item| item[:label] }
end
- def resource_results(term)
+ def resource_results(term, scope: nil)
return [] if term.length < Gitlab::Search::Params::MIN_TERM_LENGTH
+ return scope_specific_results(term, scope) if scope.present?
[
groups_autocomplete(term),
@@ -47,6 +48,19 @@ module SearchHelper
].flatten
end
+ def scope_specific_results(term, scope)
+ case scope&.to_sym
+ when :project
+ projects_autocomplete(term)
+ when :user
+ users_autocomplete(term)
+ when :issue
+ recent_issues_autocomplete(term)
+ else
+ []
+ end
+ end
+
def generic_results(term)
search_pattern = Regexp.new(Regexp.escape(term), "i")
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2ce45b90330..116108ceaf9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2055,6 +2055,10 @@ class MergeRequest < ApplicationRecord
NewMergeRequestWorker.perform_async(id, author_id)
end
+ def check_for_spam?(*)
+ spammable_attribute_changed? && project.public?
+ end
+
private
attr_accessor :skip_fetch_ref
diff --git a/app/views/shared/runners/_runner_details.html.haml b/app/views/shared/runners/_runner_details.html.haml
index 686cd1a081b..30e5587c413 100644
--- a/app/views/shared/runners/_runner_details.html.haml
+++ b/app/views/shared/runners/_runner_details.html.haml
@@ -1,4 +1,4 @@
-%h1.page-title.gl-font-size-h-display
+%h1.page-title.gl-font-size-h-display.gl-display-flex.gl-align-items-center
= s_('Runners|Runner #%{runner_id}') % { runner_id: runner.id }
= render 'shared/runners/runner_type_badge', runner: runner
diff --git a/app/views/shared/runners/_runner_type_badge.html.haml b/app/views/shared/runners/_runner_type_badge.html.haml
index a8a93f3dd76..9930f60b755 100644
--- a/app/views/shared/runners/_runner_type_badge.html.haml
+++ b/app/views/shared/runners/_runner_type_badge.html.haml
@@ -1,7 +1,7 @@
-
-- if runner.instance_type?
- = gl_badge_tag s_('Runners|shared'), variant: :success
-- elsif runner.group_type?
- = gl_badge_tag s_('Runners|group'), variant: :success
-- else
- = gl_badge_tag s_('Runners|project'), variant: :info
+.gl-ml-2
+ - if runner.instance_type?
+ = gl_badge_tag s_('Runners|shared'), variant: :success
+ - elsif runner.group_type?
+ = gl_badge_tag s_('Runners|group'), variant: :success
+ - else
+ = gl_badge_tag s_('Runners|project'), variant: :info