Commit 4eb54ae2 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'multiselect-for-projects-dropdown-filter' into 'master'

Multi-select for projects dropdown filter

See merge request gitlab-org/gitlab-ee!14720
parents 7906ae0c b03db0e7
<script>
import { __ } from '~/locale';
import { sprintf, n__, __ } from '~/locale';
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -18,16 +18,33 @@ export default {
type: Number,
required: true,
},
multiSelect: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
loading: true,
selectedProject: {},
selectedProjects: [],
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || __('Select a project');
selectedProjectsLabel() {
return this.selectedProjects.length
? sprintf(
n__(
'CycleAnalytics|%{projectName}',
'CycleAnalytics|%d projects selected',
this.selectedProjects.length,
),
{ projectName: this.selectedProjects[0].name },
)
: this.selectedProjectsPlaceholder;
},
selectedProjectsPlaceholder() {
return this.multiSelect ? __('Select projects') : __('Select a project');
},
},
mounted() {
......@@ -36,24 +53,31 @@ export default {
filterable: true,
filterRemote: true,
fieldName: 'project_id',
multiSelect: this.multiSelect,
search: {
fields: ['name'],
},
clicked: this.onClick,
data: this.fetchData,
clicked: this.onClick.bind(this),
data: this.fetchData.bind(this),
renderRow: group => this.rowTemplate(group),
text: project => project.name,
});
},
methods: {
onClick({ $el, e }) {
getSelectedProjects(selectedProject, isMarking) {
return isMarking
? this.selectedProjects.concat([selectedProject])
: this.selectedProjects.filter(project => project.id !== selectedProject.id);
},
setSelectedProjects(selectedObj, isMarking) {
this.selectedProjects = this.multiSelect
? this.getSelectedProjects(selectedObj, isMarking)
: [selectedObj];
},
onClick({ selectedObj, e, isMarking }) {
e.preventDefault();
this.selectedProject = {
id: $el.data('id'),
name: $el.data('name'),
path: $el.data('path'),
};
this.$emit('selected', this.selectedProject);
this.setSelectedProjects(selectedObj, isMarking);
this.$emit('selected', this.selectedProjects);
},
fetchData(term, callback) {
this.loading = true;
......@@ -65,9 +89,7 @@ export default {
rowTemplate(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-id="${project.id}" data-name="${
project.name
}" data-path="${project.path_with_namespace}">
<a href='#' class='dropdown-menu-link'>
${_.escape(project.name)}
</a>
</li>
......@@ -86,7 +108,8 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
{{ selectedProjectName }} <icon name="chevron-down" />
{{ selectedProjectsLabel }}
<icon name="chevron-down" />
</gl-button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">{{ __('Projects') }}</div>
......@@ -95,7 +118,7 @@ export default {
<icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><gl-loading-icon /></div>
<gl-loading-icon class="dropdown-loading" />
</div>
</div>
</div>
......
......@@ -30,7 +30,7 @@ describe('GroupsDropdownFilter component', () => {
const $el = $('<a></a>').data(group);
const e = new Event('click');
it('should emit the "setSelectedGroup" event', () => {
it('should emit the "selected" event', () => {
jest.spyOn(vm, '$emit');
vm.onClick({ $el, e });
......
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import Vue from 'vue';
import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
import 'bootstrap';
import '~/gl_dropdown';
import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import Api from '~/api';
jest.mock('~/api', () => ({
groupProjects: jest.fn(),
}));
const projects = [
{
id: 1,
name: 'foo',
},
{
id: 2,
name: 'foobar',
},
{
id: 3,
name: 'foooooooo',
},
];
describe('ProjectsDropdownFilter component', () => {
const Component = Vue.extend(ProjectsDropdownFilter);
const props = {
groupId: 1,
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(ProjectsDropdownFilter, {
sync: false,
propsData: {
groupId: 1,
...props,
},
});
};
let vm;
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
beforeEach(() => {
jest.spyOn($.fn, 'glDropdown');
vm = mountComponent(Component, props);
Api.groupProjects.mockImplementation((groupId, term, options, callback) => {
callback(projects);
});
});
it('should call glDropdown', () => {
expect($.fn.glDropdown).toHaveBeenCalled();
const findDropdown = () => wrapper.find('.dropdown');
const openDropdown = () => {
$(findDropdown().element)
.parent()
.trigger('shown.bs.dropdown');
};
const findDropdownItems = () => findDropdown().findAll('a');
describe('when multiSelect is false', () => {
beforeEach(() => {
createComponent({ multiSelect: false });
});
it('should call glDropdown', () => {
expect($.fn.glDropdown).toHaveBeenCalled();
});
describe('on project click', () => {
beforeEach(() => {
openDropdown();
return wrapper.vm.$nextTick();
});
it('should emit the "selected" event with the selected project', () => {
findDropdownItems()
.at(0)
.trigger('click');
expect(wrapper.emittedByOrder()).toEqual([
{
name: 'selected',
args: [[projects[0]]],
},
]);
});
it('should change selection when new project is clicked', () => {
findDropdownItems()
.at(1)
.trigger('click');
expect(wrapper.emittedByOrder()).toEqual([
{
name: 'selected',
args: [[projects[1]]],
},
]);
});
});
});
describe('onClick', () => {
const project = {
id: 1,
name: 'foo',
path: 'bar',
};
const $el = $('<a></a>').data(project);
const e = new Event('click');
describe('when multiSelect is true', () => {
beforeEach(() => {
createComponent({ multiSelect: true });
});
describe('on project click', () => {
beforeEach(() => {
openDropdown();
return wrapper.vm.$nextTick();
});
it('should add to selection when new project is clicked', () => {
findDropdownItems()
.at(0)
.trigger('click');
findDropdownItems()
.at(1)
.trigger('click');
expect(wrapper.emittedByOrder()).toEqual([
{
name: 'selected',
args: [[projects[0]]],
},
{
name: 'selected',
args: [[projects[0], projects[1]]],
},
]);
});
it('should emit the "setSelectedGroup" event', () => {
jest.spyOn(vm, '$emit');
it('should remove from selection when clicked again', () => {
const item = findDropdownItems().at(0);
vm.onClick({ $el, e });
item.trigger('click');
item.trigger('click');
expect(vm.$emit).toHaveBeenCalledWith('selected', project);
expect(wrapper.emittedByOrder()).toEqual([
{
name: 'selected',
args: [[projects[0]]],
},
{
name: 'selected',
args: [[]],
},
]);
});
});
});
});
......@@ -4180,6 +4180,11 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
msgid "CycleAnalytics|%{projectName}"
msgid_plural "CycleAnalytics|%d projects selected"
msgstr[0] ""
msgstr[1] ""
msgid "DNS"
msgstr ""
......@@ -12455,6 +12460,9 @@ msgstr ""
msgid "Select project to choose zone"
msgstr ""
msgid "Select projects"
msgstr ""
msgid "Select projects you want to import."
msgstr ""
......
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