summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/frequent_items
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-18 19:00:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-18 19:00:14 +0000
commit05f0ebba3a2c8ddf39e436f412dc2ab5bf1353b2 (patch)
tree11d0f2a6ec31c7793c184106cedc2ded3d9a2cc5 /app/assets/javascripts/frequent_items
parentec73467c23693d0db63a797d10194da9e72a74af (diff)
downloadgitlab-ce-05f0ebba3a2c8ddf39e436f412dc2ab5bf1353b2.tar.gz
Add latest changes from gitlab-org/gitlab@15-8-stable-eev15.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/frequent_items')
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue34
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue12
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue76
-rw-r--r--app/assets/javascripts/frequent_items/constants.js2
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js29
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js16
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js2
8 files changed, 141 insertions, 33 deletions
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 8ad9eeaa266..a4e883c96b5 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import {
mapVuexModuleState,
@@ -18,6 +18,11 @@ export default {
FrequentItemsSearchInput,
FrequentItemsList,
GlLoadingIcon,
+ GlButton,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
mixins: [frequentItemsMixin],
inject: ['vuexModule'],
@@ -40,12 +45,14 @@ export default {
...mapVuexModuleState((vm) => vm.vuexModule, [
'searchQuery',
'isLoadingItems',
+ 'isItemsListEditable',
'isFetchFailed',
+ 'isItemRemovalFailed',
'items',
]),
...mapVuexModuleGetters((vm) => vm.vuexModule, ['hasSearchQuery']),
translations() {
- return this.getTranslations(['loadingMessage', 'header']);
+ return this.getTranslations(['loadingMessage', 'header', 'headerEditToggle']);
},
},
created() {
@@ -74,6 +81,7 @@ export default {
...mapVuexModuleActions((vm) => vm.vuexModule, [
'setNamespace',
'setStorageKey',
+ 'toggleItemsListEditablity',
'fetchFrequentItems',
]),
dropdownOpenHandler() {
@@ -132,8 +140,25 @@ export default {
class="loading-animation prepend-top-20"
data-testid="loading"
/>
- <div v-if="!isLoadingItems && !hasSearchQuery" class="section-header" data-testid="header">
- {{ translations.header }}
+ <div
+ v-if="!isLoadingItems && !hasSearchQuery"
+ class="section-header gl-display-flex"
+ data-testid="header"
+ >
+ <span class="gl-flex-grow-1">{{ translations.header }}</span>
+ <gl-button
+ v-if="items.length"
+ v-gl-tooltip.left
+ size="small"
+ category="tertiary"
+ :aria-label="translations.headerEditToggle"
+ :title="translations.headerEditToggle"
+ :class="{ 'gl-bg-gray-100!': isItemsListEditable }"
+ class="gl-p-2!"
+ @click="toggleItemsListEditablity"
+ >
+ <gl-icon name="pencil" :class="{ 'gl-text-gray-900!': isItemsListEditable }" />
+ </gl-button>
</div>
<frequent-items-list
v-if="!isLoadingItems"
@@ -141,6 +166,7 @@ export default {
:namespace="namespace"
:has-search-query="hasSearchQuery"
:is-fetch-failed="isFetchFailed"
+ :is-item-removal-failed="isItemRemovalFailed"
:matcher="searchQuery"
/>
</div>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index c0bfcf9c4a9..da1d3bedaf4 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -21,6 +21,10 @@ export default {
type: Boolean,
required: true,
},
+ isItemRemovalFailed: {
+ type: Boolean,
+ required: true,
+ },
matcher: {
type: String,
required: true,
@@ -38,6 +42,9 @@ export default {
isListEmpty() {
return this.items.length === 0;
},
+ showListEmptyMessage() {
+ return this.isListEmpty || this.isItemRemovalFailed;
+ },
listEmptyMessage() {
if (this.hasSearchQuery) {
return this.isFetchFailed
@@ -45,7 +52,7 @@ export default {
: this.translations.searchListEmptyMessage;
}
- return this.isFetchFailed
+ return this.isFetchFailed || this.isItemRemovalFailed
? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage;
},
@@ -60,9 +67,10 @@ export default {
<div class="frequent-items-list-container">
<ul data-testid="frequent-items-list" class="list-unstyled">
<li
- v-if="isListEmpty"
+ v-if="showListEmptyMessage"
:class="{ 'section-failure': isFetchFailed }"
class="section-empty gl-mb-3"
+ data-testid="frequent-items-list-empty"
>
{{ listEmptyMessage }}
</li>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 89b6885091c..75ea9beb5cf 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,10 +1,10 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { snakeCase } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
-import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
+import { mapVuexModuleState, mapVuexModuleActions } from '~/lib/utils/vuex_module_mappers';
import Tracking from '~/tracking';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
@@ -12,11 +12,13 @@ const trackingMixin = Tracking.mixin();
export default {
components: {
+ GlIcon,
GlButton,
ProjectAvatar,
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin],
inject: ['vuexModule'],
@@ -51,7 +53,7 @@ export default {
},
},
computed: {
- ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType']),
+ ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType', 'isItemsListEditable']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
@@ -62,43 +64,63 @@ export default {
return `${this.dropdownType}_dropdown_frequent_items_list_item_${snakeCase(this.itemName)}`;
},
},
+ methods: {
+ ...mapVuexModuleActions((vm) => vm.vuexModule, ['removeFrequentItem']),
+ },
};
</script>
<template>
- <li class="frequent-items-list-item-container">
+ <li class="frequent-items-list-item-container gl-relative">
<gl-button
category="tertiary"
:href="webUrl"
- class="gl-text-left gl-justify-content-start!"
+ class="gl-text-left gl-w-full"
+ button-text-classes="gl-display-flex gl-w-full"
+ data-testid="frequent-item-link"
@click="track('click_link', { label: itemTrackingLabel })"
>
- <project-avatar
- class="gl-float-left gl-mr-3"
- :project-avatar-url="avatarUrl"
- :project-id="itemId"
- :project-name="itemName"
- aria-hidden="true"
- />
- <div
- data-testid="frequent-items-item-metadata-container"
- class="frequent-items-item-metadata-container"
- >
- <div
- v-safe-html="highlightedItemName"
- data-testid="frequent-items-item-title"
- :title="itemName"
- class="frequent-items-item-title"
- ></div>
+ <div class="gl-flex-grow-1">
+ <project-avatar
+ class="gl-float-left gl-mr-3"
+ :project-avatar-url="avatarUrl"
+ :project-id="itemId"
+ :project-name="itemName"
+ aria-hidden="true"
+ />
<div
- v-if="namespace"
- data-testid="frequent-items-item-namespace"
- :title="namespace"
- class="frequent-items-item-namespace"
+ data-testid="frequent-items-item-metadata-container"
+ class="frequent-items-item-metadata-container"
>
- {{ truncatedNamespace }}
+ <div
+ v-safe-html="highlightedItemName"
+ data-testid="frequent-items-item-title"
+ :title="itemName"
+ class="frequent-items-item-title"
+ ></div>
+ <div
+ v-if="namespace"
+ data-testid="frequent-items-item-namespace"
+ :title="namespace"
+ class="frequent-items-item-namespace"
+ >
+ {{ truncatedNamespace }}
+ </div>
</div>
</div>
</gl-button>
+ <gl-button
+ v-if="isItemsListEditable"
+ v-gl-tooltip.left
+ size="small"
+ category="tertiary"
+ :aria-label="__('Remove')"
+ :title="__('Remove')"
+ class="gl-align-self-center gl-p-1! gl-absolute! gl-w-auto! gl-top-4 gl-right-4"
+ data-testid="item-remove"
+ @click.stop.prevent="removeFrequentItem(itemId)"
+ >
+ <gl-icon name="close" />
+ </gl-button>
</li>
</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
index cb5d21161a9..a7c27abf58e 100644
--- a/app/assets/javascripts/frequent_items/constants.js
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -18,6 +18,7 @@ export const TRANSLATION_KEYS = {
projects: {
loadingMessage: s__('ProjectsDropdown|Loading projects'),
header: s__('ProjectsDropdown|Frequently visited'),
+ headerEditToggle: s__('ProjectsDropdown|Toggle edit mode'),
itemListErrorMessage: s__(
'ProjectsDropdown|This feature requires browser localStorage support',
),
@@ -29,6 +30,7 @@ export const TRANSLATION_KEYS = {
groups: {
loadingMessage: s__('GroupsDropdown|Loading groups'),
header: s__('GroupsDropdown|Frequently visited'),
+ headerEditToggle: s__('GroupsDropdown|Toggle edit mode'),
itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index babc2ef2e32..e5ef49ec402 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -12,6 +12,10 @@ export const setStorageKey = ({ commit }, key) => {
commit(types.SET_STORAGE_KEY, key);
};
+export const toggleItemsListEditablity = ({ commit }) => {
+ commit(types.TOGGLE_ITEMS_LIST_EDITABILITY);
+};
+
export const requestFrequentItems = ({ commit }) => {
commit(types.REQUEST_FREQUENT_ITEMS);
};
@@ -81,3 +85,28 @@ export const setSearchQuery = ({ commit, dispatch }, query) => {
dispatch('fetchFrequentItems');
}
};
+
+export const removeFrequentItemSuccess = ({ commit }, itemId) => {
+ commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS, itemId);
+};
+
+export const removeFrequentItemError = ({ commit }) => {
+ commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR);
+};
+
+export const removeFrequentItem = ({ state, dispatch }, itemId) => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ try {
+ const storedRawItems = JSON.parse(localStorage.getItem(state.storageKey));
+ localStorage.setItem(
+ state.storageKey,
+ JSON.stringify(storedRawItems.filter((item) => item.id !== itemId)),
+ );
+ dispatch('removeFrequentItemSuccess', itemId);
+ } catch {
+ dispatch('removeFrequentItemError');
+ }
+ } else {
+ dispatch('removeFrequentItemError');
+ }
+};
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
index cbe2c9401ad..9c9346081e9 100644
--- a/app/assets/javascripts/frequent_items/store/mutation_types.js
+++ b/app/assets/javascripts/frequent_items/store/mutation_types.js
@@ -1,9 +1,12 @@
export const SET_NAMESPACE = 'SET_NAMESPACE';
export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
+export const TOGGLE_ITEMS_LIST_EDITABILITY = 'TOGGLE_ITEMS_LIST_EDITABILITY';
export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
+export const RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS = 'RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS';
+export const RECEIVE_REMOVE_FREQUENT_ITEM_ERROR = 'RECEIVE_REMOVE_FREQUENT_ITEM_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
index eee00243867..65f54e6ed05 100644
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -20,6 +20,11 @@ export default {
hasSearchQuery,
});
},
+ [types.TOGGLE_ITEMS_LIST_EDITABILITY](state) {
+ Object.assign(state, {
+ isItemsListEditable: !state.isItemsListEditable,
+ });
+ },
[types.REQUEST_FREQUENT_ITEMS](state) {
Object.assign(state, {
isLoadingItems: true,
@@ -69,4 +74,15 @@ export default {
isFetchFailed: true,
});
},
+ [types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS](state, itemId) {
+ Object.assign(state, {
+ items: state.items.filter((item) => item.id !== itemId),
+ isItemRemovalFailed: false,
+ });
+ },
+ [types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR](state) {
+ Object.assign(state, {
+ isItemRemovalFailed: true,
+ });
+ },
};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
index c5c0b25fdf2..ee94e9cd221 100644
--- a/app/assets/javascripts/frequent_items/store/state.js
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -5,5 +5,7 @@ export default ({ dropdownType = '' } = {}) => ({
searchQuery: '',
isLoadingItems: false,
isFetchFailed: false,
+ isItemsListEditable: false,
+ isItemRemovalFailed: false,
items: [],
});