diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js index f6f4e36e43aef15ec2fd38795c9475c4dca860d8..7fbc29ee951d617cc291f1e74c3dbf922b20f357 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js @@ -60,3 +60,17 @@ export const GROUP_TITLES = { }; export const MAX_ROWS = 20; + +export const OVERLAY_CHANGE_CONTEXT = s__('GlobalSearch|Change context %{kbdStart}↵%{kbdEnd}'); +export const OVERLAY_SEARCH = s__('GlobalSearch|Search %{kbdStart}↵%{kbdEnd}'); +export const OVERLAY_CREATE_IN_SHELL_UI = s__( + 'GlobalSearch|Create in %{namespace} %{kbdStart}↵%{kbdEnd}', +); + +export const OVERLAY_PROFILE = s__('GlobalSearch|Go to profile %{kbdStart}↵%{kbdEnd}'); + +export const OVERLAY_PROJECT = s__('GlobalSearch|Go to project %{kbdStart}↵%{kbdEnd}'); + +export const OVERLAY_FILE = s__('GlobalSearch|Go to file %{kbdStart}↵%{kbdEnd}'); + +export const OVERLAY_GOTO = s__('GlobalSearch|Go to %{kbdStart}↵%{kbdEnd}'); diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue index 28e50dceb482c87d46fc21b66057b1b42a030292..6c57efe66ee677fc4b7e3a07cd93564293490e02 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue @@ -36,7 +36,7 @@ export default { <style scoped> .fake-input { - top: 18px; + top: 14px; left: 39px; } </style> 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 e6137bda4011daaa24566e0ed5f10d976d566834..f3ddcbb80d0385c86a2536e2970548144e5ce2d5 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 @@ -2,8 +2,6 @@ import { GlSearchBoxByType, GlOutsideDirective as Outside, - GlIcon, - GlToken, GlTooltipDirective, GlResizeObserverDirective, GlModal, @@ -33,6 +31,7 @@ import { SEARCH_RESULTS_SCOPE, } from '~/vue_shared/global_search/constants'; import { darkModeEnabled } from '~/lib/utils/color_utils'; +import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue'; import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, @@ -73,8 +72,7 @@ export default { GlobalSearchDefaultItems, GlobalSearchScopedItems, GlobalSearchAutocompleteItems, - GlIcon, - GlToken, + ScrollScrim, GlModal, CommandPaletteItems, FakeSearchInput, @@ -102,7 +100,7 @@ export default { return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS; }, showScopedSearchItems() { - return this.searchTermOverMin && this.scopedSearchOptions.length > 1; + return this.searchTermOverMin && this.scopedSearchOptions.length > 0; }, searchResultsDescription() { if (this.showDefaultItems) { @@ -300,9 +298,9 @@ export default { <form role="search" :aria-label="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER" - class="gl-relative gl-rounded-base gl-w-full gl-pb-0" + class="gl-relative gl-rounded-lg gl-w-full gl-pb-0" > - <div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-3"> + <div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-2"> <gl-search-box-by-type id="search" ref="searchInput" @@ -317,22 +315,6 @@ export default { @keydown.enter.stop.prevent="submitSearch" @keydown="onKeydown" /> - <gl-token - v-if="showScopeToken" - v-gl-resize-observer-directive="observeTokenWidth" - class="search-scope-help gl-absolute gl-sm-display-block gl-display-none" - view-only - :title="searchScope" - > - <gl-icon - v-if="scopeTokenIcon" - class="gl-mr-2" - :aria-label="scopeTokenText" - :name="scopeTokenIcon" - :size="16" - /> - {{ truncatedSearchScope }} - </gl-token> <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only"> {{ $options.i18n.SEARCH_DESCRIBED_BY_WITH_RESULTS }} </span> @@ -355,22 +337,24 @@ export default { </span> <div ref="resultsList" - class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-3" + class="global-search-results gl-overflow-y-auto gl-w-full gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden gl-pb-3" @keydown="onKeydown" > - <command-palette-items - v-if="isCommandMode" - :search-query="commandPaletteQuery" - :handle="searchTextFirstChar" - @updated="highlightFirstCommand" - /> + <scroll-scrim class="gl-flex-grow-1" data-testid="nav-container"> + <command-palette-items + v-if="isCommandMode" + :search-query="commandPaletteQuery" + :handle="searchTextFirstChar" + @updated="highlightFirstCommand" + /> - <global-search-default-items v-else-if="showDefaultItems" /> + <global-search-default-items v-else-if="showDefaultItems" /> - <template v-else> - <global-search-scoped-items v-if="showScopedSearchItems" /> - <global-search-autocomplete-items /> - </template> + <template v-else> + <global-search-autocomplete-items /> + <global-search-scoped-items v-if="showScopedSearchItems" /> + </template> + </scroll-scrim> </div> <template v-if="searchContext"> <input diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_hover_overlay.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_hover_overlay.vue new file mode 100644 index 0000000000000000000000000000000000000000..aeb400b2f1f573b0d1ab1f581e118cd166b564fd --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_hover_overlay.vue @@ -0,0 +1,32 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; + +export default { + name: 'SearchResultHoverLayover', + components: { + GlSprintf, + }, + props: { + textMessage: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <span + class="show-hover-layover-hint gl-opacity-0 gl-display-flex gl-justify-content-end gl-align-items-center" + > + <span class="gl-pr-2 gl-text-gray-700" data-testid="overlayMessage"> + <gl-sprintf :message="textMessage"> + <template #kbd="{ content }"> + <kbd class="gl-font-base gl-pb-3 vertical-align-normalization gl-vertical-align-middle">{{ + content + }}</kbd> + </template> + </gl-sprintf> + </span> + </span> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_no_results.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_no_results.vue new file mode 100644 index 0000000000000000000000000000000000000000..afd5d1d2a085e5ba4d72df18924d09277c6d1dc7 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_no_results.vue @@ -0,0 +1,18 @@ +<script> +import { NO_SEARCH_RESULTS } from '~/vue_shared/global_search/constants'; + +export default { + name: 'GlobalSearchNoResults', + i18n: { + NO_SEARCH_RESULTS, + }, +}; +</script> + +<template> + <ul class="gl-m-0 gl-p-0 gl-list-style-none"> + <li class="command-palette-px gl-py-5 gl-text-body"> + {{ $options.i18n.NO_SEARCH_RESULTS }} + </li> + </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 1f5e7e45cc1fad65a97c5a43667f54f47e3ac1a4..fd8d965f44c0368116904877ddff8df634f3d7a5 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,21 +1,37 @@ <script> -import { GlIcon, GlToken, GlDisclosureDropdownGroup } from '@gitlab/ui'; +import { GlIcon, GlDisclosureDropdownGroup } from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapState, mapGetters } from 'vuex'; import { s__, sprintf } from '~/locale'; import { truncate } from '~/lib/utils/text_utility'; +import { OVERLAY_SEARCH } from '../command_palette/constants'; import { SCOPE_TOKEN_MAX_LENGTH } from '../constants'; +import SearchResultHoverLayover from './global_search_hover_overlay.vue'; export default { name: 'GlobalSearchScopedItems', components: { GlIcon, - GlToken, GlDisclosureDropdownGroup, + SearchResultHoverLayover, + }, + i18n: { + OVERLAY_SEARCH, }, computed: { ...mapState(['search']), ...mapGetters(['scopedSearchGroup']), + group() { + return { + name: this.scopedSearchGroup.name, + items: this.scopedSearchGroup.items.map((item) => ({ + ...item, + extraAttrs: { + class: 'show-hover-layover', + }, + })), + }; + }, }, methods: { titleLabel(item) { @@ -33,20 +49,20 @@ export default { <template> <div> - <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!"> + <ul class="gl-m-0 gl-p-0 gl-pb-2 gl-list-style-none" data-testid="scoped-items"> + <gl-disclosure-dropdown-group :group="group" 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 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 class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> + <span class="gl-display-flex gl-align-items-center"> + <gl-icon + name="search-results" + class="gl-flex-shrink-0 gl-mr-2 gl-pt-2 gl-mt-n2 gl-text-gray-500" + /> + <span class="gl-flex-grow-1"> + {{ item.scope || item.description }} + </span> </span> + <search-result-hover-layover :text-message="$options.i18n.OVERLAY_SEARCH" /> </span> </template> </gl-disclosure-dropdown-group> 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 79be56f1427acba95b37c37dba21a81813d23cf7..731dff72ad25eaf936b1de0a442c2b35f8332e85 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,5 +1,6 @@ import { omitBy, isNil } from 'lodash'; import { objectToQuery } from '~/lib/utils/url_utility'; +import { sprintf } from '~/locale'; import { ISSUES_CATEGORY, MERGE_REQUEST_CATEGORY, @@ -12,6 +13,15 @@ import { PROJECTS_CATEGORY, GROUPS_CATEGORY, SEARCH_RESULTS_ORDER, + COMMAND_PALETTE_SEARCH_SCOPE_HEADER, + COMMAND_PALETTE_PAGES_SCOPE_HEADER, + COMMAND_PALETTE_USERS_SCOPE_HEADER, + COMMAND_PALETTE_PROJECTS_SCOPE_HEADER, + COMMAND_PALETTE_FILES_SCOPE_HEADER, + COMMAND_PALETTE_PAGES_CHAR, + COMMAND_PALETTE_USERS_CHAR, + COMMAND_PALETTE_PROJECTS_CHAR, + COMMAND_PALETTE_FILES_CHAR, } from '~/vue_shared/global_search/constants'; import { getFormattedItem } from '../utils'; import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants'; @@ -208,8 +218,26 @@ export const scopedSearchOptions = (state, getters) => { }; export const scopedSearchGroup = (state, getters) => { - const items = getters.scopedSearchOptions?.length ? getters.scopedSearchOptions.slice(1) : []; - return { items }; + let name = sprintf(COMMAND_PALETTE_SEARCH_SCOPE_HEADER, { searchTerm: state.search }); + const items = getters.scopedSearchOptions?.length > 0 ? getters.scopedSearchOptions : []; + + switch (state.commandChar) { + case COMMAND_PALETTE_PAGES_CHAR: + name = sprintf(COMMAND_PALETTE_PAGES_SCOPE_HEADER, { searchTerm: state.search }); + break; + case COMMAND_PALETTE_USERS_CHAR: + name = sprintf(COMMAND_PALETTE_USERS_SCOPE_HEADER, { searchTerm: state.search }); + break; + case COMMAND_PALETTE_PROJECTS_CHAR: + name = sprintf(COMMAND_PALETTE_PROJECTS_SCOPE_HEADER, { searchTerm: state.search }); + break; + case COMMAND_PALETTE_FILES_CHAR: + name = sprintf(COMMAND_PALETTE_FILES_SCOPE_HEADER, { searchTerm: state.search }); + break; + default: + break; + } + return { name, items }; }; export const autocompleteGroupedSearchOptions = (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 index 2c369cbdf5f11e76436cfb3d0483ff85f8236c70..898e1755140d1630b778297e0a84f56ddaad4552 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/utils.js +++ b/app/assets/javascripts/super_sidebar/components/global_search/utils.js @@ -53,9 +53,9 @@ const getEntityName = (item, searchContext) => { }; export const getFormattedItem = (item, searchContext) => { - const { id, category, value, label, url: href, avatar_url } = item; + const { id, category, value, label, url: href, avatar_url, name } = item; let namespace; - const text = value || label; + const text = value || label || name; if (value) { namespace = getTruncatedNamespace(label); } diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js index b3840a0adbf27787a5ad943da426cd22052bd8f7..126657999cf46ebace90479a0a1defedd2d2f04f 100644 --- a/app/assets/javascripts/vue_shared/global_search/constants.js +++ b/app/assets/javascripts/vue_shared/global_search/constants.js @@ -4,6 +4,10 @@ export const AUTOCOMPLETE_ERROR_MESSAGE = s__( 'GlobalSearch|There was an error fetching search autocomplete suggestions.', ); +export const NO_SEARCH_RESULTS = s__( + 'GlobalSearch|No results found. Edit your search and try again.', +); + export const ALL_GITLAB = __('All GitLab'); export const PLACES = s__('GlobalSearch|Places'); @@ -75,6 +79,37 @@ export const AGGREGATIONS_ERROR_MESSAGE = s__('GlobalSearch|Fetching aggregation export const NO_LABELS_FOUND = s__('GlobalSearch|No labels found'); +export const COMMAND_PALETTE_TIP = s__('GlobalSearch|Tip:'); + +export const COMMAND_PALETTE_TYPE_PAGES = s__('GlobalSearch|Pages or actions'); + +export const COMMAND_PALETTE_TYPE_FILES = s__('GlobalSearch|Files'); + +export const COMMAND_PALETTE_SEARCH_SCOPE_HEADER = s__( + 'GlobalSearch|Search for `%{searchTerm}` in...', +); + +export const COMMAND_PALETTE_PAGES_SCOPE_HEADER = s__( + 'GlobalSearch|Search for `%{searchTerm}` pages in...', +); + +export const COMMAND_PALETTE_USERS_SCOPE_HEADER = s__( + 'GlobalSearch|Search for `%{searchTerm}` users in...', +); + +export const COMMAND_PALETTE_PROJECTS_SCOPE_HEADER = s__( + 'GlobalSearch|Search for `%{searchTerm}` projects in...', +); + +export const COMMAND_PALETTE_FILES_SCOPE_HEADER = s__( + 'GlobalSearch|Search for `%{searchTerm}` files in...', +); + +export const COMMAND_PALETTE_PAGES_CHAR = '>'; +export const COMMAND_PALETTE_USERS_CHAR = '@'; +export const COMMAND_PALETTE_PROJECTS_CHAR = ':'; +export const COMMAND_PALETTE_FILES_CHAR = '~'; + export const I18N = { SEARCH_DESCRIBED_BY_DEFAULT, SEARCH_RESULTS_LOADING, diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 0343f8eb3d9c1ef1633c368d3bd3e4cd4b92e363..a74fa8202f98850d1970a23756d11d30cb2ead3f 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -1,5 +1,6 @@ $super-sidebar-transition-duration: $gl-transition-duration-medium; $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; +$command-palette-spacing: px-to-rem(14px); @mixin notification-dot($color, $size, $top, $left) { background-color: $color; @@ -481,6 +482,25 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; padding: 5rem 1rem 0; } + .vertical-align-normalization { + margin-top: -$gl-spacing-scale-3; + margin-bottom: -$gl-spacing-scale-3; + } + + + .show-hover-layover { + &:hover { + .show-hover-layover-hint { + @include gl-opacity-10; + } + } + } + + .command-palette-px { + padding-left: $command-palette-spacing; + padding-right: $command-palette-spacing; + } + // 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 @@ -493,6 +513,10 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; right: 3rem; } + .modal-content { + border-radius: $gl-border-radius-large !important; + } + .gl-search-box-by-type-input-borderless { border-radius: $gl-border-radius-base; } @@ -507,9 +531,19 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; // Target groups [id*='gl-disclosure-dropdown-group'] { - padding-left: $gl-spacing-scale-5; - padding-right: $gl-spacing-scale-5; + padding-left: $command-palette-spacing; + padding-right: $command-palette-spacing; } + + .gl-scroll-scrim { + border-width: 0 !important; + } + } + + &.gl-modal .modal-footer { + background-color: $gray-50; + padding: $command-palette-spacing; + border-top: 1px solid $gray-100; } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f1b2a07cfa3a6aba4923766cacbc6569165d0a62..1648c6d4eccfebb92f1b95c0944e330b9d385fe9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22855,15 +22855,36 @@ msgstr "" msgid "GlobalSearch|Archived" msgstr "" +msgid "GlobalSearch|Change context %{kbdStart}↵%{kbdEnd}" +msgstr "" + msgid "GlobalSearch|Command palette" msgstr "" +msgid "GlobalSearch|Create in %{namespace} %{kbdStart}↵%{kbdEnd}" +msgstr "" + msgid "GlobalSearch|Fetching aggregations error." msgstr "" +msgid "GlobalSearch|Files" +msgstr "" + msgid "GlobalSearch|Filters" msgstr "" +msgid "GlobalSearch|Go to %{kbdStart}↵%{kbdEnd}" +msgstr "" + +msgid "GlobalSearch|Go to file %{kbdStart}↵%{kbdEnd}" +msgstr "" + +msgid "GlobalSearch|Go to profile %{kbdStart}↵%{kbdEnd}" +msgstr "" + +msgid "GlobalSearch|Go to project %{kbdStart}↵%{kbdEnd}" +msgstr "" + msgid "GlobalSearch|Group" msgstr "" @@ -22912,12 +22933,18 @@ msgstr "" msgid "GlobalSearch|No labels found" msgstr "" +msgid "GlobalSearch|No results found. Edit your search and try again." +msgstr "" + msgid "GlobalSearch|Nothing found…" msgstr "" msgid "GlobalSearch|Only first %{max_shown} of not indexed projects is shown" msgstr "" +msgid "GlobalSearch|Pages or actions" +msgstr "" + msgid "GlobalSearch|Places" msgstr "" @@ -22945,6 +22972,24 @@ 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 "" +msgid "GlobalSearch|Search %{kbdStart}↵%{kbdEnd}" +msgstr "" + +msgid "GlobalSearch|Search for `%{searchTerm}` files in..." +msgstr "" + +msgid "GlobalSearch|Search for `%{searchTerm}` in..." +msgstr "" + +msgid "GlobalSearch|Search for `%{searchTerm}` pages in..." +msgstr "" + +msgid "GlobalSearch|Search for `%{searchTerm}` projects in..." +msgstr "" + +msgid "GlobalSearch|Search for `%{searchTerm}` users in..." +msgstr "" + msgid "GlobalSearch|Search for projects, issues, etc." msgstr "" @@ -22972,6 +23017,9 @@ msgstr "" msgid "GlobalSearch|There was an error fetching search autocomplete suggestions." msgstr "" +msgid "GlobalSearch|Tip:" +msgstr "" + msgid "GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search" msgstr "" 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 e36b04636ce8919b81a5ef05fe4d9986bed59cc0..934246af33c32cfa0e73bd27d819ccbd87ddc1d8 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -56,10 +56,10 @@ wait_for_all_requests end - it 'shows search scope badge' do + it 'shows search scopes list' do fill_in 'search', with: 'text' within('#super-sidebar-search-modal') do - expect(page).to have_selector('.search-scope-help', text: scope_name) + expect(page).to have_selector('[data-testid="scoped-items"]', text: scope_name) end end @@ -116,7 +116,7 @@ context 'when user is in a global scope' do include_examples 'search field examples' do let(:url) { root_path } - let(:scope_name) { 'in all GitLab' } + let(:scope_name) { 'all GitLab' } end it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do @@ -190,8 +190,9 @@ it 'does not display a link to project feature flags' do fill_in_search('Feature') - within(search_modal_results) do - expect(page).to have_link('in all GitLab Feature') + within_testid("scoped-items") do + expect(page).to have_content('Search for `Feature` in...') + expect(page).to have_link('all GitLab') expect(page).not_to have_link('Feature Flags') end end diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap index e6635672ccf56eb6b79eaa8ea700b5ccc775742c..64a2d530e58b9d447b9f3b920ec0286289b3ed70 100644 --- a/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap +++ b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap @@ -19,7 +19,9 @@ exports[`SearchItem should render the item 1`] = ` > <span class="gl-text-gray-900" - /> + > + Cole Dickinson + </span> </span> </div> `; diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_hover_overlay_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_hover_overlay_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..484f73fb67a247c2f19425b76f1aadb68bc04dc6 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_hover_overlay_spec.js @@ -0,0 +1,46 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import SearchResultHoverLayover from '~/super_sidebar/components/global_search/components/global_search_hover_overlay.vue'; + +describe('SearchResultHoverLayover', () => { + let wrapper; + + const createComponent = (textMessage = 'test') => { + wrapper = shallowMountExtended(SearchResultHoverLayover, { + propsData: { + textMessage, + }, + stubs: { + GlSprintf, + }, + }); + }; + + const findOverlayMessage = () => wrapper.findByTestId('overlayMessage'); + + describe('Render', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders correctly', () => { + expect(findOverlayMessage().exists()).toBe(true); + }); + + describe.each` + text | result + ${'Go to %{kbdStart}↵%{kbdEnd}'} | ${'Go to %{kbdStart}↵%{kbdEnd}'} + ${'Go to %{kbdStart}ABC%{kbdEnd}'} | ${'Go to %{kbdStart}ABC%{kbdEnd}'} + ${'Go to'} | ${'Go to'} + ${'Go to %{linkStart}ABC%{linkEnd}'} | ${'Go to %{linkStart}ABC%{linkEnd}'} + `('renders the layover text correctly', ({ text, result }) => { + beforeEach(() => { + createComponent(text); + }); + + it('renders the layover component', () => { + expect(wrapper.props('textMessage')).toBe(result); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_no_results_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_no_results_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ba566e898fced71f85d7c7ae2dda6bd1bce678b5 --- /dev/null +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_no_results_spec.js @@ -0,0 +1,18 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import GlobalSearchNoResults from '~/super_sidebar/components/global_search/components/global_search_no_results.vue'; + +describe('GlobalSearchNoResults', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(GlobalSearchNoResults); + }; + + beforeEach(() => { + createComponent(); + }); + + it('renders message', () => { + expect(wrapper.text()).toBe('No results found. Edit your search and try again.'); + }); +}); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js index 164eea991e57f36cc8a57de67a9242a9735a781f..1f74dd17f5cdf924c0b485a6e07f3ae81fb9d3bd 100644 --- a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js @@ -1,13 +1,10 @@ -import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlToken, GlIcon } from '@gitlab/ui'; +import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue'; -import { truncate } from '~/lib/utils/text_utility'; -import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants'; -import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants'; import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_GROUP, @@ -46,10 +43,6 @@ describe('GlobalSearchScopedItems', () => { const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem); const findItemsText = () => findItems().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 findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href')); describe('Search results scoped items', () => { @@ -62,28 +55,13 @@ describe('GlobalSearchScopedItems', () => { }); it('renders titles correctly', () => { - findItemsText().forEach((title) => expect(title).toContain(MOCK_SEARCH)); - }); - - it('renders scope names correctly', () => { - const expectedTitles = MOCK_SCOPED_SEARCH_GROUP.items.map((o) => - truncate(trimText(`in ${o.scope || o.description}`), SCOPE_TOKEN_MAX_LENGTH), - ); - - 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_GROUP.items[i].icon); + findItemsText().forEach((title, i) => { + expect(title).toContain( + MOCK_SCOPED_SEARCH_GROUP.items[i].scope || MOCK_SCOPED_SEARCH_GROUP.items[i].description, + ); }); }); - it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => { - expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false); - }); - it('renders links correctly', () => { const expectedLinks = MOCK_SCOPED_SEARCH_GROUP.items.map((o) => o.href); expect(findItemLinks()).toStrictEqual(expectedLinks); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js index c125829411069f044f5aad6cdc2ac5ab4055816d..cecf6e46166b3d6c022b0230b982f053f83897b0 100644 --- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js @@ -1,15 +1,15 @@ -import { GlModal, GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; +import { GlModal, GlSearchBoxByType } from '@gitlab/ui'; import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { s__, sprintf } from '~/locale'; import GlobalSearchModal from '~/super_sidebar/components/global_search/components/global_search.vue'; import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue'; import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue'; import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue'; import FakeSearchInput from '~/super_sidebar/components/global_search/command_palette/fake_search_input.vue'; import CommandPaletteItems from '~/super_sidebar/components/global_search/command_palette/command_palette_items.vue'; +import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue'; import { SEARCH_OR_COMMAND_MODE_PLACEHOLDER, COMMON_HANDLES, @@ -18,12 +18,7 @@ import { import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, - ICON_PROJECT, - ICON_GROUP, - ICON_SUBGROUP, - SCOPE_TOKEN_MAX_LENGTH, } from '~/super_sidebar/components/global_search/constants'; -import { truncate } from '~/lib/utils/text_utility'; import { visitUrl } from '~/lib/utils/url_utility'; import { ENTER_KEY, @@ -98,23 +93,9 @@ describe('GlobalSearchModal', () => { }); }; - const formatScopeName = (scopeName) => { - if (!scopeName) { - return false; - } - const searchResultsScope = s__('GlobalSearch|in %{scope}'); - return truncate( - sprintf(searchResultsScope, { - scope: scopeName, - }), - SCOPE_TOKEN_MAX_LENGTH, - ); - }; - const findGlobalSearchModal = () => wrapper.findComponent(GlModal); const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType); - const findScopeToken = () => wrapper.findComponent(GlToken); const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems); const findGlobalSearchScopedItems = () => wrapper.findComponent(GlobalSearchScopedItems); const findGlobalSearchAutocompleteItems = () => @@ -123,6 +104,7 @@ describe('GlobalSearchModal', () => { const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION); const findCommandPaletteItems = () => wrapper.findComponent(CommandPaletteItems); const findFakeSearchInput = () => wrapper.findComponent(FakeSearchInput); + const findScrollScrim = () => wrapper.findComponent(ScrollScrim); describe('template', () => { describe('always renders', () => { @@ -141,6 +123,9 @@ describe('GlobalSearchModal', () => { it('Search Results Description', () => { expect(findSearchResultsDescription().exists()).toBe(true); }); + it('renders the ScrollScrim component', () => { + expect(findScrollScrim().exists()).toBe(true); + }); }); describe.each` @@ -205,75 +190,6 @@ describe('GlobalSearchModal', () => { }, ); - describe('input box', () => { - describe.each` - search | hasToken - ${MOCK_SEARCH} | ${true} - ${'te'} | ${false} - ${'x'} | ${false} - ${''} | ${false} - `('token', ({ search, hasToken }) => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent({ initialState: { search } }); - findGlobalSearchInput().vm.$emit('click'); - }); - - it(`${hasToken ? 'is' : 'is NOT'} rendered when search query is "${search}"`, () => { - expect(findScopeToken().exists()).toBe(hasToken); - }); - }); - - describe.each(MOCK_SCOPED_SEARCH_OPTIONS)('token content', (searchOption) => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent({ - initialState: { search: MOCK_SEARCH }, - mockGetters: { - searchOptions: () => [searchOption], - }, - }); - findGlobalSearchInput().vm.$emit('click'); - }); - - it(`is correctly rendered`, () => { - if (searchOption.scope) { - expect(findScopeToken().text()).toBe(formatScopeName(searchOption.scope)); - } else { - expect(findScopeToken().text()).toBe(formatScopeName(searchOption.description)); - } - }); - }); - - describe.each` - searchOptions | iconName - ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${ICON_PROJECT} - ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${ICON_GROUP} - ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${ICON_SUBGROUP} - ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} - `('token', ({ searchOptions, iconName }) => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent({ - initialState: { search: MOCK_SEARCH }, - mockGetters: { - searchOptions: () => searchOptions, - }, - }); - findGlobalSearchInput().vm.$emit('click'); - }); - - it(`renders ${iconName ? `"${iconName}"` : 'NO'} icon for "${ - searchOptions[0]?.text - }" scope`, () => { - expect( - findScopeToken().findComponent(GlIcon).exists() && - findScopeToken().findComponent(GlIcon).attributes('name'), - ).toBe(iconName); - }); - }); - }); - describe('Command palette', () => { describe.each([...COMMON_HANDLES, PATH_HANDLE])('when search handle is %s', (handle) => { beforeEach(() => { @@ -292,10 +208,6 @@ describe('GlobalSearchModal', () => { SEARCH_OR_COMMAND_MODE_PLACEHOLDER, ); }); - - it('should not render the scope token', () => { - expect(findScopeToken().exists()).toBe(false); - }); }); }); }); diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js index 3c30445e9368ec3863792dfa8a274e013a3f2ecb..f56d818db96b319d9beb54ac95167eccd97d2bc5 100644 --- a/spec/frontend/super_sidebar/components/global_search/utils_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js @@ -9,30 +9,32 @@ import { MERGE_REQUEST_CATEGORY, ISSUES_CATEGORY, RECENT_EPICS_CATEGORY, + USERS_CATEGORY, } from '~/vue_shared/global_search/constants'; describe('getFormattedItem', () => { describe.each` - item | avatarSize | searchContext | entityId | entityName | trackingLabel - ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'} | ${'projects'} - ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'} | ${'groups'} - ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'help'} - ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'settings'} - ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'} | ${'groups'} - ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'} | ${'projects'} - ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'} | ${'recent_issues'} - ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'} | ${'recent_merge_requests'} - ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'} | ${'recent_epics'} - ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'} | ${'groups'} - ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'} | ${'projects'} - ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'} | ${'recent_issues'} - ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'} | ${'recent_merge_requests'} - ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'} | ${'recent_epics'} - ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'} | ${'groups'} - ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'} | ${'projects'} - ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'} | ${'recent_issues'} - ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'} | ${'recent_merge_requests'} - ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'} | ${'recent_epics'} + item | avatarSize | searchContext | entityId | entityName | trackingLabel + ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'} | ${'projects'} + ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'} | ${'groups'} + ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'help'} + ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'settings'} + ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'} | ${'groups'} + ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'} | ${'projects'} + ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'} | ${'recent_issues'} + ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'} | ${'recent_merge_requests'} + ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'} | ${'recent_epics'} + ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'} | ${'groups'} + ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'} | ${'projects'} + ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'} | ${'recent_issues'} + ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'} | ${'recent_merge_requests'} + ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'} | ${'recent_epics'} + ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'} | ${'groups'} + ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'} | ${'projects'} + ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'} | ${'recent_issues'} + ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'} | ${'recent_merge_requests'} + ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'} | ${'recent_epics'} + ${{ category: USERS_CATEGORY, group_id: 15, group_name: 'test15', name: 'text person', id: 15, label: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'} | ${'users'} `( 'formats the item', ({ item, avatarSize, searchContext, entityId, entityName, trackingLabel }) => { @@ -43,7 +45,7 @@ describe('getFormattedItem', () => { }); it(`should set text to ${item.value || item.label}`, () => { - expect(formattedItem.text).toBe(item.value || item.label); + expect(formattedItem.text).toBe(item.value || item.label || item.name); }); it(`should set avatarSize to ${avatarSize}`, () => {