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