diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-15 21:06:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-15 21:06:58 +0300 |
commit | 0905e6bd5042fa84fe66736eb9ab1be94c3d8aa9 (patch) | |
tree | 74deb17608bd1a8c0f9e86ceae0d3e079e123323 /app/assets | |
parent | 12a224d5db7aebdb30cda8ffb75c69fc66d07096 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
12 files changed, 322 insertions, 35 deletions
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 eb3402f0666..fe1a907bd91 100644 --- a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue +++ b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue @@ -91,6 +91,7 @@ export default { size="small" category="tertiary" icon="dash" + class="show-on-focus-or-hover--target" :aria-label="$options.i18n.removeItem" :title="$options.i18n.removeItem" data-testid="item-remove" diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue new file mode 100644 index 00000000000..6f0a0a1fe79 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue @@ -0,0 +1,40 @@ +<script> +import { s__ } from '~/locale'; +import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants'; +import FrequentItems from './frequent_items.vue'; + +export default { + name: 'FrequentlyVisitedGroups', + components: { + FrequentItems, + }, + inject: ['groupsPath'], + data() { + const username = gon.current_username; + + return { + storageKey: username ? `${username}/frequent-groups` : null, + }; + }, + i18n: { + groupName: s__('Navigation|Frequently visited groups'), + viewAllText: s__('Navigation|View all my groups'), + emptyStateText: s__('Navigation|Groups you visit often will appear here.'), + }, + MAX_FREQUENT_GROUPS_COUNT, +}; +</script> + +<template> + <frequent-items + :empty-state-text="$options.i18n.emptyStateText" + :group-name="$options.i18n.groupName" + :max-items="$options.MAX_FREQUENT_GROUPS_COUNT" + :storage-key="storageKey" + view-all-items-icon="group" + :view-all-items-text="$options.i18n.viewAllText" + :view-all-items-path="groupsPath" + v-bind="$attrs" + v-on="$listeners" + /> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue new file mode 100644 index 00000000000..5371887ee0f --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue @@ -0,0 +1,64 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; +import { __ } from '~/locale'; + +export default { + name: 'FrequentlyVisitedItem', + components: { + GlButton, + ProjectAvatar, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + item: { + type: Object, + required: true, + }, + }, + methods: { + onRemove() { + this.$emit('remove', this.item); + }, + }, + i18n: { + remove: __('Remove'), + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center gl-gap-3"> + <project-avatar + :project-id="item.id" + :project-name="item.title" + :project-avatar-url="item.avatar" + :size="24" + aria-hidden="true" + /> + + <div class="gl-flex-grow-1 gl-truncate-end"> + {{ item.title }} + <div + v-if="item.subtitle" + data-testid="subtitle" + class="gl-font-sm gl-text-gray-500 gl-truncate-end" + > + {{ item.subtitle }} + </div> + </div> + + <gl-button + v-gl-tooltip.left + icon="dash" + category="tertiary" + :aria-label="$options.i18n.remove" + :title="$options.i18n.remove" + class="show-on-focus-or-hover--target" + @click.stop.prevent="onRemove" + @keydown.enter.stop.prevent="onRemove" + /> + </div> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue new file mode 100644 index 00000000000..382d844ceee --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue @@ -0,0 +1,133 @@ +<script> +import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui'; +import { truncateNamespace } from '~/lib/utils/text_utility'; +import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils'; +import FrequentItem from './frequent_item.vue'; + +export default { + name: 'FrequentlyVisitedItems', + components: { + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, + GlIcon, + FrequentItem, + }, + props: { + emptyStateText: { + type: String, + required: true, + }, + groupName: { + type: String, + required: true, + }, + maxItems: { + type: Number, + required: true, + }, + storageKey: { + type: String, + required: false, + default: null, + }, + viewAllItemsText: { + type: String, + required: true, + }, + viewAllItemsIcon: { + type: String, + required: true, + }, + viewAllItemsPath: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + items: getItemsFromLocalStorage({ + storageKey: this.storageKey, + maxItems: this.maxItems, + }), + }; + }, + computed: { + formattedItems() { + // Each item needs two different representations. One is for the + // GlDisclosureDropdownItem, and the other is for the FrequentItem + // renderer component inside it. + return this.items.map((item) => ({ + forDropdown: { + id: item.id, + + // The text field satsifies GlDisclosureDropdownItem's prop + // validator, and the href field ensures it renders a link. + text: item.name, + href: item.webUrl, + }, + forRenderer: { + id: item.id, + title: item.name, + subtitle: truncateNamespace(item.namespace), + avatar: item.avatarUrl, + }, + })); + }, + showEmptyState() { + return this.items.length === 0; + }, + viewAllItem() { + return { + text: this.viewAllItemsText, + href: this.viewAllItemsPath, + }; + }, + }, + created() { + if (!this.storageKey) { + this.$emit('nothing-to-render'); + } + }, + methods: { + removeItem(item) { + removeItemFromLocalStorage({ + storageKey: this.storageKey, + item, + }); + + this.items = this.items.filter((i) => i.id !== item.id); + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown-group v-if="storageKey" v-bind="$attrs"> + <template #group-label>{{ groupName }}</template> + + <gl-disclosure-dropdown-item + v-for="item of formattedItems" + :key="item.forDropdown.id" + :item="item.forDropdown" + class="show-on-focus-or-hover--context" + > + <template #list-item + ><frequent-item :item="item.forRenderer" @remove="removeItem" + /></template> + </gl-disclosure-dropdown-item> + + <gl-disclosure-dropdown-item v-if="showEmptyState" class="gl-cursor-text"> + <span class="gl-text-gray-500 gl-font-sm gl-my-3 gl-mx-3">{{ emptyStateText }}</span> + </gl-disclosure-dropdown-item> + + <gl-disclosure-dropdown-item key="all" :item="viewAllItem"> + <template #list-item> + <span> + <gl-icon :name="viewAllItemsIcon" class="gl-w-6!" /> + {{ viewAllItemsText }} + </span> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown-group> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue new file mode 100644 index 00000000000..35b254099c2 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue @@ -0,0 +1,40 @@ +<script> +import { s__ } from '~/locale'; +import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants'; +import FrequentItems from './frequent_items.vue'; + +export default { + name: 'FrequentlyVisitedProjects', + components: { + FrequentItems, + }, + inject: ['projectsPath'], + data() { + const username = gon.current_username; + + return { + storageKey: username ? `${username}/frequent-projects` : null, + }; + }, + i18n: { + groupName: s__('Navigation|Frequently visited projects'), + viewAllText: s__('Navigation|View all my projects'), + emptyStateText: s__('Navigation|Projects you visit often will appear here.'), + }, + MAX_FREQUENT_PROJECTS_COUNT, +}; +</script> + +<template> + <frequent-items + :empty-state-text="$options.i18n.emptyStateText" + :group-name="$options.i18n.groupName" + :max-items="$options.MAX_FREQUENT_PROJECTS_COUNT" + :storage-key="storageKey" + view-all-items-icon="project" + :view-all-items-text="$options.i18n.viewAllText" + :view-all-items-path="projectsPath" + v-bind="$attrs" + v-on="$listeners" + /> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue index 0c325f6d13f..27935d92a5c 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue @@ -1,8 +1,10 @@ <script> import DefaultPlaces from './global_search_default_places.vue'; import DefaultIssuables from './global_search_default_issuables.vue'; +import FrequentGroups from './frequent_groups.vue'; +import FrequentProjects from './frequent_projects.vue'; -const components = [DefaultPlaces, DefaultIssuables]; +const components = [DefaultPlaces, FrequentProjects, FrequentGroups, DefaultIssuables]; export default { name: 'GlobalSearchDefaultItems', diff --git a/app/assets/javascripts/super_sidebar/components/items_list.vue b/app/assets/javascripts/super_sidebar/components/items_list.vue index 764db490751..1bad13f91e8 100644 --- a/app/assets/javascripts/super_sidebar/components/items_list.vue +++ b/app/assets/javascripts/super_sidebar/components/items_list.vue @@ -19,7 +19,13 @@ export default { <template> <ul class="gl-p-0 gl-list-style-none"> - <nav-item v-for="item in items" :key="item.id" :item="item" is-subitem> + <nav-item + v-for="item in items" + :key="item.id" + :item="item" + is-subitem + class="show-on-focus-or-hover--context" + > <template #icon> <project-avatar :project-id="item.id" diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 25d2b8a73ed..58d27e43e61 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -128,7 +128,7 @@ export default { :is="navItemLinkComponent" #default="{ isActive }" v-bind="linkProps" - class="nav-item-link gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus" + class="nav-item-link gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--context" :class="computedLinkClasses" data-qa-selector="nav_item_link" data-testid="nav-item-link" @@ -146,7 +146,7 @@ export default { <gl-icon v-else-if="isInPinnedSection" name="grip" - class="gl-m-auto gl-text-gray-400 draggable-icon" + class="gl-m-auto gl-text-gray-400 js-draggable-icon gl-cursor-grab show-on-focus-or-hover--target" /> </slot> </div> @@ -172,6 +172,7 @@ export default { size="small" category="tertiary" icon="thumbtack" + class="show-on-focus-or-hover--target" :aria-label="$options.i18n.pinItem" @click.prevent="$emit('pin-add', item.id)" /> @@ -182,6 +183,7 @@ export default { category="tertiary" :aria-label="$options.i18n.unpinItem" icon="thumbtack-solid" + class="show-on-focus-or-hover--target" @click.prevent="$emit('pin-remove', item.id)" /> </span> diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue index a6cc70963df..1e2201fbdff 100644 --- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue +++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue @@ -94,7 +94,7 @@ export default { v-model="draggableItems" class="gl-p-0 gl-m-0" data-testid="pinned-nav-items" - handle=".draggable-icon" + handle=".js-draggable-icon" tag="ul" @end="handleDrag" > diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index 9e3d2e383e8..2b62e7a6ede 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -81,6 +81,9 @@ export const initSuperSidebar = () => { const sidebarData = JSON.parse(sidebar); const searchData = convertObjectPropsToCamelCase(sidebarData.search); + const projectsPath = sidebarData.projects_path; + const groupsPath = sidebarData.groups_path; + const commandPaletteData = JSON.parse(commandPalette); const projectFilesPath = commandPaletteData.project_files_url; const projectBlobPath = commandPaletteData.project_blob_url; @@ -107,6 +110,8 @@ export const initSuperSidebar = () => { searchContext, projectFilesPath, projectBlobPath, + projectsPath, + groupsPath, }, store: createStore({ searchPath, diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js index 5b46425d223..cbf93155fb6 100644 --- a/app/assets/javascripts/super_sidebar/utils.js +++ b/app/assets/javascripts/super_sidebar/utils.js @@ -36,17 +36,15 @@ export const getTopFrequentItems = (items, maxCount) => { return frequentItems.slice(0, maxCount); }; -const updateItemAccess = (item) => { +const updateItemAccess = (contextItem, { lastAccessedOn, frequency = 0 } = {}) => { const now = Date.now(); - const neverAccessed = !item.lastAccessedOn; - const shouldUpdate = - neverAccessed || Math.abs(now - item.lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1; - const currentFrequency = item.frequency ?? 0; + const neverAccessed = !lastAccessedOn; + const shouldUpdate = neverAccessed || Math.abs(now - lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1; return { - ...item, - frequency: shouldUpdate ? currentFrequency + 1 : currentFrequency, - lastAccessedOn: shouldUpdate ? now : item.lastAccessedOn, + ...contextItem, + frequency: shouldUpdate ? frequency + 1 : frequency, + lastAccessedOn: shouldUpdate ? now : lastAccessedOn, }; }; @@ -63,7 +61,7 @@ export const trackContextAccess = (username, context) => { ); if (existingItemIndex > -1) { - storedItems[existingItemIndex] = updateItemAccess(storedItems[existingItemIndex]); + storedItems[existingItemIndex] = updateItemAccess(context.item, storedItems[existingItemIndex]); } else { const newItem = updateItemAccess(context.item); if (storedItems.length === FREQUENT_ITEMS.MAX_COUNT) { diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 2586f544d94..8610c41b43f 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -159,33 +159,12 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; } .nav-item-link { - button, - .draggable-icon { - opacity: 0; - } - - .draggable-icon { - cursor: grab; - } - - &:hover { - button, - .draggable-icon { - opacity: 1; - } - } - &:hover, &:focus-within { .nav-item-badge { opacity: 0; } } - - &:focus button, - button:focus { - opacity: 1; - } } #trial-status-sidebar-widget:hover { @@ -315,3 +294,20 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; } } } + +.show-on-focus-or-hover--context { + .show-on-focus-or-hover--target { + opacity: 0; + } + + &:hover, + &:focus { + .show-on-focus-or-hover--target { + opacity: 1; + } + } + + .show-on-focus-or-hover--target:focus { + opacity: 1; + } +} |