diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
commit | 3b1af5cc7ed2666ff18b718ce5d30fa5a2756674 (patch) | |
tree | 3bc4a40e0ee51ec27eabf917c537033c0c5b14d4 /app/assets/javascripts/super_sidebar | |
parent | 9bba14be3f2c211bf79e15769cd9b77bc73a13bc (diff) |
Add latest changes from gitlab-org/gitlab@16-1-stable-eev16.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/super_sidebar')
23 files changed, 558 insertions, 161 deletions
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue new file mode 100644 index 00000000000..c017fa8afa2 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue @@ -0,0 +1,45 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import logo from '../../../../views/shared/_logo.svg?raw'; + +export default { + logo, + i18n: { + homepage: __('Homepage'), + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml, + }, + inject: ['rootPath'], + props: { + logoUrl: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <a + v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage" + class="tanuki-logo-container" + :href="rootPath" + :title="$options.i18n.homepage" + data-track-action="click_link" + data-track-label="gitlab_logo_link" + data-track-property="nav_core_menu" + > + <img + v-if="logoUrl" + data-testid="brand-header-custom-logo" + :src="logoUrl" + class="gl-h-6 gl-max-w-full" + /> + <span v-else v-safe-html="$options.logo" data-testid="brand-header-default-logo"></span> + </a> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue index ad2111140a1..c5f3410a68f 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue @@ -5,7 +5,6 @@ import { s__ } from '~/locale'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import searchUserProjectsAndGroups from '../graphql/queries/search_user_groups_and_projects.query.graphql'; import { trackContextAccess, formatContextSwitcherItems } from '../utils'; -import { maxSize, applyMaxSize } from '../popper_max_size_modifier'; import NavItem from './nav_item.vue'; import ProjectsList from './projects_list.vue'; import GroupsList from './groups_list.vue'; @@ -142,9 +141,6 @@ export default { }, }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS, - popperOptions: { - modifiers: [maxSize, applyMaxSize], - }, }; </script> @@ -153,7 +149,6 @@ export default { ref="disclosure-dropdown" class="context-switcher gl-w-full" placement="center" - :popper-options="$options.popperOptions" @shown="onDisclosureDropdownShown" @hidden="onDisclosureDropdownHidden" > @@ -194,6 +189,7 @@ export default { :key="item.link" :item="item" :link-classes="{ [item.link_classes]: item.link_classes }" + is-subitem /> </ul> </li> diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue index cfb7e7732e9..17227a2b123 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue @@ -34,7 +34,7 @@ export default { <template> <button type="button" - class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 border-top border-bottom gl-border-gray-a-08 gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-h-8 gl-flex-shrink-0" + class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 border-top border-bottom gl-border-gray-a-08! gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-h-8 gl-flex-shrink-0" data-qa-selector="context_switcher" > <span diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue index fa6056aff5e..0ce856c9af8 100644 --- a/app/assets/javascripts/super_sidebar/components/create_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue @@ -42,21 +42,9 @@ export default { isInvitedMembers(groupItem) { return groupItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT; }, - closeAndFocus() { - this.$refs.dropdown.closeAndFocus(); - }, }, toggleId: 'create-menu-toggle', - popperOptions: { - modifiers: [ - { - name: 'offset', - options: { - offset: [DROPDOWN_X_OFFSET, DROPDOWN_Y_OFFSET], - }, - }, - ], - }, + dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET, crossAxis: DROPDOWN_X_OFFSET }, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, }; </script> @@ -64,14 +52,13 @@ export default { <template> <div> <gl-disclosure-dropdown - ref="dropdown" category="tertiary" icon="plus" no-caret text-sr-only :toggle-text="$options.i18n.createNew" :toggle-id="$options.toggleId" - :popper-options="$options.popperOptions" + :dropdown-offset="$options.dropdownOffset" data-qa-selector="new_menu_toggle" data-testid="new-menu-toggle" @shown="dropdownOpen = true" @@ -89,7 +76,6 @@ export default { :key="`${groupItem.text}-trigger`" trigger-source="top-nav" :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN" - @modal-opened="closeAndFocus" /> <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" /> </template> diff --git a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue index 11bf2ddbd30..02adebc50af 100644 --- a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue +++ b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue @@ -1,13 +1,19 @@ <script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import AccessorUtilities from '~/lib/utils/accessor'; +import { __ } from '~/locale'; import { getTopFrequentItems, formatContextSwitcherItems } from '../utils'; import ItemsList from './items_list.vue'; export default { components: { + GlButton, ItemsList, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { title: { type: String, @@ -68,6 +74,9 @@ export default { } }, }, + i18n: { + removeItem: __('Remove'), + }, }; </script> @@ -87,7 +96,20 @@ export default { > {{ pristineText }} </div> - <items-list :aria-label="title" :items="cachedFrequentItems" @remove-item="handleItemRemove"> + <items-list :aria-label="title" :items="cachedFrequentItems"> + <template #actions="{ item }"> + <gl-button + v-gl-tooltip.right.viewport + size="small" + category="tertiary" + icon="dash" + :aria-label="$options.i18n.removeItem" + :title="$options.i18n.removeItem" + class="gl-align-self-center gl-mr-2" + data-testid="item-remove" + @click.stop.prevent="handleItemRemove(item)" + /> + </template> <template #view-all-items> <slot name="view-all-items"></slot> </template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue new file mode 100644 index 00000000000..96e6c9bab9e --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue @@ -0,0 +1,191 @@ +<script> +import { debounce } from 'lodash'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { getFormattedItem } from '../utils'; +import { + COMMON_HANDLES, + COMMAND_HANDLE, + USER_HANDLE, + PROJECT_HANDLE, + ISSUE_HANDLE, + GLOBAL_COMMANDS_GROUP_TITLE, + PAGES_GROUP_TITLE, + GROUP_TITLES, +} from './constants'; +import SearchItem from './search_item.vue'; +import { commandMapper, linksReducer, autocompleteQuery } from './utils'; + +export default { + name: 'CommandPaletteItems', + components: { + GlDisclosureDropdownGroup, + GlLoadingIcon, + SearchItem, + }, + inject: ['commandPaletteCommands', 'commandPaletteLinks', 'autocompletePath', 'searchContext'], + props: { + searchQuery: { + type: String, + required: true, + }, + handle: { + type: String, + required: true, + validator: (value) => { + return COMMON_HANDLES.includes(value); + }, + }, + }, + data: () => ({ + groups: [], + error: null, + loading: false, + }), + computed: { + isCommandMode() { + return this.handle === COMMAND_HANDLE; + }, + isUserMode() { + return this.handle === USER_HANDLE; + }, + commands() { + return this.commandPaletteCommands.map(commandMapper); + }, + links() { + return this.commandPaletteLinks.reduce(linksReducer, []); + }, + filteredCommands() { + return this.searchQuery + ? this.commands + .map(({ name, items }) => { + return { + name: name || GLOBAL_COMMANDS_GROUP_TITLE, + items: this.filterBySearchQuery(items, 'text'), + }; + }) + .filter(({ items }) => items.length) + : this.commands; + }, + hasResults() { + return this.groups?.length && this.groups.some((group) => group.items?.length); + }, + hasSearchQuery() { + if (this.isCommandMode) { + return this.searchQuery?.length > 0; + } + return this.searchQuery?.length > 2; + }, + searchTerm() { + if (this.handle === ISSUE_HANDLE) { + return `${ISSUE_HANDLE}${this.searchQuery}`; + } + return this.searchQuery; + }, + }, + watch: { + searchQuery: { + handler() { + switch (this.handle) { + case COMMAND_HANDLE: + this.getCommandsAndPages(); + break; + case USER_HANDLE: + case PROJECT_HANDLE: + case ISSUE_HANDLE: + this.getScopedItems(); + break; + default: + break; + } + }, + immediate: true, + }, + }, + methods: { + filterBySearchQuery(items, key = 'keywords') { + return fuzzaldrinPlus.filter(items, this.searchQuery, { key }); + }, + getCommandsAndPages() { + if (!this.searchQuery) { + this.groups = [...this.commands]; + return; + } + const matchedLinks = this.filterBySearchQuery(this.links); + + if (this.filteredCommands.length || matchedLinks.length) { + this.groups = []; + } + + if (this.filteredCommands.length) { + this.groups = [...this.filteredCommands]; + } + + if (matchedLinks.length) { + this.groups.push({ + name: PAGES_GROUP_TITLE, + items: matchedLinks, + }); + } + }, + getScopedItems: debounce(function debouncedSearch() { + if (this.searchQuery && this.searchQuery.length < 3) return null; + + this.loading = true; + + return axios + .get( + autocompleteQuery({ + path: this.autocompletePath, + searchTerm: this.searchTerm, + handle: this.handle, + projectId: this.searchContext.project?.id, + }), + ) + .then(({ data }) => { + this.groups = this.getGroups(data); + }) + .catch((error) => { + this.error = error; + }) + .finally(() => { + this.loading = false; + }); + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), + getGroups(data) { + return [ + { + name: GROUP_TITLES[this.handle], + items: data.map(getFormattedItem), + }, + ]; + }, + }, +}; +</script> + +<template> + <ul class="gl-p-0 gl-m-0 gl-list-style-none"> + <gl-loading-icon v-if="loading" size="lg" class="gl-my-5" /> + + <template v-else-if="hasResults"> + <gl-disclosure-dropdown-group + v-for="(group, index) in groups" + :key="index" + :group="group" + bordered + class="{'gl-mt-0!': index===0}" + > + <template #list-item="{ item }"> + <search-item :item="item" :search-query="searchQuery" /> + </template> + </gl-disclosure-dropdown-group> + </template> + + <div v-else-if="hasSearchQuery && !hasResults" class="gl-text-gray-700 gl-pl-5 gl-py-3"> + {{ __('No results found') }} + </div> + </ul> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js new file mode 100644 index 00000000000..9dab16984f5 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js @@ -0,0 +1,45 @@ +import { s__, sprintf } from '~/locale'; + +export const COMMAND_HANDLE = '>'; +export const USER_HANDLE = '@'; +export const PROJECT_HANDLE = '&'; +export const ISSUE_HANDLE = '#'; + +export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, ISSUE_HANDLE]; +export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf( + s__( + 'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search...', + ), + { + commandHandle: COMMAND_HANDLE, + userHandle: USER_HANDLE, + issueHandle: ISSUE_HANDLE, + projectHandle: PROJECT_HANDLE, + }, + false, +); + +export const SEARCH_SCOPE_PLACEHOLDER = { + [COMMAND_HANDLE]: s__('CommandPalette|command'), + [USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'), + [PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'), + [ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'), +}; + +export const SEARCH_SCOPE = { + [USER_HANDLE]: 'user', + [PROJECT_HANDLE]: 'project', + [ISSUE_HANDLE]: 'issue', +}; + +export const GLOBAL_COMMANDS_GROUP_TITLE = s__('CommandPalette|Global Commands'); +export const USERS_GROUP_TITLE = s__('GlobalSearch|Users'); +export const PAGES_GROUP_TITLE = s__('CommandPalette|Pages'); +export const PROJECTS_GROUP_TITLE = s__('GlobalSearch|Projects'); +export const ISSUE_GROUP_TITLE = s__('GlobalSearch|Recent issues'); + +export const GROUP_TITLES = { + [USER_HANDLE]: USERS_GROUP_TITLE, + [PROJECT_HANDLE]: PROJECTS_GROUP_TITLE, + [ISSUE_HANDLE]: ISSUE_GROUP_TITLE, +}; diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue new file mode 100644 index 00000000000..dce2b24f551 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue @@ -0,0 +1,42 @@ +<script> +import { COMMON_HANDLES, SEARCH_SCOPE_PLACEHOLDER } from './constants'; + +export default { + name: 'FakeSearchInput', + props: { + userInput: { + type: String, + required: true, + }, + scope: { + type: String, + required: true, + validator: (value) => COMMON_HANDLES.includes(value), + }, + }, + computed: { + placeholder() { + return SEARCH_SCOPE_PLACEHOLDER[this.scope]; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-pointer-events-none fake-input"> + <span class="gl-opacity-0" data-testid="search-scope">{{ scope }} </span> + <span + v-if="!userInput" + data-testid="search-scope-placeholder" + class="gl-text-gray-500 gl-pointer-events-none" + >{{ placeholder }}</span + > + </div> +</template> + +<style scoped> +.fake-input { + top: 12px; + left: 33px; +} +</style> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/search_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/search_item.vue new file mode 100644 index 00000000000..b940c7c24c6 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/search_item.vue @@ -0,0 +1,57 @@ +<script> +import { GlAvatar, GlIcon } from '@gitlab/ui'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import highlight from '~/lib/utils/highlight'; +import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; + +export default { + name: 'CommandPaletteSearchItem', + components: { + GlAvatar, + GlIcon, + }, + directives: { + SafeHtml, + }, + props: { + item: { + type: Object, + required: true, + }, + searchQuery: { + type: String, + required: true, + }, + }, + computed: { + highlightedName() { + return highlight(this.item.text, this.searchQuery); + }, + }, + AVATAR_SHAPE_OPTION_RECT, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <gl-avatar + 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" + /> + <gl-icon v-if="item.icon" class="gl-mr-3" :name="item.icon" /> + <span class="gl-display-flex gl-flex-direction-column"> + <span v-safe-html="highlightedName" class="gl-text-gray-900"></span> + <span + v-if="item.namespace" + v-safe-html="item.namespace" + class="gl-font-sm gl-text-gray-500" + ></span> + </span> + </div> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js new file mode 100644 index 00000000000..5c8c0e59eaf --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js @@ -0,0 +1,47 @@ +import { isNil, omitBy } from 'lodash'; +import { objectToQuery } from '~/lib/utils/url_utility'; +import { SEARCH_SCOPE } from './constants'; + +export const commandMapper = ({ name, items }) => { + // TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here + // and is out of scope for the basic command palette items. If it proves to be useful, we can add it later. + return { + name, + items: items.filter(({ component }) => component !== 'invite_members'), + }; +}; + +export const linksReducer = (acc, menuItem) => { + acc.push({ + text: menuItem.title, + keywords: menuItem.title, + icon: menuItem.icon, + href: menuItem.link, + }); + if (menuItem.items?.length) { + const items = menuItem.items.map(({ title, link }) => ({ + keywords: title, + text: [menuItem.title, title].join(' > '), + href: link, + icon: menuItem.icon, + })); + + /* eslint-disable-next-line no-param-reassign */ + acc = [...acc, ...items]; + } + return acc; +}; + +export const autocompleteQuery = ({ path, searchTerm, handle, projectId }) => { + const query = omitBy( + { + term: searchTerm, + project_id: projectId, + filter: 'search', + scope: SEARCH_SCOPE[handle], + }, + isNil, + ); + + return `${path}?${objectToQuery(query)}`; +}; diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue index 55c28661440..cb34f2b8c26 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 @@ -24,6 +24,7 @@ import { SEARCH_RESULTS_LOADING, SEARCH_RESULTS_SCOPE, } from '~/vue_shared/global_search/constants'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION, @@ -35,6 +36,9 @@ import { SEARCH_INPUT_SELECTOR, SEARCH_RESULTS_ITEM_SELECTOR, } from '../constants'; +import CommandPaletteItems from '../command_palette/command_palette_items.vue'; +import FakeSearchInput from '../command_palette/fake_search_input.vue'; +import { COMMON_HANDLES, SEARCH_OR_COMMAND_MODE_PLACEHOLDER } from '../command_palette/constants'; import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue'; import GlobalSearchDefaultItems from './global_search_default_items.vue'; import GlobalSearchScopedItems from './global_search_scoped_items.vue'; @@ -60,7 +64,10 @@ export default { GlIcon, GlToken, GlModal, + CommandPaletteItems, + FakeSearchInput, }, + mixins: [glFeatureFlagMixin()], computed: { ...mapState(['search', 'loading', 'searchContext']), ...mapGetters(['searchQuery', 'searchOptions', 'scopedSearchOptions']), @@ -72,6 +79,9 @@ export default { this.setSearch(value); }, }, + searchPlaceholder() { + return this.glFeatures?.commandPalette ? SEARCH_OR_COMMAND_MODE_PLACEHOLDER : SEARCH_GITLAB; + }, showDefaultItems() { return !this.searchText; }, @@ -104,7 +114,7 @@ export default { }; }, showScopeHelp() { - return this.searchTermOverMin; + return this.searchTermOverMin && !this.isCommandMode; }, searchBarItem() { return this.searchOptions?.[0]; @@ -120,10 +130,26 @@ export default { scope: this.infieldHelpContent, }); }, + + searchTextFirstChar() { + return this.searchText?.trim().charAt(0); + }, + isCommandMode() { + return this.glFeatures?.commandPalette && COMMON_HANDLES.includes(this.searchTextFirstChar); + }, + commandPaletteQuery() { + if (this.isCommandMode) { + return this.searchText?.trim().substring(1); + } + return ''; + }, }, methods: { ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) { + if (this.isCommandMode) { + return; + } if (!searchTerm) { this.clearAutocomplete(); } else { @@ -222,12 +248,12 @@ export default { > <form role="search" - :aria-label="$options.i18n.SEARCH_GITLAB" + :aria-label="searchPlaceholder" class="gl-relative gl-rounded-base gl-w-full" :class="searchBarClasses" data-testid="global-search-form" > - <div class="gl-p-1"> + <div class="gl-p-1 gl-relative"> <gl-search-box-by-type id="search" ref="searchInputBox" @@ -236,7 +262,7 @@ export default { data-testid="global-search-input" data-qa-selector="global_search_input" autocomplete="off" - :placeholder="$options.i18n.SEARCH_GITLAB" + :placeholder="searchPlaceholder" :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION" borderless @input="getAutocompleteOptions" @@ -266,6 +292,13 @@ export default { <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only"> {{ $options.i18n.SEARCH_DESCRIBED_BY_WITH_RESULTS }} </span> + + <fake-search-input + v-if="isCommandMode" + :user-input="commandPaletteQuery" + :scope="searchTextFirstChar" + class="gl-absolute" + /> </div> <span role="region" @@ -282,13 +315,20 @@ export default { class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-2" @keydown="onKeydown" > - <global-search-default-items v-if="showDefaultItems" /> + <command-palette-items + v-if="isCommandMode" + :search-query="commandPaletteQuery" + :handle="searchTextFirstChar" + /> + <template v-else> - <global-search-scoped-items v-if="showScopedSearchItems" /> - <global-search-autocomplete-items /> + <global-search-default-items v-if="showDefaultItems" /> + <template v-else> + <global-search-scoped-items v-if="showScopedSearchItems" /> + <global-search-autocomplete-items /> + </template> </template> </div> - <template v-if="searchContext"> <input v-if="searchContext.group" diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue index 4fa15f1cd76..48becacebb7 100644 --- a/app/assets/javascripts/super_sidebar/components/groups_list.vue +++ b/app/assets/javascripts/super_sidebar/components/groups_list.vue @@ -64,7 +64,7 @@ export default { :search-results="searchResults" > <template #view-all-items> - <nav-item v-bind="viewAllProps" /> + <nav-item v-bind="viewAllProps" is-subitem /> </template> </search-results> <frequent-items-list @@ -75,7 +75,7 @@ export default { :pristine-text="$options.i18n.pristineText" > <template #view-all-items> - <nav-item v-bind="viewAllProps" /> + <nav-item v-bind="viewAllProps" is-subitem /> </template> </frequent-items-list> </template> diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 1fffbb05d03..1d4c24c6853 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -8,7 +8,7 @@ import { } from '@gitlab/ui'; import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { DOMAIN, PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; +import { FORUM_URL, DOCS_URL, PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; import { __, s__ } from '~/locale'; import { STORAGE_KEY } from '~/whats_new/utils/notification'; import Tracking from '~/tracking'; @@ -70,7 +70,7 @@ export default { helpLinks: { items: [ this.sidebarData.show_tanuki_bot && { - icon: 'tanuki', + icon: 'tanuki-ai', text: this.$options.i18n.chat, action: this.showTanukiBotChat, extraAttrs: { @@ -93,7 +93,7 @@ export default { }, { text: this.$options.i18n.docs, - href: `https://docs.${DOMAIN}`, + href: DOCS_URL, extraAttrs: { ...this.trackingAttrs('gitlab_documentation'), }, @@ -107,7 +107,7 @@ export default { }, { text: this.$options.i18n.forum, - href: `https://forum.${DOMAIN}/`, + href: FORUM_URL, extraAttrs: { ...this.trackingAttrs('community_forum'), }, @@ -132,7 +132,7 @@ export default { items: [ { text: this.$options.i18n.shortcuts, - action: this.showKeyboardShortcuts, + action: () => {}, extraAttrs: { class: 'js-shortcuts-modal-trigger', 'data-track-action': 'click_button', @@ -172,18 +172,11 @@ export default { return true; }, - showKeyboardShortcuts() { - this.$refs.dropdown.close(); - }, - showTanukiBotChat() { - this.$refs.dropdown.close(); - this.helpCenterState.showTanukiBotChatDrawer = true; }, async showWhatsNew() { - this.$refs.dropdown.close(); this.showWhatsNewNotification = false; if (!this.toggleWhatsNewDrawer) { @@ -211,29 +204,23 @@ export default { }); }, }, - popperOptions: { - modifiers: [ - { - name: 'offset', - options: { - offset: [DROPDOWN_X_OFFSET, DROPDOWN_Y_OFFSET], - }, - }, - ], - }, + dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET, crossAxis: DROPDOWN_X_OFFSET }, }; </script> <template> <gl-disclosure-dropdown - ref="dropdown" - :popper-options="$options.popperOptions" + :dropdown-offset="$options.dropdownOffset" @shown="trackDropdownToggle(true)" @hidden="trackDropdownToggle(false)" > <template #toggle> <gl-button category="tertiary" icon="question-o" class="btn-with-notification"> - <span v-if="showWhatsNewNotification" class="notification-dot-info"></span> + <span + v-if="showWhatsNewNotification" + data-testid="notification-dot" + class="notification-dot-info" + ></span> {{ $options.i18n.help }} </gl-button> </template> @@ -263,7 +250,7 @@ export default { <template #list-item="{ item }"> <span class="gl-display-flex gl-justify-content-space-between gl-align-items-center"> {{ item.text }} - <gl-icon v-if="item.icon" :name="item.icon" class="gl-text-orange-500" /> + <gl-icon v-if="item.icon" :name="item.icon" class="gl-text-purple-600" /> </span> </template> </gl-disclosure-dropdown-group> diff --git a/app/assets/javascripts/super_sidebar/components/items_list.vue b/app/assets/javascripts/super_sidebar/components/items_list.vue index ef27251dc6c..7d5af883651 100644 --- a/app/assets/javascripts/super_sidebar/components/items_list.vue +++ b/app/assets/javascripts/super_sidebar/components/items_list.vue @@ -1,17 +1,12 @@ <script> -import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; import NavItem from './nav_item.vue'; export default { components: { - GlButton, ProjectAvatar, NavItem, }, - directives: { - GlTooltip: GlTooltipDirective, - }, props: { items: { type: Array, @@ -29,6 +24,7 @@ export default { :key="item.id" :item="item" :link-classes="{ 'gl-py-2!': true }" + is-subitem > <template #icon> <project-avatar @@ -37,20 +33,11 @@ export default { :project-avatar-url="item.avatar" :size="24" aria-hidden="true" + class="gl-mr-n2" /> </template> <template #actions> - <gl-button - v-gl-tooltip.right.viewport - size="small" - category="tertiary" - icon="dash" - :aria-label="__('Remove')" - :title="__('Remove')" - class="gl-align-self-center gl-p-1! gl-absolute gl-right-4" - data-testid="item-remove" - @click.stop.prevent="$emit('remove-item', item)" - /> + <slot name="actions" :item="item"></slot> </template> </nav-item> <slot name="view-all-items"></slot> diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue index 93c249dffeb..b5a8241a286 100644 --- a/app/assets/javascripts/super_sidebar/components/menu_section.vue +++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue @@ -71,7 +71,7 @@ export default { <component :is="tag"> <hr v-if="separated" aria-hidden="true" class="gl-mx-4 gl-my-2" /> <button - class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-py-3 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus" + class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-line-height-normal gl-mb-2 gl-py-3 gl-px-0 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus" :class="computedLinkClasses" data-qa-selector="menu_section_button" :data-qa-section-name="item.title" diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index ec1c4069b1a..0ee9db10ee2 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -51,6 +51,11 @@ export default { required: false, default: () => ({}), }, + isSubitem: { + type: Boolean, + required: false, + default: false, + }, }, computed: { pillData() { @@ -99,6 +104,7 @@ export default { return { 'gl-py-2': this.isPinnable, 'gl-py-3': !this.isPinnable, + 'gl-mx-2': this.isSubitem, [this.item.link_classes]: this.item.link_classes, ...this.linkClasses, }; @@ -106,6 +112,9 @@ export default { navItemLinkComponent() { return this.item.to ? NavItemRouterLink : NavItemLink; }, + iconClasses() { + return this.isSubitem === true ? 'gl-ml-2 gl-mr-4' : 'gl-w-6 gl-mx-3'; + }, }, }; </script> @@ -128,7 +137,7 @@ export default { style="width: 3px; border-radius: 3px; margin-right: 1px" data-testid="active-indicator" ></div> - <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3"> + <div :class="iconClasses" class="gl-flex-shrink-0"> <slot name="icon"> <gl-icon v-if="item.icon" :name="item.icon" class="gl-ml-2 item-icon" /> <gl-icon @@ -138,14 +147,14 @@ export default { /> </slot> </div> - <div class="gl-pr-8 gl-text-gray-900 gl-truncate-end"> + <div class="gl-flex-grow-1 gl-text-gray-900 gl-truncate-end"> {{ item.title }} <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-truncate-end"> {{ item.subtitle }} </div> </div> <slot name="actions"></slot> - <span v-if="hasPill || isPinnable" class="gl-flex-grow-1 gl-text-right gl-mr-3 gl-relative"> + <span v-if="hasPill || isPinnable" class="gl-text-right gl-mr-3 gl-relative"> <gl-badge v-if="hasPill" size="sm" diff --git a/app/assets/javascripts/super_sidebar/components/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue index 78860e35eb1..8d1a5c825b5 100644 --- a/app/assets/javascripts/super_sidebar/components/projects_list.vue +++ b/app/assets/javascripts/super_sidebar/components/projects_list.vue @@ -65,7 +65,7 @@ export default { :search-results="searchResults" > <template #view-all-items> - <nav-item v-bind="viewAllProps" /> + <nav-item v-bind="viewAllProps" is-subitem /> </template> </search-results> <frequent-items-list @@ -76,7 +76,7 @@ export default { :pristine-text="$options.i18n.pristineText" > <template #view-all-items> - <nav-item v-bind="viewAllProps" /> + <nav-item v-bind="viewAllProps" is-subitem /> </template> </frequent-items-list> </template> diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index 08af9232107..287e4f57d01 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -1,6 +1,7 @@ <script> import * as Sentry from '@sentry/browser'; import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; import { PANELS_WITH_PINS } from '../constants'; import NavItem from './nav_item.vue'; import PinnedSection from './pinned_section.vue'; @@ -42,6 +43,10 @@ export default { }, }, + i18n: { + mainNavigation: s__('Navigation|Main navigation'), + }, + data() { return { // This is used as a provide and injected into the nav items. @@ -137,8 +142,8 @@ export default { </script> <template> - <nav class="gl-p-2 gl-relative"> - <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0"> + <nav :aria-label="$options.i18n.mainNavigation" class="gl-p-2 gl-relative"> + <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0" data-testid="static-items-section"> <nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static /> </ul> <pinned-section @@ -154,7 +159,7 @@ export default { class="gl-my-2 gl-mx-4" data-testid="main-menu-separator" /> - <ul class="gl-p-0 gl-list-style-none"> + <ul class="gl-p-0 gl-list-style-none" data-testid="non-static-items-section"> <template v-for="item in nonStaticItems"> <menu-section v-if="isSection(item)" diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index 768914584e8..d3b2143aaa7 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -1,13 +1,12 @@ <script> import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; -import SafeHtml from '~/vue_shared/directives/safe_html'; import { destroyUserCountsManager, createUserCountsManager, userCounts, } from '~/super_sidebar/user_counts_manager'; -import logo from '../../../../views/shared/_logo.svg'; +import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue'; import { JS_TOGGLE_COLLAPSE_CLASS } from '../constants'; import CreateMenu from './create_menu.vue'; import Counter from './counter.vue'; @@ -20,7 +19,6 @@ 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, JS_TOGGLE_COLLAPSE_CLASS, SEARCH_MODAL_ID, components: { @@ -35,6 +33,7 @@ export default { /* webpackChunkName: 'global_search_modal' */ './global_search/components/global_search.vue' ), SuperSidebarToggle, + BrandLogo, }, i18n: { createNew: __('Create new...'), @@ -53,9 +52,8 @@ export default { directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective, - SafeHtml, }, - inject: ['rootPath', 'isImpersonating'], + inject: ['isImpersonating'], props: { hasCollapseButton: { default: true, @@ -107,23 +105,7 @@ export default { <template> <div class="user-bar"> <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2"> - <a - v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage" - class="tanuki-logo-container" - :href="rootPath" - :title="$options.i18n.homepage" - data-track-action="click_link" - data-track-label="gitlab_logo_link" - data-track-property="nav_core_menu" - > - <img - v-if="sidebarData.logo_url" - data-testid="brand-header-custom-logo" - :src="sidebarData.logo_url" - class="gl-h-6" - /> - <span v-else v-safe-html="$options.logo"></span> - </a> + <brand-logo :logo-url="sidebarData.logo_url" /> <gl-badge v-if="sidebarData.gitlab_com_and_canary" variant="success" @@ -168,6 +150,7 @@ export default { category="tertiary" data-method="delete" data-testid="stop-impersonation-btn" + data-qa-selector="stop_impersonation_link" /> </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 cd5a83c86cc..7d4991fbe96 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -221,16 +221,7 @@ export default { }); }, }, - popperOptions: { - modifiers: [ - { - name: 'offset', - options: { - offset: [DROPDOWN_X_OFFSET, DROPDOWN_Y_OFFSET], - }, - }, - ], - }, + dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET, crossAxis: DROPDOWN_X_OFFSET }, }; </script> @@ -238,9 +229,10 @@ export default { <div> <gl-disclosure-dropdown ref="userDropdown" - :popper-options="$options.popperOptions" + :dropdown-offset="$options.dropdownOffset" data-testid="user-dropdown" data-qa-selector="user_menu" + :auto-close="false" @shown="onShow" > <template #toggle> diff --git a/app/assets/javascripts/super_sidebar/popper_max_size_modifier.js b/app/assets/javascripts/super_sidebar/popper_max_size_modifier.js deleted file mode 100644 index 6581d521107..00000000000 --- a/app/assets/javascripts/super_sidebar/popper_max_size_modifier.js +++ /dev/null @@ -1,43 +0,0 @@ -import { detectOverflow } from '@popperjs/core'; - -/** - * These modifiers were copied from the community modifier popper-max-size-modifier - * https://www.npmjs.com/package/popper-max-size-modifier. - * We are considering upgrading Popper.js to Floating UI, at which point the behavior this - * introduces will be available out of the box. - * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2213 - */ - -export const maxSize = { - name: 'maxSize', - enabled: true, - phase: 'main', - requiresIfExists: ['offset', 'preventOverflow', 'flip'], - fn({ state, name }) { - const overflow = detectOverflow(state); - const { x, y } = state.modifiersData.preventOverflow || { x: 0, y: 0 }; - const { width, height } = state.rects.popper; - const [basePlacement] = state.placement.split('-'); - - const widthProp = basePlacement === 'left' ? 'left' : 'right'; - const heightProp = basePlacement === 'top' ? 'top' : 'bottom'; - - state.modifiersData[name] = { - width: width - overflow[widthProp] - x, - height: height - overflow[heightProp] - y, - }; - }, -}; - -export const applyMaxSize = { - name: 'applyMaxSize', - enabled: true, - phase: 'write', - requires: ['maxSize'], - fn({ state }) { - // The `maxSize` modifier provides this data - const { width, height } = state.modifiersData.maxSize; - state.elements.popper.style.maxWidth = `${width}px`; - state.elements.popper.style.maxHeight = `${height}px`; - }, -}; diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index 63424277ffc..f6afde02fa5 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -72,6 +72,8 @@ export const initSuperSidebar = () => { const sidebarData = JSON.parse(sidebar); const searchData = convertObjectPropsToCamelCase(sidebarData.search); + const commandPaletteCommands = sidebarData.create_new_menu_groups || []; + const commandPaletteLinks = convertObjectPropsToCamelCase(sidebarData.current_menu_items || []); const { searchPath, issuesPath, mrPath, autocompletePath, searchContext } = searchData; const isImpersonating = parseBoolean(sidebarData.is_impersonating); @@ -85,6 +87,10 @@ export const initSuperSidebar = () => { toggleNewNavEndpoint, isImpersonating, ...getTrialStatusWidgetData(sidebarData), + commandPaletteCommands, + commandPaletteLinks, + autocompletePath, + searchContext, }, store: createStore({ searchPath, diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js index 1a359533435..2687ea5ccf8 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js @@ -24,7 +24,7 @@ export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => { findPage().classList.toggle(SIDEBAR_COLLAPSED_CLASS, collapsed); sidebarState.isPeek = false; - sidebarState.isPeekable = Boolean(gon.features?.superSidebarPeek) && collapsed; + sidebarState.isPeekable = collapsed; sidebarState.isCollapsed = collapsed; if (saveCookie && isDesktopBreakpoint()) { |