Commit 15d4fd73 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ss/fix-board-scope-issue' into 'master'

Fix board scope issue when issue_boards_filtered_search is enabled

See merge request gitlab-org/gitlab!73008
parents a70561c0 77772099
<script> <script>
import { pickBy } from 'lodash'; import { pickBy, isEmpty } from 'lodash';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
...@@ -20,6 +20,11 @@ export default { ...@@ -20,6 +20,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
eeFilters: {
required: false,
type: Object,
default: () => ({}),
},
}, },
data() { data() {
return { return {
...@@ -27,61 +32,6 @@ export default { ...@@ -27,61 +32,6 @@ export default {
}; };
}, },
computed: { computed: {
urlParams() {
const {
authorUsername,
labelName,
assigneeUsername,
search,
milestoneTitle,
types,
weight,
epicId,
} = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
notParams = pickBy(
{
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
'not[assignee_username]': this.filterParams.not.assigneeUsername,
'not[types]': this.filterParams.not.types,
'not[milestone_title]': this.filterParams.not.milestoneTitle,
'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId,
},
undefined,
);
}
return {
...notParams,
author_username: authorUsername,
'label_name[]': labelName,
assignee_username: assigneeUsername,
milestone_title: milestoneTitle,
search,
types,
weight,
epic_id: getIdFromGraphQLId(epicId),
};
},
},
methods: {
...mapActions(['performSearch']),
handleFilter(filters) {
this.filterParams = this.getFilterParams(filters);
updateHistory({
url: setUrlParams(this.urlParams, window.location.href, true, false, true),
title: document.title,
replace: true,
});
this.performSearch();
},
getFilteredSearchValue() { getFilteredSearchValue() {
const { const {
authorUsername, authorUsername,
...@@ -203,6 +153,66 @@ export default { ...@@ -203,6 +153,66 @@ export default {
return filteredSearchValue; return filteredSearchValue;
}, },
urlParams() {
const {
authorUsername,
labelName,
assigneeUsername,
search,
milestoneTitle,
types,
weight,
epicId,
} = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
notParams = pickBy(
{
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
'not[assignee_username]': this.filterParams.not.assigneeUsername,
'not[types]': this.filterParams.not.types,
'not[milestone_title]': this.filterParams.not.milestoneTitle,
'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId,
},
undefined,
);
}
return {
...notParams,
author_username: authorUsername,
'label_name[]': labelName,
assignee_username: assigneeUsername,
milestone_title: milestoneTitle,
search,
types,
weight,
epic_id: getIdFromGraphQLId(epicId),
};
},
},
created() {
if (!isEmpty(this.eeFilters)) {
this.filterParams = this.eeFilters;
}
},
methods: {
...mapActions(['performSearch']),
handleFilter(filters) {
this.filterParams = this.getFilterParams(filters);
updateHistory({
url: setUrlParams(this.urlParams, window.location.href, true, false, true),
title: document.title,
replace: true,
});
this.performSearch();
},
getFilterParams(filters = []) { getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!='); const notFilters = filters.filter((item) => item.value.operator === '!=');
const equalsFilters = filters.filter( const equalsFilters = filters.filter(
...@@ -266,7 +276,7 @@ export default { ...@@ -266,7 +276,7 @@ export default {
namespace="" namespace=""
:tokens="tokens" :tokens="tokens"
:search-input-placeholder="$options.i18n.search" :search-input-placeholder="$options.i18n.search"
:initial-filter-value="getFilteredSearchValue()" :initial-filter-value="getFilteredSearchValue"
@onFilter="handleFilter" @onFilter="handleFilter"
/> />
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import BoardFilteredSearchCe from '~/boards/components/board_filtered_search.vue'; import BoardFilteredSearchCe from '~/boards/components/board_filtered_search.vue';
import { transformBoardConfig } from 'ee/boards/boards_util';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { updateHistory, queryToObject } from '~/lib/utils/url_utility';
export default { export default {
components: { BoardFilteredSearchCe }, components: { BoardFilteredSearchCe },
...@@ -9,9 +14,51 @@ export default { ...@@ -9,9 +14,51 @@ export default {
type: Array, type: Array,
}, },
}, },
data() {
return {
filterParams: {},
resetFilters: false,
};
},
computed: {
...mapState({ boardScopeConfig: ({ boardConfig }) => boardConfig }),
shouldRenderComponent() {
return this.resetFilters || !isEmpty(this.boardScopeConfig);
},
},
watch: {
boardScopeConfig(newVal) {
if (!isEmpty(newVal)) {
const boardConfigPath = transformBoardConfig(newVal);
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
url: `${filterPath}${transformBoardConfig(newVal)}`,
});
this.performSearch();
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
this.filterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, {}),
};
this.resetFilters = true;
}
}
},
},
methods: {
...mapActions(['performSearch']),
},
}; };
</script> </script>
<template> <template>
<board-filtered-search-ce v-bind="{ ...$props, ...$attrs }" /> <board-filtered-search-ce
v-if="shouldRenderComponent"
:ee-filters="filterParams"
v-bind="{ ...$props, ...$attrs }"
/>
</template> </template>
...@@ -3,7 +3,7 @@ import Vue from 'vue'; ...@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import initFilteredSearch from 'ee/boards/epic_filtered_search'; import initFilteredSearch from 'ee/boards/epic_filtered_search';
import { fullEpicBoardId, transformBoardConfig } from 'ee_component/boards/boards_util'; import { fullEpicBoardId } from 'ee_component/boards/boards_util';
import toggleLabels from 'ee_component/boards/toggle_labels'; import toggleLabels from 'ee_component/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
...@@ -16,7 +16,6 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -16,7 +16,6 @@ import createDefaultClient from '~/lib/graphql';
import '~/boards/filters/due_date_filters'; import '~/boards/filters/due_date_filters';
import { NavigationType, parseBoolean } from '~/lib/utils/common_utils'; import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json'; import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -60,15 +59,6 @@ function mountBoardApp(el) { ...@@ -60,15 +59,6 @@ function mountBoardApp(el) {
}, },
}); });
const boardConfigPath = transformBoardConfig(store.state.boardConfig);
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
url: `${filterPath}${transformBoardConfig(store.state.boardConfig)}`,
});
}
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from 'ee/boards/components/board_filtered_search.vue'; import BoardFilteredSearch from 'ee/boards/components/board_filtered_search.vue';
import BoardFilteredSearchCe from '~/boards/components/board_filtered_search.vue'; import BoardFilteredSearchCe from '~/boards/components/board_filtered_search.vue';
import { createStore } from '~/boards/stores';
import * as urlUtility from '~/lib/utils/url_utility';
Vue.use(Vuex);
describe('ee/BoardFilteredSearch', () => { describe('ee/BoardFilteredSearch', () => {
let wrapper; let wrapper;
let store;
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(BoardFilteredSearch, { propsData: { tokens: [] } }); wrapper = shallowMount(BoardFilteredSearch, {
store,
propsData: { tokens: [] },
});
}; };
const findFilteredSearch = () => wrapper.findComponent(BoardFilteredSearchCe); const findFilteredSearch = () => wrapper.findComponent(BoardFilteredSearchCe);
...@@ -15,12 +25,55 @@ describe('ee/BoardFilteredSearch', () => { ...@@ -15,12 +25,55 @@ describe('ee/BoardFilteredSearch', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('default', () => { describe('when boardScopeConfig watcher is triggered', () => {
beforeEach(async () => {
store = createStore();
createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
jest.spyOn(urlUtility, 'updateHistory');
store.state.boardConfig = { labels: [{ title: 'test', color: 'black', id: '1' }] };
await wrapper.vm.$nextTick();
});
it('calls performSearch', () => {
expect(store.dispatch).toHaveBeenCalledWith('performSearch');
});
it('calls historyPushState', () => {
expect(urlUtility.updateHistory).toHaveBeenCalledWith({
url: '?label_name[]=test',
});
});
it('passes the correct props to BoardFilteredSearchCe', () => {
expect(findFilteredSearch().props()).toEqual(
expect.objectContaining({ eeFilters: { labelName: ['test'] } }),
);
});
});
describe('when resetFilters is true and boardConfig is not empty', () => {
beforeEach(() => { beforeEach(() => {
store = createStore();
createComponent(); createComponent();
}); });
it('renders FilteredSearch', () => { it('renders BoardFilteredSearchCe', async () => {
store.state.boardConfig = {};
await wrapper.vm.$nextTick();
expect(findFilteredSearch().exists()).toEqual(false);
store.state.boardConfig = { labels: [] };
await wrapper.vm.$nextTick();
expect(findFilteredSearch().exists()).toBe(true); expect(findFilteredSearch().exists()).toBe(true);
}); });
}); });
......
...@@ -7,6 +7,7 @@ import { __ } from '~/locale'; ...@@ -7,6 +7,7 @@ import { __ } from '~/locale';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { createStore } from '~/boards/stores';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -42,17 +43,13 @@ describe('BoardFilteredSearch', () => { ...@@ -42,17 +43,13 @@ describe('BoardFilteredSearch', () => {
}, },
]; ];
const createComponent = ({ initialFilterParams = {} } = {}) => { const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => {
store = new Vuex.Store({ store = createStore();
actions: {
performSearch: jest.fn(),
},
});
wrapper = shallowMount(BoardFilteredSearch, { wrapper = shallowMount(BoardFilteredSearch, {
provide: { initialFilterParams, fullPath: '' }, provide: { initialFilterParams, fullPath: '' },
store, store,
propsData: { propsData: {
...props,
tokens, tokens,
}, },
}); });
...@@ -68,11 +65,7 @@ describe('BoardFilteredSearch', () => { ...@@ -68,11 +65,7 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
jest.spyOn(store, 'dispatch'); jest.spyOn(store, 'dispatch').mockImplementation();
});
it('renders FilteredSearch', () => {
expect(findFilteredSearch().exists()).toBe(true);
}); });
it('passes the correct tokens to FilteredSearch', () => { it('passes the correct tokens to FilteredSearch', () => {
...@@ -99,6 +92,22 @@ describe('BoardFilteredSearch', () => { ...@@ -99,6 +92,22 @@ describe('BoardFilteredSearch', () => {
}); });
}); });
describe('when eeFilters is not empty', () => {
it('passes the correct initialFilterValue to FitleredSearchBarRoot', () => {
createComponent({ props: { eeFilters: { labelName: ['label'] } } });
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
{ type: 'label_name', value: { data: 'label', operator: '=' } },
]);
});
});
it('renders FilteredSearch', () => {
createComponent();
expect(findFilteredSearch().exists()).toBe(true);
});
describe('when searching', () => { describe('when searching', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
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