diff --git a/Gemfile.lock b/Gemfile.lock index d0add73fd52aab149b9c546ceeed77a6c682fd02..3dc9764ccd1751df412e2e5fe300a7de1297e1e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,15 @@ -PATH - remote: vendor/gems/error_tracking_open_api - specs: - error_tracking_open_api (1.0.0) - typhoeus (~> 1.0, >= 1.0.1) - PATH remote: vendor/gems/devise-pbkdf2-encryptable specs: devise-pbkdf2-encryptable (0.0.0) devise (~> 4.0) +PATH + remote: vendor/gems/error_tracking_open_api + specs: + error_tracking_open_api (1.0.0) + typhoeus (~> 1.0, >= 1.0.1) + PATH remote: vendor/gems/ipynbdiff specs: diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index adf304aebc76e403dabd51a09175bf0d634ca257..51f9ce9e00e3d672cb3b3c2aded5ee445110abb8 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -1,8 +1,15 @@ <script> -import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui'; +import { + GlSearchBoxByType, + GlOutsideDirective as Outside, + GlIcon, + GlToken, + GlResizeObserverDirective, +} from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; import { debounce } from 'lodash'; import { visitUrl } from '~/lib/utils/url_utility'; +import { truncate } from '~/lib/utils/text_utility'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; @@ -12,6 +19,8 @@ import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, SEARCH_SHORTCUTS_MIN_CHARACTERS, + SCOPE_TOKEN_MAX_LENGTH, + INPUT_FIELD_PADDING, } from '../constants'; import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from './header_search_default_items.vue'; @@ -34,14 +43,17 @@ export default { 'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.', ), searchResultsLoading: s__('GlobalSearch|Search results are loading'), + searchResultsScope: s__('GlobalSearch|in %{scope}'), }, - directives: { Outside }, + directives: { Outside, GlResizeObserverDirective }, components: { GlSearchBoxByType, HeaderSearchDefaultItems, HeaderSearchScopedItems, HeaderSearchAutocompleteItems, DropdownKeyboardNavigation, + GlIcon, + GlToken, }, data() { return { @@ -50,8 +62,8 @@ export default { }; }, computed: { - ...mapState(['search', 'loading']), - ...mapGetters(['searchQuery', 'searchOptions', 'autocompleteGroupedSearchOptions']), + ...mapState(['search', 'loading', 'searchContext']), + ...mapGetters(['searchQuery', 'searchOptions']), searchText: { get() { return this.search; @@ -70,16 +82,17 @@ export default { return Boolean(gon?.current_username); }, showSearchDropdown() { - const hasResultsUnderMinCharacters = - this.searchText?.length === 1 ? this?.autocompleteGroupedSearchOptions?.length > 0 : true; + if (!this.showDropdown || !this.isLoggedIn) { + return false; + } - return this.showDropdown && this.isLoggedIn && hasResultsUnderMinCharacters; + return this.searchOptions?.length > 0; }, showDefaultItems() { return !this.searchText; }, - showShortcuts() { - return this.searchText && this.searchText?.length >= SEARCH_SHORTCUTS_MIN_CHARACTERS; + showScopes() { + return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS; }, defaultIndex() { if (this.showDefaultItems) { @@ -88,11 +101,11 @@ export default { return FIRST_DROPDOWN_INDEX; }, + searchInputDescribeBy() { if (this.isLoggedIn) { return this.$options.i18n.searchInputDescribeByWithDropdown; } - return this.$options.i18n.searchInputDescribeByNoDropdown; }, dropdownResultsDescription() { @@ -112,8 +125,26 @@ export default { count: this.searchOptions.length, }); }, - headerSearchActivityDescriptor() { - return this.showDropdown ? 'is-active' : 'is-not-active'; + searchBarStateIndicator() { + const hasIcon = + this.searchContext?.project || this.searchContext?.group ? 'has-icon' : 'has-no-icon'; + const isSearching = this.showScopes ? 'is-searching' : 'is-not-searching'; + const isActive = this.showSearchDropdown ? 'is-active' : 'is-not-active'; + return `${isActive} ${isSearching} ${hasIcon}`; + }, + searchBarItem() { + return this.searchOptions?.[0]; + }, + infieldHelpContent() { + return this.searchBarItem?.scope || this.searchBarItem?.description; + }, + infieldHelpIcon() { + return this.searchBarItem?.icon; + }, + scopeTokenTitle() { + return sprintf(this.$options.i18n.searchResultsScope, { + scope: this.infieldHelpContent, + }); }, }, methods: { @@ -127,6 +158,9 @@ export default { this.$emit('toggleDropdown', this.showDropdown); }, 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) { @@ -136,8 +170,19 @@ export default { this.fetchAutocompleteOptions(); } }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), + getTruncatedScope(scope) { + return truncate(scope, SCOPE_TOKEN_MAX_LENGTH); + }, + observeTokenWidth({ contentRect: { width } }) { + const inputField = this.$refs?.searchInputBox?.$el?.querySelector('input'); + if (!inputField) { + return; + } + inputField.style.paddingRight = `${width + INPUT_FIELD_PADDING}px`; + }, }, SEARCH_BOX_INDEX, + FIRST_DROPDOWN_INDEX, SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, }; @@ -149,10 +194,12 @@ export default { role="search" :aria-label="$options.i18n.searchGitlab" class="header-search gl-relative gl-rounded-base gl-w-full" - :class="headerSearchActivityDescriptor" + :class="searchBarStateIndicator" + data-testid="header-search-form" > <gl-search-box-by-type id="search" + ref="searchInputBox" v-model="searchText" role="searchbox" class="gl-z-index-1" @@ -165,7 +212,28 @@ export default { @click="openDropdown" @input="getAutocompleteOptions" @keydown.enter.stop.prevent="submitSearch" + @keydown.esc.stop.prevent="closeDropdown" /> + <gl-token + v-if="showScopes" + 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.searchResultsScope, { + scope: infieldHelpContent, + }), + ) + }} + </gl-token> <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{ searchInputDescribeBy }}</span> @@ -187,7 +255,7 @@ export default { <dropdown-keyboard-navigation v-model="currentFocusIndex" :max="searchOptions.length - 1" - :min="$options.SEARCH_BOX_INDEX" + :min="$options.FIRST_DROPDOWN_INDEX" :default-index="defaultIndex" @tab="closeDropdown" /> @@ -197,7 +265,7 @@ export default { /> <template v-else> <header-search-scoped-items - v-if="showShortcuts" + v-if="showScopes" :current-focused-option="currentFocusedOption" /> <header-search-autocomplete-items :current-focused-option="currentFocusedOption" /> diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue index 34d1bd713997480638c3707aac63d12430224ca8..f5be1bcb78676d5bc21af2f183af23ef49dfb437 100644 --- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue +++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue @@ -1,13 +1,16 @@ <script> -import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { GlDropdownItem, GlIcon, GlToken } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; -import { __, sprintf } from '~/locale'; +import { s__, sprintf } from '~/locale'; +import { truncate } from '~/lib/utils/text_utility'; +import { SCOPE_TOKEN_MAX_LENGTH } from '../constants'; export default { name: 'HeaderSearchScopedItems', components: { GlDropdownItem, - GlDropdownDivider, + GlIcon, + GlToken, }, props: { currentFocusedOption: { @@ -25,12 +28,21 @@ export default { return this.currentFocusedOption?.html_id === option.html_id; }, ariaLabel(option) { - return sprintf(__('%{search} %{description} %{scope}'), { + return sprintf(s__('GlobalSearch| %{search} %{description} %{scope}'), { search: this.search, - description: option.description, + description: option.description || option.icon, scope: option.scope || '', }); }, + titleLabel(option) { + return sprintf(s__('GlobalSearch|in %{scope}'), { + search: this.search, + scope: option.scope || option.description, + }); + }, + getTruncatedScope(scope) { + return truncate(scope, SCOPE_TOKEN_MAX_LENGTH); + }, }, }; </script> @@ -42,18 +54,30 @@ export default { :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 aria-hidden="true"> - "<span class="gl-font-weight-bold">{{ search }}</span - >" {{ option.description }} - <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span> + <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" + > + <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-dropdown-divider v-if="autocompleteGroupedSearchOptions.length > 0" /> </div> </template> diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js index 045a552efb070e8c70993b46f979726c0fa38e47..c9b05c3deb5aa9a55b46aedceb71636a6245a270 100644 --- a/app/assets/javascripts/header_search/constants.js +++ b/app/assets/javascripts/header_search/constants.js @@ -10,15 +10,21 @@ export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a re export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created"); -export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab'); +export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|all GitLab'); -export const MSG_IN_GROUP = s__('GlobalSearch|in group'); +export const MSG_IN_GROUP = s__('GlobalSearch|group'); -export const MSG_IN_PROJECT = s__('GlobalSearch|in project'); +export const MSG_IN_PROJECT = s__('GlobalSearch|project'); -export const GROUPS_CATEGORY = 'Groups'; +export const ICON_PROJECT = 'project'; -export const PROJECTS_CATEGORY = 'Projects'; +export const ICON_GROUP = 'group'; + +export const ICON_SUBGROUP = 'subgroup'; + +export const GROUPS_CATEGORY = s__('GlobalSearch|Groups'); + +export const PROJECTS_CATEGORY = s__('GlobalSearch|Projects'); export const ISSUES_CATEGORY = 'Recent issues'; @@ -39,3 +45,7 @@ export const SEARCH_SHORTCUTS_MIN_CHARACTERS = 2; export const SEARCH_INPUT_DESCRIPTION = 'search-input-description'; export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description'; + +export const SCOPE_TOKEN_MAX_LENGTH = 36; + +export const INPUT_FIELD_PADDING = 52; diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js index 7d08aa859fb9560f4183e656e8c2611f07767d62..da7bccd35c048f9bd47543d73a3dfa8214a7291f 100644 --- a/app/assets/javascripts/header_search/store/getters.js +++ b/app/assets/javascripts/header_search/store/getters.js @@ -7,9 +7,13 @@ import { MSG_MR_ASSIGNED_TO_ME, MSG_MR_IM_REVIEWER, MSG_MR_IVE_CREATED, - MSG_IN_PROJECT, - MSG_IN_GROUP, + ICON_GROUP, + ICON_SUBGROUP, + ICON_PROJECT, MSG_IN_ALL_GITLAB, + PROJECTS_CATEGORY, + GROUPS_CATEGORY, + SEARCH_SHORTCUTS_MIN_CHARACTERS, } from '../constants'; export const searchQuery = (state) => { @@ -149,7 +153,8 @@ export const scopedSearchOptions = (state, getters) => { options.push({ html_id: 'scoped-in-project', scope: state.searchContext.project?.name || '', - description: MSG_IN_PROJECT, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, url: getters.projectUrl, }); } @@ -158,7 +163,8 @@ export const scopedSearchOptions = (state, getters) => { options.push({ html_id: 'scoped-in-group', scope: state.searchContext.group?.name || '', - description: MSG_IN_GROUP, + scopeCategory: GROUPS_CATEGORY, + icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP, url: getters.groupUrl, }); } @@ -190,6 +196,7 @@ export const autocompleteGroupedSearchOptions = (state) => { results.push(groupedOptions[option.category]); } }); + return results; }; @@ -205,5 +212,9 @@ export const searchOptions = (state, getters) => { [], ); + if (state.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS) { + return sortedAutocompleteOptions; + } + return getters.scopedSearchOptions.concat(sortedAutocompleteOptions); }; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 8755db83d35a83216117451167a0825dfa0c90d3..e23c797156a4ae59800624dc721d81ba795943e3 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -4,7 +4,8 @@ $search-sidebar-min-width: 240px; $search-sidebar-max-width: 300px; $search-input-field-x-min-width: 200px; $search-input-field-min-width: 320px; -$search-input-field-max-width: 600px; +$search-input-field-max-width: 640px; +$search-keyboard-shortcut: '/'; $border-radius-medium: 3px; @@ -67,54 +68,58 @@ input[type='checkbox']:hover { } } -// 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 -.header-search .gl-search-box-by-type-clear.btn-sm { - padding: 0.5rem !important; -} - .header-search { min-width: $search-input-field-min-width; + // 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; + } + @include media-breakpoint-between(md, lg) { min-width: $search-input-field-x-min-width; } - input, - svg { - transition: border-color ease-in-out $default-transition-duration, - background-color ease-in-out $default-transition-duration; + &.is-active { + &.is-searching { + .in-search-scope-help { + position: absolute; + top: $gl-spacing-scale-2; + right: 2.125rem; + z-index: 2; + } + } + } + + &.is-not-searching { + .in-search-scope-help { + display: none; + } } &.is-not-active { - .btn.gl-clear-icon-button { + .btn.gl-clear-icon-button, + .in-search-scope-help { display: none; } &::after { - content: '/'; - display: inline-block; + content: $search-keyboard-shortcut; + transform: translateY(calc(50% - #{$gl-spacing-scale-2})); position: absolute; top: 0; - right: 8px; - transform: translateY(calc(50% - 4px)); - padding: 4px 5px; + right: $gl-spacing-scale-3; + padding: $gl-spacing-scale-2 5px; font-size: $gl-font-size-small; font-family: $monospace-font; line-height: 1; vertical-align: middle; border-width: 0; - border-style: solid; - border-image: none; border-radius: $border-radius-medium; box-shadow: none; - white-space: pre-wrap; box-sizing: border-box; - // Safari - word-wrap: break-word; - overflow-wrap: break-word; - word-break: keep-all; } } } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 4cefa60b12a1aac7b41ea598bb5e6f6a951ecdf8..1dcc34b213f3b738313316a47e674c0d59021c38 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -1504,7 +1504,7 @@ svg.s16 { vertical-align: -3px; } .header-content .header-search-new { - max-width: 600px; + max-width: 640px; } .header-search { min-width: 320px; @@ -1516,27 +1516,20 @@ svg.s16 { } .header-search.is-not-active::after { content: "/"; - display: inline-block; + transform: translateY(calc(50% - 0.25rem)); position: absolute; top: 0; - right: 8px; - transform: translateY(calc(50% - 4px)); - padding: 4px 5px; + right: 0.5rem; + padding: 0.25rem 5px; font-size: 12px; font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; line-height: 1; vertical-align: middle; border-width: 0; - border-style: solid; - border-image: none; border-radius: 3px; box-shadow: none; - white-space: pre-wrap; box-sizing: border-box; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: keep-all; } .search { margin: 0 8px; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index cb3c97f18a30051ecc4f7e451a0217ae2d6ae036..ea7323cae904050741b619b4e6fa5f4d3420ab08 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1489,7 +1489,7 @@ svg.s16 { vertical-align: -3px; } .header-content .header-search-new { - max-width: 600px; + max-width: 640px; } .header-search { min-width: 320px; @@ -1501,27 +1501,20 @@ svg.s16 { } .header-search.is-not-active::after { content: "/"; - display: inline-block; + transform: translateY(calc(50% - 0.25rem)); position: absolute; top: 0; - right: 8px; - transform: translateY(calc(50% - 4px)); - padding: 4px 5px; + right: 0.5rem; + padding: 0.25rem 5px; font-size: 12px; font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; line-height: 1; vertical-align: middle; border-width: 0; - border-style: solid; - border-image: none; border-radius: 3px; box-shadow: none; - white-space: pre-wrap; box-sizing: border-box; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: keep-all; } .search { margin: 0 8px; diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 126d2e0f82999f81c672ed572f9ca9b2db084629..ecbcaec27bcf0f5f22de8cd1603689cc972909a6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -170,7 +170,7 @@ def search_sort_options # search_context exposes a bit too much data to the frontend, this controls what data we share and when. def header_search_context {}.tap do |hash| - hash[:group] = { id: search_context.group.id, name: search_context.group.name } if search_context.for_group? + hash[:group] = { id: search_context.group.id, name: search_context.group.name, full_name: search_context.group.full_name } if search_context.for_group? hash[:group_metadata] = search_context.group_metadata if search_context.for_group? hash[:project] = { id: search_context.project.id, name: search_context.project.name } if search_context.for_project? diff --git a/ee/app/assets/stylesheets/startup/startup-dark.scss b/ee/app/assets/stylesheets/startup/startup-dark.scss index 4cefa60b12a1aac7b41ea598bb5e6f6a951ecdf8..1dcc34b213f3b738313316a47e674c0d59021c38 100644 --- a/ee/app/assets/stylesheets/startup/startup-dark.scss +++ b/ee/app/assets/stylesheets/startup/startup-dark.scss @@ -1504,7 +1504,7 @@ svg.s16 { vertical-align: -3px; } .header-content .header-search-new { - max-width: 600px; + max-width: 640px; } .header-search { min-width: 320px; @@ -1516,27 +1516,20 @@ svg.s16 { } .header-search.is-not-active::after { content: "/"; - display: inline-block; + transform: translateY(calc(50% - 0.25rem)); position: absolute; top: 0; - right: 8px; - transform: translateY(calc(50% - 4px)); - padding: 4px 5px; + right: 0.5rem; + padding: 0.25rem 5px; font-size: 12px; font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; line-height: 1; vertical-align: middle; border-width: 0; - border-style: solid; - border-image: none; border-radius: 3px; box-shadow: none; - white-space: pre-wrap; box-sizing: border-box; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: keep-all; } .search { margin: 0 8px; diff --git a/ee/app/assets/stylesheets/startup/startup-general.scss b/ee/app/assets/stylesheets/startup/startup-general.scss index cb3c97f18a30051ecc4f7e451a0217ae2d6ae036..ea7323cae904050741b619b4e6fa5f4d3420ab08 100644 --- a/ee/app/assets/stylesheets/startup/startup-general.scss +++ b/ee/app/assets/stylesheets/startup/startup-general.scss @@ -1489,7 +1489,7 @@ svg.s16 { vertical-align: -3px; } .header-content .header-search-new { - max-width: 600px; + max-width: 640px; } .header-search { min-width: 320px; @@ -1501,27 +1501,20 @@ svg.s16 { } .header-search.is-not-active::after { content: "/"; - display: inline-block; + transform: translateY(calc(50% - 0.25rem)); position: absolute; top: 0; - right: 8px; - transform: translateY(calc(50% - 4px)); - padding: 4px 5px; + right: 0.5rem; + padding: 0.25rem 5px; font-size: 12px; font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; line-height: 1; vertical-align: middle; border-width: 0; - border-style: solid; - border-image: none; border-radius: 3px; box-shadow: none; - white-space: pre-wrap; box-sizing: border-box; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: keep-all; } .search { margin: 0 8px; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 61b600d52a4d410561dbcaefc7b5476fa6e1d34d..b132952c92495a0fc2e90015660cb71a4fbdfa82 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -938,9 +938,6 @@ msgstr "" msgid "%{scope} results for term '%{term}'" msgstr "" -msgid "%{search} %{description} %{scope}" -msgstr "" - msgid "%{seconds}s" msgstr "" @@ -17690,9 +17687,15 @@ msgstr "" msgid "Global notification settings" msgstr "" +msgid "GlobalSearch| %{search} %{description} %{scope}" +msgstr "" + msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list." msgstr "" +msgid "GlobalSearch|Groups" +msgstr "" + msgid "GlobalSearch|Issues I've created" msgstr "" @@ -17708,6 +17711,9 @@ msgstr "" msgid "GlobalSearch|Merge requests that I'm a reviewer" msgstr "" +msgid "GlobalSearch|Projects" +msgstr "" + msgid "GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit." msgstr "" @@ -17732,13 +17738,16 @@ msgstr "" msgid "GlobalSearch|What are you searching for?" msgstr "" -msgid "GlobalSearch|in all GitLab" +msgid "GlobalSearch|all GitLab" +msgstr "" + +msgid "GlobalSearch|group" msgstr "" -msgid "GlobalSearch|in group" +msgid "GlobalSearch|in %{scope}" msgstr "" -msgid "GlobalSearch|in project" +msgid "GlobalSearch|project" msgstr "" msgid "Globally-allowed IP ranges" diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 7350a54e8df5963e8a2784ee362cfaead7dc4f57..1523586ab26feb601c130f3b511f825aedd1b884 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -153,6 +153,7 @@ it 'displays search options' do fill_in_search('test') + expect(page).to have_selector(scoped_search_link('test', search_code: true)) expect(page).to have_selector(scoped_search_link('test', group_id: group.id, search_code: true)) expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id, search_code: true)) @@ -167,6 +168,7 @@ it 'displays search options' do fill_in_search('test') + sleep 0.5 expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master')) expect(page).not_to have_selector(scoped_search_link('test', search_code: true, group_id: project.namespace_id, repository_ref: 'master')) expect(page).to have_selector(scoped_search_link('test', search_code: true, project_id: project.id, repository_ref: 'master')) @@ -184,7 +186,7 @@ fill_in_search('Feature') within(dashboard_search_options_popup_menu) do - expect(page).to have_text('"Feature" in all GitLab') + expect(page).to have_text('Feature in all GitLab') expect(page).to have_no_text('Feature Flags') end end diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index f0de5b083ae476a7da91658befda6ef2619dc14c..5f2b71a22c5c6a5c07bf01c11efc22a070fa9b95 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -1,22 +1,32 @@ -import { GlSearchBoxByType } from '@gitlab/ui'; +import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__, sprintf } from '~/locale'; import HeaderSearchApp from '~/header_search/components/app.vue'; import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; -import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants'; +import { + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, + SEARCH_BOX_INDEX, + ICON_PROJECT, + ICON_GROUP, + ICON_SUBGROUP, + SCOPE_TOKEN_MAX_LENGTH, +} from '~/header_search/constants'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { ENTER_KEY } from '~/lib/utils/keys'; import { visitUrl } from '~/lib/utils/url_utility'; +import { truncate } from '~/lib/utils/text_utility'; import { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, - MOCK_SORTED_AUTOCOMPLETE_OPTIONS, + MOCK_SEARCH_CONTEXT_FULL, } from '../mock_data'; Vue.use(Vuex); @@ -52,11 +62,26 @@ describe('HeaderSearchApp', () => { }); }; + const formatScopeName = (scopeName) => { + if (!scopeName) { + return false; + } + const searchResultsScope = s__('GlobalSearch|in %{scope}'); + return truncate( + sprintf(searchResultsScope, { + scope: scopeName, + }), + SCOPE_TOKEN_MAX_LENGTH, + ); + }; + afterEach(() => { wrapper.destroy(); }); + const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form'); const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findScopeToken = () => wrapper.findComponent(GlToken); const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu'); const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems); const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); @@ -106,53 +131,38 @@ describe('HeaderSearchApp', () => { }); describe.each` - search | showDefault | showScoped | showAutocomplete | showDropdownNavigation - ${null} | ${true} | ${false} | ${false} | ${true} - ${''} | ${true} | ${false} | ${false} | ${true} - ${'1'} | ${false} | ${false} | ${false} | ${false} - ${')'} | ${false} | ${false} | ${false} | ${false} - ${'t'} | ${false} | ${false} | ${true} | ${true} - ${'te'} | ${false} | ${true} | ${true} | ${true} - ${'tes'} | ${false} | ${true} | ${true} | ${true} - ${MOCK_SEARCH} | ${false} | ${true} | ${true} | ${true} - `( - 'Header Search Dropdown Items', - ({ search, showDefault, showScoped, showAutocomplete, showDropdownNavigation }) => { - describe(`when search is ${search}`, () => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent( - { search }, - { - autocompleteGroupedSearchOptions: () => - search.match(/^[A-Za-z]+$/g) ? MOCK_SORTED_AUTOCOMPLETE_OPTIONS : [], - }, - ); - findHeaderSearchInput().vm.$emit('click'); - }); + search | showDefault | showScoped | showAutocomplete + ${null} | ${true} | ${false} | ${false} + ${''} | ${true} | ${false} | ${false} + ${'t'} | ${false} | ${false} | ${true} + ${'te'} | ${false} | ${false} | ${true} + ${'tes'} | ${false} | ${true} | ${true} + ${MOCK_SEARCH} | ${false} | ${true} | ${true} + `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent({ search }, {}); + findHeaderSearchInput().vm.$emit('click'); + }); - it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { - expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); - }); + it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { + expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + }); - it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { - expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); - }); + it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { + expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + }); - it(`should${ - showAutocomplete ? '' : ' not' - } render the Autocomplete Dropdown Items`, () => { - expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); - }); + it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { + expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); + }); - it(`should${ - showDropdownNavigation ? '' : ' not' - } render the Dropdown Navigation Component`, () => { - expect(findDropdownKeyboardNavigation().exists()).toBe(showDropdownNavigation); - }); + it(`should render the Dropdown Navigation Component`, () => { + expect(findDropdownKeyboardNavigation().exists()).toBe(true); }); - }, - ); + }); + }); describe.each` username | showDropdown | expectedDesc @@ -185,12 +195,18 @@ describe('HeaderSearchApp', () => { `( 'Search Results Description', ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { - describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${ - Boolean(username) && showDropdown - }`, () => { + describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => { beforeEach(() => { window.gon.current_username = username; - createComponent({ search, loading }, { searchOptions: () => searchOptions }); + createComponent( + { + search, + loading, + }, + { + searchOptions: () => searchOptions, + }, + ); findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); @@ -200,6 +216,121 @@ describe('HeaderSearchApp', () => { }); }, ); + + describe('input box', () => { + describe.each` + search | searchOptions | hasToken + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true} + ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false} + ${'x'} | ${[]} | ${false} + `('token', ({ search, searchOptions, hasToken }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${ + searchOptions[0]?.html_id + }"`, () => { + expect(findScopeToken().exists()).toBe(hasToken); + }); + + it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${ + searchOptions[0]?.scope || searchOptions[0]?.description + }"`, () => { + expect(findScopeToken().exists() && findScopeToken().text()).toBe( + formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description), + ); + }); + }); + }); + + describe('form wrapper', () => { + describe.each` + searchContext | search | searchOptions + ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${[]} + `('', ({ searchContext, search, searchOptions }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + + createComponent({ search, searchContext }, { searchOptions: () => searchOptions }); + + findHeaderSearchInput().vm.$emit('click'); + }); + + const hasIcon = Boolean(searchContext?.group); + const isSearching = Boolean(search); + const isActive = Boolean(searchOptions.length > 0); + + it(`${hasIcon ? 'with' : 'without'} search context classes contain "${ + hasIcon ? 'has-icon' : 'has-no-icon' + }"`, () => { + const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isSearching ? 'with' : 'without'} search string classes contain "${ + isSearching ? 'is-searching' : 'is-not-searching' + }"`, () => { + const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isActive ? 'with' : 'without'} search results classes contain "${ + isActive ? 'is-active' : 'is-not-active' + }"`, () => { + const iconClassRegex = isActive ? 'is-active' : 'is-not-active'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + }); + }); + + describe.each` + search | searchOptions | hasIcon | iconName + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false} + `('token', ({ search, searchOptions, hasIcon, iconName }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`icon for data set type "${searchOptions[0]?.html_id}" ${ + hasIcon ? 'is' : 'is NOT' + } rendered`, () => { + expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon); + }); + + it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${ + searchOptions[0]?.html_id + }"`, () => { + expect( + findScopeToken().findComponent(GlIcon).exists() && + findScopeToken().findComponent(GlIcon).attributes('name'), + ).toBe(iconName); + }); + }); }); describe('events', () => { @@ -285,18 +416,20 @@ describe('HeaderSearchApp', () => { }); describe('computed', () => { - describe('currentFocusedOption', () => { - const MOCK_INDEX = 1; - + describe.each` + MOCK_INDEX | search + ${1} | ${null} + ${SEARCH_BOX_INDEX} | ${'test'} + ${2} | ${'test1'} + `('currentFocusedOption', ({ MOCK_INDEX, search }) => { beforeEach(() => { - createComponent(); + createComponent({ search }); window.gon.current_username = MOCK_USERNAME; findHeaderSearchInput().vm.$emit('click'); }); - it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, async () => { + it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]); }); }); @@ -308,15 +441,25 @@ describe('HeaderSearchApp', () => { createComponent(); }); - it('onKey-enter submits a search', async () => { + it('onKey-enter submits a search', () => { findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); - await nextTick(); - expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); }); }); + describe('with less than min characters and no dropdown results', () => { + beforeEach(() => { + createComponent({ search: 'x' }); + }); + + it('onKey-enter will NOT submit a search', () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + describe('with currentFocusedOption', () => { const MOCK_INDEX = 1; @@ -326,9 +469,9 @@ describe('HeaderSearchApp', () => { findHeaderSearchInput().vm.$emit('click'); }); - it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => { + it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url); }); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js index 8788fb23458b3ede29b6c3e8ee05c2e053aa3b2d..2db9f71d70267bbd1954c4f31c9dd7461594708f 100644 --- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -1,9 +1,11 @@ -import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { truncate } from '~/lib/utils/text_utility'; +import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants'; import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS, @@ -41,9 +43,12 @@ describe('HeaderSearchScopedItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findGlDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findScopeTokens = () => wrapper.findAllComponents(GlToken); + const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text())); + const findScopeTokensIcons = () => + findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon)); const findDropdownItemAriaLabels = () => findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); @@ -59,15 +64,31 @@ describe('HeaderSearchScopedItems', () => { }); it('renders titles correctly', () => { + findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH)); + }); + + it('renders scope names correctly', () => { const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`"${MOCK_SEARCH}" ${o.description} ${o.scope || ''}`), + truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH), ); - expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + + expect(findScopeTokensText()).toStrictEqual(expectedTitles); + }); + + it('renders scope icons correctly', () => { + findScopeTokensIcons().forEach((icon, i) => { + const w = icon.wrappers[0]; + expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon); + }); + }); + + it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => { + expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false); }); it('renders aria-labels correctly', () => { const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`), + trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`), ); expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); }); @@ -98,21 +119,5 @@ describe('HeaderSearchScopedItems', () => { }); }); }); - - describe.each` - autosuggestResults | showDivider - ${[]} | ${false} - ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${true} - `('scoped search items', ({ autosuggestResults, showDivider }) => { - describe(`when when we have ${autosuggestResults.length} auto-sugest results`, () => { - beforeEach(() => { - createComponent({}, { autocompleteGroupedSearchOptions: () => autosuggestResults }, {}); - }); - - it(`divider should${showDivider ? '' : ' not'} be shown`, () => { - expect(findGlDropdownDivider().exists()).toBe(showDivider); - }); - }); - }); }); }); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js index b6f0fdcc29d601ef0d68c16efdeec5dda1b0ac64..8ccd7fb17e3adb23512f8fc545315fe1aaa2716a 100644 --- a/spec/frontend/header_search/mock_data.js +++ b/spec/frontend/header_search/mock_data.js @@ -4,9 +4,12 @@ import { MSG_MR_ASSIGNED_TO_ME, MSG_MR_IM_REVIEWER, MSG_MR_IVE_CREATED, - MSG_IN_PROJECT, - MSG_IN_GROUP, MSG_IN_ALL_GITLAB, + PROJECTS_CATEGORY, + ICON_PROJECT, + GROUPS_CATEGORY, + ICON_GROUP, + ICON_SUBGROUP, } from '~/header_search/constants'; export const MOCK_USERNAME = 'anyone'; @@ -27,12 +30,24 @@ export const MOCK_PROJECT = { path: '/mock-project', }; +export const MOCK_PROJECT_LONG = { + id: 124, + name: 'Mock Project Name That Is Ridiculously Long And It Goes Forever', + path: '/mock-project-name-that-is-ridiculously-long-and-it-goes-forever', +}; + export const MOCK_GROUP = { id: 321, name: 'MockGroup', path: '/mock-group', }; +export const MOCK_SUBGROUP = { + id: 322, + name: 'MockSubGroup', + path: `${MOCK_GROUP}/mock-subgroup`, +}; + export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test'; export const MOCK_SEARCH = 'test'; @@ -44,6 +59,20 @@ export const MOCK_SEARCH_CONTEXT = { group_metadata: {}, }; +export const MOCK_SEARCH_CONTEXT_FULL = { + group: { + id: 31, + name: 'testGroup', + full_name: 'testGroup', + }, + group_metadata: { + group_path: 'testGroup', + name: 'testGroup', + issues_path: '/groups/testGroup/-/issues', + mr_path: '/groups/testGroup/-/merge_requests', + }, +}; + export const MOCK_DEFAULT_SEARCH_OPTIONS = [ { html_id: 'default-issues-assigned', @@ -76,13 +105,51 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [ { html_id: 'scoped-in-project', scope: MOCK_PROJECT.name, - description: MSG_IN_PROJECT, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT.path, + }, + { + html_id: 'scoped-in-project-long', + scope: MOCK_PROJECT_LONG.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT_LONG.path, + }, + { + html_id: 'scoped-in-group', + scope: MOCK_GROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, + url: MOCK_GROUP.path, + }, + { + html_id: 'scoped-in-subgroup', + scope: MOCK_SUBGROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_SUBGROUP, + url: MOCK_SUBGROUP.path, + }, + { + html_id: 'scoped-in-all', + description: MSG_IN_ALL_GITLAB, + url: MOCK_ALL_PATH, + }, +]; + +export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [ + { + html_id: 'scoped-in-project', + scope: MOCK_PROJECT.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, url: MOCK_PROJECT.path, }, { html_id: 'scoped-in-group', scope: MOCK_GROUP.name, - description: MSG_IN_GROUP, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, url: MOCK_GROUP.path, }, { diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js index d3510de14397b8f77421feb98fe1be763aee1330..c76be3c03602625f7b773383fb0699c9ccbd08ad 100644 --- a/spec/frontend/header_search/store/getters_spec.js +++ b/spec/frontend/header_search/store/getters_spec.js @@ -9,6 +9,7 @@ import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS_DEF, MOCK_PROJECT, MOCK_GROUP, MOCK_ALL_PATH, @@ -284,7 +285,7 @@ describe('Header Search Store Getters', () => { it('returns the correct array', () => { expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual( - MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS_DEF, ); }); }); @@ -308,6 +309,11 @@ describe('Header Search Store Getters', () => { ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS} ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} + ${1} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'('} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'t'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'te'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'tes'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} `( 'searchOptions', ({ diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 4117d577f20be8c23a0078a6414e9b44d7f51ed2..1ead1fc9b8bee308b28c3e36bf755eefe64ba76c 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -741,7 +741,7 @@ def simple_sanitize(str) let(:for_group) { true } it 'adds the :group and :group_metadata correctly to hash' do - expect(header_search_context[:group]).to eq({ id: group.id, name: group.name }) + expect(header_search_context[:group]).to eq({ id: group.id, name: group.name, full_name: group.full_name }) expect(header_search_context[:group_metadata]).to eq(group_metadata) end