diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index 56fa53355fd77f4f9c4832c6f568accecb976452..c19ece80cdff1a3c95c14503c111bc0016ff49a0 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -60,7 +60,19 @@ class WorkItemType < BaseObject field :widgets, [Types::WorkItems::WidgetInterface], null: true, - description: 'Collection of widgets that belong to the work item.' + description: 'Collection of widgets that belong to the work item.' do + argument :except_types, [::Types::WorkItems::WidgetTypeEnum], + required: false, + default_value: nil, + description: 'Except widgets of the given types.' + argument :only_types, [::Types::WorkItems::WidgetTypeEnum], + required: false, + default_value: nil, + description: 'Only widgets of the given types.' + + validates mutually_exclusive: %i[except_types only_types] + end + field :work_item_type, Types::WorkItems::TypeType, null: false, description: 'Type assigned to the work item.' diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 4b1d3bc75d8775162f219739c164b4778118f991..4a5d6d56e4a400ebc30b858abd70aa1c30027342 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -146,10 +146,17 @@ def todoable_target_type_name %w[Issue WorkItem] end - def widgets - strong_memoize(:widgets) do - work_item_type.widgets(resource_parent).map do |widget_definition| - widget_definition.widget_class.new(self, widget_definition: widget_definition) + def widgets(except_types: [], only_types: nil) + raise ArgumentError, 'Only one filter is allowed' if only_types.present? && except_types.present? + + strong_memoize_with(:widgets, only_types, except_types) do + except_types = Array.wrap(except_types) + + widget_definitions.keys.filter_map do |type| + next if except_types.include?(type) + next if only_types&.exclude?(type) + + get_widget(type) end end end @@ -157,13 +164,21 @@ def widgets # Returns widget object if available # type parameter can be a symbol, for example, `:description`. def get_widget(type) - widgets.find do |widget| - widget.instance_of?(WorkItems::Widgets.const_get(type.to_s.camelize, false)) + strong_memoize_with(type) do + break unless widget_definitions.key?(type.to_sym) + + widget_definitions[type].build_widget(self) end - rescue NameError - nil end + def widget_definitions + work_item_type + .widgets(resource_parent) + .index_by(&:widget_type) + .symbolize_keys + end + strong_memoize_attr :widget_definitions + def ancestors hierarchy.ancestors(hierarchy_order: :asc) end diff --git a/app/models/work_items/widget_definition.rb b/app/models/work_items/widget_definition.rb index fa2c602a3c7a6be713461b64ca95eca90024e049..a48afd23ce13deab9867ea95d1c21f65e87451e4 100644 --- a/app/models/work_items/widget_definition.rb +++ b/app/models/work_items/widget_definition.rb @@ -69,5 +69,9 @@ def widget_class rescue NameError nil end + + def build_widget(work_item) + widget_class.new(work_item, widget_definition: self) + end end end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 440387b58293bc424a08d717467da087635e5003..d6b0931382b711b47e714015535908a30d19337b 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -39458,7 +39458,6 @@ four standard [pagination arguments](#pagination-arguments): | <a id="workitemuserdiscussionscount"></a>`userDiscussionsCount` | [`Int!`](#int) | Number of user discussions in the work item. | | <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. | | <a id="workitemweburl"></a>`webUrl` | [`String`](#string) | URL of this object. | -| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. | | <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. | #### Fields with arguments @@ -39475,6 +39474,19 @@ Returns [`String!`](#string). | ---- | ---- | ----------- | | <a id="workitemreferencefull"></a>`full` | [`Boolean`](#boolean) | Boolean option specifying whether the reference should be returned in full. | +##### `WorkItem.widgets` + +Collection of widgets that belong to the work item. + +Returns [`[WorkItemWidget!]`](#workitemwidget). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetsexcepttypes"></a>`exceptTypes` | [`[WorkItemWidgetType!]`](#workitemwidgettype) | Except widgets of the given types. | +| <a id="workitemwidgetsonlytypes"></a>`onlyTypes` | [`[WorkItemWidgetType!]`](#workitemwidgettype) | Only widgets of the given types. | + ### `WorkItemClosingMergeRequest` #### Fields diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 70b11f5c07cca219df141af26fc0c1237e65c33a..049aea5b5aa1a218f254bb404d96f54375a24453 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -187,16 +187,79 @@ end describe '#widgets' do - subject { build(:work_item).widgets } + subject(:work_item) { build(:work_item) } it 'returns instances of supported widgets' do - is_expected.to include( + expect(work_item.widgets).to match_array([ + instance_of(WorkItems::Widgets::Assignees), + instance_of(WorkItems::Widgets::AwardEmoji), + instance_of(WorkItems::Widgets::CrmContacts), + instance_of(WorkItems::Widgets::CurrentUserTodos), instance_of(WorkItems::Widgets::Description), + instance_of(WorkItems::Widgets::Designs), + instance_of(WorkItems::Widgets::Development), + instance_of(WorkItems::Widgets::EmailParticipants), + instance_of(WorkItems::Widgets::ErrorTracking), instance_of(WorkItems::Widgets::Hierarchy), instance_of(WorkItems::Widgets::Labels), - instance_of(WorkItems::Widgets::Assignees), - instance_of(WorkItems::Widgets::StartAndDueDate) - ) + instance_of(WorkItems::Widgets::LinkedItems), + instance_of(WorkItems::Widgets::Milestone), + instance_of(WorkItems::Widgets::Notes), + instance_of(WorkItems::Widgets::Notifications), + instance_of(WorkItems::Widgets::Participants), + instance_of(WorkItems::Widgets::StartAndDueDate), + instance_of(WorkItems::Widgets::TimeTracking) + ]) + end + + context 'when filters are given' do + context 'when both only_types and except_types are given' do + it 'raises an error' do + expect { work_item.widgets(only_types: [true], except_types: [true]) } + .to raise_error(ArgumentError, 'Only one filter is allowed') + end + end + + context 'when filtering by only_types' do + it 'only returns widgets on the given list' do + expect(work_item.widgets(only_types: [:milestone, :description])).to match_array([ + instance_of(WorkItems::Widgets::Milestone), + instance_of(WorkItems::Widgets::Description) + ]) + end + end + + context 'when passing explicitly nil to except_types' do + it 'only returns widgets on the given list' do + expect(work_item.widgets(only_types: [:milestone, :description], except_types: nil)).to match_array([ + instance_of(WorkItems::Widgets::Milestone), + instance_of(WorkItems::Widgets::Description) + ]) + end + end + + context 'when filtering by except_types' do + it 'only returns widgets on the given list' do + expect(work_item.widgets(except_types: [:milestone, :description])).to match_array([ + instance_of(WorkItems::Widgets::Assignees), + instance_of(WorkItems::Widgets::AwardEmoji), + instance_of(WorkItems::Widgets::CrmContacts), + instance_of(WorkItems::Widgets::CurrentUserTodos), + instance_of(WorkItems::Widgets::Designs), + instance_of(WorkItems::Widgets::Development), + instance_of(WorkItems::Widgets::EmailParticipants), + instance_of(WorkItems::Widgets::ErrorTracking), + instance_of(WorkItems::Widgets::Hierarchy), + instance_of(WorkItems::Widgets::Labels), + instance_of(WorkItems::Widgets::LinkedItems), + instance_of(WorkItems::Widgets::Notes), + instance_of(WorkItems::Widgets::Notifications), + instance_of(WorkItems::Widgets::Participants), + instance_of(WorkItems::Widgets::StartAndDueDate), + instance_of(WorkItems::Widgets::TimeTracking) + ]) + end + end end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 1600da2d2afb81e865680472d85086013dac2ead..473fe4d070716c2a689d4015f498d2c7f5209863 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -791,6 +791,64 @@ def pagination_results_data(nodes) ) end end + + context 'when filtering' do + context 'when selecting widgets' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets(onlyTypes: [DESCRIPTION]) { + type + } + GRAPHQL + end + + it 'only returns selected widgets' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => [{ + 'type' => 'DESCRIPTION' + }] + ) + end + end + + context 'when excluding widgets' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets(exceptTypes: [DESCRIPTION]) { + type + } + GRAPHQL + end + + it 'does not return excluded widgets' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => [ + { "type" => "ASSIGNEES" }, + { "type" => "AWARD_EMOJI" }, + { "type" => "CRM_CONTACTS" }, + { "type" => "CURRENT_USER_TODOS" }, + { "type" => "DESIGNS" }, + { "type" => "DEVELOPMENT" }, + { "type" => "EMAIL_PARTICIPANTS" }, + { "type" => "ERROR_TRACKING" }, + { "type" => "HIERARCHY" }, + { "type" => "LABELS" }, + { "type" => "LINKED_ITEMS" }, + { "type" => "MILESTONE" }, + { "type" => "NOTES" }, + { "type" => "NOTIFICATIONS" }, + { "type" => "PARTICIPANTS" }, + { "type" => "START_AND_DUE_DATE" }, + { "type" => "TIME_TRACKING" } + ] + ) + end + end + end end describe 'notes widget' do