diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue | 260 |
1 files changed, 160 insertions, 100 deletions
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 bff34743344..ffa37424c2c 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,38 +1,91 @@ <script> -import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; +import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import { mapState, mapGetters, mapActions } from 'vuex'; - +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: { - GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink, LabelItem, }, + inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'], + props: { + selectedLabels: { + type: Array, + required: true, + }, + allowMultiselect: { + 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], }; }, + apollo: { + labels: { + query: projectLabelsQuery, + variables() { + return { + fullPath: this.projectPath, + searchTerm: this.searchKey, + }; + }, + skip() { + return this.searchKey.length === 1; + }, + update: (data) => data.workspace?.labels?.nodes || [], + async result() { + if (this.$refs.searchInput) { + await this.$nextTick(); + this.$refs.searchInput.focusInput(); + } + }, + error() { + createFlash({ message: __('Error fetching labels.') }); + }, + }, + }, computed: { - ...mapState([ - 'allowLabelCreate', - 'allowMultiselect', - 'labelsManagePath', - 'labels', - 'labelsFetchInProgress', - 'labelsListTitle', - 'footerCreateLabelTitle', - 'footerManageLabelTitle', - ]), - ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), + isDropdownVariantSidebar() { + return this.variant === DropdownVariant.Sidebar; + }, + isDropdownVariantEmbedded() { + return this.variant === DropdownVariant.Embedded; + }, + labelsFetchInProgress() { + return this.$apollo.queries.labels.loading; + }, + localSelectedLabelsIds() { + return this.localSelectedLabels.map((label) => label.id); + }, visibleLabels() { if (this.searchKey) { return fuzzaldrinPlus.filter(this.labels, this.searchKey, { @@ -55,17 +108,16 @@ export default { } }, }, + created() { + this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + }, + beforeDestroy() { + this.$emit('closeDropdown', this.localSelectedLabels); + this.debouncedSearchKeyUpdate.cancel(); + }, methods: { - ...mapActions([ - 'toggleDropdownContents', - 'toggleDropdownContentsCreateView', - 'fetchLabels', - 'receiveLabelsSuccess', - 'updateSelectedLabels', - 'toggleDropdownContents', - ]), isLabelSelected(label) { - return this.selectedLabelsList.includes(label.id); + return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id)); }, /** * This method scrolls item from dropdown into @@ -86,23 +138,17 @@ export default { } } }, - handleComponentAppear() { - // We can avoid putting `catch` block here - // as failure is handled within actions.js already. - return this.fetchLabels().then(() => { - this.$refs.searchInput.focusInput(); - }); - }, - /** - * We want to remove loaded labels to ensure component - * fetches fresh set of labels every time when shown. - */ - handleComponentDisappear() { - this.receiveLabelsSuccess([]); - }, - handleCreateLabelClick() { - this.receiveLabelsSuccess([]); - this.toggleDropdownContentsCreateView(); + updateSelectedLabels(label) { + if (this.isLabelSelected(label)) { + this.localSelectedLabels = this.localSelectedLabels.filter( + ({ id }) => id !== getIdFromGraphQLId(label.id), + ); + } else { + this.localSelectedLabels.push({ + ...label, + id: getIdFromGraphQLId(label.id), + }); + } }, /** * This method enables keyboard navigation support for @@ -117,10 +163,10 @@ export default { ) { this.currentHighlightItem += 1; } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) { - this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]); + this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]); this.searchKey = ''; } else if (e.keyCode === ESC_KEY_CODE) { - this.toggleDropdownContents(); + this.$emit('closeDropdown', this.localSelectedLabels); } if (e.keyCode !== ESC_KEY_CODE) { @@ -132,68 +178,82 @@ export default { } }, handleLabelClick(label) { - this.updateSelectedLabels([label]); - if (!this.allowMultiselect) this.toggleDropdownContents(); + this.updateSelectedLabels(label); + if (!this.allowMultiselect) { + this.$emit('closeDropdown', this.localSelectedLabels); + } + }, + setSearchKey(value) { + this.searchKey = value; }, }, }; </script> <template> - <gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear"> - <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown"> - <div class="dropdown-input" @click.stop="() => {}"> - <gl-search-box-by-type - ref="searchInput" - v-model="searchKey" - :disabled="labelsFetchInProgress" - data-qa-selector="dropdown_input_field" - /> - </div> - <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content"> - <gl-loading-icon - v-if="labelsFetchInProgress" - class="labels-fetch-loading gl-align-items-center w-100 h-100" - size="md" + <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-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" + :key="label.id" + :label="label" + :is-label-set="isLabelSelected(label)" + :highlight="index === currentHighlightItem" + @clickLabel="handleLabelClick(label)" /> - <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word"> - <label-item - v-for="(label, index) in visibleLabels" - :key="label.id" - :label="label" - :is-label-set="label.set" - :highlight="index === currentHighlightItem" - @clickLabel="handleLabelClick(label)" - /> - <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center"> - {{ __('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 w-100 flex-row text-break-word label-item" - @click="handleCreateLabelClick" - > - {{ footerCreateLabelTitle }} - </gl-link> - </li> - <li> - <gl-link - :href="labelsManagePath" - class="gl-display-flex flex-row text-break-word label-item" - > - {{ footerManageLabelTitle }} - </gl-link> - </li> - </ul> - </div> + <li + 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> </div> - </gl-intersection-observer> + </div> </template> |