Commit 81e42621 authored by charlie ablett's avatar charlie ablett

Merge branch 'negated_params2' into 'master'

Allows passing negative issue filters to epic swimlanes GraphQL query

See merge request gitlab-org/gitlab!39089
parents b3b2ceee 8014a2a9
...@@ -1135,42 +1135,47 @@ type BoardEdge { ...@@ -1135,42 +1135,47 @@ type BoardEdge {
input BoardEpicIssueInput { input BoardEpicIssueInput {
""" """
Username of a user assigned to issues Filter by assignee username
""" """
assigneeUsername: [String] assigneeUsername: [String]
""" """
Username of the issues author Filter by author username
""" """
authorUsername: String authorUsername: String
""" """
Epic ID applied to issues Filter by epic ID
""" """
epicId: String epicId: String
""" """
Label applied to issues Filter by label name
""" """
labelName: [String] labelName: [String]
""" """
Milestone applied to issues Filter by milestone title
""" """
milestoneTitle: String milestoneTitle: String
""" """
Reaction emoji applied to issues Filter by reaction emoji
""" """
myReactionEmoji: String myReactionEmoji: String
""" """
Release applied to issues List of negated params. Warning: this argument is experimental and a subject to change in future
"""
not: NegatedBoardEpicIssueInput
"""
Filter by release tag
""" """
releaseTag: String releaseTag: String
""" """
Weight applied to issues Filter by weight
""" """
weight: String weight: String
} }
...@@ -9795,6 +9800,48 @@ type NamespaceIncreaseStorageTemporarilyPayload { ...@@ -9795,6 +9800,48 @@ type NamespaceIncreaseStorageTemporarilyPayload {
namespace: Namespace namespace: Namespace
} }
input NegatedBoardEpicIssueInput {
"""
Filter by assignee username
"""
assigneeUsername: [String]
"""
Filter by author username
"""
authorUsername: String
"""
Filter by epic ID
"""
epicId: String
"""
Filter by label name
"""
labelName: [String]
"""
Filter by milestone title
"""
milestoneTitle: String
"""
Filter by reaction emoji
"""
myReactionEmoji: String
"""
Filter by release tag
"""
releaseTag: String
"""
Filter by weight
"""
weight: String
}
type Note implements ResolvableInterface { type Note implements ResolvableInterface {
""" """
User who wrote this note User who wrote this note
......
...@@ -3049,7 +3049,7 @@ ...@@ -3049,7 +3049,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "labelName", "name": "labelName",
"description": "Label applied to issues", "description": "Filter by label name",
"type": { "type": {
"kind": "LIST", "kind": "LIST",
"name": null, "name": null,
...@@ -3063,7 +3063,7 @@ ...@@ -3063,7 +3063,7 @@
}, },
{ {
"name": "milestoneTitle", "name": "milestoneTitle",
"description": "Milestone applied to issues", "description": "Filter by milestone title",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -3073,7 +3073,7 @@ ...@@ -3073,7 +3073,7 @@
}, },
{ {
"name": "assigneeUsername", "name": "assigneeUsername",
"description": "Username of a user assigned to issues", "description": "Filter by assignee username",
"type": { "type": {
"kind": "LIST", "kind": "LIST",
"name": null, "name": null,
...@@ -3087,7 +3087,7 @@ ...@@ -3087,7 +3087,7 @@
}, },
{ {
"name": "authorUsername", "name": "authorUsername",
"description": "Username of the issues author", "description": "Filter by author username",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -3097,7 +3097,7 @@ ...@@ -3097,7 +3097,7 @@
}, },
{ {
"name": "releaseTag", "name": "releaseTag",
"description": "Release applied to issues", "description": "Filter by release tag",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -3107,7 +3107,7 @@ ...@@ -3107,7 +3107,7 @@
}, },
{ {
"name": "epicId", "name": "epicId",
"description": "Epic ID applied to issues", "description": "Filter by epic ID",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -3117,7 +3117,7 @@ ...@@ -3117,7 +3117,7 @@
}, },
{ {
"name": "myReactionEmoji", "name": "myReactionEmoji",
"description": "Reaction emoji applied to issues", "description": "Filter by reaction emoji",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -3127,13 +3127,23 @@ ...@@ -3127,13 +3127,23 @@
}, },
{ {
"name": "weight", "name": "weight",
"description": "Weight applied to issues", "description": "Filter by weight",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
},
{
"name": "not",
"description": "List of negated params. Warning: this argument is experimental and a subject to change in future",
"type": {
"kind": "INPUT_OBJECT",
"name": "NegatedBoardEpicIssueInput",
"ofType": null
},
"defaultValue": null
} }
], ],
"interfaces": null, "interfaces": null,
...@@ -29245,6 +29255,105 @@ ...@@ -29245,6 +29255,105 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "NegatedBoardEpicIssueInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "labelName",
"description": "Filter by label name",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Filter by milestone title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Filter by assignee username",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter by author username",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "releaseTag",
"description": "Filter by release tag",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "epicId",
"description": "Filter by epic ID",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "myReactionEmoji",
"description": "Filter by reaction emoji",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "Filter by weight",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Note", "name": "Note",
...@@ -16,13 +16,15 @@ module Resolvers ...@@ -16,13 +16,15 @@ module Resolvers
return Epic.none unless group.present? return Epic.none unless group.present?
return unless ::Feature.enabled?(:boards_with_swimlanes, group) return unless ::Feature.enabled?(:boards_with_swimlanes, group)
Epic.for_ids(board_epic_ids(args[:issue_filters])) Epic.for_ids(board_epic_ids(args[:issue_filters].to_h))
end end
private private
def board_epic_ids(issue_params) def board_epic_ids(issue_params)
params = issue_params.to_h.merge(all_lists: true, board_id: board.id) params = issue_params.to_h.merge(all_lists: true, board_id: board.id)
params[:not] = params[:not].to_h if params[:not].present?
list_service = Boards::Issues::ListService.new( list_service = Boards::Issues::ListService.new(
board.resource_parent, board.resource_parent,
current_user, current_user,
......
...@@ -2,40 +2,49 @@ ...@@ -2,40 +2,49 @@
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
class BoardEpicIssueInputType < BaseInputObject class BoardEpicIssueInputBaseType < BaseInputObject
graphql_name 'BoardEpicIssueInput'
argument :label_name, GraphQL::STRING_TYPE.to_list_type, argument :label_name, GraphQL::STRING_TYPE.to_list_type,
required: false, required: false,
description: 'Label applied to issues' description: 'Filter by label name'
argument :milestone_title, GraphQL::STRING_TYPE, argument :milestone_title, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Milestone applied to issues' description: 'Filter by milestone title'
argument :assignee_username, GraphQL::STRING_TYPE.to_list_type, argument :assignee_username, GraphQL::STRING_TYPE.to_list_type,
required: false, required: false,
description: 'Username of a user assigned to issues' description: 'Filter by assignee username'
argument :author_username, GraphQL::STRING_TYPE, argument :author_username, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Username of the issues author' description: 'Filter by author username'
argument :release_tag, GraphQL::STRING_TYPE, argument :release_tag, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Release applied to issues' description: 'Filter by release tag'
argument :epic_id, GraphQL::STRING_TYPE, argument :epic_id, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Epic ID applied to issues' description: 'Filter by epic ID'
argument :my_reaction_emoji, GraphQL::STRING_TYPE, argument :my_reaction_emoji, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Reaction emoji applied to issues' description: 'Filter by reaction emoji'
argument :weight, GraphQL::STRING_TYPE, argument :weight, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Weight applied to issues' description: 'Filter by weight'
end
class NegatedBoardEpicIssueInputType < BoardEpicIssueInputBaseType
end
class BoardEpicIssueInputType < BoardEpicIssueInputBaseType
graphql_name 'BoardEpicIssueInput'
argument :not, Types::NegatedBoardEpicIssueInputType,
required: false,
description: 'List of negated params. Warning: this argument is experimental and a subject to change in future'
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
end end
---
title: Allow using of negated filters in board epic issues GraphQL query
merge_request: 39089
author:
type: added
...@@ -13,11 +13,12 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do ...@@ -13,11 +13,12 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do
let_it_be(:board) { create(:board, project: project) } let_it_be(:board) { create(:board, project: project) }
let_it_be(:group_board) { create(:board, group: group) } let_it_be(:group_board) { create(:board, group: group) }
let_it_be(:label) { create(:label, project: project, name: 'foo') } let_it_be(:label1) { create(:label, project: project, name: 'foo') }
let_it_be(:list) { create(:list, board: board, label: label) } let_it_be(:label2) { create(:label, project: project, name: 'bar') }
let_it_be(:list) { create(:list, board: board, label: label1) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label]) } let_it_be(:issue1) { create(:issue, project: project, labels: [label1]) }
let_it_be(:issue2) { create(:issue, project: project) } let_it_be(:issue2) { create(:issue, project: project, labels: [label1, label2]) }
let_it_be(:issue3) { create(:issue, project: other_project) } let_it_be(:issue3) { create(:issue, project: other_project) }
let_it_be(:epic1) { create(:epic, group: parent_group) } let_it_be(:epic1) { create(:epic, group: parent_group) }
...@@ -71,10 +72,21 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do ...@@ -71,10 +72,21 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do
end end
it 'finds only epics for issues matching issue filters' do it 'finds only epics for issues matching issue filters' do
result = resolve_board_epics(group_board, { issue_filters: { label_name: label.title } }) result = resolve_board_epics(
group_board, { issue_filters: { label_name: label1.title, not: { label_name: label2.title } } })
expect(result).to match_array([epic1]) expect(result).to match_array([epic1])
end end
it 'accepts negated issue params' do
expect(Boards::Issues::ListService).to receive(:new).with(
group_board.resource_parent,
current_user,
{ all_lists: true, board_id: group_board.id, label_name: 'foo', not: { label_name: %w(foo bar) } }
).and_call_original
resolve_board_epics(group_board, { issue_filters: { label_name: 'foo', not: { label_name: %w(foo bar) } } })
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardEpicIssueInput'] do
it { expect(described_class.graphql_name).to eq('BoardEpicIssueInput') }
it 'exposes negated issue arguments' do
allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername
releaseTag epicId myReactionEmoji weight not)
expect(described_class.arguments.keys).to match_array(allowed_args)
expect(described_class.arguments['not'].type).to eq(Types::NegatedBoardEpicIssueInputType)
end
end
...@@ -21,7 +21,7 @@ RSpec.describe 'get list of boards' do ...@@ -21,7 +21,7 @@ RSpec.describe 'get list of boards' do
def board_epic_query(board) def board_epic_query(board)
epic_query = <<~EPIC epic_query = <<~EPIC
epics(issueFilters: {labelName: "#{label.title}"}) { epics(issueFilters: {labelName: "#{label.title}", not: {authorUsername: "#{current_user.username}"}}) {
nodes { nodes {
id id
title title
...@@ -46,6 +46,7 @@ RSpec.describe 'get list of boards' do ...@@ -46,6 +46,7 @@ RSpec.describe 'get list of boards' do
issue2 = create(:issue, project: issue_project, labels: [label]) issue2 = create(:issue, project: issue_project, labels: [label])
issue3 = create(:issue, project: issue_project) issue3 = create(:issue, project: issue_project)
issue4 = create(:issue, project: issue_project, labels: [label]) issue4 = create(:issue, project: issue_project, labels: [label])
issue5 = create(:issue, project: issue_project, labels: [label], author: current_user)
epic1 = create(:epic, group: parent_group) epic1 = create(:epic, group: parent_group)
epic2 = create(:epic, group: parent_group) epic2 = create(:epic, group: parent_group)
epic3 = create(:epic, :closed, group: parent_group) epic3 = create(:epic, :closed, group: parent_group)
...@@ -53,6 +54,7 @@ RSpec.describe 'get list of boards' do ...@@ -53,6 +54,7 @@ RSpec.describe 'get list of boards' do
create(:epic_issue, issue: issue2, epic: epic1) create(:epic_issue, issue: issue2, epic: epic1)
create(:epic_issue, issue: issue3, epic: epic2) create(:epic_issue, issue: issue3, epic: epic2)
create(:epic_issue, issue: issue4, epic: epic3) create(:epic_issue, issue: issue4, epic: epic3)
create(:epic_issue, issue: issue5, epic: epic2)
post_graphql(board_epic_query(board), current_user: current_user) post_graphql(board_epic_query(board), current_user: current_user)
......
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