From 799a20fe5e405c5c858ac831edef035fc3388514 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 27 Aug 2020 09:52:45 +1200 Subject: [PATCH] Add pendingTodo for some todoables in GraphQL This allows the client to query for any pending todos for the current_user on types that implement the interface. The interface has been added to DesignType, IssueType, MergeRequestType and EpicType. https://gitlab.com/gitlab-org/gitlab/-/issues/198439 --- app/graphql/types/current_user_todos.rb | 24 + .../types/design_management/design_type.rb | 1 + app/graphql/types/issue_type.rb | 1 + app/graphql/types/merge_request_type.rb | 1 + app/graphql/types/todo_type.rb | 2 +- .../198439-expose-pending-todo-in-graphql.yml | 5 + .../graphql/reference/gitlab_schema.graphql | 194 +++++++- doc/api/graphql/reference/gitlab_schema.json | 466 +++++++++++++++++- doc/api/graphql/reference/index.md | 2 +- ee/app/graphql/types/epic_type.rb | 1 + ee/spec/graphql/types/epic_type_spec.rb | 3 + .../types/current_user_todos_type_spec.rb | 9 + .../design_management/design_type_spec.rb | 4 +- spec/graphql/types/issue_type_spec.rb | 4 +- spec/graphql/types/merge_request_type_spec.rb | 4 +- .../api/graphql/current_user_todos_spec.rb | 81 +++ 16 files changed, 790 insertions(+), 12 deletions(-) create mode 100644 app/graphql/types/current_user_todos.rb create mode 100644 changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml create mode 100644 spec/graphql/types/current_user_todos_type_spec.rb create mode 100644 spec/requests/api/graphql/current_user_todos_spec.rb diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb new file mode 100644 index 00000000000000..e610286c1a9d9b --- /dev/null +++ b/app/graphql/types/current_user_todos.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Interface to expose todos for the current_user on the `object` +module Types + module CurrentUserTodos + include BaseInterface + + field_class Types::BaseField + + field :current_user_todos, Types::TodoType.connection_type, + description: 'Todos for the current user', + null: false do + argument :state, Types::TodoStateEnum, + description: 'State of the todos', + required: false + end + + def current_user_todos(state: nil) + state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending` + + TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute + end + end +end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index 3c84dc151bda16..4e11a7aaf094bd 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -12,6 +12,7 @@ class DesignType < BaseObject implements(Types::Notes::NoteableType) implements(Types::DesignManagement::DesignFields) + implements(Types::CurrentUserTodos) field :versions, Types::DesignManagement::VersionType.connection_type, diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 37af317996dca9..811323c6345beb 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -7,6 +7,7 @@ class IssueType < BaseObject connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_issue diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 01b02b7976f954..805ae111ff71be 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -7,6 +7,7 @@ class MergeRequestType < BaseObject connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_merge_request diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 08e7fabeb74ec8..4f21da3d8979f1 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -26,7 +26,7 @@ class TodoType < BaseObject resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find } field :author, Types::UserType, - description: 'The owner of this todo', + description: 'The author of this todo', null: false, resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find } diff --git a/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml new file mode 100644 index 00000000000000..221279e8b19b6c --- /dev/null +++ b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Expose the todos of the current user on relevant objects in GraphQL +merge_request: 40555 +author: +type: added diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 492bcfe952b404..04935a0526f202 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2939,6 +2939,38 @@ type CreateSnippetPayload { snippet: Snippet } +interface CurrentUserTodos { + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! +} + """ Autogenerated input type of DastOnDemandScanCreate """ @@ -3496,7 +3528,37 @@ type DeleteJobsResponse { """ A single design """ -type Design implements DesignFields & Noteable { +type Design implements CurrentUserTodos & DesignFields & Noteable { + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ The diff refs for this design """ @@ -4891,7 +4953,7 @@ type EnvironmentEdge { """ Represents an epic. """ -type Epic implements Noteable { +type Epic implements CurrentUserTodos & Noteable { """ Author of the epic """ @@ -4994,6 +5056,36 @@ type Epic implements Noteable { """ createdAt: Time + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Number of open and closed descendant epics and issues """ @@ -5433,7 +5525,7 @@ type EpicHealthStatus { """ Relationship between an epic and an issue """ -type EpicIssue implements Noteable { +type EpicIssue implements CurrentUserTodos & Noteable { """ Alert associated to this issue """ @@ -5489,6 +5581,36 @@ type EpicIssue implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Description of the issue """ @@ -7313,7 +7435,7 @@ enum IssuableState { opened } -type Issue implements Noteable { +type Issue implements CurrentUserTodos & Noteable { """ Alert associated to this issue """ @@ -7369,6 +7491,36 @@ type Issue implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Description of the issue """ @@ -8904,7 +9056,7 @@ type MemberInterfaceEdge { node: MemberInterface } -type MergeRequest implements Noteable { +type MergeRequest implements CurrentUserTodos & Noteable { """ Indicates if members of the target project can push to the fork """ @@ -8980,6 +9132,36 @@ type MergeRequest implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Default merge commit message of the merge request """ @@ -16113,7 +16295,7 @@ type Todo { action: TodoActionEnum! """ - The owner of this todo + The author of this todo """ author: User! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index ff30c1455e4f5f..035f9f65cef9c6 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -7967,6 +7967,110 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "description": null, + "fields": [ + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Design", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Epic", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "EpicIssue", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MergeRequest", + "ofType": null + } + ] + }, { "kind": "INPUT_OBJECT", "name": "DastOnDemandScanCreateInput", @@ -9542,6 +9646,73 @@ "name": "Design", "description": "A single design", "fields": [ + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "diffRefs", "description": "The diff refs for this design", @@ -9921,6 +10092,11 @@ "kind": "INTERFACE", "name": "DesignFields", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -13980,6 +14156,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "descendantCounts", "description": "Number of open and closed descendant epics and issues", @@ -14759,6 +15002,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -15357,6 +15605,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "description", "description": "Description of the issue", @@ -16137,6 +16452,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -20333,6 +20653,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "description", "description": "Description of the issue", @@ -21085,6 +21472,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -24962,6 +25354,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "defaultMergeCommitMessage", "description": "Default merge commit message of the merge request", @@ -26120,6 +26579,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -47417,7 +47881,7 @@ }, { "name": "author", - "description": "The owner of this todo", + "description": "The author of this todo", "args": [ ], diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2262e1b6a3eb7d..db1f204ccf1b87 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2362,7 +2362,7 @@ Representing a todo entry | Name | Type | Description | | --- | ---- | ---------- | | `action` | TodoActionEnum! | Action of the todo | -| `author` | User! | The owner of this todo | +| `author` | User! | The author of this todo | | `body` | String! | Body of the todo | | `createdAt` | Time! | Timestamp this todo was created | | `group` | Group | Group this todo is associated with | diff --git a/ee/app/graphql/types/epic_type.rb b/ee/app/graphql/types/epic_type.rb index 12b866f4b60779..998863ee747ac2 100644 --- a/ee/app/graphql/types/epic_type.rb +++ b/ee/app/graphql/types/epic_type.rb @@ -14,6 +14,7 @@ class EpicType < BaseObject present_using EpicPresenter implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the epic' diff --git a/ee/spec/graphql/types/epic_type_spec.rb b/ee/spec/graphql/types/epic_type_spec.rb index 22b5ef82ae0ec1..62318adbac518f 100644 --- a/ee/spec/graphql/types/epic_type_spec.rb +++ b/ee/spec/graphql/types/epic_type_spec.rb @@ -12,9 +12,12 @@ web_path web_url relation_path reference issues user_permissions notes discussions relative_position subscribed participants descendant_counts descendant_weight_sum upvotes downvotes health_status + current_user_todos ] end + it { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Epic) } it { expect(described_class.graphql_name).to eq('Epic') } diff --git a/spec/graphql/types/current_user_todos_type_spec.rb b/spec/graphql/types/current_user_todos_type_spec.rb new file mode 100644 index 00000000000000..a0015e967886bc --- /dev/null +++ b/spec/graphql/types/current_user_todos_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CurrentUserTodos'] do + specify { expect(described_class.graphql_name).to eq('CurrentUserTodos') } + + specify { expect(described_class).to have_graphql_fields(:current_user_todos).only } +end diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb index 7a38b397965c19..cae98a013e11ca 100644 --- a/spec/graphql/types/design_management/design_type_spec.rb +++ b/spec/graphql/types/design_management/design_type_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Design'] do + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it_behaves_like 'a GraphQL type with design fields' do - let(:extra_design_fields) { %i[notes discussions versions] } + let(:extra_design_fields) { %i[notes current_user_todos discussions versions] } let_it_be(:design) { create(:design, :with_versions) } let(:object_id) { GitlabSchema.id_from_object(design) } let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) } diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index db2a1751be07ba..c55e624dd1116e 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -11,11 +11,13 @@ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) } + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it 'has specific fields' do fields = %i[id iid title description state reference author assignees participants labels milestone due_date confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status - designs design_collection alert_management_alert severity] + designs design_collection alert_management_alert severity current_user_todos] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index a9a74114dda328..1279f01f104c73 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -9,6 +9,8 @@ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) } + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it 'has the expected fields' do expected_fields = %w[ notes discussions user_permissions id iid title title_html description @@ -24,7 +26,7 @@ source_branch_exists target_branch_exists upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate - total_time_spent reference author merged_at commit_count + total_time_spent reference author merged_at commit_count current_user_todos ] if Gitlab.ee? diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb new file mode 100644 index 00000000000000..b657f15d0e9849 --- /dev/null +++ b/spec/requests/api/graphql/current_user_todos_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:todoable) { create(:issue, project: project) } + let_it_be(:done_todo) { create(:todo, state: :done, target: todoable, user: current_user) } + let_it_be(:pending_todo) { create(:todo, state: :pending, target: todoable, user: current_user) } + let(:state) { 'null' } + + let(:todoable_response) do + graphql_data_at(:project, :issue, :currentUserTodos, :nodes) + end + + let(:query) do + <<~GQL + { + project(fullPath: "#{project.full_path}") { + issue(iid: "#{todoable.iid}") { + currentUserTodos(state: #{state}) { + nodes { + #{all_graphql_fields_for('Todo', max_depth: 1)} + } + } + } + } + } + GQL + end + + it 'returns todos of the current user' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)), + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + + it 'does not return todos of another user', :aggregate_failures do + post_graphql(query, current_user: create(:user)) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + it 'does not error when there is no logged in user', :aggregate_failures do + post_graphql(query) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + context 'when `state` argument is `pending`' do + let(:state) { 'pending' } + + it 'returns just the pending todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + end + + context 'when `state` argument is `done`' do + let(:state) { 'done' } + + it 'returns just the done todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)) + ) + end + end +end -- GitLab