Commit 31192e2f authored by Scott Stern's avatar Scott Stern Committed by Kushal Pandya

Add iteration token to the board issues filter

parent 8d2c7f09
...@@ -39,6 +39,7 @@ export default { ...@@ -39,6 +39,7 @@ export default {
assigneeUsername, assigneeUsername,
search, search,
milestoneTitle, milestoneTitle,
iterationId,
types, types,
weight, weight,
epicId, epicId,
...@@ -83,6 +84,13 @@ export default { ...@@ -83,6 +84,13 @@ export default {
}); });
} }
if (iterationId) {
filteredSearchValue.push({
type: 'iteration',
value: { data: iterationId, operator: '=' },
});
}
if (weight) { if (weight) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'weight', type: 'weight',
...@@ -118,6 +126,13 @@ export default { ...@@ -118,6 +126,13 @@ export default {
}); });
} }
if (this.filterParams['not[iteration_id]']) {
filteredSearchValue.push({
type: 'iteration_id',
value: { data: this.filterParams['not[iteration_id]'], operator: '!=' },
});
}
if (this.filterParams['not[weight]']) { if (this.filterParams['not[weight]']) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'weight', type: 'weight',
...@@ -179,8 +194,8 @@ export default { ...@@ -179,8 +194,8 @@ export default {
weight, weight,
epicId, epicId,
myReactionEmoji, myReactionEmoji,
iterationId,
} = this.filterParams; } = this.filterParams;
let notParams = {}; let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
...@@ -194,6 +209,7 @@ export default { ...@@ -194,6 +209,7 @@ export default {
'not[weight]': this.filterParams.not.weight, 'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId, 'not[epic_id]': this.filterParams.not.epicId,
'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji, 'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
'not[iteration_id]': this.filterParams.not.iterationId,
}, },
undefined, undefined,
); );
...@@ -205,6 +221,7 @@ export default { ...@@ -205,6 +221,7 @@ export default {
'label_name[]': labelName, 'label_name[]': labelName,
assignee_username: assigneeUsername, assignee_username: assigneeUsername,
milestone_title: milestoneTitle, milestone_title: milestoneTitle,
iteration_id: iterationId,
search, search,
types, types,
weight, weight,
...@@ -261,6 +278,9 @@ export default { ...@@ -261,6 +278,9 @@ export default {
case 'milestone_title': case 'milestone_title':
filterParams.milestoneTitle = filter.value.data; filterParams.milestoneTitle = filter.value.data;
break; break;
case 'iteration':
filterParams.iterationId = filter.value.data;
break;
case 'weight': case 'weight':
filterParams.weight = filter.value.data; filterParams.weight = filter.value.data;
break; break;
......
...@@ -12,6 +12,7 @@ import { __ } from '~/locale'; ...@@ -12,6 +12,7 @@ import { __ } from '~/locale';
import { import {
DEFAULT_MILESTONES_GRAPHQL, DEFAULT_MILESTONES_GRAPHQL,
TOKEN_TITLE_MY_REACTION, TOKEN_TITLE_MY_REACTION,
OPERATOR_IS_AND_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants'; } 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 EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
...@@ -35,8 +36,6 @@ export default { ...@@ -35,8 +36,6 @@ export default {
issue: __('Issue'), issue: __('Issue'),
milestone: __('Milestone'), milestone: __('Milestone'),
weight: __('Weight'), weight: __('Weight'),
is: __('is'),
isNot: __('is not'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
inject: ['isSignedIn'], inject: ['isSignedIn'],
...@@ -62,8 +61,6 @@ export default { ...@@ -62,8 +61,6 @@ export default {
tokensCE() { tokensCE() {
const { const {
label, label,
is,
isNot,
author, author,
assignee, assignee,
issue, issue,
...@@ -84,10 +81,7 @@ export default { ...@@ -84,10 +81,7 @@ export default {
icon: 'user', icon: 'user',
title: assignee, title: assignee,
type: 'assignee_username', type: 'assignee_username',
operators: [ operators: OPERATOR_IS_AND_IS_NOT,
{ value: '=', description: is },
{ value: '!=', description: isNot },
],
token: AuthorToken, token: AuthorToken,
unique: true, unique: true,
fetchAuthors, fetchAuthors,
...@@ -97,10 +91,7 @@ export default { ...@@ -97,10 +91,7 @@ export default {
icon: 'pencil', icon: 'pencil',
title: author, title: author,
type: 'author_username', type: 'author_username',
operators: [ operators: OPERATOR_IS_AND_IS_NOT,
{ value: '=', description: is },
{ value: '!=', description: isNot },
],
symbol: '@', symbol: '@',
token: AuthorToken, token: AuthorToken,
unique: true, unique: true,
...@@ -111,10 +102,7 @@ export default { ...@@ -111,10 +102,7 @@ export default {
icon: 'labels', icon: 'labels',
title: label, title: label,
type: 'label_name', type: 'label_name',
operators: [ operators: OPERATOR_IS_AND_IS_NOT,
{ value: '=', description: is },
{ value: '!=', description: isNot },
],
token: LabelToken, token: LabelToken,
unique: false, unique: false,
symbol: '~', symbol: '~',
......
...@@ -373,7 +373,6 @@ export default { ...@@ -373,7 +373,6 @@ export default {
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext }); commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { fullPath, fullBoardId, boardType, filterParams } = state; const { fullPath, fullBoardId, boardType, filterParams } = state;
const variables = { const variables = {
fullPath, fullPath,
boardId: fullBoardId, boardId: fullBoardId,
......
<script> <script>
import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui'; import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
...@@ -43,7 +42,7 @@ export default { ...@@ -43,7 +42,7 @@ export default {
}, },
methods: { methods: {
getActiveIteration(iterations, data) { getActiveIteration(iterations, data) {
return iterations.find((iteration) => this.getValue(iteration) === data); return iterations.find((iteration) => iteration.id === data);
}, },
groupIterationsByCadence(iterations) { groupIterationsByCadence(iterations) {
const cadences = []; const cadences = [];
...@@ -80,9 +79,6 @@ export default { ...@@ -80,9 +79,6 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
getValue(iteration) {
return String(getIdFromGraphQLId(iteration.id));
},
/** /**
* TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619 * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
* This method also exists as a utility function in ee/../iterations/utils.js * This method also exists as a utility function in ee/../iterations/utils.js
...@@ -125,7 +121,7 @@ export default { ...@@ -125,7 +121,7 @@ export default {
<gl-filtered-search-suggestion <gl-filtered-search-suggestion
v-for="iteration in cadence.iterations" v-for="iteration in cadence.iterations"
:key="iteration.id" :key="iteration.id"
:value="getValue(iteration)" :value="iteration.id"
> >
{{ iteration.title }} {{ iteration.title }}
<div v-if="glFeatures.iterationCadences" class="gl-text-gray-400"> <div v-if="glFeatures.iterationCadences" class="gl-text-gray-400">
......
...@@ -2,17 +2,23 @@ ...@@ -2,17 +2,23 @@
// This is a false violation of @gitlab/no-runtime-template-compiler, since it // This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component. // extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */ /* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapActions } from 'vuex';
import IssueBoardFilteredSearchFoss from '~/boards/components/issue_board_filtered_search.vue'; import IssueBoardFilteredSearchFoss from '~/boards/components/issue_board_filtered_search.vue';
import { BoardType } from '~/boards/constants'; import { BoardType } from '~/boards/constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { OPERATOR_IS_AND_IS_NOT } from '~/vue_shared/components/filtered_search_bar/constants';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
extends: IssueBoardFilteredSearchFoss, extends: IssueBoardFilteredSearchFoss,
i18n: { i18n: {
...IssueBoardFilteredSearchFoss.i18n, ...IssueBoardFilteredSearchFoss.i18n,
epic: __('Epic'), epic: __('Epic'),
iteration: __('Iteration'),
}, },
mixins: [glFeatureFlagMixin()],
computed: { computed: {
isGroupBoard() { isGroupBoard() {
return this.boardType === BoardType.group; return this.boardType === BoardType.group;
...@@ -23,7 +29,7 @@ export default { ...@@ -23,7 +29,7 @@ export default {
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/')); : this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
}, },
tokens() { tokens() {
const { epic } = this.$options.i18n; const { epic, iteration } = this.$options.i18n;
return [ return [
...this.tokensCE, ...this.tokensCE,
...@@ -38,8 +44,24 @@ export default { ...@@ -38,8 +44,24 @@ export default {
useIdValue: true, useIdValue: true,
fullPath: this.epicsGroupPath, fullPath: this.epicsGroupPath,
}, },
...(this.glFeatures.iterationCadences
? [
{
icon: 'iteration',
title: iteration,
type: 'iteration',
operators: OPERATOR_IS_AND_IS_NOT,
token: IterationToken,
unique: true,
fetchIterations: this.fetchIterations,
},
]
: []),
]; ];
}, },
}, },
methods: {
...mapActions(['fetchIterations']),
},
}; };
</script> </script>
#import "../../sidebar/queries/iteration.fragment.graphql"
query GroupBoardIterations($fullPath: ID!, $title: String) { query GroupBoardIterations($fullPath: ID!, $title: String) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) { iterations(includeAncestors: true, title: $title) {
nodes { nodes {
id ...IterationFragment
title
} }
} }
} }
......
#import "../../sidebar/queries/iteration.fragment.graphql"
query ProjectBoardIterations($fullPath: ID!, $title: String) { query ProjectBoardIterations($fullPath: ID!, $title: String) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) { iterations(includeAncestors: true, title: $title) {
nodes { nodes {
id ...IterationFragment
title
} }
} }
} }
......
...@@ -301,6 +301,7 @@ export default { ...@@ -301,6 +301,7 @@ export default {
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext }); commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { epicId, ...filterParams } = state.filterParams; const { epicId, ...filterParams } = state.filterParams;
if (noEpicIssues && epicId !== undefined) { if (noEpicIssues && epicId !== undefined) {
return null; return null;
} }
......
...@@ -12,7 +12,12 @@ describe('IssueBoardFilter', () => { ...@@ -12,7 +12,12 @@ describe('IssueBoardFilter', () => {
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(IssueBoardFilteredSpec, { wrapper = shallowMount(IssueBoardFilteredSpec, {
propsData: { fullPath: 'gitlab-org', boardType: 'group' }, propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: { isSignedIn: true }, provide: {
isSignedIn: true,
glFeatures: {
iterationCadences: true,
},
},
}); });
}; };
...@@ -42,7 +47,12 @@ describe('IssueBoardFilter', () => { ...@@ -42,7 +47,12 @@ describe('IssueBoardFilter', () => {
}); });
it('passes the correct tokens to BoardFilteredSearch including epics', () => { it('passes the correct tokens to BoardFilteredSearch including epics', () => {
const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones); const tokens = mockTokens(
fetchLabelsSpy,
fetchAuthorsSpy,
wrapper.vm.fetchMilestones,
wrapper.vm.fetchIterations,
);
expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens); expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
}); });
......
...@@ -7,6 +7,7 @@ import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji ...@@ -7,6 +7,7 @@ import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji
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';
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
export const mockEpicBoardResponse = { export const mockEpicBoardResponse = {
data: { data: {
...@@ -398,7 +399,7 @@ export const mockGroup2 = { ...@@ -398,7 +399,7 @@ export const mockGroup2 = {
export const mockSubGroups = [mockGroup0, mockGroup1, mockGroup2]; export const mockSubGroups = [mockGroup0, mockGroup1, mockGroup2];
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, fetchIterations) => [
{ {
icon: 'user', icon: 'user',
title: __('Assignee'), title: __('Assignee'),
...@@ -486,4 +487,16 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ ...@@ -486,4 +487,16 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
useIdValue: true, useIdValue: true,
fullPath: 'gitlab-org', fullPath: 'gitlab-org',
}, },
{
type: 'iteration',
icon: 'iteration',
title: 'Iteration',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
unique: true,
fetchIterations,
token: IterationToken,
},
]; ];
...@@ -123,6 +123,7 @@ describe('BoardFilteredSearch', () => { ...@@ -123,6 +123,7 @@ describe('BoardFilteredSearch', () => {
{ type: 'milestone_title', value: { data: 'New Milestone', operator: '=' } }, { type: 'milestone_title', value: { data: 'New Milestone', operator: '=' } },
{ type: 'types', value: { data: 'INCIDENT', operator: '=' } }, { type: 'types', value: { data: 'INCIDENT', operator: '=' } },
{ type: 'weight', value: { data: '2', operator: '=' } }, { type: 'weight', value: { data: '2', operator: '=' } },
{ type: 'iteration', value: { data: '3341', operator: '=' } },
]; ];
jest.spyOn(urlUtility, 'updateHistory'); jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters); findFilteredSearch().vm.$emit('onFilter', mockFilters);
...@@ -131,7 +132,7 @@ describe('BoardFilteredSearch', () => { ...@@ -131,7 +132,7 @@ describe('BoardFilteredSearch', () => {
title: '', title: '',
replace: true, replace: true,
url: url:
'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone&types=INCIDENT&weight=2', 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2',
}); });
}); });
}); });
......
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