From f30a777da6f797914968867f953c81714aecb9c5 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Wed, 29 Jun 2022 15:50:43 +0800 Subject: [PATCH] Add assignees to work item widgets in GraphQL Allows querying for a work item's assignees via GraphQL --- .../graphql_shared/possible_types.json | 1 + .../types/work_items/widget_interface.rb | 5 +- .../work_items/widgets/assignees_type.rb | 24 +++++++ app/models/work_items/type.rb | 4 +- app/models/work_items/widgets/assignees.rb | 10 +++ doc/api/graphql/reference/index.md | 14 ++++ .../types/work_items/widget_interface_spec.rb | 1 + .../work_items/widgets/assignees_type_spec.rb | 11 +++ spec/models/work_item_spec.rb | 3 +- spec/models/work_items/type_spec.rb | 3 +- .../work_items/widgets/assignees_spec.rb | 31 +++++++++ spec/requests/api/graphql/work_item_spec.rb | 68 +++++++++++++------ 12 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 app/graphql/types/work_items/widgets/assignees_type.rb create mode 100644 app/models/work_items/widgets/assignees.rb create mode 100644 spec/graphql/types/work_items/widgets/assignees_type_spec.rb create mode 100644 spec/models/work_items/widgets/assignees_spec.rb diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index 50b40526ee04d..1c0e27ac3ea8b 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -131,6 +131,7 @@ "VulnerabilityLocationSecretDetection" ], "WorkItemWidget": [ + "WorkItemWidgetAssignees", "WorkItemWidgetDescription", "WorkItemWidgetHierarchy" ] diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb index f3cf1d74829fb..6520de39b7e12 100644 --- a/app/graphql/types/work_items/widget_interface.rb +++ b/app/graphql/types/work_items/widget_interface.rb @@ -16,13 +16,16 @@ def self.resolve_type(object, context) ::Types::WorkItems::Widgets::DescriptionType when ::WorkItems::Widgets::Hierarchy ::Types::WorkItems::Widgets::HierarchyType + when ::WorkItems::Widgets::Assignees + ::Types::WorkItems::Widgets::AssigneesType else raise "Unknown GraphQL type for widget #{object}" end end orphan_types ::Types::WorkItems::Widgets::DescriptionType, - ::Types::WorkItems::Widgets::HierarchyType + ::Types::WorkItems::Widgets::HierarchyType, + ::Types::WorkItems::Widgets::AssigneesType end end end diff --git a/app/graphql/types/work_items/widgets/assignees_type.rb b/app/graphql/types/work_items/widgets/assignees_type.rb new file mode 100644 index 0000000000000..001ace77d6ef2 --- /dev/null +++ b/app/graphql/types/work_items/widgets/assignees_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class AssigneesType < BaseObject + graphql_name 'WorkItemWidgetAssignees' + description 'Represents an assignees widget' + + implements Types::WorkItems::WidgetInterface + + field :assignees, Types::UserType.connection_type, null: true, + description: 'Assignees of the work item.' + + field :allows_multiple_assignees, GraphQL::Types::Boolean, null: true, method: :allows_multiple_assignees?, + description: 'Indicates whether multiple assignees are allowed.' + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index bf251a3ade5c7..843e7a7fb32d2 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -21,11 +21,11 @@ class Type < ApplicationRecord }.freeze WIDGETS_FOR_TYPE = { - issue: [Widgets::Description, Widgets::Hierarchy], + issue: [Widgets::Description, Widgets::Hierarchy, Widgets::Assignees], incident: [Widgets::Description], test_case: [Widgets::Description], requirement: [Widgets::Description], - task: [Widgets::Description, Widgets::Hierarchy] + task: [Widgets::Description, Widgets::Hierarchy, Widgets::Assignees] }.freeze cache_markdown_field :description, pipeline: :single_line diff --git a/app/models/work_items/widgets/assignees.rb b/app/models/work_items/widgets/assignees.rb new file mode 100644 index 0000000000000..ecbbee1bcfb1b --- /dev/null +++ b/app/models/work_items/widgets/assignees.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + class Assignees < Base + delegate :assignees, to: :work_item + delegate :allows_multiple_assignees?, to: :work_item + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9de564449ef0a..8b7af37e40ff8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18366,6 +18366,18 @@ Check permissions for the current user on a work item. | <a id="workitemtypeid"></a>`id` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type. | | <a id="workitemtypename"></a>`name` | [`String!`](#string) | Name of the work item type. | +### `WorkItemWidgetAssignees` + +Represents an assignees widget. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetassigneesallowsmultipleassignees"></a>`allowsMultipleAssignees` | [`Boolean`](#boolean) | Indicates whether multiple assignees are allowed. | +| <a id="workitemwidgetassigneesassignees"></a>`assignees` | [`UserCoreConnection`](#usercoreconnection) | Assignees of the work item. (see [Connections](#connections)) | +| <a id="workitemwidgetassigneestype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. | + ### `WorkItemWidgetDescription` Represents a description widget. @@ -20234,6 +20246,7 @@ Type of a work item widget. | Value | Description | | ----- | ----------- | +| <a id="workitemwidgettypeassignees"></a>`ASSIGNEES` | Assignees widget. | | <a id="workitemwidgettypedescription"></a>`DESCRIPTION` | Description widget. | | <a id="workitemwidgettypehierarchy"></a>`HIERARCHY` | Hierarchy widget. | @@ -21454,6 +21467,7 @@ four standard [pagination arguments](#connection-pagination-arguments): Implementations: +- [`WorkItemWidgetAssignees`](#workitemwidgetassignees) - [`WorkItemWidgetDescription`](#workitemwidgetdescription) - [`WorkItemWidgetHierarchy`](#workitemwidgethierarchy) diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb index ee40bcc10caea..caf986c961fc4 100644 --- a/spec/graphql/types/work_items/widget_interface_spec.rb +++ b/spec/graphql/types/work_items/widget_interface_spec.rb @@ -17,6 +17,7 @@ where(:widget_class, :widget_type_name) do WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType + WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType end with_them do diff --git a/spec/graphql/types/work_items/widgets/assignees_type_spec.rb b/spec/graphql/types/work_items/widgets/assignees_type_spec.rb new file mode 100644 index 0000000000000..2db0667faea4e --- /dev/null +++ b/spec/graphql/types/work_items/widgets/assignees_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::Widgets::AssigneesType do + it 'exposes the expected fields' do + expected_fields = %i[assignees allows_multiple_assignees type] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 5e757c11f9988..d49c745242169 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -38,7 +38,8 @@ it 'returns instances of supported widgets' do is_expected.to match_array([instance_of(WorkItems::Widgets::Description), - instance_of(WorkItems::Widgets::Hierarchy)]) + instance_of(WorkItems::Widgets::Hierarchy), + instance_of(WorkItems::Widgets::Assignees)]) end end diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb index 81663d0eb419b..342c1eb5f50d2 100644 --- a/spec/models/work_items/type_spec.rb +++ b/spec/models/work_items/type_spec.rb @@ -65,7 +65,8 @@ it 'returns list of all possible widgets' do is_expected.to match_array([::WorkItems::Widgets::Description, - ::WorkItems::Widgets::Hierarchy]) + ::WorkItems::Widgets::Hierarchy, + ::WorkItems::Widgets::Assignees]) end end diff --git a/spec/models/work_items/widgets/assignees_spec.rb b/spec/models/work_items/widgets/assignees_spec.rb new file mode 100644 index 0000000000000..a2c93c07fde85 --- /dev/null +++ b/spec/models/work_items/widgets/assignees_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::Assignees do + let_it_be(:work_item) { create(:work_item, assignees: [create(:user)]) } + + describe '.type' do + subject { described_class.type } + + it { is_expected.to eq(:assignees) } + end + + describe '#type' do + subject { described_class.new(work_item).type } + + it { is_expected.to eq(:assignees) } + end + + describe '#assignees' do + subject { described_class.new(work_item).assignees } + + it { is_expected.to eq(work_item.assignees) } + end + + describe '#allows_multiple_assignees?' do + subject { described_class.new(work_item).allows_multiple_assignees? } + + it { is_expected.to eq(work_item.allows_multiple_assignees?) } + end +end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 09bda8ee0d5bb..a7edfc6ee2da8 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -64,16 +64,13 @@ it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ + 'widgets' => include( hash_including( 'type' => 'DESCRIPTION', 'description' => work_item.description, 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) - ), - hash_including( - 'type' => 'HIERARCHY' ) - ]) + ) ) end end @@ -101,10 +98,7 @@ it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -113,7 +107,7 @@ hash_including('id' => child_link2.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end @@ -137,10 +131,7 @@ it 'filters out not accessible children or parent' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -148,7 +139,7 @@ hash_including('id' => child_link1.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end end @@ -160,20 +151,57 @@ it 'returns parent information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s), 'children' => { 'nodes' => match_array([]) } ) - ]) + ) ) end end end + + describe 'assignees widget' do + let(:assignees) { create_list(:user, 2) } + let(:work_item) { create(:work_item, project: project, assignees: assignees) } + + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetAssignees { + allowsMultipleAssignees + assignees { + nodes { + id + username + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'ASSIGNEES', + 'allowsMultipleAssignees' => boolean, + 'assignees' => { + 'nodes' => match_array( + assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } + ) + } + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do -- GitLab