Commit 87a24bd9 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '118742-iterations-filter-graphql' into 'master'

Support filtering by iterations with GraphQL

See merge request gitlab-org/gitlab!47263
parents 0e8c176d 7487755d
...@@ -1904,6 +1904,16 @@ input BoardIssueInput { ...@@ -1904,6 +1904,16 @@ input BoardIssueInput {
""" """
epicWildcardId: EpicWildcardId epicWildcardId: EpicWildcardId
"""
Filter by iteration title
"""
iterationTitle: String
"""
Filter by iteration ID wildcard
"""
iterationWildcardId: IterationWildcardId
""" """
Filter by label name Filter by label name
""" """
...@@ -11371,6 +11381,21 @@ enum IterationState { ...@@ -11371,6 +11381,21 @@ enum IterationState {
upcoming upcoming
} }
"""
Iteration ID wildcard values
"""
enum IterationWildcardId {
"""
An iteration is assigned
"""
ANY
"""
No iteration is assigned
"""
NONE
}
""" """
Represents untyped JSON Represents untyped JSON
""" """
...@@ -13883,6 +13908,11 @@ input NegatedBoardIssueInput { ...@@ -13883,6 +13908,11 @@ input NegatedBoardIssueInput {
""" """
epicId: EpicID epicId: EpicID
"""
Filter by iteration title
"""
iterationTitle: String
""" """
Filter by label name Filter by label name
""" """
......
...@@ -5111,6 +5111,16 @@ ...@@ -5111,6 +5111,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "iterationTitle",
"description": "Filter by iteration title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "weight", "name": "weight",
"description": "Filter by weight", "description": "Filter by weight",
...@@ -5150,6 +5160,16 @@ ...@@ -5150,6 +5160,16 @@
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
},
{
"name": "iterationWildcardId",
"description": "Filter by iteration ID wildcard",
"type": {
"kind": "ENUM",
"name": "IterationWildcardId",
"ofType": null
},
"defaultValue": null
} }
], ],
"interfaces": null, "interfaces": null,
...@@ -31135,6 +31155,29 @@ ...@@ -31135,6 +31155,29 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "IterationWildcardId",
"description": "Iteration ID wildcard values",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "NONE",
"description": "No iteration is assigned",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ANY",
"description": "An iteration is assigned",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "SCALAR", "kind": "SCALAR",
"name": "JSON", "name": "JSON",
...@@ -41116,6 +41159,16 @@ ...@@ -41116,6 +41159,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "iterationTitle",
"description": "Filter by iteration title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "weight", "name": "weight",
"description": "Filter by weight", "description": "Filter by weight",
...@@ -3994,6 +3994,15 @@ State of a GitLab iteration. ...@@ -3994,6 +3994,15 @@ State of a GitLab iteration.
| `started` | | | `started` | |
| `upcoming` | | | `upcoming` | |
### IterationWildcardId
Iteration ID wildcard values.
| Value | Description |
| ----- | ----------- |
| `ANY` | An iteration is assigned |
| `NONE` | No iteration is assigned |
### ListLimitMetric ### ListLimitMetric
List limit metric setting. List limit metric setting.
......
...@@ -6,6 +6,11 @@ export const EpicFilterType = { ...@@ -6,6 +6,11 @@ export const EpicFilterType = {
none: 'None', none: 'None',
}; };
export const IterationFilterType = {
any: 'Any',
none: 'None',
};
export const GroupByParamType = { export const GroupByParamType = {
epic: 'epic', epic: 'epic',
}; };
......
...@@ -7,7 +7,7 @@ import { historyPushState, parseBoolean } from '~/lib/utils/common_utils'; ...@@ -7,7 +7,7 @@ import { historyPushState, parseBoolean } from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import actionsCE from '~/boards/stores/actions'; import actionsCE from '~/boards/stores/actions';
import { BoardType, ListType } from '~/boards/constants'; import { BoardType, ListType } from '~/boards/constants';
import { EpicFilterType, GroupByParamType } from '../constants'; import { EpicFilterType, IterationFilterType, GroupByParamType } from '../constants';
import boardsStoreEE from './boards_store_ee'; import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as typesCE from '~/boards/stores/mutation_types'; import * as typesCE from '~/boards/stores/mutation_types';
...@@ -79,6 +79,7 @@ export default { ...@@ -79,6 +79,7 @@ export default {
'epicId', 'epicId',
'labelName', 'labelName',
'milestoneTitle', 'milestoneTitle',
'iterationTitle',
'releaseTag', 'releaseTag',
'search', 'search',
'weight', 'weight',
...@@ -94,6 +95,14 @@ export default { ...@@ -94,6 +95,14 @@ export default {
} else if (filterParams.epicId) { } else if (filterParams.epicId) {
filterParams.epicId = fullEpicId(filterParams.epicId); filterParams.epicId = fullEpicId(filterParams.epicId);
} }
if (
filters.iterationId === IterationFilterType.any ||
filters.iterationId === IterationFilterType.none
) {
filterParams.iterationWildcardId = filters.iterationId.toUpperCase();
}
commit(types.SET_FILTERS, filterParams); commit(types.SET_FILTERS, filterParams);
}, },
......
...@@ -8,6 +8,13 @@ module EE ...@@ -8,6 +8,13 @@ module EE
override :set_filter_values override :set_filter_values
def set_filter_values(filters) def set_filter_values(filters)
filter_by_epic(filters)
filter_by_iteration(filters)
end
private
def filter_by_epic(filters)
epic_id = filters.delete(:epic_id) epic_id = filters.delete(:epic_id)
epic_wildcard_id = filters.delete(:epic_wildcard_id) epic_wildcard_id = filters.delete(:epic_wildcard_id)
...@@ -21,6 +28,14 @@ module EE ...@@ -21,6 +28,14 @@ module EE
filters[:epic_id] = epic_wildcard_id filters[:epic_id] = epic_wildcard_id
end end
end end
def filter_by_iteration(filters)
iteration_wildcard_id = filters.delete(:iteration_wildcard_id)
if iteration_wildcard_id
filters[:iteration_id] = iteration_wildcard_id
end
end
end end
end end
end end
...@@ -11,6 +11,10 @@ module EE ...@@ -11,6 +11,10 @@ module EE
required: false, required: false,
description: 'Filter by epic ID. Incompatible with epicWildcardId' description: 'Filter by epic ID. Incompatible with epicWildcardId'
argument :iteration_title, GraphQL::STRING_TYPE,
required: false,
description: 'Filter by iteration title'
argument :weight, GraphQL::STRING_TYPE, argument :weight, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Filter by weight' description: 'Filter by weight'
......
...@@ -11,6 +11,10 @@ module EE ...@@ -11,6 +11,10 @@ module EE
argument :epic_wildcard_id, ::Types::Boards::EpicWildcardIdEnum, argument :epic_wildcard_id, ::Types::Boards::EpicWildcardIdEnum,
required: false, required: false,
description: 'Filter by epic ID wildcard. Incompatible with epicId' description: 'Filter by epic ID wildcard. Incompatible with epicId'
argument :iteration_wildcard_id, ::Types::Boards::IterationWildcardIdEnum,
required: false,
description: 'Filter by iteration ID wildcard'
end end
end end
end end
......
# frozen_string_literal: true
module Types
module Boards
class IterationWildcardIdEnum < BaseEnum
graphql_name 'IterationWildcardId'
description 'Iteration ID wildcard values'
value 'NONE', 'No iteration is assigned'
value 'ANY', 'An iteration is assigned'
end
end
end
---
title: Add filtering by iteration in GraphQL board list issues query
merge_request: 47263
author:
type: added
...@@ -104,18 +104,39 @@ RSpec.describe 'Filter issues by iteration', :js do ...@@ -104,18 +104,39 @@ RSpec.describe 'Filter issues by iteration', :js do
let(:issue_title_selector) { '.board-card .board-card-title' } let(:issue_title_selector) { '.board-card .board-card-title' }
it_behaves_like 'filters by iteration' it_behaves_like 'filters by iteration'
context 'when graphql_board_lists is disabled' do
before do
stub_feature_flags(graphql_board_lists: false)
end
it_behaves_like 'filters by iteration'
end
end end
context 'group board' do context 'group board' do
let_it_be(:board) { create(:board, group: group) } let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let(:page_path) { group_board_path(group, board) } let(:page_path) { group_board_path(group, board) }
let(:issue_title_selector) { '.board-card .board-card-title' } let(:issue_title_selector) { '.board-card .board-card-title' }
before_all do
group.add_developer(user)
end
before do before do
stub_feature_flags(graphql_board_lists: false) sign_in user
end end
it_behaves_like 'filters by iteration' it_behaves_like 'filters by iteration'
context 'when graphql_board_lists is disabled' do
before do
stub_feature_flags(graphql_board_lists: false)
end
it_behaves_like 'filters by iteration'
end
end end
end end
...@@ -74,6 +74,23 @@ describe('setFilters', () => { ...@@ -74,6 +74,23 @@ describe('setFilters', () => {
); );
}); });
it('should commit mutation SET_FILTERS, updates iterationWildcardId', () => {
const state = {
filters: {},
};
const filters = { labelName: 'label', iterationId: 'None' };
const updatedFilters = { labelName: 'label', iterationWildcardId: 'NONE' };
return testAction(
actions.setFilters,
filters,
state,
[{ type: types.SET_FILTERS, payload: updatedFilters }],
[],
);
});
it('should commit mutation SET_FILTERS, dispatches setEpicSwimlanes action if filters contain groupBy epic', () => { it('should commit mutation SET_FILTERS, dispatches setEpicSwimlanes action if filters contain groupBy epic', () => {
const state = { const state = {
filters: {}, filters: {},
......
...@@ -11,32 +11,56 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -11,32 +11,56 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let_it_be(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
let_it_be(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
let_it_be(:list) { create(:list, board: board, label: label) } let_it_be(:list) { create(:list, board: board, label: label) }
let_it_be(:issue) { create(:issue, project: project, labels: [label]) }
let_it_be(:epic) { create(:epic, group: group) } before_all do
let_it_be(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) } group.add_developer(user)
end
describe '#resolve' do describe '#resolve' do
before do context 'filtering by epic' do
stub_licensed_features(epics: true) let_it_be(:issue) { create(:issue, project: project, labels: [label]) }
group.add_developer(user) let_it_be(:epic) { create(:epic, group: group) }
end let_it_be(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) }
it 'raises an exception if both epic_id and epic_wildcard_id are present' do before do
expect do stub_licensed_features(epics: true)
resolve_board_list_issues({ filters: { epic_id: epic.to_global_id, epic_wildcard_id: 'NONE' } }) end
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end it 'raises an exception if both epic_id and epic_wildcard_id are present' do
expect do
resolve_board_list_issues({ filters: { epic_id: epic.to_global_id, epic_wildcard_id: 'NONE' } })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
it 'accepts epic global id' do it 'accepts epic global id' do
result = resolve_board_list_issues({ filters: { epic_id: epic.to_global_id } }).items result = resolve_board_list_issues({ filters: { epic_id: epic.to_global_id } }).items
expect(result).to match_array([issue]) expect(result).to match_array([issue])
end
it 'accepts epic wildcard id' do
result = resolve_board_list_issues({ filters: { epic_wildcard_id: 'NONE' } }).items
expect(result).to match_array([])
end
end end
it 'accepts epic wildcard id' do context 'filtering by iteration' do
result = resolve_board_list_issues({ filters: { epic_wildcard_id: 'NONE' } }).items let_it_be(:iteration) { create(:iteration, group: group) }
let_it_be(:issue_with_iteration) { create(:issue, project: project, labels: [label], iteration: iteration) }
let_it_be(:issue_without_iteration) { create(:issue, project: project, labels: [label]) }
it 'accepts iteration title' do
result = resolve_board_list_issues({ filters: { iteration_title: iteration.title } }).items
expect(result).to contain_exactly(issue_with_iteration)
end
it 'accepts iteration wildcard id' do
result = resolve_board_list_issues({ filters: { iteration_wildcard_id: 'NONE' } }).items
expect(result).to match_array([]) expect(result).to contain_exactly(issue_without_iteration)
end
end end
end end
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardIssueInput'] do RSpec.describe GitlabSchema.types['BoardIssueInput'] do
it 'has specific fields' do it 'has specific fields' do
allowed_args = %w(epicId epicWildcardId weight) allowed_args = %w(epicId epicWildcardId iterationTitle iterationWildcardId weight)
expect(described_class.arguments.keys).to include(*allowed_args) expect(described_class.arguments.keys).to include(*allowed_args)
end end
......
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