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-03-21 21:15:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-21 21:15:17 +0300
commit248492cc573e85aea19d7493c3a15d459be016c5 (patch)
treec25388f4af2e9a87e06121318982001b964e7573 /app
parent97a128c1d1bf45bcc00d5fae037f840eff1ae4e0 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js6
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/lib/utils/chart_utils.js38
-rw-r--r--app/assets/javascripts/lib/utils/keys.js4
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js40
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue37
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue379
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue147
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue42
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_scoped_items.vue79
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/constants.js15
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/getters.js82
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/mutations.js6
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/utils.js81
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue24
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue5
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js16
-rw-r--r--app/assets/javascripts/vue_shared/global_search/constants.js4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue127
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue34
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue2
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss35
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss13
-rw-r--r--app/controllers/concerns/integrations/params.rb2
-rw-r--r--app/controllers/concerns/kas_cookie.rb12
-rw-r--r--app/controllers/dashboard/application_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/graphql/resolvers/data_transfer_resolver.rb12
-rw-r--r--app/helpers/sidebars_helper.rb13
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/integrations/base_issue_tracker.rb6
-rw-r--r--app/models/integrations/ewm.rb2
-rw-r--r--app/models/integrations/jira.rb29
-rw-r--r--app/models/integrations/youtrack.rb11
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/iteration.rb18
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/project.rb2
40 files changed, 751 insertions, 599 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 6a7ce4f1c41..301dd1c5669 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -204,7 +204,11 @@ export default class Shortcuts {
}
static focusSearch(e) {
- $('#search').focus();
+ if (gon.use_new_navigation) {
+ document.querySelector('#super-sidebar-search')?.click();
+ } else {
+ document.querySelector('#search')?.focus();
+ }
if (e.preventDefault) {
e.preventDefault();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 9cb96283689..5a002784937 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -135,6 +135,8 @@ function initNewNavToggle() {
});
}
-requestIdleCallback(initStatusTriggers);
+if (!gon?.use_new_navigation) {
+ requestIdleCallback(initStatusTriggers);
+}
requestIdleCallback(initNavUserDropdownTracking);
requestIdleCallback(initNewNavToggle);
diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js
index 7da3bab0a4b..520d7f627f6 100644
--- a/app/assets/javascripts/lib/utils/chart_utils.js
+++ b/app/assets/javascripts/lib/utils/chart_utils.js
@@ -1,3 +1,6 @@
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { __ } from '~/locale';
+
const commonTooltips = () => ({
mode: 'x',
intersect: false,
@@ -98,3 +101,38 @@ export const firstAndLastY = (data) => {
return [firstY, lastY];
};
+
+const toolboxIconSvgPath = async (name) => {
+ return `path://${await getSvgIconPathContent(name)}`;
+};
+
+export const getToolboxOptions = async () => {
+ const promises = ['marquee-selection', 'redo', 'repeat', 'download'].map(toolboxIconSvgPath);
+
+ try {
+ const [marqueeSelectionPath, redoPath, repeatPath, downloadPath] = await Promise.all(promises);
+
+ return {
+ toolbox: {
+ feature: {
+ dataZoom: {
+ icon: { zoom: marqueeSelectionPath, back: redoPath },
+ },
+ restore: {
+ icon: repeatPath,
+ },
+ saveAsImage: {
+ icon: downloadPath,
+ },
+ },
+ },
+ };
+ } catch (e) {
+ if (process.env.NODE_ENV !== 'production') {
+ // eslint-disable-next-line no-console
+ console.warn(__('SVG could not be rendered correctly: '), e);
+ }
+
+ return {};
+ }
+};
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index bd47f10b3ac..7cfcd11ece9 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,3 +1,7 @@
export const ESC_KEY = 'Escape';
export const ENTER_KEY = 'Enter';
export const BACKSPACE_KEY = 'Backspace';
+export const ARROW_DOWN_KEY = 'ArrowDown';
+export const ARROW_UP_KEY = 'ArrowUp';
+export const END_KEY = 'End';
+export const HOME_KEY = 'Home';
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
new file mode 100644
index 00000000000..e6679323563
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -0,0 +1,40 @@
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { __ } from '~/locale';
+
+export const i18n = {
+ defaultPrompt: __('This comment appears to have a token in it. Are you sure you want to add it?'),
+ primaryBtnText: __('Proceed'),
+};
+
+const sensitiveDataPatterns = [
+ {
+ name: 'GitLab Personal Access Token',
+ regex: 'glpat-[0-9a-zA-Z_-]{20}',
+ },
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ name: 'Feed Token',
+ regex: 'feed_token=[0-9a-zA-Z_-]{20}',
+ },
+];
+
+export const containsSensitiveToken = (message) => {
+ for (const rule of sensitiveDataPatterns) {
+ const regex = new RegExp(rule.regex, 'gi');
+ if (regex.test(message)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+export async function confirmSensitiveAction(prompt = i18n.defaultPrompt) {
+ const confirmed = await confirmAction(prompt, {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: i18n.primaryBtnText,
+ });
+ if (!confirmed) {
+ return false;
+ }
+ return true;
+}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 4bcddb260e1..d06358aaef4 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,6 +7,7 @@ import { createAlert } from '~/alert';
import { badgeState } from '~/issuable/components/status_box.vue';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import {
capitalizeFirstCharacter,
convertToCamelCase,
@@ -224,7 +225,7 @@ export default {
handleSaveDraft() {
this.handleSave({ isDraft: true });
},
- handleSave({ withIssueAction = false, isDraft = false } = {}) {
+ async handleSave({ withIssueAction = false, isDraft = false } = {}) {
this.errors = [];
if (this.note.length) {
@@ -246,6 +247,13 @@ export default {
noteData.data.note.type = constants.DISCUSSION_NOTE;
}
+ if (containsSensitiveToken(this.note)) {
+ const confirmed = await confirmSensitiveAction();
+ if (!confirmed) {
+ return;
+ }
+ }
+
this.note = ''; // Empty textarea while being requested. Repopulate in catch
this.stopPolling();
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 19e72da65f2..dfae43bf19c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -81,7 +81,7 @@ export default {
},
},
apollo: {
- currentAttribute: {
+ issuable: {
query() {
const { current } = this.issuableAttributeQuery;
const { query } = current[this.issuableType];
@@ -95,11 +95,12 @@ export default {
};
},
update(data) {
+ return data.workspace?.issuable || {};
+ },
+ result({ data }) {
if (this.glFeatures?.epicWidgetEditConfirmation && this.isEpic) {
this.hasCurrentAttribute = data?.workspace?.issuable.hasEpic;
}
-
- return data?.workspace?.issuable.attribute;
},
error(error) {
createAlert({
@@ -108,13 +109,26 @@ export default {
error,
});
},
+ subscribeToMore: {
+ document() {
+ return issuableAttributesQueries[this.issuableAttribute].subscription;
+ },
+ variables() {
+ return {
+ issuableId: this.issuableId,
+ };
+ },
+ skip() {
+ return this.shouldSkipRealTimeEpicLinkUpdates;
+ },
+ },
},
},
data() {
return {
updating: false,
selectedTitle: null,
- currentAttribute: null,
+ issuable: {},
hasCurrentAttribute: false,
editConfirmation: false,
tracking: {
@@ -125,6 +139,12 @@ export default {
};
},
computed: {
+ currentAttribute() {
+ return this.issuable.attribute;
+ },
+ issuableId() {
+ return this.issuable.id;
+ },
issuableAttributeQuery() {
return this.issuableAttributesQueries[this.issuableAttribute];
},
@@ -135,7 +155,7 @@ export default {
return this.currentAttribute?.webUrl;
},
loading() {
- return this.$apollo.queries.currentAttribute.loading;
+ return this.$apollo.queries.issuable.loading;
},
attributeTypeTitle() {
return this.widgetTitleText[this.issuableAttribute];
@@ -170,6 +190,13 @@ export default {
? !this.editConfirmation
: false;
},
+ shouldSkipRealTimeEpicLinkUpdates() {
+ return (
+ !this.issuableId ||
+ this.issuableAttribute !== IssuableAttributeType.Epic ||
+ !this.glFeatures?.realTimeIssueEpicLinks
+ );
+ },
},
methods: {
updateAttribute({ id }) {
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 6798607b954..e8a54b0515e 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
@@ -6,73 +6,64 @@ import {
GlToken,
GlTooltipDirective,
GlResizeObserverDirective,
+ GlModal,
} from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { debounce } from 'lodash';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { debounce, clamp } from 'lodash';
import { truncate } from '~/lib/utils/text_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
-import Tracking from '~/tracking';
-import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
import {
+ MIN_SEARCH_TERM,
SEARCH_GITLAB,
- SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
- SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
- KBD_HELP,
} from '~/vue_shared/global_search/constants';
import {
- FIRST_DROPDOWN_INDEX,
- SEARCH_BOX_INDEX,
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
SCOPE_TOKEN_MAX_LENGTH,
INPUT_FIELD_PADDING,
IS_SEARCHING,
- IS_FOCUSED,
- IS_NOT_FOCUSED,
+ SEARCH_MODAL_ID,
+ SEARCH_INPUT_SELECTOR,
+ SEARCH_RESULTS_ITEM_SELECTOR,
} from '../constants';
-import HeaderSearchAutocompleteItems from './global_search_autocomplete_items.vue';
-import HeaderSearchDefaultItems from './global_search_default_items.vue';
-import HeaderSearchScopedItems from './global_search_scoped_items.vue';
+import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue';
+import GlobalSearchDefaultItems from './global_search_default_items.vue';
+import GlobalSearchScopedItems from './global_search_scoped_items.vue';
export default {
- name: 'HeaderSearchApp',
+ name: 'GlobalSearchModal',
+ SEARCH_MODAL_ID,
i18n: {
SEARCH_GITLAB,
- SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
- SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
+ SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
- KBD_HELP,
+ MIN_SEARCH_TERM,
},
directives: { Outside, GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
components: {
GlSearchBoxByType,
- HeaderSearchDefaultItems,
- HeaderSearchScopedItems,
- HeaderSearchAutocompleteItems,
- DropdownKeyboardNavigation,
+ GlobalSearchDefaultItems,
+ GlobalSearchScopedItems,
+ GlobalSearchAutocompleteItems,
GlIcon,
GlToken,
- },
- data() {
- return {
- showDropdown: false,
- isFocused: false,
- currentFocusIndex: SEARCH_BOX_INDEX,
- };
+ GlModal,
},
computed: {
...mapState(['search', 'loading', 'searchContext']),
- ...mapGetters(['searchQuery', 'searchOptions']),
+ ...mapGetters(['searchQuery', 'searchOptions', 'scopedSearchOptions']),
searchText: {
get() {
return this.search;
@@ -81,51 +72,26 @@ export default {
this.setSearch(value);
},
},
- currentFocusedOption() {
- return this.searchOptions[this.currentFocusIndex];
- },
- currentFocusedId() {
- return this.currentFocusedOption?.html_id;
- },
- isLoggedIn() {
- return Boolean(gon?.current_username);
- },
- showSearchDropdown() {
- if (!this.showDropdown || !this.isLoggedIn) {
- return false;
- }
- return this.searchOptions?.length > 0;
- },
showDefaultItems() {
return !this.searchText;
},
searchTermOverMin() {
return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
},
- defaultIndex() {
- if (this.showDefaultItems) {
- return SEARCH_BOX_INDEX;
- }
- return FIRST_DROPDOWN_INDEX;
- },
-
- searchInputDescribeBy() {
- if (this.isLoggedIn) {
- return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN;
- }
- return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN;
+ showScopedSearchItems() {
+ return this.searchTermOverMin && this.scopedSearchOptions.length > 1;
},
- dropdownResultsDescription() {
- if (!this.showSearchDropdown) {
- return ''; // This allows aria-live to see register an update when the dropdown is shown
- }
-
+ searchResultsDescription() {
if (this.showDefaultItems) {
return sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_DEFAULT, {
count: this.searchOptions.length,
});
}
+ if (!this.searchTermOverMin) {
+ return this.$options.i18n.MIN_SEARCH_TERM;
+ }
+
return this.loading
? this.$options.i18n.SEARCH_RESULTS_LOADING
: sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_UPDATED, {
@@ -135,12 +101,10 @@ export default {
searchBarClasses() {
return {
[IS_SEARCHING]: this.searchTermOverMin,
- [IS_FOCUSED]: this.isFocused,
- [IS_NOT_FOCUSED]: !this.isFocused,
};
},
showScopeHelp() {
- return this.searchTermOverMin && this.isFocused;
+ return this.searchTermOverMin;
},
searchBarItem() {
return this.searchOptions?.[0];
@@ -159,47 +123,7 @@ export default {
},
methods: {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
- openDropdown() {
- this.showDropdown = true;
-
- // check isFocused state to avoid firing duplicate events
- if (!this.isFocused) {
- this.isFocused = true;
- this.$emit('expandSearchBar', true);
-
- Tracking.event(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- }
- },
- closeDropdown() {
- this.showDropdown = false;
- },
- collapseAndCloseSearchBar() {
- // we need a delay on this method
- // for the search bar not to remove
- // the clear button from dom
- // and register clicks on dropdown items
- setTimeout(() => {
- this.showDropdown = false;
- this.isFocused = false;
- this.$emit('collapseSearchBar');
-
- Tracking.event(undefined, 'blur_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- }, 200);
- },
- submitSearch() {
- if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS && this.currentFocusIndex < 0) {
- return null;
- }
- return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
- },
getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
- this.openDropdown();
if (!searchTerm) {
this.clearAutocomplete();
} else {
@@ -216,105 +140,174 @@ export default {
}
inputField.style.paddingRight = `${width + INPUT_FIELD_PADDING}px`;
},
+ getFocusableOptions() {
+ return Array.from(
+ this.$refs.resultsList?.querySelectorAll(SEARCH_RESULTS_ITEM_SELECTOR) || [],
+ );
+ },
+ onKeydown(event) {
+ const { code, target } = event;
+
+ let stop = true;
+
+ const elements = this.getFocusableOptions();
+ if (elements.length < 1) return;
+
+ const isSearchInput = target.matches(SEARCH_INPUT_SELECTOR);
+
+ if (code === HOME_KEY) {
+ this.focusItem(0, elements);
+ } else if (code === END_KEY) {
+ this.focusItem(elements.length - 1, elements);
+ } else if (code === ARROW_UP_KEY) {
+ if (isSearchInput) return;
+
+ if (elements.indexOf(target) === 0) {
+ this.focusSearchInput();
+ return;
+ }
+ this.focusNextItem(event, elements, -1);
+ } else if (code === ARROW_DOWN_KEY) {
+ this.focusNextItem(event, elements, 1);
+ } else if (code === ESC_KEY) {
+ this.$refs.searchModal.close();
+ } else {
+ stop = false;
+ }
+
+ if (stop) {
+ event.preventDefault();
+ }
+ },
+ focusSearchInput() {
+ this.$refs.searchInputBox.$el.querySelector('input').focus();
+ },
+ focusNextItem(event, elements, offset) {
+ const { target } = event;
+ const currentIndex = elements.indexOf(target);
+ const nextIndex = clamp(currentIndex + offset, 0, elements.length - 1);
+
+ this.focusItem(nextIndex, elements);
+ },
+ focusItem(index, elements) {
+ this.nextFocusedItemIndex = index;
+
+ elements[index]?.focus();
+ },
+ submitSearch() {
+ if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS) {
+ return;
+ }
+ visitUrl(this.searchQuery);
+ },
},
- SEARCH_BOX_INDEX,
- FIRST_DROPDOWN_INDEX,
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
};
</script>
<template>
- <form
- v-outside="closeDropdown"
- role="search"
- :aria-label="$options.i18n.SEARCH_GITLAB"
- class="header-search gl-relative gl-rounded-base gl-w-full"
- :class="searchBarClasses"
- data-testid="header-search-form"
+ <gl-modal
+ ref="searchModal"
+ :modal-id="$options.SEARCH_MODAL_ID"
+ hide-header
+ hide-footer
+ hide-header-close
+ scrollable
+ body-class="gl-p-0!"
+ modal-class="global-search-modal"
+ :centered="false"
>
- <gl-search-box-by-type
- id="search"
- ref="searchInputBox"
- v-model="searchText"
- role="searchbox"
- class="gl-z-index-1"
- data-qa-selector="search_term_field"
- autocomplete="off"
- :placeholder="$options.i18n.SEARCH_GITLAB"
- :aria-activedescendant="currentFocusedId"
- :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
- @focus="openDropdown"
- @click="openDropdown"
- @blur="collapseAndCloseSearchBar"
- @input="getAutocompleteOptions"
- @keydown.enter.stop.prevent="submitSearch"
- @keydown.esc.stop.prevent="closeDropdown"
- />
- <gl-token
- v-if="showScopeHelp"
- v-gl-resize-observer-directive="observeTokenWidth"
- class="in-search-scope-help"
- :view-only="true"
- :title="scopeTokenTitle"
- ><gl-icon
- v-if="infieldHelpIcon"
- class="gl-mr-2"
- :aria-label="infieldHelpContent"
- :name="infieldHelpIcon"
- :size="16"
- />{{
- getTruncatedScope(
- sprintf($options.i18n.SEARCH_RESULTS_SCOPE, {
- scope: infieldHelpContent,
- }),
- )
- }}
- </gl-token>
- <kbd
- v-show="!isFocused"
- v-gl-tooltip.bottom.hover.html
- class="gl-absolute gl-right-3 gl-top-0 gl-z-index-1 keyboard-shortcut-helper"
- :title="$options.i18n.KBD_HELP"
- >/</kbd
- >
- <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"
+ <form
+ role="search"
+ :aria-label="$options.i18n.SEARCH_GITLAB"
+ class="gl-relative gl-rounded-base gl-w-full"
+ :class="searchBarClasses"
+ data-testid="global-search-form"
>
- {{ dropdownResultsDescription }}
- </span>
- <div
- v-if="showSearchDropdown"
- 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"
- >
- <div class="header-search-dropdown-content gl-py-2">
- <dropdown-keyboard-navigation
- v-model="currentFocusIndex"
- :max="searchOptions.length - 1"
- :min="$options.FIRST_DROPDOWN_INDEX"
- :default-index="defaultIndex"
- @tab="closeDropdown"
- />
- <header-search-default-items
- v-if="showDefaultItems"
- :current-focused-option="currentFocusedOption"
+ <div class="gl-p-1">
+ <gl-search-box-by-type
+ id="search"
+ ref="searchInputBox"
+ v-model="searchText"
+ role="searchbox"
+ data-testid="global-search-input"
+ autocomplete="off"
+ :placeholder="$options.i18n.SEARCH_GITLAB"
+ :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
+ borderless
+ @input="getAutocompleteOptions"
+ @keydown.enter.stop.prevent="submitSearch"
+ @keydown="onKeydown"
/>
- <template v-else>
- <header-search-scoped-items
- v-if="searchTermOverMin"
- :current-focused-option="currentFocusedOption"
+ <gl-token
+ v-if="showScopeHelp"
+ v-gl-resize-observer-directive="observeTokenWidth"
+ class="in-search-scope-help gl-sm-display-block gl-display-none"
+ view-only
+ :title="scopeTokenTitle"
+ >
+ <gl-icon
+ v-if="infieldHelpIcon"
+ class="gl-mr-2"
+ :aria-label="infieldHelpContent"
+ :name="infieldHelpIcon"
+ :size="16"
/>
- <header-search-autocomplete-items :current-focused-option="currentFocusedOption" />
+ {{
+ getTruncatedScope(
+ sprintf($options.i18n.SEARCH_RESULTS_SCOPE, { scope: infieldHelpContent }),
+ )
+ }}
+ </gl-token>
+ <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">
+ {{ $options.i18n.SEARCH_DESCRIBED_BY_WITH_RESULTS }}
+ </span>
+ </div>
+ <span
+ role="region"
+ :data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
+ class="gl-sr-only"
+ aria-live="polite"
+ aria-atomic="true"
+ >
+ {{ searchResultsDescription }}
+ </span>
+ <div
+ ref="resultsList"
+ data-testid="global-search-results"
+ class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-2"
+ @keydown="onKeydown"
+ >
+ <global-search-default-items v-if="showDefaultItems" />
+ <template v-else>
+ <global-search-scoped-items v-if="showScopedSearchItems" />
+ <global-search-autocomplete-items />
</template>
</div>
- </div>
- </form>
+
+ <template v-if="searchContext">
+ <input
+ v-if="searchContext.group"
+ type="hidden"
+ name="group_id"
+ :value="searchContext.group.id"
+ />
+ <input
+ v-if="searchContext.project"
+ type="hidden"
+ name="project_id"
+ :value="searchContext.project.id"
+ />
+
+ <template v-if="searchContext.group || searchContext.project">
+ <input type="hidden" name="scope" :value="searchContext.scope" />
+ <input type="hidden" name="search_code" :value="searchContext.code_search" />
+ </template>
+
+ <input type="hidden" name="snippets" :value="searchContext.for_snippets" />
+ <input type="hidden" name="repository_ref" :value="searchContext.ref" />
+ </template>
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
index 1838214def6..cd623200b03 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue
@@ -1,113 +1,36 @@
<script>
-import {
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlDropdownDivider,
- GlAvatar,
- GlAlert,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlAvatar, GlAlert, GlLoadingIcon, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-import { truncateNamespace } from '~/lib/utils/text_utility';
-import {
- GROUPS_CATEGORY,
- PROJECTS_CATEGORY,
- MERGE_REQUEST_CATEGORY,
- ISSUES_CATEGORY,
- RECENT_EPICS_CATEGORY,
- AUTOCOMPLETE_ERROR_MESSAGE,
-} from '~/vue_shared/global_search/constants';
-import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
+import { AUTOCOMPLETE_ERROR_MESSAGE } from '~/vue_shared/global_search/constants';
export default {
- name: 'HeaderSearchAutocompleteItems',
+ name: 'GlobalSearchAutocompleteItems',
i18n: {
AUTOCOMPLETE_ERROR_MESSAGE,
},
components: {
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlDropdownDivider,
GlAvatar,
GlAlert,
GlLoadingIcon,
+ GlDisclosureDropdownGroup,
},
directives: {
SafeHtml,
},
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
- },
computed: {
- ...mapState(['search', 'loading', 'autocompleteError', 'searchContext']),
- ...mapGetters(['autocompleteGroupedSearchOptions']),
- },
- watch: {
- currentFocusedOption() {
- const focusedElement = this.$refs[this.currentFocusedOption?.html_id]?.[0]?.$el;
-
- if (focusedElement) {
- focusedElement.scrollIntoView(false);
- }
+ ...mapState(['search', 'loading', 'autocompleteError']),
+ ...mapGetters(['autocompleteGroupedSearchOptions', 'scopedSearchOptions']),
+ isPrecededByScopedOptions() {
+ return this.scopedSearchOptions.length > 1;
},
},
methods: {
- truncateNamespace(string) {
- if (string.split(' / ').length > 2) {
- return truncateNamespace(string);
- }
-
- return string;
- },
highlightedName(val) {
return highlight(val, this.search);
},
- avatarSize(data) {
- if (data.category === GROUPS_CATEGORY || data.category === PROJECTS_CATEGORY) {
- return LARGE_AVATAR_PX;
- }
-
- return SMALL_AVATAR_PX;
- },
- isOptionFocused(data) {
- return this.currentFocusedOption?.html_id === data.html_id;
- },
- isProjectsCategory(data) {
- return data.category === PROJECTS_CATEGORY;
- },
- getEntityId(data) {
- switch (data.category) {
- case GROUPS_CATEGORY:
- case RECENT_EPICS_CATEGORY:
- return data.group_id || data.id || this.searchContext?.group?.id;
- case PROJECTS_CATEGORY:
- case ISSUES_CATEGORY:
- case MERGE_REQUEST_CATEGORY:
- return data.project_id || data.id || this.searchContext?.project?.id;
- default:
- return data.id;
- }
- },
- getEntitytName(data) {
- switch (data.category) {
- case GROUPS_CATEGORY:
- case RECENT_EPICS_CATEGORY:
- return data.group_name || data.value || data.label || this.searchContext?.group?.name;
- case PROJECTS_CATEGORY:
- case ISSUES_CATEGORY:
- case MERGE_REQUEST_CATEGORY:
- return data.project_name || data.value || data.label || this.searchContext?.project?.name;
- default:
- return data.label;
- }
- },
},
AVATAR_SHAPE_OPTION_RECT,
};
@@ -115,46 +38,46 @@ export default {
<template>
<div>
- <template v-if="!loading">
- <div v-for="(option, index) in autocompleteGroupedSearchOptions" :key="option.category">
- <gl-dropdown-divider v-if="index > 0" />
- <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="data in option.data"
- :id="data.html_id"
- :ref="data.html_id"
- :key="data.html_id"
- :class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
- :aria-selected="isOptionFocused(data)"
- :aria-label="data.label"
- tabindex="-1"
- :href="data.url"
- >
- <div class="gl-display-flex gl-align-items-center" aria-hidden="true">
+ <ul v-if="!loading" class="gl-m-0 gl-p-0 gl-list-style-none">
+ <gl-disclosure-dropdown-group
+ v-for="group in autocompleteGroupedSearchOptions"
+ :key="group.name"
+ :class="{ 'gl-mt-0!': !isPrecededByScopedOptions }"
+ :group="group"
+ bordered
+ >
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-align-items-center">
<gl-avatar
- v-if="data.avatar_url !== undefined"
- :src="data.avatar_url"
- :entity-id="getEntityId(data)"
- :entity-name="getEntitytName(data)"
- :size="avatarSize(data)"
+ v-if="item.avatar_url !== undefined"
+ class="gl-mr-3"
+ :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"
/>
<span class="gl-display-flex gl-flex-direction-column">
<span
- v-safe-html="highlightedName(data.value || data.label)"
+ v-safe-html="highlightedName(item.text)"
class="gl-text-gray-900"
+ data-testid="autocomplete-item-name"
></span>
<span
- v-if="data.value"
- v-safe-html="truncateNamespace(data.label)"
+ v-if="item.value"
+ v-safe-html="item.namespace"
class="gl-font-sm gl-text-gray-500"
+ data-testid="autocomplete-item-namespace"
></span>
</span>
</div>
- </gl-dropdown-item>
- </div>
- </template>
+ </template>
+ </gl-disclosure-dropdown-group>
+ </ul>
+
<gl-loading-icon v-else size="lg" class="my-4" />
+
<gl-alert
v-if="autocompleteError"
class="gl-text-body gl-mt-2"
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue
index f0d398297e9..239c61fd750 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue
@@ -1,23 +1,15 @@
<script>
-import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { ALL_GITLAB } from '~/vue_shared/global_search/constants';
export default {
- name: 'HeaderSearchDefaultItems',
+ name: 'GlobalSearchDefaultItems',
i18n: {
ALL_GITLAB,
},
components: {
- GlDropdownSectionHeader,
- GlDropdownItem,
- },
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
+ GlDisclosureDropdownGroup,
},
computed: {
...mapState(['searchContext']),
@@ -29,30 +21,18 @@ export default {
this.$options.i18n.ALL_GITLAB
);
},
- },
- methods: {
- isOptionFocused(option) {
- return this.currentFocusedOption?.html_id === option.html_id;
+ defaultItemsGroup() {
+ return {
+ name: this.sectionHeader,
+ items: this.defaultSearchOptions,
+ };
},
},
};
</script>
<template>
- <div>
- <gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="option in defaultSearchOptions"
- :id="option.html_id"
- :ref="option.html_id"
- :key="option.html_id"
- :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
- :aria-selected="isOptionFocused(option)"
- :aria-label="option.title"
- tabindex="-1"
- :href="option.url"
- >
- <span aria-hidden="true">{{ option.title }}</span>
- </gl-dropdown-item>
- </div>
+ <ul class="gl-p-0 gl-m-0 gl-list-style-none">
+ <gl-disclosure-dropdown-group :group="defaultItemsGroup" bordered class="gl-mt-0!" />
+ </ul>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_scoped_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_scoped_items.vue
index 1ef88492b23..76600f829f6 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_scoped_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_scoped_items.vue
@@ -1,47 +1,26 @@
<script>
-import { GlDropdownItem, GlIcon, GlToken } from '@gitlab/ui';
+import { GlIcon, GlToken, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__, sprintf } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
-import { SCOPED_SEARCH_ITEM_ARIA_LABEL } from '~/vue_shared/global_search/constants';
import { SCOPE_TOKEN_MAX_LENGTH } from '../constants';
export default {
- name: 'HeaderSearchScopedItems',
- i18n: {
- SCOPED_SEARCH_ITEM_ARIA_LABEL,
- },
+ name: 'GlobalSearchScopedItems',
components: {
- GlDropdownItem,
GlIcon,
GlToken,
- },
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
+ GlDisclosureDropdownGroup,
},
computed: {
...mapState(['search']),
- ...mapGetters(['scopedSearchOptions', 'autocompleteGroupedSearchOptions']),
+ ...mapGetters(['scopedSearchGroup']),
},
methods: {
- isOptionFocused(option) {
- return this.currentFocusedOption?.html_id === option.html_id;
- },
- ariaLabel(option) {
- return sprintf(this.$options.i18n.SCOPED_SEARCH_ITEM_ARIA_LABEL, {
- search: this.search,
- description: option.description || option.icon,
- scope: option.scope || '',
- });
- },
- titleLabel(option) {
+ titleLabel(item) {
return sprintf(s__('GlobalSearch|in %{scope}'), {
search: this.search,
- scope: option.scope || option.description,
+ scope: item.scope || item.description,
});
},
getTruncatedScope(scope) {
@@ -53,35 +32,23 @@ export default {
<template>
<div>
- <gl-dropdown-item
- v-for="option in scopedSearchOptions"
- :id="option.html_id"
- :ref="option.html_id"
- :key="option.html_id"
- class="gl-max-w-full"
- :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
- :aria-selected="isOptionFocused(option)"
- :aria-label="ariaLabel(option)"
- tabindex="-1"
- :href="option.url"
- :title="titleLabel(option)"
- >
- <span
- ref="token-text-content"
- class="gl-display-flex gl-justify-content-start search-text-content gl-line-height-24 gl-align-items-start gl-flex-direction-row gl-w-full"
- >
- <gl-icon name="search" class="gl-flex-shrink-0 gl-mr-2 gl-relative gl-pt-2" />
- <span class="gl-flex-grow-1 gl-relative">
- <gl-token
- class="in-dropdown-scope-help has-icon gl-flex-shrink-0 gl-relative gl-white-space-nowrap gl-float-right gl-mr-n3!"
- :view-only="true"
+ <ul class="gl-m-0 gl-p-0 gl-pb-2 gl-list-style-none">
+ <gl-disclosure-dropdown-group :group="scopedSearchGroup" bordered class="gl-mt-0!">
+ <template #list-item="{ item }">
+ <span
+ class="gl-display-flex gl-align-items-center gl-line-height-24 gl-flex-direction-row gl-w-full"
>
- <gl-icon v-if="option.icon" :name="option.icon" class="gl-mr-2" />
- <span>{{ getTruncatedScope(titleLabel(option)) }}</span>
- </gl-token>
- {{ search }}
- </span>
- </span>
- </gl-dropdown-item>
+ <gl-icon name="search" class="gl-flex-shrink-0 gl-mr-2 gl-pt-2 gl-mt-n2" />
+ <span class="gl-flex-grow-1">
+ <gl-token class="gl-flex-shrink-0 gl-white-space-nowrap gl-float-right" view-only>
+ <gl-icon v-if="item.icon" :name="item.icon" class="gl-mr-2" />
+ <span>{{ getTruncatedScope(titleLabel(item)) }}</span>
+ </gl-token>
+ {{ search }}
+ </span>
+ </span>
+ </template>
+ </gl-disclosure-dropdown-group>
+ </ul>
</div>
</template>
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 b9bb4e573fd..cb267df6122 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/constants.js
@@ -8,10 +8,6 @@ export const LARGE_AVATAR_PX = 32;
export const SMALL_AVATAR_PX = 16;
-export const FIRST_DROPDOWN_INDEX = 0;
-
-export const SEARCH_BOX_INDEX = -1;
-
export const SEARCH_SHORTCUTS_MIN_CHARACTERS = 2;
export const SEARCH_INPUT_DESCRIPTION = 'search-input-description';
@@ -20,14 +16,13 @@ export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description';
export const SCOPE_TOKEN_MAX_LENGTH = 36;
-export const INPUT_FIELD_PADDING = 52;
-
-export const HEADER_INIT_EVENTS = ['input', 'focus'];
+export const INPUT_FIELD_PADDING = 84;
export const IS_SEARCHING = 'is-searching';
-export const IS_FOCUSED = 'is-focused';
-export const IS_NOT_FOCUSED = 'is-not-focused';
export const FETCH_TYPES = ['generic', 'search'];
+export const SEARCH_MODAL_ID = 'super-sidebar-search-modal';
+
+export const SEARCH_INPUT_SELECTOR = '.gl-search-box-by-type-input-borderless';
-export const SEARCH_INPUT_FIELD_MAX_WIDTH = '640px';
+export const SEARCH_RESULTS_ITEM_SELECTOR = '.gl-new-dropdown-item';
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
index f86463b94d1..4a42f416206 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
@@ -1,6 +1,5 @@
import { omitBy, isNil } from 'lodash';
import { objectToQuery } from '~/lib/utils/url_utility';
-
import {
MSG_ISSUES_ASSIGNED_TO_ME,
MSG_ISSUES_IVE_CREATED,
@@ -10,8 +9,10 @@ import {
MSG_IN_ALL_GITLAB,
PROJECTS_CATEGORY,
GROUPS_CATEGORY,
- DROPDOWN_ORDER,
+ SEARCH_RESULTS_ORDER,
} from '~/vue_shared/global_search/constants';
+import { getFormattedItem } from '../utils';
+
import {
ICON_GROUP,
ICON_SUBGROUP,
@@ -62,32 +63,27 @@ export const defaultSearchOptions = (state, getters) => {
const issues = [
{
- html_id: 'default-issues-assigned',
- title: MSG_ISSUES_ASSIGNED_TO_ME,
- url: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
+ text: MSG_ISSUES_ASSIGNED_TO_ME,
+ href: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
},
{
- html_id: 'default-issues-created',
- title: MSG_ISSUES_IVE_CREATED,
- url: `${getters.scopedIssuesPath}/?author_username=${userName}`,
+ text: MSG_ISSUES_IVE_CREATED,
+ href: `${getters.scopedIssuesPath}/?author_username=${userName}`,
},
];
const mergeRequests = [
{
- html_id: 'default-mrs-assigned',
- title: MSG_MR_ASSIGNED_TO_ME,
- url: `${getters.scopedMRPath}/?assignee_username=${userName}`,
+ text: MSG_MR_ASSIGNED_TO_ME,
+ href: `${getters.scopedMRPath}/?assignee_username=${userName}`,
},
{
- html_id: 'default-mrs-reviewer',
- title: MSG_MR_IM_REVIEWER,
- url: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
+ text: MSG_MR_IM_REVIEWER,
+ href: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
},
{
- html_id: 'default-mrs-created',
- title: MSG_MR_IVE_CREATED,
- url: `${getters.scopedMRPath}/?author_username=${userName}`,
+ text: MSG_MR_IVE_CREATED,
+ href: `${getters.scopedMRPath}/?author_username=${userName}`,
},
];
return [...(getters.scopedIssuesPath ? issues : []), ...mergeRequests];
@@ -145,58 +141,64 @@ export const allUrl = (state) => {
};
export const scopedSearchOptions = (state, getters) => {
- const options = [];
+ const items = [];
if (state.searchContext?.project) {
- options.push({
- html_id: 'scoped-in-project',
+ items.push({
+ text: 'scoped-in-project',
scope: state.searchContext.project?.name || '',
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
- url: getters.projectUrl,
+ href: getters.projectUrl,
});
}
if (state.searchContext?.group) {
- options.push({
- html_id: 'scoped-in-group',
+ items.push({
+ text: 'scoped-in-group',
scope: state.searchContext.group?.name || '',
scopeCategory: GROUPS_CATEGORY,
icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
- url: getters.groupUrl,
+ href: getters.groupUrl,
});
}
- options.push({
- html_id: 'scoped-in-all',
+ items.push({
+ text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
- url: getters.allUrl,
+ href: getters.allUrl,
});
- return options;
+ return items;
+};
+
+export const scopedSearchGroup = (state, getters) => {
+ const items = getters.scopedSearchOptions?.length ? getters.scopedSearchOptions.slice(1) : [];
+ return { items };
};
export const autocompleteGroupedSearchOptions = (state) => {
const groupedOptions = {};
const results = [];
- state.autocompleteOptions.forEach((option) => {
- const category = groupedOptions[option.category];
+ state.autocompleteOptions.forEach((item) => {
+ const group = groupedOptions[item.category];
+ const formattedItem = getFormattedItem(item, state.searchContext);
- if (category) {
- category.data.push(option);
+ if (group) {
+ group.items.push(formattedItem);
} else {
- groupedOptions[option.category] = {
- category: option.category,
- data: [option],
+ groupedOptions[item.category] = {
+ name: formattedItem.category,
+ items: [formattedItem],
};
- results.push(groupedOptions[option.category]);
+ results.push(groupedOptions[formattedItem.category]);
}
});
return results.sort(
- (a, b) => DROPDOWN_ORDER.indexOf(a.category) - DROPDOWN_ORDER.indexOf(b.category),
+ (a, b) => SEARCH_RESULTS_ORDER.indexOf(a.name) - SEARCH_RESULTS_ORDER.indexOf(b.name),
);
};
@@ -206,8 +208,8 @@ export const searchOptions = (state, getters) => {
}
const sortedAutocompleteOptions = Object.values(getters.autocompleteGroupedSearchOptions).reduce(
- (options, group) => {
- return [...options, ...group.data];
+ (items, group) => {
+ return [...items, ...group.items];
},
[],
);
@@ -216,5 +218,5 @@ export const searchOptions = (state, getters) => {
return sortedAutocompleteOptions;
}
- return getters.scopedSearchOptions.concat(sortedAutocompleteOptions);
+ return (getters.scopedSearchOptions ?? []).concat(sortedAutocompleteOptions);
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/mutation_types.js b/app/assets/javascripts/super_sidebar/components/global_search/store/mutation_types.js
index 6e65345757f..d7d9ebecd16 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/mutation_types.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/mutation_types.js
@@ -2,5 +2,4 @@ export const REQUEST_AUTOCOMPLETE = 'REQUEST_AUTOCOMPLETE';
export const RECEIVE_AUTOCOMPLETE_SUCCESS = 'RECEIVE_AUTOCOMPLETE_SUCCESS';
export const RECEIVE_AUTOCOMPLETE_ERROR = 'RECEIVE_AUTOCOMPLETE_ERROR';
export const CLEAR_AUTOCOMPLETE = 'CLEAR_AUTOCOMPLETE';
-
export const SET_SEARCH = 'SET_SEARCH';
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/mutations.js b/app/assets/javascripts/super_sidebar/components/global_search/store/mutations.js
index 19b4d4ec330..9936c3f59d8 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/mutations.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/mutations.js
@@ -8,11 +8,7 @@ export default {
},
[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
state.loading = false;
- state.autocompleteOptions = [...state.autocompleteOptions].concat(
- data.map((d, i) => {
- return { html_id: `autocomplete-${d.category}-${i}`, ...d };
- }),
- );
+ state.autocompleteOptions = [...state.autocompleteOptions].concat(data);
state.autocompleteError = false;
},
[types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
new file mode 100644
index 00000000000..11d1fa1ab95
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
@@ -0,0 +1,81 @@
+import { pickBy } from 'lodash';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import {
+ GROUPS_CATEGORY,
+ PROJECTS_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ ISSUES_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+} from '~/vue_shared/global_search/constants';
+import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from './constants';
+
+const getTruncatedNamespace = (string) => {
+ if (string.split(' / ').length > 2) {
+ return truncateNamespace(string);
+ }
+
+ return string;
+};
+const getAvatarSize = (category) => {
+ if (category === GROUPS_CATEGORY || category === PROJECTS_CATEGORY) {
+ return LARGE_AVATAR_PX;
+ }
+
+ return SMALL_AVATAR_PX;
+};
+
+const getEntityId = (item, searchContext) => {
+ switch (item.category) {
+ case GROUPS_CATEGORY:
+ case RECENT_EPICS_CATEGORY:
+ return item.group_id || item.id || searchContext?.group?.id;
+ case PROJECTS_CATEGORY:
+ case ISSUES_CATEGORY:
+ case MERGE_REQUEST_CATEGORY:
+ return item.project_id || item.id || searchContext?.project?.id;
+ default:
+ return item.id;
+ }
+};
+const getEntityName = (item, searchContext) => {
+ switch (item.category) {
+ case GROUPS_CATEGORY:
+ case RECENT_EPICS_CATEGORY:
+ return item.group_name || item.value || item.label || searchContext?.group?.name;
+ case PROJECTS_CATEGORY:
+ case ISSUES_CATEGORY:
+ case MERGE_REQUEST_CATEGORY:
+ return item.project_name || item.value || item.label || searchContext?.project?.name;
+ default:
+ return item.label;
+ }
+};
+
+export const getFormattedItem = (item, searchContext) => {
+ const { id, category, value, label, url: href, avatar_url } = item;
+ let namespace;
+ const text = value || label;
+ if (value) {
+ namespace = getTruncatedNamespace(label);
+ }
+ const avatarSize = getAvatarSize(category);
+ const entityId = getEntityId(item, searchContext);
+ const entityName = getEntityName(item, searchContext);
+
+ return pickBy(
+ {
+ id,
+ category,
+ value,
+ label,
+ text,
+ href,
+ avatar_url,
+ avatar_size: avatarSize,
+ namespace,
+ entity_id: entityId,
+ entity_name: entityName,
+ },
+ (val) => val !== undefined,
+ );
+};
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index e27acb60372..2597d0518e6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,6 +1,6 @@
<script>
-import { GlBadge, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import logo from '../../../../views/shared/_logo.svg';
import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
@@ -8,12 +8,14 @@ import CreateMenu from './create_menu.vue';
import Counter from './counter.vue';
import MergeRequestMenu from './merge_request_menu.vue';
import UserMenu from './user_menu.vue';
+import { SEARCH_MODAL_ID } from './global_search/constants';
export default {
// "GitLab Next" is a proper noun, so don't translate "Next"
/* eslint-disable-next-line @gitlab/require-i18n-strings */
NEXT_LABEL: 'Next',
logo,
+ SEARCH_MODAL_ID,
components: {
Counter,
CreateMenu,
@@ -21,6 +23,10 @@ export default {
GlButton,
MergeRequestMenu,
UserMenu,
+ SearchModal: () =>
+ import(
+ /* webpackChunkName: 'global_search_modal' */ './global_search/components/global_search.vue'
+ ),
},
i18n: {
collapseSidebar: __('Collapse sidebar'),
@@ -28,10 +34,16 @@ export default {
issues: __('Issues'),
mergeRequests: __('Merge requests'),
search: __('Search'),
+ searchKbdHelp: sprintf(
+ s__('GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}'),
+ { kbdOpen: '<kbd>', kbdClose: '</kbd>' },
+ false,
+ ),
todoList: __('To-Do list'),
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
SafeHtml,
},
inject: ['rootPath'],
@@ -77,12 +89,18 @@ export default {
@click="collapseSidebar"
/>
<create-menu :groups="sidebarData.create_new_menu_groups" />
+
<gl-button
+ id="super-sidebar-search"
+ v-gl-tooltip.bottom.hover.html="$options.i18n.searchKbdHelp"
+ v-gl-modal="$options.SEARCH_MODAL_ID"
+ data-testid="super-sidebar-search-button"
icon="search"
:aria-label="$options.i18n.search"
category="tertiary"
- href="/search"
/>
+ <search-modal />
+
<user-menu :data="sidebarData" />
</div>
<div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2">
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 34bbb3ce177..de3f3241366 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -167,6 +167,9 @@ export default {
this.trackEvents();
this.initCallout();
},
+ closeDropdown() {
+ this.$refs.userDropdown.close();
+ },
initCallout() {
if (this.showNotificationDot) {
PersistentUserCallout.factory(this.$refs?.buyPipelineMinutesNotificationCallout.$el);
@@ -189,6 +192,7 @@ export default {
<template>
<div>
<gl-disclosure-dropdown
+ ref="userDropdown"
placement="right"
data-testid="user-dropdown"
data-qa-selector="user_menu"
@@ -220,6 +224,7 @@ export default {
v-if="data.status.can_update"
:item="statusItem"
data-testid="status-item"
+ @action="closeDropdown"
/>
<gl-disclosure-dropdown-item
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 4395cc2f5f0..c5e8c68b940 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,7 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
+import createStore from './components/global_search/store';
import {
bindSuperSidebarCollapsedEvents,
initSuperSidebarCollapsedState,
@@ -23,6 +25,10 @@ export const initSuperSidebar = () => {
initSuperSidebarCollapsedState();
const { rootPath, sidebar, toggleNewNavEndpoint } = el.dataset;
+ const sidebarData = JSON.parse(sidebar);
+ const searchData = convertObjectPropsToCamelCase(sidebarData.search);
+
+ const { searchPath, issuesPath, mrPath, autocompletePath, searchContext } = searchData;
return new Vue({
el,
@@ -32,10 +38,18 @@ export const initSuperSidebar = () => {
rootPath,
toggleNewNavEndpoint,
},
+ store: createStore({
+ searchPath,
+ issuesPath,
+ mrPath,
+ autocompletePath,
+ searchContext,
+ search: '',
+ }),
render(h) {
return h(SuperSidebar, {
props: {
- sidebarData: JSON.parse(sidebar),
+ sidebarData,
},
});
},
diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js
index 388e7c92f03..4211b9578a2 100644
--- a/app/assets/javascripts/vue_shared/global_search/constants.js
+++ b/app/assets/javascripts/vue_shared/global_search/constants.js
@@ -27,6 +27,10 @@ export const KBD_HELP = sprintf(
{ kbdOpen: '<kbd>', kbdClose: '</kbd>' },
false,
);
+export const MIN_SEARCH_TERM = s__(
+ 'GlobalSearch|The search term must be at least 3 characters long.',
+);
+
export const SCOPED_SEARCH_ITEM_ARIA_LABEL = s__('GlobalSearch| %{search} %{description} %{scope}');
export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index d119cdc2785..7657bf8567b 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -1,7 +1,8 @@
<script>
-import { GlButton, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
@@ -24,6 +25,7 @@ import WorkItemTreeChildren from './work_item_tree_children.vue';
export default {
components: {
+ GlLabel,
GlLink,
GlButton,
GlIcon,
@@ -71,6 +73,12 @@ export default {
};
},
computed: {
+ labels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
+ },
+ allowsScopedLabels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
+ },
canHaveChildren() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
},
@@ -166,6 +174,9 @@ export default {
this.isLoadingChildren = false;
}
},
+ showScopedLabel(label) {
+ return isScopedLabel(label) && this.allowsScopedLabels;
+ },
},
};
</script>
@@ -190,66 +201,72 @@ export default {
@click="toggleItem"
/>
<div
- class="work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-rounded-base"
- :class="[hasMetadata ? 'gl-py-3' : 'gl-py-0']"
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-rounded-base"
data-testid="links-child"
>
- <span
- :id="`stateIcon-${childItem.id}`"
- class="gl-cursor-help gl-mr-3 gl-line-height-32"
- :class="{ 'gl-display-flex': hasMetadata }"
- data-testid="item-status-icon"
- >
- <gl-icon
- class="gl-text-secondary"
- :class="iconClass"
- :name="iconName"
- :aria-label="stateTimestampTypeText"
- />
- </span>
- <div
- class="gl-display-flex gl-flex-grow-1"
- :class="{
- 'gl-flex-direction-column gl-align-items-flex-start': hasMetadata,
- 'gl-align-items-center': !hasMetadata,
- }"
- >
- <div class="gl-display-flex">
- <rich-timestamp-tooltip
- :target="`stateIcon-${childItem.id}`"
- :raw-timestamp="stateTimestamp"
- :timestamp-type-text="stateTimestampTypeText"
+ <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
+ <div
+ class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
+ >
+ <div class="item-title gl-display-flex gl-gap-3 gl-min-w-0">
+ <span
+ :id="`stateIcon-${childItem.id}`"
+ class="gl-cursor-help"
+ data-testid="item-status-icon"
+ >
+ <gl-icon
+ class="gl-text-secondary"
+ :class="iconClass"
+ :name="iconName"
+ :aria-label="stateTimestampTypeText"
+ />
+ </span>
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <span v-if="childItem.confidential">
+ <gl-icon
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="__('Confidential')"
+ :title="__('Confidential')"
+ />
+ </span>
+ <gl-link
+ :href="childPath"
+ class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ data-testid="item-title"
+ @click="$emit('click', $event)"
+ @mouseover="$emit('mouseover')"
+ @mouseout="$emit('mouseout')"
+ >
+ {{ childItem.title }}
+ </gl-link>
+ </div>
+ <work-item-link-child-metadata
+ v-if="hasMetadata"
+ :metadata-widgets="metadataWidgets"
+ class="gl-ml-6 ml-xl-0"
/>
- <gl-icon
- v-if="childItem.confidential"
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-mr-2 gl-text-orange-500"
- data-testid="confidential-icon"
- :aria-label="__('Confidential')"
- :title="__('Confidential')"
+ </div>
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <gl-label
+ v-for="label in labels"
+ :key="label.id"
+ :title="label.title"
+ :background-color="label.color"
+ :description="label.description"
+ :scoped="showScopedLabel(label)"
+ class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
+ tooltip-placement="top"
/>
- <gl-link
- :href="childPath"
- class="gl-overflow-wrap-break gl-line-height-normal gl-text-black-normal! gl-font-weight-bold"
- data-testid="item-title"
- @click="$emit('click', $event)"
- @mouseover="$emit('mouseover')"
- @mouseout="$emit('mouseout')"
- >
- {{ childItem.title }}
- </gl-link>
</div>
- <work-item-link-child-metadata
- v-if="hasMetadata"
- :metadata-widgets="metadataWidgets"
- class="gl-mt-1"
- />
</div>
- <div
- v-if="canUpdate"
- class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
- >
+ <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
<work-item-links-menu
:work-item-id="childItem.id"
:parent-work-item-id="issuableGid"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
index 80802cb3858..ddeac2b92ae 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
@@ -1,16 +1,14 @@
<script>
-import { GlLabel, GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import { isScopedLabel } from '~/lib/utils/common_utils';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
-import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS } from '../../constants';
+import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ASSIGNEES } from '../../constants';
export default {
components: {
- GlLabel,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
@@ -33,12 +31,6 @@ export default {
assignees() {
return this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes || [];
},
- labels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
- },
- allowsScopedLabels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
- },
assigneesCollapsedTooltip() {
if (this.assignees.length > 2) {
return sprintf(s__('WorkItem|%{count} more assignees'), {
@@ -56,21 +48,16 @@ export default {
return '';
},
},
- methods: {
- showScopedLabel(label) {
- return isScopedLabel(label) && this.allowsScopedLabels;
- },
- },
};
</script>
<template>
- <div class="gl-display-flex gl-flex-wrap gl-align-items-center">
+ <div class="gl-display-flex gl-md-justify-content-end gl-gap-3">
<slot></slot>
<item-milestone
v-if="milestone"
:milestone="milestone"
- class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-line-height-normal gl-text-secondary! gl-cursor-help! gl-text-decoration-none!"
+ class="gl-display-flex gl-align-items-center gl-max-w-15 gl-font-sm gl-line-height-normal gl-text-secondary! gl-cursor-help! gl-text-decoration-none!"
/>
<gl-avatars-inline
v-if="assignees.length"
@@ -81,7 +68,6 @@ export default {
badge-tooltip-prop="name"
:badge-sr-only-text="assigneesCollapsedTooltip"
:class="assigneesContainerClass"
- class="gl-mr-5"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
@@ -89,18 +75,6 @@ export default {
</gl-avatar-link>
</template>
</gl-avatars-inline>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap">
- <gl-label
- v-for="label in labels"
- :key="label.id"
- :title="label.title"
- :background-color="label.color"
- :description="label.description"
- :scoped="showScopedLabel(label)"
- class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
- tooltip-placement="top"
- />
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index 97eaf2c0422..b72de98199e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -186,7 +186,7 @@ export default {
</template>
<template #body>
<div v-if="!isShownAddForm && children.length === 0" data-testid="tree-empty">
- <p class="gl-mb-3">
+ <p class="gl-mb-0 gl-py-2 gl-ml-3 gl-text-gray-500">
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
</p>
</div>
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index bf447d417e6..bd0400cdaa3 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -220,3 +220,38 @@
}
}
}
+
+.global-search-modal {
+ padding: 3rem 0.5rem 0;
+
+ &.gl-modal .modal-dialog {
+ align-items: flex-start;
+ }
+
+ @include gl-media-breakpoint-up(sm) {
+ padding: 5rem 1rem 0;
+ }
+
+ // This is a temporary workaround!
+ // the button in GitLab UI Search components need to be updated to not be the small size
+ // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
+ .gl-search-box-by-type-clear.btn-sm {
+ padding: 0.5rem !important;
+ }
+
+ .is-searching {
+ .in-search-scope-help {
+ position: absolute;
+ top: 0.625rem;
+ right: 2.5rem;
+ }
+ }
+
+ .gl-search-box-by-type-input-borderless {
+ @include gl-rounded-base;
+ }
+
+ .global-search-results {
+ max-height: 30rem;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 00c86c46ac8..5f6883623b2 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -87,19 +87,6 @@
}
}
-.work-item-link-child {
- @include gl-border-1;
- @include gl-border-solid;
- @include gl-border-transparent;
- @include gl-rounded-base;
-
- &:hover,
- &:focus-within {
- @include gl-bg-white;
- @include gl-border-gray-50;
- }
-}
-
// sticky error placement for errors in modals , by default it is 83px for full view
#work-item-detail-modal {
.flash-container.flash-container-page.sticky {
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index 7e1ba49d442..d33d3b046e3 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -53,6 +53,8 @@ module Integrations
:issues_events,
:issues_url,
:jenkins_url,
+ :jira_issue_prefix,
+ :jira_issue_regex,
:jira_issue_transition_automatic,
:jira_issue_transition_id,
:manual_configuration,
diff --git a/app/controllers/concerns/kas_cookie.rb b/app/controllers/concerns/kas_cookie.rb
index ef58ab1972b..c66bf7c9e8c 100644
--- a/app/controllers/concerns/kas_cookie.rb
+++ b/app/controllers/concerns/kas_cookie.rb
@@ -3,6 +3,18 @@
module KasCookie
extend ActiveSupport::Concern
+ included do
+ content_security_policy_with_context do |p|
+ next unless ::Gitlab::Kas::UserAccess.enabled?
+
+ kas_url = ::Gitlab::Kas.tunnel_url
+ next if URI(kas_url).host == ::Gitlab.config.gitlab.host # already allowed, no need for exception
+
+ kas_url += '/' unless kas_url.end_with?('/')
+ p.connect_src(*Array.wrap(p.directives['connect-src']), kas_url)
+ end
+ end
+
def set_kas_cookie
return unless ::Gitlab::Kas::UserAccess.enabled?
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 95deacdc5b9..80c65948fff 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -14,3 +14,5 @@ class Dashboard::ApplicationController < ApplicationController
@projects ||= current_user.authorized_projects.sorted_by_updated_desc.non_archived
end
end
+
+Dashboard::ApplicationController.prepend_mod
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a204023e34d..0851d2ef3e2 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -264,6 +264,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
status = merge!
+ Gitlab::ApplicationContext.push(merge_action_status: status.to_s)
+
if @merge_request.merge_error
render json: { status: status, merge_error: @merge_request.merge_error }
else
diff --git a/app/graphql/resolvers/data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer_resolver.rb
index 1a240d2811f..ed97de0a256 100644
--- a/app/graphql/resolvers/data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer_resolver.rb
@@ -38,16 +38,18 @@ module Resolvers
def resolve(**_args)
return unless Feature.enabled?(:data_transfer_monitoring)
+ # TODO: This is mock data as this feature is in development
+ # Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330
start_date = Date.new(2023, 0o1, 0o1)
date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
- nodes = 0.upto(3).map do |i|
+ nodes = 0.upto(11).map do |i|
{
date: date_for_index.call(i),
- repository_egress: 250_000,
- artifacts_egress: 250_000,
- packages_egress: 250_000,
- registry_egress: 250_000
+ repository_egress: rand(70000..550000),
+ artifacts_egress: rand(70000..550000),
+ packages_egress: rand(70000..550000),
+ registry_egress: rand(70000..550000)
}
end
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 1efbd4acdd9..56f4187ae42 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -81,7 +81,8 @@ module SidebarsHelper
gitlab_com_and_canary: Gitlab.com_and_canary?,
canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url,
current_context: super_sidebar_current_context(project: project, group: group),
- context_switcher_links: context_switcher_links
+ context_switcher_links: context_switcher_links,
+ search: search_data
}
end
@@ -112,6 +113,16 @@ module SidebarsHelper
private
+ def search_data
+ {
+ search_path: search_path,
+ issues_path: issues_dashboard_path,
+ mr_path: merge_requests_dashboard_path,
+ autocomplete_path: search_autocomplete_path,
+ search_context: header_search_context
+ }
+ end
+
def user_status_menu_data(user)
{
can_update: can?(user, :update_user_status, user),
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index b05beb6c764..e68574c5fca 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -29,7 +29,7 @@ module Mentionable
def self.external_pattern
strong_memoize(:external_pattern) do
- issue_pattern = Integrations::BaseIssueTracker.reference_pattern
+ issue_pattern = Integrations::BaseIssueTracker.base_reference_pattern
link_patterns = URI::DEFAULT_PARSER.make_regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
index e0994305e9d..7a54d354007 100644
--- a/app/models/integrations/base_issue_tracker.rb
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -14,7 +14,7 @@ module Integrations
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overridden patterns. See ReferenceRegexes.external_pattern
- def self.reference_pattern(only_long: false)
+ def self.base_reference_pattern(only_long: false)
if only_long
/(\b[A-Z][A-Z0-9_]*-)#{Gitlab::Regex.issue}/
else
@@ -22,6 +22,10 @@ module Integrations
end
end
+ def reference_pattern(only_long: false)
+ self.class.base_reference_pattern(only_long: only_long)
+ end
+
def handle_properties
# this has been moved from initialize_properties and should be improved
# as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb
index 1b86ef73c85..003c896704a 100644
--- a/app/models/integrations/ewm.rb
+++ b/app/models/integrations/ewm.rb
@@ -6,7 +6,7 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def self.reference_pattern(only_long: true)
+ def reference_pattern(only_long: true)
@reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index a1cdd55ceae..aa3730d9559 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -23,6 +23,8 @@ module Integrations
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
+ validates :jira_issue_prefix, untrusted_regexp: true, length: { maximum: 255 }, if: :activated?
+ validates :jira_issue_regex, untrusted_regexp: true, length: { maximum: 255 }, if: :activated?
validates :jira_issue_transition_id,
format: {
@@ -72,6 +74,18 @@ module Integrations
non_empty_password_help: -> { s_('JiraService|Leave blank to use your current password or API token.') },
help: -> { s_('JiraService|Password for the server version or an API token for the cloud version') }
+ field :jira_issue_regex,
+ section: SECTION_TYPE_CONFIGURATION,
+ required: false,
+ title: -> { s_('JiraService|Jira issue regex') },
+ help: -> { s_('JiraService|Use regular expression to match Jira issue keys.') }
+
+ field :jira_issue_prefix,
+ section: SECTION_TYPE_CONFIGURATION,
+ required: false,
+ title: -> { s_('JiraService|Jira issue prefix') },
+ help: -> { s_('JiraService|Use a prefix to match Jira issue keys.') }
+
field :jira_issue_transition_id, api_only: true
# TODO: we can probably just delegate as part of
@@ -90,8 +104,8 @@ module Integrations
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def self.reference_pattern(only_long: true)
- @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
+ def reference_pattern(only_long: true)
+ @reference_pattern ||= jira_issue_match_regex
end
def self.valid_jira_cloud_url?(url)
@@ -166,6 +180,11 @@ module Integrations
type: SECTION_TYPE_JIRA_TRIGGER,
title: _('Trigger'),
description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
+ },
+ {
+ type: SECTION_TYPE_CONFIGURATION,
+ title: _('Jira issue matching'),
+ description: s_('Configure custom rules for Jira issue key matching')
}
]
@@ -325,6 +344,12 @@ module Integrations
private
+ def jira_issue_match_regex
+ match_regex = (jira_issue_regex.presence || Gitlab::Regex.jira_issue_key_regex)
+
+ /\b#{jira_issue_prefix}(?<issue>#{match_regex})/
+ end
+
def parse_project_from_issue_key(issue_key)
issue_key.gsub(Gitlab::Regex.jira_issue_key_project_key_extraction_regex, '')
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index fa719f925ed..15246a37aa7 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -7,12 +7,11 @@ module Integrations
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
- def self.reference_pattern(only_long: false)
- if only_long
- /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/
- else
- /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})/
- end
+ def reference_pattern(only_long: false)
+ return @reference_pattern if defined?(@reference_pattern)
+
+ regex_suffix = "|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})"
+ @reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/
end
def title
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b7290fa1842..160894a0c4f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -57,11 +57,10 @@ class Issue < ApplicationRecord
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
- belongs_to :iteration, foreign_key: 'sprint_id'
belongs_to :work_item_type, class_name: 'WorkItems::Type', inverse_of: :work_items
- belongs_to :moved_to, class_name: 'Issue'
- has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
+ belongs_to :moved_to, class_name: 'Issue', inverse_of: :moved_from
+ has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id, inverse_of: :moved_to
has_internal_id :iid, scope: :namespace, track_if: -> { !importing? }, init: ->(issue, scope) do
# we need this init for the case where the IID allocation in internal_ids#last_value
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
deleted file mode 100644
index ebec24731ed..00000000000
--- a/app/models/iteration.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-# Placeholder class for model that is implemented in EE
-class Iteration < ApplicationRecord
- include IgnorableColumns
-
- self.table_name = 'sprints'
-
- def self.reference_prefix
- '*iteration:'
- end
-
- def self.reference_pattern
- nil
- end
-end
-
-Iteration.prepend_mod_with('Iteration')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 85e95a556a8..c1511ee1233 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -44,7 +44,6 @@ class MergeRequest < ApplicationRecord
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
- belongs_to :iteration, foreign_key: 'sprint_id'
has_internal_id :iid, scope: :target_project, track_if: -> { !importing? },
init: ->(mr, scope) do
diff --git a/app/models/project.rb b/app/models/project.rb
index 8a8e4848eb1..03aa131e71b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1625,7 +1625,7 @@ class Project < ApplicationRecord
end
def external_issue_reference_pattern
- external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
+ external_issue_tracker.reference_pattern(only_long: issues_enabled?)
end
def default_issues_tracker?