diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..a70d90ce253f1725153a2733ac8a93b88ddda4ba --- /dev/null +++ b/app/graphql/types/boards/board_issuable_input_base_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Boards + # Common arguments that we can be used to filter boards epics and issues + class BoardIssuableInputBaseType < BaseInputObject + argument :label_name, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Filter by label name.' + + argument :author_username, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by author username.' + + argument :my_reaction_emoji, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by reaction emoji applied by the current user.' + end + end +end diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb index b762cef6e5839c87aa735e2111e114b6de59379b..5bd2334c7221d25ff53d5361377ebf4d7113ce6b 100644 --- a/app/graphql/types/boards/board_issue_input_base_type.rb +++ b/app/graphql/types/boards/board_issue_input_base_type.rb @@ -2,11 +2,8 @@ module Types module Boards - class BoardIssueInputBaseType < BaseInputObject - argument :label_name, GraphQL::STRING_TYPE.to_list_type, - required: false, - description: 'Filter by label name.' - + # rubocop: disable Graphql/AuthorizeTypes + class BoardIssueInputBaseType < BoardIssuableInputBaseType argument :milestone_title, GraphQL::STRING_TYPE, required: false, description: 'Filter by milestone title.' @@ -15,17 +12,9 @@ class BoardIssueInputBaseType < BaseInputObject required: false, description: 'Filter by assignee username.' - argument :author_username, GraphQL::STRING_TYPE, - required: false, - description: 'Filter by author username.' - argument :release_tag, GraphQL::STRING_TYPE, required: false, description: 'Filter by release tag.' - - argument :my_reaction_emoji, GraphQL::STRING_TYPE, - required: false, - description: 'Filter by reaction emoji.' end end end diff --git a/ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb b/ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb index a4de0d96d59c39244391cdca34f2d3d5c3a7f65c..c62f8ac21bbc22fa5e25593c0321caf3b694e346 100644 --- a/ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb +++ b/ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb @@ -7,8 +7,13 @@ class BoardListEpicsResolver < BaseResolver alias_method :list, :object - def resolve(**args) - filter_params = { board_id: list.epic_board.id, id: list.id } + argument :filters, Types::Boards::BoardEpicInputType, + required: false, + description: 'Filters applied when selecting epics in the board list.' + + def resolve(filters: {}, **args) + filter_params = { board_id: list.epic_board.id, id: list.id }.merge(filters) + service = ::Boards::Epics::ListService.new(list.epic_board.group, context[:current_user], filter_params) offset_pagination(service.execute) diff --git a/ee/app/graphql/types/boards/board_epic_input_type.rb b/ee/app/graphql/types/boards/board_epic_input_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..f90865ff000b18e64a64b3607b6faa05bbf50ceb --- /dev/null +++ b/ee/app/graphql/types/boards/board_epic_input_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Boards + class BoardEpicInputType < BoardIssuableInputBaseType + graphql_name 'EpicFilters' + + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search query for epic title or description.' + end + end +end diff --git a/ee/changelogs/unreleased/issue_322686-allow_to_filter_epics_on_epic_lists.yml b/ee/changelogs/unreleased/issue_322686-allow_to_filter_epics_on_epic_lists.yml new file mode 100644 index 0000000000000000000000000000000000000000..197b9a48fc27e54837dbed50339a7cc265d6420b --- /dev/null +++ b/ee/changelogs/unreleased/issue_322686-allow_to_filter_epics_on_epic_lists.yml @@ -0,0 +1,5 @@ +--- +title: GraphQL - Allow to filter epics on epic lists +merge_request: 56026 +author: +type: added diff --git a/ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb b/ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb index 727f03aafb3c17158de9079cc53d20fd8b051303..f5e247a4f6e0c4a534019ba097aedc05007d3d7e 100644 --- a/ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb @@ -25,10 +25,12 @@ expect(described_class).to have_nullable_graphql_type(Types::EpicType.connection_type) end - describe '#resolve' do - let(:args) { {} } + def resolve_board_list_epics(args: {}) + resolve(described_class, ctx: { current_user: user }, obj: list1, args: args) + end - subject(:result) { resolve(described_class, ctx: { current_user: user }, obj: list1, args: args) } + describe '#resolve' do + subject(:result) { resolve_board_list_epics } before do stub_licensed_features(epics: true) @@ -38,5 +40,45 @@ it 'returns epics on the board list ordered by position on the board' do expect(result.to_a).to eq([list1_epic2, list1_epic1]) end + + context 'with filters' do + let_it_be(:production_label) { create(:group_label, group: group, name: 'production') } + let_it_be(:list1_epic3) { create(:labeled_epic, group: group, labels: [development, production_label], title: 'filter_this 1') } + let_it_be(:list1_epic4) { create(:labeled_epic, group: group, labels: [development], description: 'filter_this 2') } + + it 'filters epics by label' do + args = { filters: { label_name: [production_label.title] } } + + result = resolve_board_list_epics(args: args) + + expect(result).to contain_exactly(list1_epic3) + end + + it 'filters epics by author' do + args = { filters: { author_username: list1_epic4.author.username } } + + result = resolve_board_list_epics(args: args) + + expect(result).to contain_exactly(list1_epic4) + end + + it 'filters epics by reaction emoji' do + emoji_name = 'thumbsup' + create(:award_emoji, name: emoji_name, awardable: list1_epic1, user: user) + args = { filters: { my_reaction_emoji: emoji_name } } + + result = resolve_board_list_epics(args: args) + + expect(result).to contain_exactly(list1_epic1) + end + + it 'filters epics by title and description' do + args = { filters: { search: 'filter_this' } } + + result = resolve_board_list_epics(args: args) + + expect(result).to contain_exactly(list1_epic3, list1_epic4) + end + end end end diff --git a/ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb b/ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb index 28f4385fcf2e53864a4e4d2df4333e211c37422c..bd7d89da4ffd8a11abce28151a127c2847c1e46b 100644 --- a/ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb +++ b/ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb @@ -8,16 +8,18 @@ let_it_be(:current_user) { create(:user) } let_it_be(:group) { create(:group, :private) } let_it_be(:development) { create(:group_label, group: group, name: 'Development') } + let_it_be(:staging) { create(:group_label, group: group, name: 'Staging') } let_it_be(:board) { create(:epic_board, group: group) } let_it_be(:list) { create(:epic_list, epic_board: board, label: development) } let_it_be(:epic1) { create(:labeled_epic, group: group, labels: [development]) } let_it_be(:epic2) { create(:labeled_epic, group: group, labels: [development]) } - let_it_be(:epic3) { create(:labeled_epic, group: group, labels: [development]) } + let_it_be(:epic3) { create(:labeled_epic, group: group, labels: [development, staging], author: current_user) } let_it_be(:epic4) { create(:labeled_epic, group: group) } let_it_be(:epic_pos1) { create(:epic_board_position, epic: epic1, epic_board: board, relative_position: 20) } let_it_be(:epic_pos2) { create(:epic_board_position, epic: epic2, epic_board: board, relative_position: 10) } + let(:data_path) { [:group, :epicBoard, :lists, :nodes, 0, :epics] } def pagination_query(params = {}) graphql_query_for(:group, { full_path: group.full_path }, @@ -25,7 +27,7 @@ def pagination_query(params = {}) epicBoard(id: "#{board.to_global_id}") { lists(id: "#{list.to_global_id}") { nodes { - #{query_nodes(:epics, all_graphql_fields_for('epics'.classify), include_pagination_info: true, args: params)} + #{query_nodes(:epics, epic_fields, include_pagination_info: true, args: params)} } } } @@ -39,7 +41,7 @@ def pagination_query(params = {}) end describe 'sorting and pagination' do - let(:data_path) { [:group, :epicBoard, :lists, :nodes, 0, :epics] } + let(:epic_fields) { all_graphql_fields_for('epics'.classify) } let(:expected_results) { [epic2.to_global_id.to_s, epic1.to_global_id.to_s, epic3.to_global_id.to_s] } def pagination_results_data(nodes) @@ -53,4 +55,18 @@ def pagination_results_data(nodes) let(:first_param) { 2 } end end + + context 'filters' do + let(:epic_fields) { 'id' } + + it 'finds only epics matching the filter' do + filter_params = { filters: { author_username: current_user.username, label_name: [staging.title] } } + query = pagination_query(filter_params) + + post_graphql(query, current_user: current_user) + + boards = graphql_data_at(*data_path, :nodes, :id) + expect(boards).to contain_exactly(global_id_of(epic3)) + end + end end