debian-mirror-gitlab/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue

300 lines
8.2 KiB
Vue

<script>
import {
GlIcon,
GlLoadingIcon,
GlAvatar,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { filterBySearchTerm } from '~/analytics/shared/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import getProjects from '../graphql/projects.query.graphql';
const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name));
export default {
name: 'ProjectsDropdownFilter',
components: {
GlIcon,
GlLoadingIcon,
GlAvatar,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlSearchBoxByType,
},
props: {
groupId: {
type: Number,
required: false,
default: null,
},
groupNamespace: {
type: String,
required: true,
},
multiSelect: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: s__('CycleAnalytics|project dropdown filter'),
},
queryParams: {
type: Object,
required: false,
default: () => ({}),
},
defaultProjects: {
type: Array,
required: false,
default: () => [],
},
loadingDefaultProjects: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
loading: true,
projects: [],
selectedProjects: this.defaultProjects || [],
searchTerm: '',
isDirty: false,
};
},
computed: {
selectedProjectsLabel() {
if (this.selectedProjects.length === 1) {
return this.selectedProjects[0].name;
} else if (this.selectedProjects.length > 1) {
return n__(
'CycleAnalytics|Project selected',
'CycleAnalytics|%d projects selected',
this.selectedProjects.length,
);
}
return this.selectedProjectsPlaceholder;
},
selectedProjectsPlaceholder() {
return this.multiSelect ? __('Select projects') : __('Select a project');
},
isOnlyOneProjectSelected() {
return this.selectedProjects.length === 1;
},
selectedProjectIds() {
return this.selectedProjects.map((p) => p.id);
},
hasSelectedProjects() {
return Boolean(this.selectedProjects.length);
},
availableProjects() {
return filterBySearchTerm(this.projects, this.searchTerm);
},
noResultsAvailable() {
const { loading, availableProjects } = this;
return !loading && !availableProjects.length;
},
selectedItems() {
return sortByProjectName(this.selectedProjects);
},
unselectedItems() {
return this.availableProjects.filter(({ id }) => !this.selectedProjectIds.includes(id));
},
},
watch: {
searchTerm() {
this.search();
},
defaultProjects(projects) {
this.selectedProjects = [...projects];
},
},
mounted() {
this.search();
},
methods: {
handleUpdatedSelectedProjects() {
this.$emit('selected', this.selectedProjects);
},
search: debounce(function debouncedSearch() {
this.fetchData();
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
getSelectedProjects(selectedProject, isSelected) {
return isSelected
? this.selectedProjects.concat([selectedProject])
: this.selectedProjects.filter((project) => project.id !== selectedProject.id);
},
singleSelectedProject(selectedObj, isMarking) {
return isMarking ? [selectedObj] : [];
},
setSelectedProjects(project) {
this.selectedProjects = this.multiSelect
? this.getSelectedProjects(project, !this.isProjectSelected(project))
: this.singleSelectedProject(project, !this.isProjectSelected(project));
},
onClick(project) {
this.setSelectedProjects(project);
this.handleUpdatedSelectedProjects();
},
onMultiSelectClick(project) {
this.setSelectedProjects(project);
this.isDirty = true;
},
onSelected(project) {
if (this.multiSelect) {
this.onMultiSelectClick(project);
} else {
this.onClick(project);
}
},
onHide() {
if (this.multiSelect && this.isDirty) {
this.handleUpdatedSelectedProjects();
}
this.searchTerm = '';
this.isDirty = false;
},
onClearAll() {
if (this.hasSelectedProjects) {
this.isDirty = true;
}
this.selectedProjects = [];
},
fetchData() {
this.loading = true;
return this.$apollo
.query({
query: getProjects,
variables: {
groupFullPath: this.groupNamespace,
search: this.searchTerm,
...this.queryParams,
},
})
.then((response) => {
const {
data: {
group: {
projects: { nodes },
},
},
} = response;
this.loading = false;
this.projects = nodes;
});
},
isProjectSelected(project) {
return this.selectedProjectIds.includes(project.id);
},
getEntityId(project) {
return getIdFromGraphQLId(project.id);
},
},
};
</script>
<template>
<gl-dropdown
ref="projectsDropdown"
class="dropdown dropdown-projects"
toggle-class="gl-shadow-none"
:loading="loadingDefaultProjects"
:show-clear-all="hasSelectedProjects"
show-highlighted-items-title
highlighted-items-title-class="gl-p-3"
@clear-all.stop="onClearAll"
@hide="onHide"
>
<template #button-content>
<gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2" />
<div class="gl-display-flex gl-flex-grow-1">
<gl-avatar
v-if="isOnlyOneProjectSelected"
:src="selectedProjects[0].avatarUrl"
:entity-id="getEntityId(selectedProjects[0])"
:entity-name="selectedProjects[0].name"
:size="16"
shape="rect"
:alt="selectedProjects[0].name"
class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2"
/>
{{ selectedProjectsLabel }}
</div>
<gl-icon class="gl-ml-2" name="chevron-down" />
</template>
<template #header>
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<template #highlighted-items>
<gl-dropdown-item
v-for="project in selectedItems"
:key="project.id"
is-check-item
:is-checked="isProjectSelected(project)"
@click.native.capture.stop="onSelected(project)"
>
<div class="gl-display-flex">
<gl-avatar
class="gl-mr-2 gl-vertical-align-middle"
:alt="project.name"
:size="16"
:entity-id="getEntityId(project)"
:entity-name="project.name"
:src="project.avatarUrl"
shape="rect"
/>
<div>
<div data-testid="project-name">{{ project.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
{{ project.fullPath }}
</div>
</div>
</div>
</gl-dropdown-item>
</template>
<gl-dropdown-item
v-for="project in unselectedItems"
:key="project.id"
@click.native.capture.stop="onSelected(project)"
>
<div class="gl-display-flex">
<gl-avatar
class="gl-mr-2 vertical-align-middle"
:alt="project.name"
:size="16"
:entity-id="getEntityId(project)"
:entity-name="project.name"
:src="project.avatarUrl"
shape="rect"
/>
<div>
<div data-testid="project-name">{{ project.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
{{ project.fullPath }}
</div>
</div>
</div>
</gl-dropdown-item>
<gl-dropdown-item v-show="noResultsAvailable" class="gl-pointer-events-none text-secondary">{{
__('No matching results')
}}</gl-dropdown-item>
<gl-dropdown-item v-if="loading">
<gl-loading-icon size="lg" />
</gl-dropdown-item>
</gl-dropdown>
</template>