Commit 0100d939 authored by Allison Browne's avatar Allison Browne Committed by Adam Hegyi

Support getting a todo for an alert in graphql api

parent dc6fa046
......@@ -10,6 +10,7 @@
# action_id: integer
# author_id: integer
# project_id; integer
# target_id; integer
# state: 'pending' (default) or 'done'
# type: 'Issue' or 'MergeRequest' or ['Issue', 'MergeRequest']
#
......@@ -23,7 +24,7 @@ class TodosFinder
NONE = '0'
TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design)).freeze
TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design AlertManagement::Alert)).freeze
attr_accessor :current_user, :params
......@@ -47,6 +48,7 @@ class TodosFinder
items = by_action(items)
items = by_author(items)
items = by_state(items)
items = by_target_id(items)
items = by_types(items)
items = by_group(items)
# Filtering by project HAS TO be the last because we use
......@@ -198,6 +200,12 @@ class TodosFinder
items.with_states(params[:state])
end
def by_target_id(items)
return items if params[:target_id].blank?
items.for_target(params[:target_id])
end
def by_types(items)
if types.any?
items.for_type(types)
......
......@@ -4,7 +4,7 @@ module Resolvers
class TodoResolver < BaseResolver
type Types::TodoType, null: true
alias_method :user, :object
alias_method :target, :object
argument :action, [Types::TodoActionEnum],
required: false,
......@@ -31,9 +31,10 @@ module Resolvers
description: 'The type of the todo'
def resolve(**args)
return Todo.none if user != context[:current_user]
return Todo.none unless current_user.present? && target.present?
return Todo.none if target.is_a?(User) && target != current_user
TodosFinder.new(user, todo_finder_params(args)).execute
TodosFinder.new(current_user, todo_finder_params(args)).execute
end
private
......@@ -46,6 +47,15 @@ module Resolvers
author_id: args[:author_id],
action_id: args[:action],
project_id: args[:project_id]
}.merge(target_params)
end
def target_params
return {} unless TodosFinder::TODO_TYPES.include?(target.class.name)
{
type: target.class.name,
target_id: target.id
}
end
end
......
......@@ -97,6 +97,12 @@ module Types
description: 'URL for metrics embed for the alert',
resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url }
field :todos,
Types::TodoType.connection_type,
null: true,
description: 'Todos of the current user for the alert',
resolver: Resolvers::TodoResolver
def notes
object.ordered_notes
end
......
......@@ -163,7 +163,8 @@ module TodosHelper
{ id: '', text: 'Any Type' },
{ id: 'Issue', text: 'Issue' },
{ id: 'MergeRequest', text: 'Merge Request' },
{ id: 'DesignManagement::Design', text: 'Design' }
{ id: 'DesignManagement::Design', text: 'Design' },
{ id: 'AlertManagement::Alert', text: 'Alert' }
]
end
......
---
title: Support getting a todo for an alert in GraphQL API
merge_request: 34789
author:
type: added
......@@ -319,6 +319,61 @@ type AlertManagementAlert implements Noteable {
"""
title: String
"""
Todos of the current user for the alert
"""
todos(
"""
The action to be filtered
"""
action: [TodoActionEnum!]
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
The ID of an author
"""
authorId: [ID!]
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
The ID of a group
"""
groupId: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The ID of a project
"""
projectId: [ID!]
"""
The state of the todo
"""
state: [TodoStateEnum!]
"""
The type of the todo
"""
type: [TodoTargetEnum!]
): TodoConnection
"""
Timestamp the alert was last updated
"""
......
......@@ -871,6 +871,167 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todos",
"description": "Todos of the current user for the alert",
"args": [
{
"name": "action",
"description": "The action to be filtered",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "TodoActionEnum",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "authorId",
"description": "The ID of an author",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "projectId",
"description": "The ID of a project",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "groupId",
"description": "The ID of a group",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "The state of the todo",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "TodoStateEnum",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "type",
"description": "The type of the todo",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "TodoTargetEnum",
"ofType": null
}
}
},
"defaultValue": null
},
{
"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
}
],
"type": {
"kind": "OBJECT",
"name": "TodoConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "Timestamp the alert was last updated",
......@@ -232,6 +232,26 @@ RSpec.describe TodosFinder do
expect(todos).to match_array([todo2, todo1])
end
end
context 'when filtering by target id' do
it 'returns the expected todos for the target' do
todos = finder.new(user, { target_id: issue.id }).execute
expect(todos).to match_array([todo1])
end
it 'returns the expected todos for multiple target ids' do
todos = finder.new(user, { target_id: [issue.id, merge_request.id] }).execute
expect(todos).to match_array([todo1, todo2])
end
it 'returns the expected todos for empty target id collection' do
todos = finder.new(user, { target_id: [] }).execute
expect(todos).to match_array([todo1, todo2])
end
end
end
context 'external authorization' do
......@@ -307,9 +327,9 @@ RSpec.describe TodosFinder do
it 'returns the expected types' do
expected_result =
if Gitlab.ee?
%w[Epic Issue MergeRequest DesignManagement::Design]
%w[Epic Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
else
%w[Issue MergeRequest DesignManagement::Design]
%w[Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
end
expect(described_class.todo_types).to contain_exactly(*expected_result)
......
......@@ -99,7 +99,7 @@ RSpec.describe Resolvers::TodoResolver do
end
end
context 'when no user is provided' do
context 'when no target is provided' do
it 'returns no todos' do
todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
......@@ -107,7 +107,7 @@ RSpec.describe Resolvers::TodoResolver do
end
end
context 'when provided user is not current user' do
context 'when target user is not the current user' do
it 'returns no todos' do
other_user = create(:user)
......@@ -116,6 +116,16 @@ RSpec.describe Resolvers::TodoResolver do
expect(todos).to be_empty
end
end
context 'when request is for a todo target' do
it 'returns only the todos for the target' do
target = issue_todo_pending.target
todos = resolve(described_class, obj: target, args: {}, ctx: { current_user: current_user })
expect(todos).to contain_exactly(issue_todo_pending)
end
end
end
def resolve_todos(args = {}, context = { current_user: current_user })
......
......@@ -28,6 +28,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
notes
discussions
metrics_dashboard_url
todos
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting Alert Management Alert Assignees' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
let_it_be(:other_alert) { create(:alert_management_alert, project: project) }
let_it_be(:todo) { create(:todo, :pending, target: alert, user: current_user, project: project) }
let_it_be(:other_todo) { create(:todo, :pending, target: other_alert, user: current_user, project: project) }
let(:fields) do
<<~QUERY
nodes {
iid
todos {
nodes {
id
}
}
}
QUERY
end
let(:graphql_query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementAlerts', {}, fields)
)
end
let(:gql_alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
let(:gql_todos) { gql_alerts.map { |gql_alert| [gql_alert['iid'], gql_alert['todos']['nodes']] }.to_h }
let(:gql_alert_todo) { gql_todos[alert.iid.to_s].first }
let(:gql_other_alert_todo) { gql_todos[other_alert.iid.to_s].first }
before do
project.add_developer(current_user)
end
it 'includes the correct metrics dashboard url' do
post_graphql(graphql_query, current_user: current_user)
expect(gql_alert_todo['id']).to eq(todo.to_global_id.to_s)
expect(gql_other_alert_todo['id']).to eq(other_todo.to_global_id.to_s)
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