Commit 5d95f4d6 authored by Nathan Friend's avatar Nathan Friend Committed by Illya Klymov

Add Vue ref selector component

Adds a Vue component that allows branches, tags, or commits to be
searched and selected.
parent 1ef0df07
<script>
import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
GlNewDropdownHeader,
GlNewDropdownItem,
GlBadge,
GlIcon,
},
props: {
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: '',
},
},
computed: {
totalCountText() {
return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`;
},
},
methods: {
showCheck(item) {
return item.name === this.selectedRef || item.value === this.selectedRef;
},
},
};
</script>
<template>
<div>
<gl-new-dropdown-header>
<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-new-dropdown-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-new-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-600">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
</gl-new-dropdown-item>
</template>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createStore from '../stores';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
import RefResultsSection from './ref_results_section.vue';
export default {
name: 'RefSelector',
store: createStore(),
components: {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
RefResultsSection,
},
props: {
value: {
type: String,
required: false,
default: '',
},
projectId: {
type: String,
required: true,
},
translations: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
query: '',
};
},
computed: {
...mapState({
matches: state => state.matches,
lastQuery: state => state.query,
selectedRef: state => state.selectedRef,
}),
...mapGetters(['isLoading', 'isQueryPossiblyASha']),
i18n() {
return {
...DEFAULT_I18N,
...this.translations,
};
},
showBranchesSection() {
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error);
},
showTagsSection() {
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error);
},
showCommitsSection() {
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error);
},
showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
},
created() {
this.setProjectId(this.projectId);
this.search(this.query);
},
methods: {
...mapActions(['setProjectId', 'setSelectedRef', 'search']),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxInput: debounce(function search() {
this.search(this.query);
}, SEARCH_DEBOUNCE_MS),
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
},
},
};
</script>
<template>
<gl-new-dropdown class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span>
</span>
<gl-icon name="chevron-down" />
</template>
<div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content">
<gl-new-dropdown-header>
<span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span>
</gl-new-dropdown-header>
<gl-new-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
v-model.trim="query"
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
<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"
data-testid="branches-section"
@selected="selectRef($event)"
/>
<gl-new-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"
data-testid="tags-section"
@selected="selectRef($event)"
/>
<gl-new-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"
data-testid="commits-section"
@selected="selectRef($event)"
/>
</template>
</template>
</div>
</div>
</gl-new-dropdown>
</template>
// This eslint-disable can be removed once a second import { __ } from '~/locale';
// value is added to this file.
/* eslint-disable import/prefer-default-export */
export const X_TOTAL_HEADER = 'x-total'; export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = 250;
export const DEFAULT_I18N = Object.freeze({
dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
noResults: __('No matching results'),
branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
tagsErrorMessage: __('An error occurred while fetching tags. Retry the search.'),
commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
branches: __('Branches'),
tags: __('Tags'),
commits: __('Commits'),
noRefSelected: __('No ref selected'),
});
.ref-selector {
& &-dropdown-content {
// Setting a max height is necessary to allow the dropdown's content
// to control where and how scrollbars appear.
// This content is limited to the max-height of the dropdown
// ($dropdown-max-height-lg) minus the additional padding
// on the top and bottom (2 * $gl-padding-8)
max-height: $dropdown-max-height-lg - 2 * $gl-padding-8;
}
.dropdown-menu.show {
// Make the dropdown a little wider and longer than usual
// since it contains quite a bit of content.
width: 20rem;
max-height: $dropdown-max-height-lg;
}
}
...@@ -2475,6 +2475,12 @@ msgstr "" ...@@ -2475,6 +2475,12 @@ msgstr ""
msgid "An error occurred while enabling Service Desk." msgid "An error occurred while enabling Service Desk."
msgstr "" msgstr ""
msgid "An error occurred while fetching branches. Retry the search."
msgstr ""
msgid "An error occurred while fetching commits. Retry the search."
msgstr ""
msgid "An error occurred while fetching coverage reports." msgid "An error occurred while fetching coverage reports."
msgstr "" msgstr ""
...@@ -2505,6 +2511,9 @@ msgstr "" ...@@ -2505,6 +2511,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data" msgid "An error occurred while fetching sidebar data"
msgstr "" msgstr ""
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
msgid "An error occurred while fetching terraform reports." msgid "An error occurred while fetching terraform reports."
msgstr "" msgstr ""
...@@ -7441,6 +7450,9 @@ msgstr "" ...@@ -7441,6 +7450,9 @@ msgstr ""
msgid "Default: Map a FogBugz account ID to a full name" msgid "Default: Map a FogBugz account ID to a full name"
msgstr "" msgstr ""
msgid "DefaultBranchLabel|default"
msgstr ""
msgid "Define a custom pattern with cron syntax" msgid "Define a custom pattern with cron syntax"
msgstr "" msgstr ""
...@@ -15598,6 +15610,9 @@ msgstr "" ...@@ -15598,6 +15610,9 @@ msgstr ""
msgid "No matching results" msgid "No matching results"
msgstr "" msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
msgstr "" msgstr ""
...@@ -15634,6 +15649,9 @@ msgstr "" ...@@ -15634,6 +15649,9 @@ msgstr ""
msgid "No public groups" msgid "No public groups"
msgstr "" msgstr ""
msgid "No ref selected"
msgstr ""
msgid "No related merge requests found." msgid "No related merge requests found."
msgstr "" msgstr ""
...@@ -20253,6 +20271,9 @@ msgstr "" ...@@ -20253,6 +20271,9 @@ msgstr ""
msgid "Search branches and tags" msgid "Search branches and tags"
msgstr "" msgstr ""
msgid "Search by Git revision"
msgstr ""
msgid "Search by author" msgid "Search by author"
msgstr "" msgstr ""
...@@ -24632,6 +24653,9 @@ msgstr "" ...@@ -24632,6 +24653,9 @@ msgstr ""
msgid "Total: %{total}" msgid "Total: %{total}"
msgstr "" msgstr ""
msgid "TotalRefCountIndicator|1000+"
msgstr ""
msgid "Trace" msgid "Trace"
msgstr "" msgstr ""
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment