diff options
Diffstat (limited to 'app/assets/javascripts/ref')
-rw-r--r-- | app/assets/javascripts/ref/components/ref_results_section.vue | 138 | ||||
-rw-r--r-- | app/assets/javascripts/ref/components/ref_selector.vue | 220 | ||||
-rw-r--r-- | app/assets/javascripts/ref/constants.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/ref/format_refs.js | 60 |
4 files changed, 143 insertions, 279 deletions
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue deleted file mode 100644 index 52d1ed96b21..00000000000 --- a/app/assets/javascripts/ref/components/ref_results_section.vue +++ /dev/null @@ -1,138 +0,0 @@ -<script> -import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui'; -import { s__ } from '~/locale'; - -export default { - name: 'RefResultsSection', - components: { - GlDropdownSectionHeader, - GlDropdownItem, - GlBadge, - GlIcon, - }, - props: { - showHeader: { - type: Boolean, - required: false, - default: true, - }, - - sectionTitle: { - type: String, - required: true, - }, - - totalCount: { - type: Number, - required: true, - }, - - /** - * An array of object that have the following properties: - * - * - name (String, required): The name of the ref that will be displayed - * - value (String, optional): The value that will be selected when the ref - * is selected. If not provided, `name` will be used as the value. - * For example, commits use the short SHA for `name` - * and long SHA for `value`. - * - subtitle (String, optional): Text to render underneath the name. - * For example, used to render the commit's title underneath its SHA. - * - default (Boolean, optional): Whether or not to render a "default" - * indicator next to the item. Used to indicate - * the project's default branch. - * - */ - items: { - type: Array, - required: true, - validator: (items) => Array.isArray(items) && items.every((item) => item.name), - }, - - /** - * The currently selected ref. - * Used to render a check mark by the selected item. - * */ - selectedRef: { - type: String, - required: false, - default: '', - }, - - /** - * An error object that indicates that an error - * occurred while fetching items for this section - */ - error: { - type: Error, - required: false, - default: null, - }, - - /** The message to display if an error occurs */ - errorMessage: { - type: String, - required: false, - default: '', - }, - shouldShowCheck: { - type: Boolean, - required: false, - default: true, - }, - }, - computed: { - totalCountText() { - return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`; - }, - }, - methods: { - showCheck(item) { - if (!this.shouldShowCheck) { - return false; - } - return item.name === this.selectedRef || item.value === this.selectedRef; - }, - }, -}; -</script> - -<template> - <div> - <gl-dropdown-section-header v-if="showHeader"> - <div class="gl-display-flex align-items-center" data-testid="section-header"> - <span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span> - <gl-badge variant="neutral">{{ totalCountText }}</gl-badge> - </div> - </gl-dropdown-section-header> - <template v-if="error"> - <div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3"> - <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" /> - <span>{{ errorMessage }}</span> - </div> - </template> - <template v-else> - <gl-dropdown-item - v-for="item in items" - :key="item.name" - @click="$emit('selected', item.value || item.name)" - > - <div class="gl-display-flex align-items-start"> - <gl-icon - name="mobile-issue-close" - class="gl-mr-2 gl-flex-shrink-0" - :class="{ 'gl-visibility-hidden': !showCheck(item) }" - /> - - <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column"> - <span class="gl-font-monospace">{{ item.name }}</span> - <span class="gl-text-gray-400">{{ item.subtitle }}</span> - </div> - - <gl-badge v-if="item.default" size="sm" variant="info">{{ - s__('DefaultBranchLabel|default') - }}</gl-badge> - </div> - </gl-dropdown-item> - </template> - </div> -</template> 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" diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js index f4faa535166..4b5b18cf6c1 100644 --- a/app/assets/javascripts/ref/constants.js +++ b/app/assets/javascripts/ref/constants.js @@ -1,5 +1,5 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { __ } from '~/locale'; +import { s__, __ } from '~/locale'; export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES'; export const REF_TYPE_TAGS = 'REF_TYPE_TAGS'; @@ -13,6 +13,7 @@ export const X_TOTAL_HEADER = 'x-total'; export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS; export const DEFAULT_I18N = Object.freeze({ + defaultLabelText: __('default'), dropdownHeader: __('Select Git revision'), searchPlaceholder: __('Search by Git revision'), noResultsWithQuery: __('No matching results for "%{query}"'), @@ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({ tags: __('Tags'), commits: __('Commits'), noRefSelected: __('No ref selected'), + totalCountLabel: s__('TotalRefCountIndicator|1000+'), }); diff --git a/app/assets/javascripts/ref/format_refs.js b/app/assets/javascripts/ref/format_refs.js new file mode 100644 index 00000000000..af310a35ef4 --- /dev/null +++ b/app/assets/javascripts/ref/format_refs.js @@ -0,0 +1,60 @@ +import { DEFAULT_I18N } from './constants'; + +function convertToListBoxItems(items) { + return items.map((item) => ({ + text: item.name, + value: item.value || item.name, + default: item.default, + })); +} + +/** + * Format multiple lists to array of group options for listbox + * @param branches list of branches + * @param tags list of tags + * @param commits list of commits + * @returns {*[]} array of group items with header and options + */ +export const formatListBoxItems = (branches, tags, commits) => { + const listBoxItems = []; + + const addToFinalResult = (items, header) => { + if (items && items.length > 0) { + listBoxItems.push({ + text: header, + options: convertToListBoxItems(items), + }); + } + }; + + addToFinalResult(branches, DEFAULT_I18N.branches); + addToFinalResult(tags, DEFAULT_I18N.tags); + addToFinalResult(commits, DEFAULT_I18N.commits); + + return listBoxItems; +}; + +/** + * Check error existence and add to final array + * @param branches list of branches + * @param tags list of tags + * @param commits list of commits + * @returns {*[]} array of error messages + */ +export const formatErrors = (branches, tags, commits) => { + const errorsList = []; + + if (branches && branches.error) { + errorsList.push(DEFAULT_I18N.branchesErrorMessage); + } + + if (tags && tags.error) { + errorsList.push(DEFAULT_I18N.tagsErrorMessage); + } + + if (commits && commits.error) { + errorsList.push(DEFAULT_I18N.commitsErrorMessage); + } + + return errorsList; +}; |