diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/sidebar')
18 files changed, 257 insertions, 699 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue index 46ccb9470e5..35ac9ef8565 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue @@ -1,5 +1,6 @@ <script> import { GlLabel } from '@gitlab/ui'; +import { sortBy } from 'lodash'; import { mapState } from 'vuex'; import { isScopedLabel } from '~/lib/utils/common_utils'; @@ -23,6 +24,9 @@ export default { 'labelsFilterBasePath', 'labelsFilterParam', ]), + sortedSelectedLabels() { + return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1)); + }, }, methods: { labelFilterUrl(label) { @@ -47,7 +51,7 @@ export default { <span v-if="!selectedLabels.length" class="text-secondary"> <slot></slot> </span> - <template v-for="label in selectedLabels" v-else> + <template v-for="label in sortedSelectedLabels" v-else> <gl-label :key="label.id" data-qa-selector="selected_label_content" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue index e8fdf4bb0c2..dd40add6376 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue @@ -56,7 +56,7 @@ export default { const labelLink = h( GlLink, { - class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal', + class: 'gl-display-flex gl-align-items-center label-item gl-text-body', on: { click: () => { listeners.clickLabel(label); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue deleted file mode 100644 index 60111210f5d..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue +++ /dev/null @@ -1,42 +0,0 @@ -<script> -import { GlButton, GlIcon } from '@gitlab/ui'; -import { mapActions, mapGetters } from 'vuex'; - -export default { - components: { - GlButton, - GlIcon, - }, - computed: { - ...mapGetters([ - 'dropdownButtonText', - 'isDropdownVariantStandalone', - 'isDropdownVariantEmbedded', - ]), - }, - methods: { - ...mapActions(['toggleDropdownContents']), - handleButtonClick(e) { - if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) { - this.toggleDropdownContents(); - } - - if (this.isDropdownVariantStandalone) { - e.stopPropagation(); - } - }, - }, -}; -</script> - -<template> - <gl-button - class="labels-select-dropdown-button js-dropdown-button w-100 text-left" - @click="handleButtonClick" - > - <span class="dropdown-toggle-text gl-pointer-events-none flex-fill"> - {{ dropdownButtonText }} - </span> - <gl-icon name="chevron-down" class="gl-pointer-events-none float-right" /> - </gl-button> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue index 6694e349b6e..0fcc67c0ffa 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue @@ -1,22 +1,21 @@ <script> -import { GlButton } from '@gitlab/ui'; -import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; +import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils'; export default { components: { DropdownContentsLabelsView, DropdownContentsCreateView, GlButton, + GlDropdown, + GlDropdownItem, + GlLink, }, + inject: ['allowLabelCreate', 'labelsManagePath'], props: { - renderOnTop: { - type: Boolean, - required: false, - default: false, - }, labelsCreateTitle: { type: String, required: true, @@ -33,6 +32,10 @@ export default { type: String, required: true, }, + dropdownButtonText: { + type: String, + required: true, + }, footerCreateLabelTitle: { type: String, required: true, @@ -41,70 +44,105 @@ export default { type: String, required: true, }, + variant: { + type: String, + required: true, + }, + }, + data() { + return { + showDropdownContentsCreateView: false, + }; }, computed: { - ...mapState(['showDropdownContentsCreateView']), - ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), dropdownContentsView() { if (this.showDropdownContentsCreateView) { return 'dropdown-contents-create-view'; } return 'dropdown-contents-labels-view'; }, - directionStyle() { - const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem'; - return this.renderOnTop ? { bottom } : {}; - }, dropdownTitle() { return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle; }, + showDropdownFooter() { + return ( + !this.showDropdownContentsCreateView && + (this.isDropdownVariantSidebar(this.variant) || + this.isDropdownVariantEmbedded(this.variant)) + ); + }, }, methods: { - ...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']), + showDropdown() { + this.$refs.dropdown.show(); + }, + toggleDropdownContentsCreateView() { + this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView; + }, + toggleDropdownContent() { + this.toggleDropdownContentsCreateView(); + // Required to recalculate dropdown position as its size changes + this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate(); + }, + isDropdownVariantSidebar, + isDropdownVariantEmbedded, }, }; </script> <template> - <div - class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute" + <gl-dropdown + ref="dropdown" + :text="dropdownButtonText" + class="gl-w-full gl-mt-2" data-qa-selector="labels_dropdown_content" - :style="directionStyle" > - <div - v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" - class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" - data-testid="dropdown-title" - > - <gl-button - v-if="showDropdownContentsCreateView" - :aria-label="__('Go back')" - variant="link" - size="small" - class="js-btn-back dropdown-header-button p-0" - icon="arrow-left" - @click="toggleDropdownContentsCreateView" - /> - <span class="flex-grow-1">{{ dropdownTitle }}</span> - <gl-button - :aria-label="__('Close')" - variant="link" - size="small" - class="dropdown-header-button gl-p-0!" - icon="close" - @click="toggleDropdownContents" - /> - </div> + <template #header> + <div + v-if="isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)" + class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" + > + <gl-button + v-if="showDropdownContentsCreateView" + :aria-label="__('Go back')" + variant="link" + size="small" + class="js-btn-back dropdown-header-button gl-p-0" + icon="arrow-left" + data-testid="go-back-button" + @click.stop="toggleDropdownContent" + /> + <span class="gl-flex-grow-1">{{ dropdownTitle }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + size="small" + class="dropdown-header-button gl-p-0!" + icon="close" + @click="$emit('closeDropdown')" + /> + </div> + </template> <component :is="dropdownContentsView" :selected-labels="selectedLabels" :allow-multiselect="allowMultiselect" - :labels-list-title="labelsListTitle" - :footer-create-label-title="footerCreateLabelTitle" - :footer-manage-label-title="footerManageLabelTitle" @hideCreateView="toggleDropdownContentsCreateView" - @closeDropdown="$emit('closeDropdown', $event)" - @toggleDropdownContentsCreateView="toggleDropdownContentsCreateView" + @setLabels="$emit('setLabels', $event)" /> - </div> + <template #footer> + <div v-if="showDropdownFooter" data-testid="dropdown-footer"> + <gl-dropdown-item + v-if="allowLabelCreate" + data-testid="create-label-button" + @click.native.capture.stop="toggleDropdownContent" + > + {{ footerCreateLabelTitle }} + </gl-dropdown-item> + <gl-dropdown-item :href="labelsManagePath" @click.native.capture.stop> + {{ footerManageLabelTitle }} + </gl-dropdown-item> + </div> + </template> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index 4651e7a1576..2e31b386fdd 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -1,8 +1,10 @@ <script> import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import produce from 'immer'; import createFlash from '~/flash'; import { __ } from '~/locale'; import createLabelMutation from './graphql/create_label.mutation.graphql'; +import projectLabelsQuery from './graphql/project_labels.query.graphql'; const errorMessage = __('Error creating label.'); @@ -47,6 +49,25 @@ export default { handleColorClick(color) { this.selectedColor = this.getColorCode(color); }, + updateLabelsInCache(store, label) { + const sourceData = store.readQuery({ + query: projectLabelsQuery, + variables: { fullPath: this.projectPath, searchTerm: '' }, + }); + + const collator = new Intl.Collator('en'); + const data = produce(sourceData, (draftData) => { + const { nodes } = draftData.workspace.labels; + nodes.push(label); + nodes.sort((a, b) => collator.compare(a.title, b.title)); + }); + + store.writeQuery({ + query: projectLabelsQuery, + variables: { fullPath: this.projectPath, searchTerm: '' }, + data, + }); + }, async createLabel() { this.labelCreateInProgress = true; try { @@ -59,6 +80,14 @@ export default { color: this.selectedColor, projectPath: this.projectPath, }, + update: ( + store, + { + data: { + labelCreate: { label }, + }, + }, + ) => this.updateLabelsInCache(store, label), }); if (labelCreate.errors.length) { createFlash({ message: errorMessage }); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index ffa37424c2c..857367a0721 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -1,24 +1,23 @@ <script> -import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; +import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { debounce } from 'lodash'; import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { __ } from '~/locale'; -import { DropdownVariant } from './constants'; import projectLabelsQuery from './graphql/project_labels.query.graphql'; import LabelItem from './label_item.vue'; export default { components: { + GlDropdownForm, + GlDropdownItem, GlLoadingIcon, GlSearchBoxByType, - GlLink, LabelItem, }, - inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'], + inject: ['projectPath'], props: { selectedLabels: { type: Array, @@ -28,24 +27,11 @@ export default { type: Boolean, required: true, }, - labelsListTitle: { - type: String, - required: true, - }, - footerCreateLabelTitle: { - type: String, - required: true, - }, - footerManageLabelTitle: { - type: String, - required: true, - }, }, data() { return { searchKey: '', labels: [], - currentHighlightItem: -1, localSelectedLabels: [...this.selectedLabels], }; }, @@ -74,12 +60,6 @@ export default { }, }, computed: { - isDropdownVariantSidebar() { - return this.variant === DropdownVariant.Sidebar; - }, - isDropdownVariantEmbedded() { - return this.variant === DropdownVariant.Embedded; - }, labelsFetchInProgress() { return this.$apollo.queries.labels.loading; }, @@ -98,21 +78,11 @@ export default { return Boolean(this.searchKey) && this.visibleLabels.length === 0; }, }, - watch: { - searchKey(value) { - // When there is search string present - // and there are matching results, - // highlight first item by default. - if (value && this.visibleLabels.length) { - this.currentHighlightItem = 0; - } - }, - }, created() { this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); }, beforeDestroy() { - this.$emit('closeDropdown', this.localSelectedLabels); + this.$emit('setLabels', this.localSelectedLabels); this.debouncedSearchKeyUpdate.cancel(); }, methods: { @@ -150,33 +120,6 @@ export default { }); } }, - /** - * This method enables keyboard navigation support for - * the dropdown. - */ - handleKeyDown(e) { - if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) { - this.currentHighlightItem -= 1; - } else if ( - e.keyCode === DOWN_KEY_CODE && - this.currentHighlightItem < this.visibleLabels.length - 1 - ) { - this.currentHighlightItem += 1; - } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) { - this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]); - this.searchKey = ''; - } else if (e.keyCode === ESC_KEY_CODE) { - this.$emit('closeDropdown', this.localSelectedLabels); - } - - if (e.keyCode !== ESC_KEY_CODE) { - // Scroll the list only after highlighting - // styles are rendered completely. - this.$nextTick(() => { - this.scrollIntoViewIfNeeded(); - }); - } - }, handleLabelClick(label) { this.updateSelectedLabels(label); if (!this.allowMultiselect) { @@ -191,69 +134,41 @@ export default { </script> <template> - <div - class="labels-select-contents-list js-labels-list" - data-testid="dropdown-wrapper" - @keydown="handleKeyDown" - > - <div class="dropdown-input" @click.stop="() => {}"> - <gl-search-box-by-type - ref="searchInput" - :value="searchKey" - :disabled="labelsFetchInProgress" - data-qa-selector="dropdown_input_field" - data-testid="dropdown-input-field" - @input="debouncedSearchKeyUpdate" - /> - </div> - <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content"> + <gl-dropdown-form class="labels-select-contents-list js-labels-list"> + <gl-search-box-by-type + ref="searchInput" + :value="searchKey" + :disabled="labelsFetchInProgress" + data-qa-selector="dropdown_input_field" + data-testid="dropdown-input-field" + @input="debouncedSearchKeyUpdate" + /> + <div ref="labelsListContainer" data-testid="dropdown-content"> <gl-loading-icon v-if="labelsFetchInProgress" class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full" size="md" /> - <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list"> - <label-item - v-for="(label, index) in visibleLabels" + <template v-else> + <gl-dropdown-item + v-for="label in visibleLabels" :key="label.id" - :label="label" - :is-label-set="isLabelSelected(label)" - :highlight="index === currentHighlightItem" - @clickLabel="handleLabelClick(label)" - /> - <li + :is-checked="isLabelSelected(label)" + :is-check-centered="true" + :is-check-item="true" + data-testid="labels-list" + @click.native.capture.stop="handleLabelClick(label)" + > + <label-item :label="label" /> + </gl-dropdown-item> + <gl-dropdown-item v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center" data-testid="no-results" > {{ __('No matching results') }} - </li> - </ul> - </div> - <div - v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" - class="dropdown-footer" - data-testid="dropdown-footer" - > - <ul class="list-unstyled"> - <li v-if="allowLabelCreate"> - <gl-link - class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item" - data-testid="create-label-button" - @click="$emit('toggleDropdownContentsCreateView')" - > - {{ footerCreateLabelTitle }} - </gl-link> - </li> - <li> - <gl-link - :href="labelsManagePath" - class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item" - > - {{ footerManageLabelTitle }} - </gl-link> - </li> - </ul> + </gl-dropdown-item> + </template> </div> - </div> + </gl-dropdown-form> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue deleted file mode 100644 index 46edfa1c42a..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue +++ /dev/null @@ -1,40 +0,0 @@ -<script> -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; - -export default { - components: { - GlButton, - GlLoadingIcon, - }, - props: { - labelsSelectInProgress: { - type: Boolean, - required: true, - }, - }, - computed: { - ...mapState(['allowLabelEdit', 'labelsFetchInProgress']), - }, - methods: { - ...mapActions(['toggleDropdownContents']), - }, -}; -</script> - -<template> - <div class="title hide-collapsed gl-mb-3"> - {{ __('Labels') }} - <template v-if="allowLabelEdit"> - <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline /> - <gl-button - category="tertiary" - size="small" - class="float-right js-sidebar-dropdown-toggle gl-mr-n2" - data-qa-selector="labels_edit_button" - @click="toggleDropdownContents" - >{{ __('Edit') }}</gl-button - > - </template> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 58a940bca3b..71d3d87cce5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -1,5 +1,6 @@ <script> import { GlLabel } from '@gitlab/ui'; +import { sortBy } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isScopedLabel } from '~/lib/utils/common_utils'; @@ -7,6 +8,7 @@ export default { components: { GlLabel, }, + inject: ['allowScopedLabels'], props: { disableLabels: { type: Boolean, @@ -21,10 +23,6 @@ export default { type: Boolean, required: true, }, - allowScopedLabels: { - type: Boolean, - required: true, - }, labelsFilterBasePath: { type: String, required: true, @@ -34,6 +32,11 @@ export default { required: true, }, }, + computed: { + sortedSelectedLabels() { + return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1)); + }, + }, methods: { labelFilterUrl(label) { return `${this.labelsFilterBasePath}?${this.labelsFilterParam}[]=${encodeURIComponent( @@ -63,7 +66,7 @@ export default { </span> <template v-else> <gl-label - v-for="label in selectedLabels" + v-for="label in sortedSelectedLabels" :key="label.id" data-qa-selector="selected_label_content" :data-qa-label-name="label.title" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql index 9aa4f5d165e..eb478645a03 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql @@ -6,9 +6,7 @@ mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPa id color description - descriptionHtml title - textColor } errors } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue index e8fdf4bb0c2..f27f0b4e34c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue @@ -1,82 +1,21 @@ <script> -import { GlLink, GlIcon } from '@gitlab/ui'; - export default { - functional: true, props: { label: { type: Object, required: true, }, - isLabelSet: { - type: Boolean, - required: true, - }, - highlight: { - type: Boolean, - required: false, - default: false, - }, - }, - render(h, { props, listeners }) { - const { label, highlight, isLabelSet } = props; - - const labelColorBox = h('span', { - class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3', - style: { - backgroundColor: label.color, - }, - attrs: { - 'data-testid': 'label-color-box', - }, - }); - - const checkedIcon = h(GlIcon, { - class: { - 'gl-mr-3 gl-flex-shrink-0': true, - hidden: !isLabelSet, - }, - props: { - name: 'mobile-issue-close', - }, - }); - - const noIcon = h('span', { - class: { - 'gl-mr-5 gl-pr-3': true, - hidden: isLabelSet, - }, - attrs: { - 'data-testid': 'no-icon', - }, - }); - - const labelTitle = h('span', label.title); - - const labelLink = h( - GlLink, - { - class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal', - on: { - click: () => { - listeners.clickLabel(label); - }, - }, - }, - [noIcon, checkedIcon, labelColorBox, labelTitle], - ); - - return h( - 'li', - { - class: { - 'gl-display-block': true, - 'gl-text-left': true, - 'is-focused': highlight, - }, - }, - [labelLink], - ); }, }; </script> + +<template> + <div> + <span + class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3" + :style="{ 'background-color': label.color }" + data-testid="label-color-box" + ></span> + <span>{{ label.title }}</span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue index 0499dfe468f..3c834770563 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue @@ -1,57 +1,40 @@ <script> -import $ from 'jquery'; import Vue from 'vue'; -import Vuex, { mapState, mapActions, mapGetters } from 'vuex'; -import { isInViewport } from '~/lib/utils/common_utils'; +import Vuex from 'vuex'; import { __ } from '~/locale'; - +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import { DropdownVariant } from './constants'; -import DropdownButton from './dropdown_button.vue'; import DropdownContents from './dropdown_contents.vue'; -import DropdownTitle from './dropdown_title.vue'; import DropdownValue from './dropdown_value.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import issueLabelsQuery from './graphql/issue_labels.query.graphql'; -import labelsSelectModule from './store'; +import { + isDropdownVariantSidebar, + isDropdownVariantStandalone, + isDropdownVariantEmbedded, +} from './utils'; Vue.use(Vuex); export default { - store: new Vuex.Store(labelsSelectModule()), components: { - DropdownTitle, DropdownValue, - DropdownButton, DropdownContents, DropdownValueCollapsed, + SidebarEditableItem, }, - inject: ['iid', 'projectPath'], + inject: ['iid', 'projectPath', 'allowLabelEdit'], props: { allowLabelRemove: { type: Boolean, required: false, default: false, }, - allowLabelEdit: { - type: Boolean, - required: false, - default: false, - }, - allowLabelCreate: { - type: Boolean, - required: false, - default: false, - }, allowMultiselect: { type: Boolean, required: false, default: false, }, - allowScopedLabels: { - type: Boolean, - required: false, - default: false, - }, variant: { type: String, required: false, @@ -67,16 +50,6 @@ export default { required: false, default: false, }, - labelsFetchPath: { - type: String, - required: false, - default: '', - }, - labelsManagePath: { - type: String, - required: false, - default: '', - }, labelsFilterBasePath: { type: String, required: false, @@ -138,149 +111,25 @@ export default { }, }, }, - computed: { - ...mapState(['showDropdownButton', 'showDropdownContents']), - ...mapGetters([ - 'isDropdownVariantSidebar', - 'isDropdownVariantStandalone', - 'isDropdownVariantEmbedded', - ]), - dropdownButtonVisible() { - return this.isDropdownVariantSidebar ? this.showDropdownButton : true; - }, - }, - watch: { - selectedLabels(selectedLabels) { - this.setInitialState({ - selectedLabels, - }); - }, - showDropdownContents(showDropdownContents) { - this.setContentIsOnViewport(showDropdownContents); - }, - isEditing(newVal) { - if (newVal) { - this.toggleDropdownContents(); - } - }, - }, - mounted() { - this.setInitialState({ - variant: this.variant, - allowLabelRemove: this.allowLabelRemove, - allowLabelEdit: this.allowLabelEdit, - allowLabelCreate: this.allowLabelCreate, - allowMultiselect: this.allowMultiselect, - allowScopedLabels: this.allowScopedLabels, - dropdownButtonText: this.dropdownButtonText, - selectedLabels: this.selectedLabels, - labelsFetchPath: this.labelsFetchPath, - labelsManagePath: this.labelsManagePath, - labelsFilterBasePath: this.labelsFilterBasePath, - labelsFilterParam: this.labelsFilterParam, - labelsListTitle: this.labelsListTitle, - footerCreateLabelTitle: this.footerCreateLabelTitle, - footerManageLabelTitle: this.footerManageLabelTitle, - }); - - this.$store.subscribeAction({ - after: this.handleVuexActionDispatch, - }); - - document.addEventListener('mousedown', this.handleDocumentMousedown); - document.addEventListener('click', this.handleDocumentClick); - }, - beforeDestroy() { - document.removeEventListener('mousedown', this.handleDocumentMousedown); - document.removeEventListener('click', this.handleDocumentClick); - }, methods: { - ...mapActions(['setInitialState', 'toggleDropdownContents']), - /** - * This method stores a mousedown event's target. - * Required by the click listener because the click - * event itself has no reference to this element. - */ - handleDocumentMousedown({ target }) { - this.mousedownTarget = target; - }, - /** - * This method listens for document-wide click event - * and toggle dropdown if user clicks anywhere outside - * the dropdown while dropdown is visible. - */ - handleDocumentClick({ target }) { - // We also perform the toggle exception check for the - // last mousedown event's target to avoid hiding the - // box when the mousedown happened inside the box and - // only the mouseup did not. - if ( - this.showDropdownContents && - !this.preventDropdownToggleOnClick(target) && - !this.preventDropdownToggleOnClick(this.mousedownTarget) - ) { - this.toggleDropdownContents(); - } - }, - /** - * This method checks whether a given click target - * should prevent the dropdown from being toggled. - */ - preventDropdownToggleOnClick(target) { - // This approach of element detection is needed - // as the dropdown wrapper is not using `GlDropdown` as - // it will also require us to use `BDropdownForm` - // which is yet to be implemented in GitLab UI. - const hasExceptionClass = [ - 'js-dropdown-button', - 'js-btn-cancel-create', - 'js-sidebar-dropdown-toggle', - ].some( - (className) => - target?.classList.contains(className) || - target?.parentElement?.classList.contains(className), - ); - - const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some( - (className) => $(target).parents(className).length, - ); - - const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target); - - const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target); - - return ( - hasExceptionClass || - hasExceptionParent || - isInDropdownButtonCollapsed || - isInDropdownContents - ); - }, handleDropdownClose(labels) { - // Only emit label updates if there are any labels to update - // on UI. - if (this.showDropdownContents) { - this.toggleDropdownContents(); - } if (labels.length) this.$emit('updateSelectedLabels', labels); this.$emit('onDropdownClose'); }, + collapseDropdown() { + this.$refs.editable.collapse(); + }, handleCollapsedValueClick() { this.$emit('toggleCollapse'); }, - setContentIsOnViewport(showDropdownContents) { - if (!showDropdownContents) { - this.contentIsOnViewport = true; - - return; - } - + showDropdown() { this.$nextTick(() => { - if (this.$refs.dropdownContents) { - this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el); - } + this.$refs.dropdownContents.showDropdown(); }); }, + isDropdownVariantSidebar, + isDropdownVariantStandalone, + isDropdownVariantEmbedded, }, }; </script> @@ -289,58 +138,63 @@ export default { <div class="labels-select-wrapper position-relative" :class="{ - 'is-standalone': isDropdownVariantStandalone, - 'is-embedded': isDropdownVariantEmbedded, + 'is-standalone': isDropdownVariantStandalone(variant), + 'is-embedded': isDropdownVariantEmbedded(variant), }" > - <template v-if="isDropdownVariantSidebar"> + <template v-if="isDropdownVariantSidebar(variant)"> <dropdown-value-collapsed ref="dropdownButtonCollapsed" :labels="issueLabels" @onValueClick="handleCollapsedValueClick" /> - <dropdown-title - :allow-label-edit="allowLabelEdit" - :labels-select-in-progress="labelsSelectInProgress" - /> - <dropdown-value - :disable-labels="labelsSelectInProgress" - :selected-labels="issueLabels" - :allow-label-remove="allowLabelRemove" - :allow-scoped-labels="allowScopedLabels" - :labels-filter-base-path="labelsFilterBasePath" - :labels-filter-param="labelsFilterParam" - @onLabelRemove="$emit('onLabelRemove', $event)" + <sidebar-editable-item + ref="editable" + :title="__('Labels')" + :loading="labelsSelectInProgress" + :can-edit="allowLabelEdit" + @open="showDropdown" > - <slot></slot> - </dropdown-value> - <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" /> - <dropdown-contents - v-if="dropdownButtonVisible && showDropdownContents" - ref="dropdownContents" - :allow-multiselect="allowMultiselect" - :labels-list-title="labelsListTitle" - :footer-create-label-title="footerCreateLabelTitle" - :footer-manage-label-title="footerManageLabelTitle" - :render-on-top="!contentIsOnViewport" - :labels-create-title="labelsCreateTitle" - :selected-labels="selectedLabels" - @closeDropdown="handleDropdownClose" - /> - </template> - <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded"> - <dropdown-button v-show="dropdownButtonVisible" /> - <dropdown-contents - v-if="dropdownButtonVisible && showDropdownContents" - ref="dropdownContents" - :allow-multiselect="allowMultiselect" - :labels-list-title="labelsListTitle" - :footer-create-label-title="footerCreateLabelTitle" - :footer-manage-label-title="footerManageLabelTitle" - :render-on-top="!contentIsOnViewport" - :selected-labels="selectedLabels" - @closeDropdown="handleDropdownClose" - /> + <template #collapsed> + <dropdown-value + :disable-labels="labelsSelectInProgress" + :selected-labels="issueLabels" + :allow-label-remove="allowLabelRemove" + :labels-filter-base-path="labelsFilterBasePath" + :labels-filter-param="labelsFilterParam" + @onLabelRemove="$emit('onLabelRemove', $event)" + > + <slot></slot> + </dropdown-value> + </template> + <template #default="{ edit }"> + <dropdown-value + :disable-labels="labelsSelectInProgress" + :selected-labels="issueLabels" + :allow-label-remove="allowLabelRemove" + :labels-filter-base-path="labelsFilterBasePath" + :labels-filter-param="labelsFilterParam" + class="gl-mb-2" + @onLabelRemove="$emit('onLabelRemove', $event)" + > + <slot></slot> + </dropdown-value> + <dropdown-contents + v-if="edit" + ref="dropdownContents" + :dropdown-button-text="dropdownButtonText" + :allow-multiselect="allowMultiselect" + :labels-list-title="labelsListTitle" + :footer-create-label-title="footerCreateLabelTitle" + :footer-manage-label-title="footerManageLabelTitle" + :labels-create-title="labelsCreateTitle" + :selected-labels="selectedLabels" + :variant="variant" + @closeDropdown="collapseDropdown" + @setLabels="handleDropdownClose" + /> + </template> + </sidebar-editable-item> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js deleted file mode 100644 index b3d4a204a81..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as types from './mutation_types'; - -export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props); - -export const toggleDropdownButton = ({ commit }) => commit(types.TOGGLE_DROPDOWN_BUTTON); -export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDOWN_CONTENTS); - -export const toggleDropdownContentsCreateView = ({ commit }) => - commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW); - -export const updateSelectedLabels = ({ commit }, labels) => - commit(types.UPDATE_SELECTED_LABELS, { labels }); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js deleted file mode 100644 index d14f96720b7..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js +++ /dev/null @@ -1,52 +0,0 @@ -import { __, s__, sprintf } from '~/locale'; -import { DropdownVariant } from '../constants'; - -/** - * Returns string representing current labels - * selection on dropdown button. - * - * @param {object} state - */ -export const dropdownButtonText = (state, getters) => { - const selectedLabels = getters.isDropdownVariantSidebar - ? state.labels.filter((label) => label.set) - : state.selectedLabels; - - if (!selectedLabels.length) { - return state.dropdownButtonText || __('Label'); - } else if (selectedLabels.length > 1) { - return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), { - firstLabelName: selectedLabels[0].title, - remainingLabelCount: selectedLabels.length - 1, - }); - } - return selectedLabels[0].title; -}; - -/** - * Returns array containing only label IDs from - * selectedLabels array. - * @param {object} state - */ -export const selectedLabelsList = (state) => state.selectedLabels.map((label) => label.id); - -/** - * Returns boolean representing whether dropdown variant - * is `sidebar` - * @param {object} state - */ -export const isDropdownVariantSidebar = (state) => state.variant === DropdownVariant.Sidebar; - -/** - * Returns boolean representing whether dropdown variant - * is `standalone` - * @param {object} state - */ -export const isDropdownVariantStandalone = (state) => state.variant === DropdownVariant.Standalone; - -/** - * Returns boolean representing whether dropdown variant - * is `embedded` - * @param {object} state - */ -export const isDropdownVariantEmbedded = (state) => state.variant === DropdownVariant.Embedded; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js deleted file mode 100644 index 5f61cb732c8..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default () => ({ - namespaced: true, - state: state(), - actions, - getters, - mutations, -}); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js deleted file mode 100644 index bd71c3b85f1..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; - -export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; -export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; - -export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS'; - -export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW'; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js deleted file mode 100644 index 45ec4d7ae04..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js +++ /dev/null @@ -1,50 +0,0 @@ -import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; -import { DropdownVariant } from '../constants'; -import * as types from './mutation_types'; - -export default { - [types.SET_INITIAL_STATE](state, props) { - Object.assign(state, { ...props }); - }, - - [types.TOGGLE_DROPDOWN_BUTTON](state) { - state.showDropdownButton = !state.showDropdownButton; - }, - - [types.TOGGLE_DROPDOWN_CONTENTS](state) { - if (state.variant === DropdownVariant.Sidebar) { - state.showDropdownButton = !state.showDropdownButton; - } - state.showDropdownContents = !state.showDropdownContents; - // Ensure that Create View is hidden by default - // when dropdown contents are revealed. - if (state.showDropdownContents) { - state.showDropdownContentsCreateView = false; - } - }, - - [types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) { - state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView; - }, - [types.UPDATE_SELECTED_LABELS](state, { labels }) { - // Find the label to update from all the labels - // and change `set` prop value to represent their current state. - const labelId = labels.pop()?.id; - const candidateLabel = state.labels.find((label) => labelId === label.id); - if (candidateLabel) { - candidateLabel.touched = true; - candidateLabel.set = !candidateLabel.set; - } - - if (isScopedLabel(candidateLabel)) { - const scopedBase = scopedLabelKey(candidateLabel); - const currentActiveScopedLabel = state.labels.find( - ({ title }) => title.indexOf(scopedBase) === 0 && title !== candidateLabel.title, - ); - - if (currentActiveScopedLabel) { - currentActiveScopedLabel.set = false; - } - } - }, -}; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js deleted file mode 100644 index 220bab05ed2..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js +++ /dev/null @@ -1,28 +0,0 @@ -export default () => ({ - // Initial Data - labels: [], - selectedLabels: [], - labelsListTitle: '', - footerCreateLabelTitle: '', - footerManageLabelTitle: '', - dropdownButtonText: '', - - // Paths - namespace: '', - labelsFetchPath: '', - labelsFilterBasePath: '', - - // UI Flags - variant: '', - allowLabelRemove: false, - allowLabelCreate: false, - allowLabelEdit: false, - allowScopedLabels: false, - allowMultiselect: false, - showDropdownButton: false, - showDropdownContents: false, - showDropdownContentsCreateView: false, - labelsFetchInProgress: false, - labelCreateInProgress: false, - selectedLabelsUpdated: false, -}); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js new file mode 100644 index 00000000000..b5cd946a189 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js @@ -0,0 +1,22 @@ +import { DropdownVariant } from './constants'; + +/** + * Returns boolean representing whether dropdown variant + * is `sidebar` + * @param {string} variant + */ +export const isDropdownVariantSidebar = (variant) => variant === DropdownVariant.Sidebar; + +/** + * Returns boolean representing whether dropdown variant + * is `standalone` + * @param {string} variant + */ +export const isDropdownVariantStandalone = (variant) => variant === DropdownVariant.Standalone; + +/** + * Returns boolean representing whether dropdown variant + * is `embedded` + * @param {string} variant + */ +export const isDropdownVariantEmbedded = (variant) => variant === DropdownVariant.Embedded; |