diff options
Diffstat (limited to 'app/assets/javascripts/ref/components/ref_selector.vue')
-rw-r--r-- | app/assets/javascripts/ref/components/ref_selector.vue | 220 |
1 files changed, 80 insertions, 140 deletions
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index 10967fb84ed..359909b8f3b 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -1,13 +1,8 @@ <script> -import { - GlDropdown, - GlDropdownDivider, - GlSearchBoxByType, - GlSprintf, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui'; import { debounce, isArray } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { sprintf } from '~/locale'; import { ALL_REF_TYPES, SEARCH_DEBOUNCE_MS, @@ -15,21 +10,16 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS, - BRANCH_REF_TYPE, - TAG_REF_TYPE, } from '../constants'; import createStore from '../stores'; -import RefResultsSection from './ref_results_section.vue'; +import { formatListBoxItems, formatErrors } from '../format_refs'; export default { name: 'RefSelector', components: { - GlDropdown, - GlDropdownDivider, - GlSearchBoxByType, - GlSprintf, - GlLoadingIcon, - RefResultsSection, + GlBadge, + GlIcon, + GlCollapsibleListbox, }, inheritAttrs: false, props: { @@ -87,6 +77,11 @@ export default { required: false, default: '', }, + toggleButtonClass: { + type: [String, Object, Array], + required: false, + default: null, + }, }, data() { return { @@ -106,35 +101,33 @@ export default { ...this.translations, }; }, - showBranchesSection() { - return ( - this.enabledRefTypes.includes(REF_TYPE_BRANCHES) && - Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error) - ); + listBoxItems() { + return formatListBoxItems(this.branches, this.tags, this.commits); }, - showTagsSection() { - return ( - this.enabledRefTypes.includes(REF_TYPE_TAGS) && - Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error) - ); + branches() { + return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : []; }, - showCommitsSection() { - return ( - this.enabledRefTypes.includes(REF_TYPE_COMMITS) && - Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error) - ); + tags() { + return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : []; }, - showNoResults() { - return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection; + commits() { + return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : []; }, - showSectionHeaders() { - return this.enabledRefTypes.length > 1; - }, - toggleButtonClass() { - return { - 'gl-inset-border-1-red-500!': !this.state, - 'gl-font-monospace': Boolean(this.selectedRef), - }; + extendedToggleButtonClass() { + const classes = [ + { + 'gl-inset-border-1-red-500!': !this.state, + 'gl-font-monospace': Boolean(this.selectedRef), + }, + ]; + + if (Array.isArray(this.toggleButtonClass)) { + classes.push(...this.toggleButtonClass); + } else { + classes.push(this.toggleButtonClass); + } + + return classes; }, footerSlotProps() { return { @@ -143,6 +136,9 @@ export default { query: this.lastQuery, }; }, + errors() { + return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits); + }, selectedRefForDisplay() { if (this.useSymbolicRefNames && this.selectedRef) { return this.selectedRef.replace(/^refs\/(tags|heads)\//, ''); @@ -153,11 +149,12 @@ export default { buttonText() { return this.selectedRefForDisplay || this.i18n.noRefSelected; }, - isTagRefType() { - return this.refType === TAG_REF_TYPE; - }, - isBranchRefType() { - return this.refType === BRANCH_REF_TYPE; + noResultsMessage() { + return this.lastQuery + ? sprintf(this.i18n.noResultsWithQuery, { + query: this.lastQuery, + }) + : this.i18n.noResults; }, }, watch: { @@ -185,9 +182,7 @@ export default { // because we need to access the .cancel() method // lodash attaches to the function, which is // made inaccessible by Vue. - this.debouncedSearch = debounce(function search() { - this.search(); - }, SEARCH_DEBOUNCE_MS); + this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS); this.setProjectId(this.projectId); @@ -214,14 +209,8 @@ export default { 'setSelectedRef', ]), ...mapActions({ storeSearch: 'search' }), - focusSearchBox() { - this.$refs.searchBox.$el.querySelector('input').focus(); - }, - onSearchBoxEnter() { - this.debouncedSearch.cancel(); - this.search(); - }, - onSearchBoxInput() { + onSearchBoxInput(searchQuery = '') { + this.query = searchQuery?.trim(); this.debouncedSearch(); }, selectRef(ref) { @@ -231,104 +220,55 @@ export default { search() { this.storeSearch(this.query); }, + totalCountText(count) { + return count > 999 ? this.i18n.totalCountLabel : `${count}`; + }, }, }; </script> <template> <div> - <gl-dropdown - :header-text="i18n.dropdownHeader" - :toggle-class="toggleButtonClass" - :text="buttonText" + <gl-collapsible-listbox class="ref-selector gl-w-full" + block + searchable + :selected="selectedRef" + :header-text="i18n.dropdownHeader" + :items="listBoxItems" + :no-results-text="noResultsMessage" + :searching="isLoading" + :search-placeholder="i18n.searchPlaceholder" + :toggle-class="extendedToggleButtonClass" + :toggle-text="buttonText" v-bind="$attrs" v-on="$listeners" - @shown="focusSearchBox" + @hidden="$emit('hide')" + @search="onSearchBoxInput" + @select="selectRef" > - <template #header> - <gl-search-box-by-type - ref="searchBox" - v-model.trim="query" - :placeholder="i18n.searchPlaceholder" - autocomplete="off" - data-qa-selector="ref_selector_searchbox" - @input="onSearchBoxInput" - @keydown.enter.prevent="onSearchBoxEnter" - /> + <template #group-label="{ group }"> + {{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge> </template> - - <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" /> - - <div - v-else-if="showNoResults" - class="gl-text-center gl-mx-3 gl-py-3" - data-testid="no-results" - > - <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery"> - <template #query> - <b class="gl-word-break-all">{{ lastQuery }}</b> - </template> - </gl-sprintf> - - <span v-else>{{ i18n.noResults }}</span> - </div> - - <template v-else> - <template v-if="showBranchesSection"> - <ref-results-section - :section-title="i18n.branches" - :total-count="matches.branches.totalCount" - :items="matches.branches.list" - :selected-ref="selectedRef" - :error="matches.branches.error" - :error-message="i18n.branchesErrorMessage" - :show-header="showSectionHeaders" - data-testid="branches-section" - data-qa-selector="branches_section" - :should-show-check="!useSymbolicRefNames || isBranchRefType" - @selected="selectRef($event)" - /> - - <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" /> - </template> - - <template v-if="showTagsSection"> - <ref-results-section - :section-title="i18n.tags" - :total-count="matches.tags.totalCount" - :items="matches.tags.list" - :selected-ref="selectedRef" - :error="matches.tags.error" - :error-message="i18n.tagsErrorMessage" - :show-header="showSectionHeaders" - data-testid="tags-section" - :should-show-check="!useSymbolicRefNames || isTagRefType" - @selected="selectRef($event)" - /> - - <gl-dropdown-divider v-if="showCommitsSection" /> - </template> - - <template v-if="showCommitsSection"> - <ref-results-section - :section-title="i18n.commits" - :total-count="matches.commits.totalCount" - :items="matches.commits.list" - :selected-ref="selectedRef" - :error="matches.commits.error" - :error-message="i18n.commitsErrorMessage" - :show-header="showSectionHeaders" - data-testid="commits-section" - @selected="selectRef($event)" - /> - </template> + <template #list-item="{ item }"> + {{ item.text }} + <gl-badge v-if="item.default" size="sm" variant="info">{{ + i18n.defaultLabelText + }}</gl-badge> </template> - <template #footer> <slot name="footer" v-bind="footerSlotProps"></slot> + <div + v-for="errorMessage in errors" + :key="errorMessage" + data-testid="red-selector-error-list" + class="gl-display-flex gl-align-items-flex-start gl-text-red-500 gl-mx-4 gl-my-3" + > + <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" /> + <span>{{ errorMessage }}</span> + </div> </template> - </gl-dropdown> + </gl-collapsible-listbox> <input v-if="name" data-testid="selected-ref-form-field" |