Commit b78e10f6 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '337686-add-ability-to-filter-by-releases-on-boards-through-graphql' into 'master'

Add Release token to board filtering

See merge request gitlab-org/gitlab!74762
parents 3f76ee1f f40b198b
...@@ -44,6 +44,7 @@ export default { ...@@ -44,6 +44,7 @@ export default {
weight, weight,
epicId, epicId,
myReactionEmoji, myReactionEmoji,
releaseTag,
} = this.filterParams; } = this.filterParams;
const filteredSearchValue = []; const filteredSearchValue = [];
...@@ -105,6 +106,13 @@ export default { ...@@ -105,6 +106,13 @@ export default {
}); });
} }
if (releaseTag) {
filteredSearchValue.push({
type: 'release',
value: { data: releaseTag, operator: '=' },
});
}
if (epicId) { if (epicId) {
filteredSearchValue.push({ filteredSearchValue.push({
type: 'epic_id', type: 'epic_id',
...@@ -177,6 +185,13 @@ export default { ...@@ -177,6 +185,13 @@ export default {
}); });
} }
if (this.filterParams['not[releaseTag]']) {
filteredSearchValue.push({
type: 'release',
value: { data: this.filterParams['not[releaseTag]'], operator: '!=' },
});
}
if (search) { if (search) {
filteredSearchValue.push(search); filteredSearchValue.push(search);
} }
...@@ -195,6 +210,7 @@ export default { ...@@ -195,6 +210,7 @@ export default {
epicId, epicId,
myReactionEmoji, myReactionEmoji,
iterationId, iterationId,
releaseTag,
} = this.filterParams; } = this.filterParams;
let notParams = {}; let notParams = {};
...@@ -210,6 +226,7 @@ export default { ...@@ -210,6 +226,7 @@ export default {
'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, 'not[iteration_id]': this.filterParams.not.iterationId,
'not[release_tag]': this.filterParams.not.releaseTag,
}, },
undefined, undefined,
); );
...@@ -227,6 +244,7 @@ export default { ...@@ -227,6 +244,7 @@ export default {
weight, weight,
epic_id: getIdFromGraphQLId(epicId), epic_id: getIdFromGraphQLId(epicId),
my_reaction_emoji: myReactionEmoji, my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
}; };
}, },
}, },
...@@ -290,6 +308,9 @@ export default { ...@@ -290,6 +308,9 @@ export default {
case 'my_reaction_emoji': case 'my_reaction_emoji':
filterParams.myReactionEmoji = filter.value.data; filterParams.myReactionEmoji = filter.value.data;
break; break;
case 'release':
filterParams.releaseTag = 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;
......
...@@ -5,6 +5,7 @@ import { mapActions } from 'vuex'; ...@@ -5,6 +5,7 @@ import { mapActions } from 'vuex';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue'; import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import { BoardType } from '~/boards/constants'; import { BoardType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
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';
...@@ -18,6 +19,7 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth ...@@ -18,6 +19,7 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth
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';
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 ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export default { export default {
types: { types: {
...@@ -34,9 +36,10 @@ export default { ...@@ -34,9 +36,10 @@ export default {
incident: __('Incident'), incident: __('Incident'),
issue: __('Issue'), issue: __('Issue'),
milestone: __('Milestone'), milestone: __('Milestone'),
release: __('Release'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
inject: ['isSignedIn'], inject: ['isSignedIn', 'releasesFetchPath'],
props: { props: {
fullPath: { fullPath: {
type: String, type: String,
...@@ -57,7 +60,16 @@ export default { ...@@ -57,7 +60,16 @@ export default {
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/')); : this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
}, },
tokensCE() { tokensCE() {
const { label, author, assignee, issue, incident, type, milestone } = this.$options.i18n; const {
label,
author,
assignee,
issue,
incident,
type,
milestone,
release,
} = this.$options.i18n;
const { types } = this.$options; const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters( const { fetchAuthors, fetchLabels } = issueBoardFilters(
this.$apollo, this.$apollo,
...@@ -144,6 +156,25 @@ export default { ...@@ -144,6 +156,25 @@ export default {
{ icon: 'issue-type-incident', value: types.INCIDENT, title: incident }, { icon: 'issue-type-incident', value: types.INCIDENT, title: incident },
], ],
}, },
{
type: 'release',
title: release,
icon: 'rocket',
token: ReleaseToken,
fetchReleases: (search) => {
// TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/337686
return axios
.get(joinPaths(gon.relative_url_root, this.releasesFetchPath))
.then(({ data }) => {
if (search) {
return fuzzaldrinPlus.filter(data, search, {
key: ['tag'],
});
}
return data;
});
},
},
]; ];
}, },
tokens() { tokens() {
......
...@@ -110,7 +110,8 @@ export default () => { ...@@ -110,7 +110,8 @@ export default () => {
}); });
if (gon?.features?.issueBoardsFilteredSearch) { if (gon?.features?.issueBoardsFilteredSearch) {
initBoardsFilteredSearch(apolloProvider, isLoggedIn()); const { releasesFetchPath } = $boardApp.dataset;
initBoardsFilteredSearch(apolloProvider, isLoggedIn(), releasesFetchPath);
} }
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, isSignedIn) => { export default (apolloProvider, isSignedIn, releasesFetchPath) => {
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 });
...@@ -21,6 +21,7 @@ export default (apolloProvider, isSignedIn) => { ...@@ -21,6 +21,7 @@ export default (apolloProvider, isSignedIn) => {
provide: { provide: {
initialFilterParams, initialFilterParams,
isSignedIn, isSignedIn,
releasesFetchPath,
}, },
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094 store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider, apolloProvider,
......
...@@ -23,6 +23,7 @@ module BoardsHelper ...@@ -23,6 +23,7 @@ module BoardsHelper
labels_filter_base_path: build_issue_link_base, labels_filter_base_path: build_issue_link_base,
labels_fetch_path: labels_fetch_path, labels_fetch_path: labels_fetch_path,
labels_manage_path: labels_manage_path, labels_manage_path: labels_manage_path,
releases_fetch_path: releases_fetch_path,
board_type: board.to_type board_type: board.to_type
} }
end end
...@@ -65,6 +66,14 @@ module BoardsHelper ...@@ -65,6 +66,14 @@ module BoardsHelper
end end
end end
def releases_fetch_path
if board.group_board?
group_releases_path(@group)
else
project_releases_path(@project)
end
end
def board_base_url def board_base_url
if board.group_board? if board.group_board?
group_boards_url(@group) group_boards_url(@group)
......
...@@ -14,6 +14,7 @@ describe('IssueBoardFilter', () => { ...@@ -14,6 +14,7 @@ describe('IssueBoardFilter', () => {
propsData: { fullPath: 'gitlab-org', boardType: 'group' }, propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: { provide: {
isSignedIn: true, isSignedIn: true,
releasesFetchPath: '/releases',
glFeatures: { glFeatures: {
iterationCadences: true, iterationCadences: true,
}, },
......
...@@ -7,6 +7,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label ...@@ -7,6 +7,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
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 EpicToken from 'ee/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; import EpicToken from 'ee/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
import IterationToken from 'ee/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import IterationToken from 'ee/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import WeightToken from 'ee/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import WeightToken from 'ee/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockEpicBoardResponse = { export const mockEpicBoardResponse = {
...@@ -469,6 +470,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, fetchIter ...@@ -469,6 +470,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, fetchIter
{ icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' }, { icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' },
], ],
}, },
{
type: 'release',
title: __('Release'),
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
},
{ {
type: 'epic_id', type: 'epic_id',
icon: 'epic', icon: 'epic',
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Issue board filters', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) }
let_it_be(:issue) { create(:issue, project: project, milestone: milestone_1) }
let_it_be(:issue_2) { create(:issue, project: project, milestone: milestone_2) }
let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') }
let(:filter_input) { find('.gl-filtered-search-term-input')}
let(:filter_dropdown) { find('.gl-filtered-search-suggestion-list') }
let(:filter_first_suggestion) { find('.gl-filtered-search-suggestion-list').first('.gl-filtered-search-suggestion') }
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
before do
stub_feature_flags(issue_boards_filtered_search: true)
project.add_maintainer(user)
sign_in(user)
visit_project_board
end
describe 'filters by releases' do
before do
filter_input.click
filter_input.set('release:')
filter_first_suggestion.click # Select `=` operator
end
it 'loads all the releases when opened and submit one as filter', :aggregate_failures do
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2)
expect_filtered_search_dropdown_results(filter_dropdown, 2)
click_on release.tag
filter_submit.click
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1)
end
end
def expect_filtered_search_dropdown_results(filter_dropdown, count)
expect(filter_dropdown).to have_selector('.gl-new-dropdown-item', count: count)
end
def visit_project_board
visit project_board_path(project, board)
wait_for_requests
end
end
...@@ -124,6 +124,7 @@ describe('BoardFilteredSearch', () => { ...@@ -124,6 +124,7 @@ describe('BoardFilteredSearch', () => {
{ 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: '=' } }, { type: 'iteration', value: { data: '3341', operator: '=' } },
{ type: 'release', value: { data: 'v1.0.0', operator: '=' } },
]; ];
jest.spyOn(urlUtility, 'updateHistory'); jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters); findFilteredSearch().vm.$emit('onFilter', mockFilters);
...@@ -132,7 +133,7 @@ describe('BoardFilteredSearch', () => { ...@@ -132,7 +133,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&iteration_id=3341&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&release_tag=v1.0.0',
}); });
}); });
}); });
......
...@@ -16,6 +16,7 @@ describe('IssueBoardFilter', () => { ...@@ -16,6 +16,7 @@ describe('IssueBoardFilter', () => {
propsData: { fullPath: 'gitlab-org', boardType: 'group' }, propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: { provide: {
isSignedIn, isSignedIn,
releasesFetchPath: '/releases',
}, },
}); });
}; };
......
...@@ -7,6 +7,7 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth ...@@ -7,6 +7,7 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth
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';
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 ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export const boardObj = { export const boardObj = {
id: 1, id: 1,
...@@ -615,6 +616,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji) ...@@ -615,6 +616,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji)
{ icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' }, { icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' },
], ],
}, },
{
type: 'release',
title: __('Release'),
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
},
]; ];
export const mockLabel1 = { export const mockLabel1 = {
......
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