Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-15 21:06:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-15 21:06:58 +0300
commit0905e6bd5042fa84fe66736eb9ab1be94c3d8aa9 (patch)
tree74deb17608bd1a8c0f9e86ceae0d3e079e123323 /app/assets
parent12a224d5db7aebdb30cda8ffb75c69fc66d07096 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/super_sidebar/components/frequent_items_list.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue40
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue64
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue133
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue40
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/items_list.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js5
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js16
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss38
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;
+ }
+}