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