Commit 043a67ac authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Fatih Acet

UX fixes for projects dropdown filter

parent 1d6db92c
<script> <script>
import { sprintf, n__, __ } from '~/locale'; import { sprintf, n__, s__, __ } 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';
import { GlLoadingIcon, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlAvatar } from '@gitlab/ui';
import Api from '~/api'; import Api from '~/api';
import { renderAvatar, renderIdenticon } from '~/helpers/avatar_helper';
export default { export default {
name: 'ProjectsDropdownFilter', name: 'ProjectsDropdownFilter',
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
Icon, Icon,
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
GlAvatar,
}, },
props: { props: {
groupId: { groupId: {
...@@ -23,6 +25,11 @@ export default { ...@@ -23,6 +25,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
label: {
type: String,
required: false,
default: s__('CycleAnalytics|project dropdown filter'),
},
}, },
data() { data() {
return { return {
...@@ -46,6 +53,9 @@ export default { ...@@ -46,6 +53,9 @@ export default {
selectedProjectsPlaceholder() { selectedProjectsPlaceholder() {
return this.multiSelect ? __('Select projects') : __('Select a project'); return this.multiSelect ? __('Select projects') : __('Select a project');
}, },
isOnlyOneProjectSelected() {
return this.selectedProjects.length === 1;
},
}, },
mounted() { mounted() {
$(this.$refs.projectsDropdown).glDropdown({ $(this.$refs.projectsDropdown).glDropdown({
...@@ -61,6 +71,7 @@ export default { ...@@ -61,6 +71,7 @@ export default {
data: this.fetchData.bind(this), data: this.fetchData.bind(this),
renderRow: group => this.rowTemplate(group), renderRow: group => this.rowTemplate(group),
text: project => project.name, text: project => project.name,
opened: e => e.target.querySelector('.dropdown-input-field').focus(),
}); });
}, },
methods: { methods: {
...@@ -90,11 +101,20 @@ export default { ...@@ -90,11 +101,20 @@ export default {
return ` return `
<li> <li>
<a href='#' class='dropdown-menu-link'> <a href='#' class='dropdown-menu-link'>
${_.escape(project.name)} ${this.avatarTemplate(project)}
<div class="align-middle">${_.escape(project.name)}</div>
</a> </a>
</li> </li>
`; `;
}, },
avatarTemplate(project) {
const identiconSizeClass = 's16 rect-avatar d-flex justify-content-center flex-column';
return project.avatar_url
? renderAvatar(project, { sizeClass: 's16 rect-avatar' })
: renderIdenticon(project, {
sizeClass: identiconSizeClass,
});
},
}, },
}; };
</script> </script>
...@@ -107,7 +127,18 @@ export default { ...@@ -107,7 +127,18 @@ export default {
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
:aria-label="label"
> >
<gl-avatar
v-if="isOnlyOneProjectSelected"
:src="selectedProjects[0].avatar_url"
:entity-id="selectedProjects[0].id"
:entity-name="selectedProjects[0].name"
:size="16"
shape="rect"
:alt="selectedProjects[0].name"
class="prepend-top-2"
/>
{{ selectedProjectsLabel }} {{ selectedProjectsLabel }}
<icon name="chevron-down" /> <icon name="chevron-down" />
</gl-button> </gl-button>
......
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import $ from 'jquery'; import $ from 'jquery';
import 'bootstrap'; import 'bootstrap';
import '~/gl_dropdown'; 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 Api from '~/api'; import Api from '~/api';
import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/api', () => ({ jest.mock('~/api', () => ({
groupProjects: jest.fn(), groupProjects: jest.fn(),
...@@ -13,14 +14,17 @@ const projects = [ ...@@ -13,14 +14,17 @@ const projects = [
{ {
id: 1, id: 1,
name: 'foo', name: 'foo',
avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
}, },
{ {
id: 2, id: 2,
name: 'foobar', name: 'foobar',
avatar_url: null,
}, },
{ {
id: 3, id: 3,
name: 'foooooooo', name: 'foooooooo',
avatar_url: null,
}, },
]; ];
...@@ -28,7 +32,7 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -28,7 +32,7 @@ describe('ProjectsDropdownFilter component', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(ProjectsDropdownFilter, { wrapper = mount(ProjectsDropdownFilter, {
sync: false, sync: false,
propsData: { propsData: {
groupId: 1, groupId: 1,
...@@ -55,16 +59,55 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -55,16 +59,55 @@ describe('ProjectsDropdownFilter component', () => {
.trigger('shown.bs.dropdown'); .trigger('shown.bs.dropdown');
}; };
const findDropdownItems = () => findDropdown().findAll('a'); const findDropdownItems = () => findDropdown().findAll('a');
const findDropdownButton = () => findDropdown().find('button');
describe('when multiSelect is false', () => { describe('when multiSelect is false', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ multiSelect: false }); createComponent({ multiSelect: false });
}); });
it('should call glDropdown', () => { it('calls glDropdown', () => {
expect($.fn.glDropdown).toHaveBeenCalled(); expect($.fn.glDropdown).toHaveBeenCalled();
}); });
describe('displays the correct information', () => {
beforeEach(() => {
openDropdown();
return wrapper.vm.$nextTick();
});
it('contains 3 items', () => {
expect(findDropdownItems().length).toEqual(3);
});
it('renders an avatar when the project has an avatar_url', () => {
expect(
findDropdownItems()
.at(0)
.contains('img.avatar'),
).toBe(true);
expect(
findDropdownItems()
.at(0)
.contains('div.identicon'),
).toBe(false);
});
it("renders an identicon when the project doesn't have an avatar_url", () => {
expect(
findDropdownItems()
.at(1)
.contains('img.avatar'),
).toBe(false);
expect(
findDropdownItems()
.at(1)
.contains('div.identicon'),
).toBe(true);
});
});
describe('on project click', () => { describe('on project click', () => {
beforeEach(() => { beforeEach(() => {
openDropdown(); openDropdown();
...@@ -97,6 +140,30 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -97,6 +140,30 @@ describe('ProjectsDropdownFilter component', () => {
}, },
]); ]);
}); });
it('renders an avatar in the dropdown button when the project has an avatar_url', done => {
findDropdownItems()
.at(0)
.trigger('click');
wrapper.vm.$nextTick(() => {
expect(findDropdownButton().contains('img.avatar')).toBe(true);
expect(findDropdownButton().contains('div.identicon')).toBe(false);
done();
});
});
it("renders an identicon in the dropdown button when the project doesn't have an avatar_url", done => {
findDropdownItems()
.at(1)
.trigger('click');
wrapper.vm.$nextTick(() => {
expect(findDropdownButton().contains('img.avatar')).toBe(false);
expect(findDropdownButton().contains('div.identicon')).toBe(true);
done();
});
});
}); });
}); });
...@@ -105,6 +172,44 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -105,6 +172,44 @@ describe('ProjectsDropdownFilter component', () => {
createComponent({ multiSelect: true }); createComponent({ multiSelect: true });
}); });
describe('displays the correct information', () => {
beforeEach(() => {
openDropdown();
return wrapper.vm.$nextTick();
});
it('contains 3 items', () => {
expect(findDropdownItems().length).toEqual(3);
});
it('renders an avatar when the project has an avatar_url', () => {
expect(
findDropdownItems()
.at(0)
.contains('img.avatar'),
).toBe(true);
expect(
findDropdownItems()
.at(0)
.contains('div.identicon'),
).toBe(false);
});
it("renders an identicon when the project doesn't have an avatar_url", () => {
expect(
findDropdownItems()
.at(1)
.contains('img.avatar'),
).toBe(false);
expect(
findDropdownItems()
.at(1)
.contains('div.identicon'),
).toBe(true);
});
});
describe('on project click', () => { describe('on project click', () => {
beforeEach(() => { beforeEach(() => {
openDropdown(); openDropdown();
...@@ -150,6 +255,21 @@ describe('ProjectsDropdownFilter component', () => { ...@@ -150,6 +255,21 @@ describe('ProjectsDropdownFilter component', () => {
}, },
]); ]);
}); });
it('renders the correct placeholder text when multiple projects are selected', done => {
findDropdownItems()
.at(0)
.trigger('click');
findDropdownItems()
.at(1)
.trigger('click');
wrapper.vm.$nextTick(() => {
expect(findDropdownButton().text()).toBe('2 projects selected');
done();
});
});
}); });
}); });
}); });
...@@ -4368,6 +4368,9 @@ msgstr[1] "" ...@@ -4368,6 +4368,9 @@ msgstr[1] ""
msgid "CycleAnalytics|group dropdown filter" msgid "CycleAnalytics|group dropdown filter"
msgstr "" msgstr ""
msgid "CycleAnalytics|project dropdown filter"
msgstr ""
msgid "DNS" msgid "DNS"
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