diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue')
-rw-r--r-- | app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue new file mode 100644 index 00000000000..c1939dc7785 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue @@ -0,0 +1,177 @@ +<script> +import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { createAlert } from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { __ } from '~/locale'; +import { workspaceLabelsQueries } from '../../../constants'; +import LabelItem from './label_item.vue'; + +export default { + components: { + GlDropdownForm, + GlDropdownItem, + GlLoadingIcon, + GlIntersectionObserver, + LabelItem, + }, + model: { + prop: 'localSelectedLabels', + }, + props: { + allowMultiselect: { + type: Boolean, + required: true, + }, + localSelectedLabels: { + type: Array, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + searchKey: { + type: String, + required: true, + }, + workspaceType: { + type: String, + required: true, + }, + }, + data() { + return { + labels: [], + isVisible: false, + }; + }, + apollo: { + labels: { + query() { + return workspaceLabelsQueries[this.workspaceType].query; + }, + variables() { + return { + fullPath: this.fullPath, + searchTerm: this.searchKey, + }; + }, + skip() { + return this.searchKey.length === 1 || !this.isVisible; + }, + update: (data) => data.workspace?.labels?.nodes || [], + error() { + createAlert({ message: __('Error fetching labels.') }); + }, + }, + }, + computed: { + labelsFetchInProgress() { + return this.$apollo.queries.labels.loading; + }, + localSelectedLabelsIds() { + return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id)); + }, + visibleLabels() { + if (this.searchKey) { + return fuzzaldrinPlus.filter(this.labels, this.searchKey, { + key: ['title'], + }); + } + return this.labels; + }, + showNoMatchingResultsMessage() { + return Boolean(this.searchKey) && this.visibleLabels.length === 0; + }, + shouldHighlightFirstItem() { + return this.searchKey !== '' && this.visibleLabels.length > 0; + }, + }, + methods: { + isLabelSelected(label) { + return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id)); + }, + /** + * This method scrolls item from dropdown into + * the view if it is off the viewable area of the + * container. + */ + scrollIntoViewIfNeeded() { + const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused'); + + if (highlightedLabel) { + const container = this.$refs.labelsListContainer.getBoundingClientRect(); + const label = highlightedLabel.getBoundingClientRect(); + + if (label.bottom > container.bottom) { + this.$refs.labelsListContainer.scrollTop += label.bottom - container.bottom; + } else if (label.top < container.top) { + this.$refs.labelsListContainer.scrollTop -= container.top - label.top; + } + } + }, + updateSelectedLabels(label) { + let labels; + if (this.isLabelSelected(label)) { + labels = this.localSelectedLabels.filter( + ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id, + ); + } else { + labels = [...this.localSelectedLabels, label]; + } + this.$emit('input', labels); + }, + handleLabelClick(label) { + this.updateSelectedLabels(label); + if (!this.allowMultiselect) { + this.$emit('closeDropdown', this.localSelectedLabels); + } + }, + onDropdownAppear() { + this.isVisible = true; + }, + selectFirstItem() { + if (this.shouldHighlightFirstItem) { + this.handleLabelClick(this.visibleLabels[0]); + } + }, + }, +}; +</script> + +<template> + <gl-intersection-observer @appear="onDropdownAppear"> + <gl-dropdown-form class="labels-select-contents-list js-labels-list"> + <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 gl-mb-3" + size="lg" + /> + <template v-else> + <gl-dropdown-item + v-for="(label, index) in visibleLabels" + :key="label.id" + :is-checked="isLabelSelected(label)" + is-check-centered + is-check-item + :active="shouldHighlightFirstItem && index === 0" + active-class="is-focused" + 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') }} + </gl-dropdown-item> + </template> + </div> + </gl-dropdown-form> + </gl-intersection-observer> +</template> |