Commit 2bf60538 authored by Florie Guibert's avatar Florie Guibert

Add Epic to new filtered search in issue boards

Allow user to filter issues by epic on boards
parent 1d98c6c6
<script> <script>
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
...@@ -35,7 +36,9 @@ export default { ...@@ -35,7 +36,9 @@ export default {
milestoneTitle, milestoneTitle,
types, types,
weight, weight,
epicId,
} = this.filterParams; } = this.filterParams;
let notParams = {}; let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
...@@ -61,6 +64,7 @@ export default { ...@@ -61,6 +64,7 @@ export default {
search, search,
types, types,
weight, weight,
epic_id: getIdFromGraphQLId(epicId),
}; };
}, },
}, },
...@@ -86,6 +90,7 @@ export default { ...@@ -86,6 +90,7 @@ export default {
milestoneTitle, milestoneTitle,
types, types,
weight, weight,
epicId,
} = this.filterParams; } = this.filterParams;
const filteredSearchValue = []; const filteredSearchValue = [];
...@@ -133,6 +138,13 @@ export default { ...@@ -133,6 +138,13 @@ export default {
}); });
} }
if (epicId) {
filteredSearchValue.push({
type: 'epic_id',
value: { data: epicId },
});
}
if (this.filterParams['not[authorUsername]']) { if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'author_username', type: 'author_username',
...@@ -216,6 +228,9 @@ export default { ...@@ -216,6 +228,9 @@ export default {
case 'weight': case 'weight':
filterParams.weight = filter.value.data; filterParams.weight = filter.value.data;
break; break;
case 'epic_id':
filterParams.epicId = filter.value.data;
break;
case 'filtered-search-term': case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data); if (filter.value.data) plainText.push(filter.value.data);
break; break;
......
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
import { GlFilteredSearchToken } from '@gitlab/ui'; import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import { BoardType } from '~/boards/constants';
import issueBoardFilters from '~/boards/issue_board_filters'; import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants'; import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants'; import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
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 EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_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 MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
}, },
i18n: { i18n: {
search: __('Search'), search: __('Search'),
epic: __('Epic'),
label: __('Label'), label: __('Label'),
author: __('Author'), author: __('Author'),
assignee: __('Assignee'), assignee: __('Assignee'),
...@@ -31,6 +34,7 @@ export default { ...@@ -31,6 +34,7 @@ export default {
isNot: __('is not'), isNot: __('is not'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
inject: ['epicFeatureAvailable'],
props: { props: {
fullPath: { fullPath: {
type: String, type: String,
...@@ -42,8 +46,17 @@ export default { ...@@ -42,8 +46,17 @@ export default {
}, },
}, },
computed: { computed: {
isGroupBoard() {
return this.boardType === BoardType.group;
},
epicsGroupPath() {
return this.isGroupBoard
? this.fullPath
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
},
tokens() { tokens() {
const { const {
epic,
label, label,
is, is,
isNot, isNot,
...@@ -90,6 +103,21 @@ export default { ...@@ -90,6 +103,21 @@ export default {
fetchAuthors, fetchAuthors,
preloadedAuthors: this.preloadedAuthors(), preloadedAuthors: this.preloadedAuthors(),
}, },
...(this.epicFeatureAvailable
? [
{
type: 'epic_id',
title: epic,
icon: 'epic',
token: EpicToken,
unique: true,
symbol: '&',
idProperty: 'id',
useIdValue: true,
fullPath: this.epicsGroupPath,
},
]
: []),
{ {
icon: 'labels', icon: 'labels',
title: label, title: label,
......
...@@ -109,7 +109,7 @@ export default () => { ...@@ -109,7 +109,7 @@ export default () => {
}); });
if (gon?.features?.issueBoardsFilteredSearch) { if (gon?.features?.issueBoardsFilteredSearch) {
initBoardsFilteredSearch(apolloProvider); initBoardsFilteredSearch(apolloProvider, parseBoolean($boardApp.dataset.epicFeatureAvailable));
} }
mountBoardApp($boardApp); mountBoardApp($boardApp);
......
...@@ -4,7 +4,7 @@ import store from '~/boards/stores'; ...@@ -4,7 +4,7 @@ import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility'; import { queryToObject } from '~/lib/utils/url_utility';
export default (apolloProvider) => { export default (apolloProvider, epicFeatureAvailable = false) => {
const el = document.getElementById('js-issue-board-filtered-search'); const el = document.getElementById('js-issue-board-filtered-search');
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true }); const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
...@@ -20,6 +20,7 @@ export default (apolloProvider) => { ...@@ -20,6 +20,7 @@ export default (apolloProvider) => {
el, el,
provide: { provide: {
initialFilterParams, initialFilterParams,
epicFeatureAvailable,
}, },
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094 store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider, apolloProvider,
......
...@@ -9,28 +9,33 @@ jest.mock('~/boards/issue_board_filters'); ...@@ -9,28 +9,33 @@ jest.mock('~/boards/issue_board_filters');
describe('IssueBoardFilter', () => { describe('IssueBoardFilter', () => {
let wrapper; let wrapper;
const createComponent = () => { const createComponent = ({ epicFeatureAvailable = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, { wrapper = shallowMount(IssueBoardFilteredSpec, {
props: { fullPath: '', boardType: '' }, propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: {
epicFeatureAvailable,
},
}); });
}; };
let fetchAuthorsSpy;
let fetchLabelsSpy;
beforeEach(() => {
fetchAuthorsSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
fetchAuthors: fetchAuthorsSpy,
fetchLabels: fetchLabelsSpy,
});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('default', () => { describe('default', () => {
let fetchAuthorsSpy;
let fetchLabelsSpy;
beforeEach(() => { beforeEach(() => {
fetchAuthorsSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
fetchAuthors: fetchAuthorsSpy,
fetchLabels: fetchLabelsSpy,
});
createComponent(); createComponent();
}); });
...@@ -44,4 +49,16 @@ describe('IssueBoardFilter', () => { ...@@ -44,4 +49,16 @@ describe('IssueBoardFilter', () => {
expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens); expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
}); });
}); });
describe('when epics are available', () => {
beforeEach(() => {
createComponent({ epicFeatureAvailable: true });
});
it('passes the correct tokens to BoardFilteredSearch including epics', () => {
const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones, true);
expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
});
});
}); });
...@@ -4,6 +4,7 @@ import { ListType } from '~/boards/constants'; ...@@ -4,6 +4,7 @@ import { ListType } from '~/boards/constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants'; import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
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 EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_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 MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
...@@ -538,7 +539,19 @@ export const mockMoveData = { ...@@ -538,7 +539,19 @@ export const mockMoveData = {
...mockMoveIssueParams, ...mockMoveIssueParams,
}; };
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ export const mockEpicToken = {
type: 'epic_id',
icon: 'epic',
title: 'Epic',
unique: true,
symbol: '&',
token: EpicToken,
idProperty: 'id',
useIdValue: true,
fullPath: 'gitlab-org',
};
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, epics = false) => [
{ {
icon: 'user', icon: 'user',
title: __('Assignee'), title: __('Assignee'),
...@@ -566,6 +579,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ ...@@ -566,6 +579,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
fetchAuthors, fetchAuthors,
preloadedAuthors: [], preloadedAuthors: [],
}, },
...(epics ? [mockEpicToken] : []),
{ {
icon: 'labels', icon: 'labels',
title: __('Label'), title: __('Label'),
......
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