Commit 93c3f5f8 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '337685-add-epic-to-new-filtered-search-in-issue-boards' into 'master'

Add Epic to new filtered search in issue boards

See merge request gitlab-org/gitlab!70476
parents 533bb51e 272936fa
<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')) {
...@@ -47,6 +50,7 @@ export default { ...@@ -47,6 +50,7 @@ export default {
'not[types]': this.filterParams.not.types, 'not[types]': this.filterParams.not.types,
'not[milestone_title]': this.filterParams.not.milestoneTitle, 'not[milestone_title]': this.filterParams.not.milestoneTitle,
'not[weight]': this.filterParams.not.weight, 'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId,
}, },
undefined, undefined,
); );
...@@ -61,6 +65,7 @@ export default { ...@@ -61,6 +65,7 @@ export default {
search, search,
types, types,
weight, weight,
epic_id: getIdFromGraphQLId(epicId),
}; };
}, },
}, },
...@@ -86,6 +91,7 @@ export default { ...@@ -86,6 +91,7 @@ export default {
milestoneTitle, milestoneTitle,
types, types,
weight, weight,
epicId,
} = this.filterParams; } = this.filterParams;
const filteredSearchValue = []; const filteredSearchValue = [];
...@@ -133,6 +139,13 @@ export default { ...@@ -133,6 +139,13 @@ export default {
}); });
} }
if (epicId) {
filteredSearchValue.push({
type: 'epic_id',
value: { data: epicId, operator: '=' },
});
}
if (this.filterParams['not[authorUsername]']) { if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'author_username', type: 'author_username',
...@@ -177,6 +190,13 @@ export default { ...@@ -177,6 +190,13 @@ export default {
}); });
} }
if (this.filterParams['not[epicId]']) {
filteredSearchValue.push({
type: 'epic_id',
value: { data: this.filterParams['not[epicId]'], operator: '!=' },
});
}
if (search) { if (search) {
filteredSearchValue.push(search); filteredSearchValue.push(search);
} }
...@@ -216,6 +236,9 @@ export default { ...@@ -216,6 +236,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,6 +2,7 @@ ...@@ -2,6 +2,7 @@
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';
...@@ -19,6 +20,7 @@ export default { ...@@ -19,6 +20,7 @@ export default {
}, },
i18n: { i18n: {
search: __('Search'), search: __('Search'),
epic: __('Epic'),
label: __('Label'), label: __('Label'),
author: __('Author'), author: __('Author'),
assignee: __('Assignee'), assignee: __('Assignee'),
...@@ -42,7 +44,15 @@ export default { ...@@ -42,7 +44,15 @@ export default {
}, },
}, },
computed: { computed: {
tokens() { isGroupBoard() {
return this.boardType === BoardType.group;
},
epicsGroupPath() {
return this.isGroupBoard
? this.fullPath
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
},
tokensCE() {
const { const {
label, label,
is, is,
...@@ -134,6 +144,9 @@ export default { ...@@ -134,6 +144,9 @@ export default {
}, },
]; ];
}, },
tokens() {
return this.tokensCE;
},
}, },
methods: { methods: {
...mapActions(['fetchMilestones']), ...mapActions(['fetchMilestones']),
......
...@@ -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);
......
import Vue from 'vue'; import Vue from 'vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue'; import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import store from '~/boards/stores'; 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';
......
<script>
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import IssueBoardFilteredSearchFoss from '~/boards/components/issue_board_filtered_search.vue';
import { BoardType } from '~/boards/constants';
import { __ } from '~/locale';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
export default {
extends: IssueBoardFilteredSearchFoss,
i18n: {
...IssueBoardFilteredSearchFoss.i18n,
epic: __('Epic'),
},
computed: {
isGroupBoard() {
return this.boardType === BoardType.group;
},
epicsGroupPath() {
return this.isGroupBoard
? this.fullPath
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
},
tokens() {
const { epic } = this.$options.i18n;
return [
...this.tokensCE,
{
type: 'epic_id',
title: epic,
icon: 'epic',
token: EpicToken,
unique: true,
symbol: '&',
idProperty: 'id',
useIdValue: true,
fullPath: this.epicsGroupPath,
},
];
},
},
};
</script>
import { shallowMount } from '@vue/test-utils';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import IssueBoardFilteredSpec from 'ee/boards/components/issue_board_filtered_search.vue';
import issueBoardFilters from '~/boards/issue_board_filters';
import { mockTokens } from '../mock_data';
jest.mock('~/boards/issue_board_filters');
describe('IssueBoardFilter', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
propsData: { fullPath: 'gitlab-org', boardType: 'group' },
});
};
let fetchAuthorsSpy;
let fetchLabelsSpy;
beforeEach(() => {
fetchAuthorsSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
fetchAuthors: fetchAuthorsSpy,
fetchLabels: fetchLabelsSpy,
});
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('finds BoardFilteredSearch', () => {
expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true);
});
it('passes the correct tokens to BoardFilteredSearch including epics', () => {
const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones);
expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
});
});
});
import { GlFilteredSearchToken } from '@gitlab/ui';
import { __ } from '~/locale';
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 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 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';
export const mockLabel = { export const mockLabel = {
id: 'gid://gitlab/GroupLabel/121', id: 'gid://gitlab/GroupLabel/121',
title: 'To Do', title: 'To Do',
...@@ -365,3 +374,86 @@ export const mockGroup2 = { ...@@ -365,3 +374,86 @@ export const mockGroup2 = {
}; };
export const mockSubGroups = [mockGroup0, mockGroup1, mockGroup2]; export const mockSubGroups = [mockGroup0, mockGroup1, mockGroup2];
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
{
icon: 'user',
title: __('Assignee'),
type: 'assignee_username',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
token: AuthorToken,
unique: true,
fetchAuthors,
preloadedAuthors: [],
},
{
icon: 'pencil',
title: __('Author'),
type: 'author_username',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
symbol: '@',
token: AuthorToken,
unique: true,
fetchAuthors,
preloadedAuthors: [],
},
{
icon: 'labels',
title: __('Label'),
type: 'label_name',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
token: LabelToken,
unique: false,
symbol: '~',
fetchLabels,
},
{
icon: 'clock',
title: __('Milestone'),
symbol: '%',
type: 'milestone_title',
token: MilestoneToken,
unique: true,
defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
fetchMilestones,
},
{
icon: 'issues',
title: __('Type'),
type: 'types',
operators: [{ value: '=', description: 'is' }],
token: GlFilteredSearchToken,
unique: true,
options: [
{ icon: 'issue-type-issue', value: 'ISSUE', title: 'Issue' },
{ icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' },
],
},
{
icon: 'weight',
title: __('Weight'),
type: 'weight',
token: WeightToken,
unique: true,
},
{
type: 'epic_id',
icon: 'epic',
title: 'Epic',
unique: true,
symbol: '&',
token: EpicToken,
idProperty: 'id',
useIdValue: true,
fullPath: 'gitlab-org',
},
];
...@@ -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();
}); });
......
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