Commit 794d41ad authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '210327-update-vulnerability-filter-style' into 'master'

Update security dashboard vulnerability filter styling

See merge request gitlab-org/gitlab!45468
parents e9ae18aa b2dfabd0
<script>
import { mapGetters, mapActions } from 'vuex';
import DashboardFilter from './filter.vue';
import DashboardFilter from './filters/filter.vue';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
export default {
......
<script>
import { GlDeprecatedDropdown, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
import { GlDropdown, GlSearchBoxByType, GlIcon, GlTruncate, GlDropdownText } from '@gitlab/ui';
import FilterItem from './filter_item.vue';
export default {
components: {
GlDeprecatedDropdown,
GlDropdown,
GlSearchBoxByType,
GlIcon,
GlTruncate,
GlDropdownText,
FilterItem,
},
props: {
filter: {
......@@ -47,9 +51,6 @@ export default {
isSelected(option) {
return this.selection.has(option.id);
},
closeDropdown() {
this.$refs.dropdown.$children[0].hide(true);
},
},
};
</script>
......@@ -57,74 +58,41 @@ export default {
<template>
<div class="dashboard-filter">
<strong class="js-name">{{ filter.name }}</strong>
<gl-deprecated-dropdown
ref="dropdown"
class="d-block mt-1"
<gl-dropdown
class="gl-mt-2 gl-w-full"
menu-class="dropdown-extended-height"
toggle-class="d-flex w-100 justify-content-between align-items-center"
:header-text="filter.name"
toggle-class="gl-w-full"
>
<template slot="button-content">
<span class="text-truncate" :data-qa-selector="qaSelector">
{{ firstSelectedOption }}
</span>
<span v-if="extraOptionCount" class="flex-grow-1 ml-1">
<template #button-content>
<gl-truncate
:text="firstSelectedOption"
class="gl-min-w-0 gl-mr-2"
:data-qa-selector="qaSelector"
/>
<span v-if="extraOptionCount" class="gl-mr-2">
{{ n__('+%d more', '+%d more', extraOptionCount) }}
</span>
<i class="fa fa-chevron-down" aria-hidden="true"></i>
<gl-icon name="chevron-down" class="gl-flex-shrink-0 gl-ml-auto" />
</template>
<div class="dropdown-title mb-0">
{{ filter.name }}
<button
ref="close"
class="btn-blank float-right"
type="button"
:aria-label="__('Close')"
@click="closeDropdown"
>
<gl-icon name="close" aria-hidden="true" class="vertical-align-middle" />
</button>
</div>
<gl-search-box-by-type
v-if="filter.options.length >= 20"
ref="searchBox"
v-model="filterTerm"
:placeholder="__('Filter...')"
/>
<div
data-qa-selector="filter_dropdown_content"
:class="{ 'dropdown-content': filterId === 'project_id' }"
>
<button
v-for="option in filteredOptions"
:key="option.id"
role="menuitem"
type="button"
class="dropdown-item"
@click="clickFilter(option)"
>
<span class="d-flex">
<gl-icon
v-if="isSelected(option)"
class="flex-shrink-0 js-check"
name="mobile-issue-close"
/>
<span class="gl-white-space-nowrap gl-ml-2" :class="{ 'gl-pl-5': !isSelected(option) }">
{{ option.name }}
</span>
</span>
</button>
</div>
<filter-item
v-for="option in filteredOptions"
:key="option.id"
:is-checked="isSelected(option)"
:text="option.name"
@click="clickFilter(option)"
/>
<button
v-if="filteredOptions.length === 0"
type="button"
class="dropdown-item no-pointer-events text-secondary"
>
{{ __('No matching results') }}
</button>
</gl-deprecated-dropdown>
<gl-dropdown-text v-if="filteredOptions.length <= 0">
<span class="gl-text-gray-500">{{ __('No matching results') }}</span>
</gl-dropdown-text>
</gl-dropdown>
</div>
</template>
<script>
import { GlDropdownItem, GlTruncate } from '@gitlab/ui';
export default {
components: { GlDropdownItem, GlTruncate },
props: {
isChecked: {
type: Boolean,
required: true,
},
text: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<gl-dropdown-item
is-check-item
:is-checked="isChecked"
@click.native.capture.stop="$emit('click')"
>
<slot>
<gl-truncate :text="text" />
</slot>
</gl-dropdown-item>
</template>
......@@ -2,7 +2,7 @@
import { isEqual } from 'lodash';
import { ALL, STATE } from 'ee/security_dashboard/store/modules/filters/constants';
import { setFilter } from 'ee/security_dashboard/store/modules/filters/utils';
import DashboardFilter from 'ee/security_dashboard/components/filter.vue';
import DashboardFilter from 'ee/security_dashboard/components/filters/filter.vue';
import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
export default {
......
---
title: Update security dashboard filter styling
merge_request: 45468
author:
type: other
import { GlDropdownItem, GlTruncate } from '@gitlab/ui';
import FilterItem from 'ee/security_dashboard/components/filters/filter_item.vue';
import { shallowMount } from '@vue/test-utils';
describe('Filter Item component', () => {
let wrapper;
const defaultProps = {
isChecked: false,
};
const createWrapper = (props, slotContent = '') => {
wrapper = shallowMount(FilterItem, {
propsData: { ...defaultProps, ...props },
slots: { default: slotContent },
});
};
const dropdownItem = () => wrapper.find(GlDropdownItem);
const name = () => wrapper.find(GlTruncate);
afterEach(() => {
wrapper.destroy();
});
describe('name', () => {
it('shows the name when the name prop is passed in', () => {
const text = 'some name';
createWrapper({ text });
expect(name().props('text')).toBe(text);
});
it('shows slot content when slot content is passed in', () => {
const slotContent = 'custom slot content';
createWrapper({}, slotContent);
expect(name().exists()).toBe(false);
expect(wrapper.text()).toContain(slotContent);
});
});
it.each([true, false])('shows the expected checkmark when isSelected is %s', isChecked => {
createWrapper({ isChecked });
expect(dropdownItem().props('isChecked')).toBe(isChecked);
});
it('emits click event when clicked', () => {
createWrapper();
dropdownItem().element.click();
expect(wrapper.emitted('click')).toHaveLength(1);
});
});
import Filter from 'ee/security_dashboard/components/filter.vue';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import Filter from 'ee/security_dashboard/components/filters/filter.vue';
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import { trimText } from 'helpers/text_helper';
const generateOption = index => ({
......@@ -16,26 +16,12 @@ describe('Filter component', () => {
let wrapper;
const createWrapper = propsData => {
wrapper = mount(Filter, {
stubs: {
...stubChildren(Filter),
GlDeprecatedDropdown: false,
GlSearchBoxByType: false,
},
propsData,
attachToDocument: true,
});
wrapper = mount(Filter, { propsData });
};
const findSearchInput = () =>
wrapper.find({ ref: 'searchBox' }).exists() && wrapper.find({ ref: 'searchBox' }).find('input');
const findDropdownToggle = () => wrapper.find('.dropdown-toggle');
const dropdownItemsCount = () => wrapper.findAll('.dropdown-item').length;
function isDropdownOpen() {
const toggleButton = findDropdownToggle();
return toggleButton.attributes('aria-expanded') === 'true';
}
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const isDropdownOpen = () => wrapper.find(GlDropdown).classes('show');
const dropdownItemsCount = () => wrapper.findAll(GlDropdownItem).length;
afterEach(() => {
wrapper.destroy();
......@@ -60,7 +46,9 @@ describe('Filter component', () => {
});
it('should display a check next to only the selected items', () => {
expect(wrapper.findAll('.dropdown-item .js-check')).toHaveLength(3);
expect(
wrapper.findAll(`[data-testid="mobile-issue-close-icon"]:not(.gl-visibility-hidden)`),
).toHaveLength(3);
});
it('should correctly display the selected text', () => {
......@@ -74,7 +62,7 @@ describe('Filter component', () => {
});
it('should not have a search box', () => {
expect(findSearchInput()).toBe(false);
expect(findSearchBox().exists()).toBe(false);
});
it('should not be open', () => {
......@@ -83,26 +71,16 @@ describe('Filter component', () => {
describe('when the dropdown is open', () => {
beforeEach(done => {
findDropdownToggle().trigger('click');
wrapper.vm.$root.$on('bv::dropdown::shown', () => {
done();
});
wrapper.find('.dropdown-toggle').trigger('click');
wrapper.vm.$root.$on('bv::dropdown::shown', () => done());
});
it('should keep the menu open after clicking on an item', () => {
it('should keep the menu open after clicking on an item', async () => {
expect(isDropdownOpen()).toBe(true);
wrapper.find('.dropdown-item').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isDropdownOpen()).toBe(true);
});
});
wrapper.find(GlDropdownItem).trigger('click');
await wrapper.vm.$nextTick();
it('should close the menu when the close button is clicked', () => {
expect(isDropdownOpen()).toBe(true);
wrapper.find({ ref: 'close' }).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isDropdownOpen()).toBe(false);
});
});
});
});
......@@ -124,20 +102,18 @@ describe('Filter component', () => {
});
it('should display a search box', () => {
expect(findSearchInput().exists()).toBe(true);
expect(findSearchBox().exists()).toBe(true);
});
it(`should show all projects`, () => {
expect(dropdownItemsCount()).toBe(LOTS);
});
it('should show only matching projects when a search term is entered', () => {
const input = findSearchInput();
input.vm.$el.value = '0';
input.vm.$el.dispatchEvent(new Event('input'));
return wrapper.vm.$nextTick().then(() => {
expect(dropdownItemsCount()).toBe(3);
});
it('should show only matching projects when a search term is entered', async () => {
findSearchBox().vm.$emit('input', '0');
await wrapper.vm.$nextTick();
expect(dropdownItemsCount()).toBe(3);
});
});
});
......
......@@ -2,7 +2,7 @@ import VueRouter from 'vue-router';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import Filter from 'ee/security_dashboard/components/filter.vue';
import Filter from 'ee/security_dashboard/components/filters/filter.vue';
const router = new VueRouter();
const localVue = createLocalVue();
......
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