gitlab-org--gitlab-foss/app/assets/javascripts/milestones/components/milestone_combobox.vue

251 lines
7.3 KiB
Vue
Raw Normal View History

<script>
import {
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
import { debounce, isEqual } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, __, sprintf } from '~/locale';
import createStore from '../stores';
import MilestoneResultsSection from './milestone_results_section.vue';
const SEARCH_DEBOUNCE_MS = 250;
export default {
name: 'MilestoneCombobox',
store: createStore(),
components: {
GlDropdown,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
MilestoneResultsSection,
},
props: {
value: {
type: Array,
required: false,
default: () => [],
},
projectId: {
type: String,
required: true,
},
groupId: {
type: String,
required: false,
default: '',
},
groupMilestonesAvailable: {
type: Boolean,
required: false,
default: false,
},
extraLinks: {
type: Array,
default: () => [],
required: false,
},
},
data() {
return {
searchQuery: '',
};
},
translations: {
milestone: s__('MilestoneCombobox|Milestone'),
selectMilestone: s__('MilestoneCombobox|Select milestone'),
noMilestone: s__('MilestoneCombobox|No milestone'),
noResultsLabel: s__('MilestoneCombobox|No matching results'),
searchMilestones: s__('MilestoneCombobox|Search Milestones'),
searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
},
computed: {
...mapState(['matches', 'selectedMilestones']),
...mapGetters(['isLoading', 'groupMilestonesEnabled']),
selectedMilestonesLabel() {
const { selectedMilestones } = this;
const firstMilestoneName = selectedMilestones[0];
if (selectedMilestones.length === 0) {
return this.$options.translations.noMilestone;
}
if (selectedMilestones.length === 1) {
return firstMilestoneName;
}
const numberOfOtherMilestones = selectedMilestones.length - 1;
return sprintf(__('%{firstMilestoneName} + %{numberOfOtherMilestones} more'), {
firstMilestoneName,
numberOfOtherMilestones,
});
},
showProjectMilestoneSection() {
return Boolean(
this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
);
},
showGroupMilestoneSection() {
return (
this.groupMilestonesEnabled &&
Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
);
},
showNoResults() {
return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
},
},
watch: {
// Keep the Vuex store synchronized if the parent
// component updates the selected milestones through v-model
value: {
immediate: true,
handler() {
const milestoneTitles = this.value.map((milestone) =>
milestone.title ? milestone.title : milestone,
);
if (!isEqual(milestoneTitles, this.selectedMilestones)) {
this.setSelectedMilestones(milestoneTitles);
}
},
},
},
created() {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this.debouncedSearch = debounce(function search() {
this.search(this.searchQuery);
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
this.setGroupId(this.groupId);
this.setGroupMilestonesAvailable(this.groupMilestonesAvailable);
this.fetchMilestones();
},
methods: {
...mapActions([
'setProjectId',
'setGroupId',
'setGroupMilestonesAvailable',
'setSelectedMilestones',
'clearSelectedMilestones',
'toggleMilestones',
'search',
'fetchMilestones',
]),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxEnter() {
this.debouncedSearch.cancel();
this.search(this.searchQuery);
},
onSearchBoxInput() {
this.debouncedSearch();
},
selectMilestone(milestone) {
this.toggleMilestones(milestone);
this.$emit('input', this.selectedMilestones);
},
selectNoMilestone() {
this.clearSelectedMilestones();
this.$emit('input', this.selectedMilestones);
},
},
};
</script>
<template>
<gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
<template slot="button-content">
<span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
selectedMilestonesLabel
}}</span>
<gl-icon name="chevron-down" />
</template>
<gl-dropdown-section-header>
<span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
</gl-dropdown-section-header>
<gl-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
v-model.trim="searchQuery"
class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
<gl-dropdown-item @click="selectNoMilestone()">
<span :class="{ 'gl-pl-6': true, 'selected-item': selectedMilestones.length === 0 }">
{{ $options.translations.noMilestone }}
</span>
</gl-dropdown-item>
<gl-dropdown-divider />
<template v-if="isLoading">
<gl-loading-icon />
<gl-dropdown-divider />
</template>
<template v-else-if="showNoResults">
<div class="dropdown-item-space">
<span data-testid="milestone-combobox-no-results" class="gl-pl-6">{{
$options.translations.noResultsLabel
}}</span>
</div>
<gl-dropdown-divider />
</template>
<template v-else>
<milestone-results-section
v-if="showProjectMilestoneSection"
:section-title="$options.translations.projectMilestones"
:total-count="matches.projectMilestones.totalCount"
:items="matches.projectMilestones.list"
:selected-milestones="selectedMilestones"
:error="matches.projectMilestones.error"
:error-message="$options.translations.searchErrorMessage"
data-testid="project-milestones-section"
@selected="selectMilestone($event)"
/>
<milestone-results-section
v-if="showGroupMilestoneSection"
:section-title="$options.translations.groupMilestones"
:total-count="matches.groupMilestones.totalCount"
:items="matches.groupMilestones.list"
:selected-milestones="selectedMilestones"
:error="matches.groupMilestones.error"
:error-message="$options.translations.searchErrorMessage"
data-testid="group-milestones-section"
@selected="selectMilestone($event)"
/>
</template>
<gl-dropdown-item
v-for="(item, idx) in extraLinks"
:key="idx"
:href="item.url"
data-testid="milestone-combobox-extra-links"
>
<span class="gl-pl-6">{{ item.text }}</span>
</gl-dropdown-item>
</gl-dropdown>
</template>