diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-18 19:00:14 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-18 19:00:14 +0000 |
commit | 05f0ebba3a2c8ddf39e436f412dc2ab5bf1353b2 (patch) | |
tree | 11d0f2a6ec31c7793c184106cedc2ded3d9a2cc5 /app/assets/javascripts/frequent_items | |
parent | ec73467c23693d0db63a797d10194da9e72a74af (diff) | |
download | gitlab-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')
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: [], }); |