summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ref
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ref')
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue138
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue220
-rw-r--r--app/assets/javascripts/ref/constants.js4
-rw-r--r--app/assets/javascripts/ref/format_refs.js60
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;
+};