Commit c342641e authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '217595-persist-filters' into 'master'

Persist filters while navigating

See merge request gitlab-org/gitlab!33289
parents 32b6f737 0d0b5c9f
<script>
import { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
import { isEqual } from 'lodash';
import { ALL } 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 { initFirstClassVulnerabilityFilters, mapProjects } from 'ee/security_dashboard/helpers';
export default {
components: {
......@@ -16,6 +17,17 @@ export default {
filters: initFirstClassVulnerabilityFilters(this.projects),
};
},
computed: {
selectedFilters() {
return this.filters.reduce((acc, { id, selection }) => {
if (!selection.has(ALL)) {
acc[id] = Array.from(selection);
}
return acc;
}, {});
},
},
watch: {
/**
* Initially the project list empty. We fetch them dynamically from GraphQL while
......@@ -28,19 +40,38 @@ export default {
projectFilter.options = [projectFilter.options[0], ...mapProjects(this.projects)];
}
},
'$route.query': {
immediate: true,
handler(newQuery) {
let changed;
this.filters.forEach((filter, i) => {
let urlFilter = newQuery[filter.id];
if (typeof urlFilter === 'undefined') {
urlFilter = [ALL];
} else if (!Array.isArray(urlFilter)) {
urlFilter = [urlFilter];
}
if (isEqual(this.selectedFilters[filter.id], newQuery[filter.id]) === false) {
changed = true;
this.filters[i].selection = new Set(urlFilter);
}
});
if (changed) {
this.$emit('filterChange', this.selectedFilters);
}
},
},
},
methods: {
setFilter(options) {
const selectedFilters = {};
this.filters = setFilter(this.filters, options);
this.filters.forEach(({ id, selection }) => {
if (!selection.has(ALL)) {
selectedFilters[id] = Array.from(selection);
}
});
this.$emit('filterChange', selectedFilters);
this.$router.push({ query: this.selectedFilters });
this.$emit('filterChange', this.selectedFilters);
},
},
};
......
import { shallowMount } from '@vue/test-utils';
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';
const router = new VueRouter();
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('First class vulnerability filters component', () => {
let wrapper;
let filters;
const projects = [
{ id: 'gid://gitlab/Project/11', name: 'GitLab Org' },
{ id: 'gid://gitlab/Project/12', name: 'GitLab Com' },
];
const findFilters = () => wrapper.findAll(Filter);
const findFirstFilter = () => findFilters().at(0);
const findStateFilter = () => findFilters().at(0);
const findSeverityFilter = () => findFilters().at(1);
const findReportTypeFilter = () => findFilters().at(2);
const findProjectFilter = () => findFilters().at(3);
const findLastFilter = () => findFilters().at(filters.length - 1);
const createComponent = ({ propsData } = {}) => {
return shallowMount(Filters, { propsData });
const createComponent = ({ propsData, listeners } = {}) => {
return shallowMount(Filters, { localVue, router, propsData, listeners });
};
afterEach(() => {
......@@ -35,7 +44,7 @@ describe('First class vulnerability filters component', () => {
});
it('should pass down the filter information to the first filter', () => {
expect(findFirstFilter().props().filter).toEqual(filters[0]);
expect(findStateFilter().props().filter).toEqual(filters[0]);
});
it('should call the setFilter mutation when setting a filter', () => {
......@@ -43,7 +52,7 @@ describe('First class vulnerability filters component', () => {
const options = { foo: 'bar' };
wrapper.setMethods({ setFilter: stub });
findFirstFilter().vm.$emit('setFilter', options);
findStateFilter().vm.$emit('setFilter', options);
expect(stub).toHaveBeenCalledWith(options);
});
......@@ -99,27 +108,105 @@ describe('First class vulnerability filters component', () => {
});
});
describe('when setFilter is called', () => {
let filterId;
let optionId;
describe.each`
filter | value | selector
${'state'} | ${'DETECTED,DISMISSED'} | ${findStateFilter}
${'severity'} | ${'MEDIUM'} | ${findSeverityFilter}
${'reportType'} | ${'SAST'} | ${findReportTypeFilter}
${'projectId'} | ${'12'} | ${findProjectFilter}
`('when filters are persisted', ({ filter, value, selector }) => {
describe(`with filter set to ${filter}: ${value}`, () => {
let filterChangeSpy;
beforeEach(() => {
filterChangeSpy = jest.fn();
wrapper = createComponent({
propsData: { projects },
listeners: { filterChange: filterChangeSpy },
});
router.push({ query: { [filter]: value.split(',') } });
});
beforeEach(() => {
filterId = filters[0].id;
optionId = filters[0].options[1].id;
wrapper = createComponent();
wrapper.vm.setFilter({ filterId, optionId });
});
it(`should have the ${filter} filter as pre-selected`, () => {
expect(selector().props('filter').selection).toEqual(new Set(value.split(',')));
});
it('should set the filters locally', () => {
const expectedFilters = initFirstClassVulnerabilityFilters();
expectedFilters[0].selection = new Set([optionId]);
it('should emit a filterChange event', () => {
expect(wrapper.emitted().filterChange).toBeTruthy();
});
it('should not trigger the filterChange additonally when the filters do not change', () => {
router.push({
query: {
...wrapper.vm.$route.query,
'some-unrelated-query-param': 'true',
},
});
return wrapper.vm.$nextTick(() => {
expect(filterChangeSpy).toHaveBeenCalledTimes(1);
});
});
it('should trigger the filterChange when the filters are reset', () => {
router.push({ query: {} });
return wrapper.vm.$nextTick(() => {
expect(filterChangeSpy).toHaveBeenNthCalledWith(2, {});
});
});
expect(wrapper.vm.filters).toEqual(expectedFilters);
it('should reset the filters when the URL contains no more filters', () => {
router.push({ query: {} });
return wrapper.vm.$nextTick(() => {
expect(selector().props('filter').selection).toEqual(new Set(['all']));
});
});
});
});
describe.each`
filter | selector | index
${'state'} | ${findStateFilter} | ${0}
${'severity'} | ${findSeverityFilter} | ${1}
${'reportType'} | ${findReportTypeFilter} | ${2}
${'projectId'} | ${findProjectFilter} | ${3}
`('when setFilter is called', ({ filter, selector, index }) => {
describe(filter, () => {
let filterId;
let optionId;
let routePushSpy;
beforeEach(() => {
filters = initFirstClassVulnerabilityFilters(projects);
filterId = filters[index].id;
optionId = filters[index].options[1].id;
wrapper = createComponent({ propsData: { projects } });
routePushSpy = jest.spyOn(router, 'push');
selector().vm.$emit('setFilter', { optionId, filterId });
});
afterEach(() => {
// This will reset the query state
router.push('/');
});
it('should set the filter', () => {
expect(selector().props('filter').selection).toEqual(new Set([optionId]));
});
it('should emit selected filters when a filter is set', () => {
expect(wrapper.emitted().filterChange).toBeTruthy();
expect(wrapper.emitted().filterChange[0]).toEqual([{ [filterId]: [optionId] }]);
it('should emit a filterChange event', () => {
expect(wrapper.emitted().filterChange).toBeTruthy();
});
it('should update the path', () => {
expect(routePushSpy).toHaveBeenCalledWith({
query: { [filterId]: [optionId] },
});
});
});
});
});
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