Commit b03db0e7 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Paul Slaughter

Multiselect for projects dropdown filter

This commit enables multiselect for the projects
dropdown filter. This is required for cycle analytics
and will soon be used for productivity analytics too.

This is required as multiple projects need to be selected.
parent 7906ae0c
<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