Commit 799a20fe authored by Luke Duncalfe's avatar Luke Duncalfe

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
parent 69e1d6fe
# 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
......@@ -12,6 +12,7 @@ module Types
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
implements(Types::CurrentUserTodos)
field :versions,
Types::DesignManagement::VersionType.connection_type,
......
......@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
authorize :read_issue
......
......@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
authorize :read_merge_request
......
......@@ -26,7 +26,7 @@ module Types
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 }
......
---
title: Expose the todos of the current user on relevant objects in GraphQL
merge_request: 40555
author:
type: added
......@@ -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!
......
......@@ -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": [
],
......@@ -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 |
......
......@@ -14,6 +14,7 @@ module Types
present_using EpicPresenter
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the epic'
......
......@@ -12,9 +12,12 @@ RSpec.describe GitlabSchema.types['Epic'] do
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') }
......
# 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
......@@ -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)) }
......
......@@ -11,11 +11,13 @@ RSpec.describe GitlabSchema.types['Issue'] do
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)
......
......@@ -9,6 +9,8 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
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 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
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?
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment