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 {
weight,
epicId,
myReactionEmoji,
releaseTag,
} = this.filterParams;
const filteredSearchValue = [];
......@@ -105,6 +106,13 @@ export default {
});
}
if (releaseTag) {
filteredSearchValue.push({
type: 'release',
value: { data: releaseTag, operator: '=' },
});
}
if (epicId) {
filteredSearchValue.push({
type: 'epic_id',
......@@ -177,6 +185,13 @@ export default {
});
}
if (this.filterParams['not[releaseTag]']) {
filteredSearchValue.push({
type: 'release',
value: { data: this.filterParams['not[releaseTag]'], operator: '!=' },
});
}
if (search) {
filteredSearchValue.push(search);
}
......@@ -195,6 +210,7 @@ export default {
epicId,
myReactionEmoji,
iterationId,
releaseTag,
} = this.filterParams;
let notParams = {};
......@@ -210,6 +226,7 @@ export default {
'not[epic_id]': this.filterParams.not.epicId,
'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
'not[iteration_id]': this.filterParams.not.iterationId,
'not[release_tag]': this.filterParams.not.releaseTag,
},
undefined,
);
......@@ -227,6 +244,7 @@ export default {
weight,
epic_id: getIdFromGraphQLId(epicId),
my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
};
},
},
......@@ -290,6 +308,9 @@ export default {
case 'my_reaction_emoji':
filterParams.myReactionEmoji = filter.value.data;
break;
case 'release':
filterParams.releaseTag = filter.value.data;
break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
......
......@@ -5,6 +5,7 @@ import { mapActions } from 'vuex';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import { BoardType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
......@@ -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 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 ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export default {
types: {
......@@ -34,9 +36,10 @@ export default {
incident: __('Incident'),
issue: __('Issue'),
milestone: __('Milestone'),
release: __('Release'),
},
components: { BoardFilteredSearch },
inject: ['isSignedIn'],
inject: ['isSignedIn', 'releasesFetchPath'],
props: {
fullPath: {
type: String,
......@@ -57,7 +60,16 @@ export default {
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
},
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 { fetchAuthors, fetchLabels } = issueBoardFilters(
this.$apollo,
......@@ -144,6 +156,25 @@ export default {
{ 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() {
......
......@@ -110,7 +110,8 @@ export default () => {
});
if (gon?.features?.issueBoardsFilteredSearch) {
initBoardsFilteredSearch(apolloProvider, isLoggedIn());
const { releasesFetchPath } = $boardApp.dataset;
initBoardsFilteredSearch(apolloProvider, isLoggedIn(), releasesFetchPath);
}
mountBoardApp($boardApp);
......
......@@ -4,7 +4,7 @@ import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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 rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
......@@ -21,6 +21,7 @@ export default (apolloProvider, isSignedIn) => {
provide: {
initialFilterParams,
isSignedIn,
releasesFetchPath,
},
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider,
......
......@@ -23,6 +23,7 @@ module BoardsHelper
labels_filter_base_path: build_issue_link_base,
labels_fetch_path: labels_fetch_path,
labels_manage_path: labels_manage_path,
releases_fetch_path: releases_fetch_path,
board_type: board.to_type
}
end
......@@ -65,6 +66,14 @@ module BoardsHelper
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
if board.group_board?
group_boards_url(@group)
......
......@@ -14,6 +14,7 @@ describe('IssueBoardFilter', () => {
propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: {
isSignedIn: true,
releasesFetchPath: '/releases',
glFeatures: {
iterationCadences: true,
},
......
......@@ -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 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 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';
export const mockEpicBoardResponse = {
......@@ -469,6 +470,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, fetchIter
{ icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' },
],
},
{
type: 'release',
title: __('Release'),
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
},
{
type: 'epic_id',
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', () => {
{ type: 'types', value: { data: 'INCIDENT', operator: '=' } },
{ type: 'weight', value: { data: '2', operator: '=' } },
{ type: 'iteration', value: { data: '3341', operator: '=' } },
{ type: 'release', value: { data: 'v1.0.0', operator: '=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
......@@ -132,7 +133,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
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', () => {
propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: {
isSignedIn,
releasesFetchPath: '/releases',
},
});
};
......
......@@ -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 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 ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export const boardObj = {
id: 1,
......@@ -615,6 +616,13 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji)
{ icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' },
],
},
{
type: 'release',
title: __('Release'),
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
},
];
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