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> <script>
import { mapGetters, mapActions } from 'vuex'; 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'; import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
export default { export default {
......
<script> <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 { export default {
components: { components: {
GlDeprecatedDropdown, GlDropdown,
GlSearchBoxByType, GlSearchBoxByType,
GlIcon, GlIcon,
GlTruncate,
GlDropdownText,
FilterItem,
}, },
props: { props: {
filter: { filter: {
...@@ -47,9 +51,6 @@ export default { ...@@ -47,9 +51,6 @@ export default {
isSelected(option) { isSelected(option) {
return this.selection.has(option.id); return this.selection.has(option.id);
}, },
closeDropdown() {
this.$refs.dropdown.$children[0].hide(true);
},
}, },
}; };
</script> </script>
...@@ -57,74 +58,41 @@ export default { ...@@ -57,74 +58,41 @@ export default {
<template> <template>
<div class="dashboard-filter"> <div class="dashboard-filter">
<strong class="js-name">{{ filter.name }}</strong> <strong class="js-name">{{ filter.name }}</strong>
<gl-deprecated-dropdown <gl-dropdown
ref="dropdown" class="gl-mt-2 gl-w-full"
class="d-block mt-1"
menu-class="dropdown-extended-height" 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"> <template #button-content>
<span class="text-truncate" :data-qa-selector="qaSelector"> <gl-truncate
{{ firstSelectedOption }} :text="firstSelectedOption"
</span> class="gl-min-w-0 gl-mr-2"
<span v-if="extraOptionCount" class="flex-grow-1 ml-1"> :data-qa-selector="qaSelector"
/>
<span v-if="extraOptionCount" class="gl-mr-2">
{{ n__('+%d more', '+%d more', extraOptionCount) }} {{ n__('+%d more', '+%d more', extraOptionCount) }}
</span> </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> </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 <gl-search-box-by-type
v-if="filter.options.length >= 20" v-if="filter.options.length >= 20"
ref="searchBox"
v-model="filterTerm" v-model="filterTerm"
:placeholder="__('Filter...')" :placeholder="__('Filter...')"
/> />
<div <filter-item
data-qa-selector="filter_dropdown_content" v-for="option in filteredOptions"
:class="{ 'dropdown-content': filterId === 'project_id' }" :key="option.id"
> :is-checked="isSelected(option)"
<button :text="option.name"
v-for="option in filteredOptions" @click="clickFilter(option)"
: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>
<button <gl-dropdown-text v-if="filteredOptions.length <= 0">
v-if="filteredOptions.length === 0" <span class="gl-text-gray-500">{{ __('No matching results') }}</span>
type="button" </gl-dropdown-text>
class="dropdown-item no-pointer-events text-secondary" </gl-dropdown>
>
{{ __('No matching results') }}
</button>
</gl-deprecated-dropdown>
</div> </div>
</template> </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 @@ ...@@ -2,7 +2,7 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { ALL, STATE } from 'ee/security_dashboard/store/modules/filters/constants'; import { ALL, STATE } from 'ee/security_dashboard/store/modules/filters/constants';
import { setFilter } from 'ee/security_dashboard/store/modules/filters/utils'; 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'; import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
export default { 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 { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
const generateOption = index => ({ const generateOption = index => ({
...@@ -16,26 +16,12 @@ describe('Filter component', () => { ...@@ -16,26 +16,12 @@ describe('Filter component', () => {
let wrapper; let wrapper;
const createWrapper = propsData => { const createWrapper = propsData => {
wrapper = mount(Filter, { wrapper = mount(Filter, { propsData });
stubs: {
...stubChildren(Filter),
GlDeprecatedDropdown: false,
GlSearchBoxByType: false,
},
propsData,
attachToDocument: true,
});
}; };
const findSearchInput = () => const findSearchBox = () => wrapper.find(GlSearchBoxByType);
wrapper.find({ ref: 'searchBox' }).exists() && wrapper.find({ ref: 'searchBox' }).find('input'); const isDropdownOpen = () => wrapper.find(GlDropdown).classes('show');
const findDropdownToggle = () => wrapper.find('.dropdown-toggle'); const dropdownItemsCount = () => wrapper.findAll(GlDropdownItem).length;
const dropdownItemsCount = () => wrapper.findAll('.dropdown-item').length;
function isDropdownOpen() {
const toggleButton = findDropdownToggle();
return toggleButton.attributes('aria-expanded') === 'true';
}
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -60,7 +46,9 @@ describe('Filter component', () => { ...@@ -60,7 +46,9 @@ describe('Filter component', () => {
}); });
it('should display a check next to only the selected items', () => { 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', () => { it('should correctly display the selected text', () => {
...@@ -74,7 +62,7 @@ describe('Filter component', () => { ...@@ -74,7 +62,7 @@ describe('Filter component', () => {
}); });
it('should not have a search box', () => { it('should not have a search box', () => {
expect(findSearchInput()).toBe(false); expect(findSearchBox().exists()).toBe(false);
}); });
it('should not be open', () => { it('should not be open', () => {
...@@ -83,26 +71,16 @@ describe('Filter component', () => { ...@@ -83,26 +71,16 @@ describe('Filter component', () => {
describe('when the dropdown is open', () => { describe('when the dropdown is open', () => {
beforeEach(done => { beforeEach(done => {
findDropdownToggle().trigger('click'); wrapper.find('.dropdown-toggle').trigger('click');
wrapper.vm.$root.$on('bv::dropdown::shown', () => { wrapper.vm.$root.$on('bv::dropdown::shown', () => done());
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); expect(isDropdownOpen()).toBe(true);
wrapper.find('.dropdown-item').trigger('click'); wrapper.find(GlDropdownItem).trigger('click');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(isDropdownOpen()).toBe(true);
});
});
it('should close the menu when the close button is clicked', () => {
expect(isDropdownOpen()).toBe(true); 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', () => { ...@@ -124,20 +102,18 @@ describe('Filter component', () => {
}); });
it('should display a search box', () => { it('should display a search box', () => {
expect(findSearchInput().exists()).toBe(true); expect(findSearchBox().exists()).toBe(true);
}); });
it(`should show all projects`, () => { it(`should show all projects`, () => {
expect(dropdownItemsCount()).toBe(LOTS); expect(dropdownItemsCount()).toBe(LOTS);
}); });
it('should show only matching projects when a search term is entered', () => { it('should show only matching projects when a search term is entered', async () => {
const input = findSearchInput(); findSearchBox().vm.$emit('input', '0');
input.vm.$el.value = '0'; await wrapper.vm.$nextTick();
input.vm.$el.dispatchEvent(new Event('input'));
return wrapper.vm.$nextTick().then(() => { expect(dropdownItemsCount()).toBe(3);
expect(dropdownItemsCount()).toBe(3);
});
}); });
}); });
}); });
......
...@@ -2,7 +2,7 @@ import VueRouter from 'vue-router'; ...@@ -2,7 +2,7 @@ import VueRouter from 'vue-router';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers'; import { initFirstClassVulnerabilityFilters } from 'ee/security_dashboard/helpers';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue'; 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 router = new VueRouter();
const localVue = createLocalVue(); 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