Commit 2c908cc1 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'swimlanes_collapse_api' into 'master'

Expose board epic user preferences

See merge request gitlab-org/gitlab!42569
parents 4b9426ed 4beef405
...@@ -14,7 +14,7 @@ module Resolvers ...@@ -14,7 +14,7 @@ module Resolvers
def resolve(**args) def resolve(**args)
filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id) filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute) Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute)
end end
......
...@@ -27,7 +27,7 @@ module Resolvers ...@@ -27,7 +27,7 @@ module Resolvers
private private
def board_lists(id) def board_lists(id)
service = Boards::Lists::ListService.new( service = ::Boards::Lists::ListService.new(
board.resource_parent, board.resource_parent,
context[:current_user], context[:current_user],
list_id: extract_list_id(id) list_id: extract_list_id(id)
......
...@@ -16,7 +16,7 @@ module Resolvers ...@@ -16,7 +16,7 @@ module Resolvers
return Board.none unless parent return Board.none unless parent
Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false) ::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
Board.none Board.none
end end
......
...@@ -1055,7 +1055,7 @@ type Board { ...@@ -1055,7 +1055,7 @@ type Board {
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
""" """
last: Int last: Int
): EpicConnection ): BoardEpicConnection
""" """
Whether or not backlog list is hidden. Whether or not backlog list is hidden.
...@@ -1153,6 +1153,478 @@ type BoardEdge { ...@@ -1153,6 +1153,478 @@ type BoardEdge {
node: Board node: Board
} }
"""
Represents an epic on an issue board
"""
type BoardEpic implements CurrentUserTodos & Noteable {
"""
Author of the epic
"""
author: User!
"""
Children (sub-epics) of the epic
"""
children(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Filter epics by author
"""
authorUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
List items within a time frame where items.end_date is between startDate and
endDate parameters (startDate parameter must be present)
"""
endDate: Time
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
IID of the epic, e.g., "1"
"""
iid: ID
"""
Filter epics by iid for autocomplete
"""
iidStartsWith: String
"""
List of IIDs of epics, e.g., [1, 2]
"""
iids: [ID!]
"""
Filter epics by labels
"""
labelName: [String!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Filter epics by milestone title, computed from epic's issues
"""
milestoneTitle: String
"""
Search query for epic title or description
"""
search: String
"""
List epics by sort order
"""
sort: EpicSort
"""
List items within a time frame where items.start_date is between startDate
and endDate parameters (endDate parameter must be present)
"""
startDate: Time
"""
Filter epics by state
"""
state: EpicState
): EpicConnection
"""
Timestamp of when the epic was closed
"""
closedAt: Time
"""
Indicates if the epic is confidential
"""
confidential: Boolean
"""
Timestamp of when the epic was created
"""
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
"""
descendantCounts: EpicDescendantCount
"""
Total weight of open and closed issues in the epic and its descendants
"""
descendantWeightSum: EpicDescendantWeights
"""
Description of the epic
"""
description: String
"""
All discussions on this noteable
"""
discussions(
"""
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
): DiscussionConnection!
"""
Number of downvotes the epic has received
"""
downvotes: Int!
"""
Due date of the epic
"""
dueDate: Time
"""
Fixed due date of the epic
"""
dueDateFixed: Time
"""
Inherited due date of the epic from milestones
"""
dueDateFromMilestones: Time
"""
Indicates if the due date has been manually set
"""
dueDateIsFixed: Boolean
"""
Group to which the epic belongs
"""
group: Group!
"""
Indicates if the epic has children
"""
hasChildren: Boolean!
"""
Indicates if the epic has direct issues
"""
hasIssues: Boolean!
"""
Indicates if the epic has a parent epic
"""
hasParent: Boolean!
"""
Current health status of the epic
"""
healthStatus: EpicHealthStatus
"""
ID of the epic
"""
id: ID!
"""
Internal ID of the epic
"""
iid: ID!
"""
A list of issues associated with the epic
"""
issues(
"""
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
): EpicIssueConnection
"""
Labels assigned to the epic
"""
labels(
"""
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
): LabelConnection
"""
All notes on this noteable
"""
notes(
"""
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
): NoteConnection!
"""
Parent epic of the epic
"""
parent: Epic
"""
List of participants for the epic
"""
participants(
"""
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
): UserConnection
"""
Internal reference of the epic. Returned in shortened format by default
"""
reference(
"""
Indicates if the reference should be returned in full
"""
full: Boolean = false
): String!
"""
URI path of the epic-issue relationship
"""
relationPath: String
"""
The relative position of the epic in the epic tree
"""
relativePosition: Int
"""
Start date of the epic
"""
startDate: Time
"""
Fixed start date of the epic
"""
startDateFixed: Time
"""
Inherited start date of the epic from milestones
"""
startDateFromMilestones: Time
"""
Indicates if the start date has been manually set
"""
startDateIsFixed: Boolean
"""
State of the epic
"""
state: EpicState!
"""
Indicates the currently logged in user is subscribed to the epic
"""
subscribed: Boolean!
"""
Title of the epic
"""
title: String
"""
Timestamp of when the epic was updated
"""
updatedAt: Time
"""
Number of upvotes the epic has received
"""
upvotes: Int!
"""
Permissions for the current user on the resource
"""
userPermissions: EpicPermissions!
"""
User preferences for the epic on the issue board
"""
userPreferences: BoardEpicUserPreferences
"""
Web path of the epic
"""
webPath: String!
"""
Web URL of the epic
"""
webUrl: String!
}
"""
The connection type for BoardEpic.
"""
type BoardEpicConnection {
"""
A list of edges.
"""
edges: [BoardEpicEdge]
"""
A list of nodes.
"""
nodes: [BoardEpic]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type BoardEpicEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: BoardEpic
}
"""
Represents user preferences for a board epic
"""
type BoardEpicUserPreferences {
"""
Indicates epic should be displayed as collapsed
"""
collapsed: Boolean!
}
""" """
Identifier of Board Identifier of Board
""" """
...@@ -5277,7 +5749,7 @@ type Epic implements CurrentUserTodos & Noteable { ...@@ -5277,7 +5749,7 @@ type Epic implements CurrentUserTodos & Noteable {
): EpicConnection ): EpicConnection
""" """
Timestamp of the epic's closure Timestamp of when the epic was closed
""" """
closedAt: Time closedAt: Time
...@@ -5287,7 +5759,7 @@ type Epic implements CurrentUserTodos & Noteable { ...@@ -5287,7 +5759,7 @@ type Epic implements CurrentUserTodos & Noteable {
confidential: Boolean confidential: Boolean
""" """
Timestamp of the epic's creation Timestamp of when the epic was created
""" """
createdAt: Time createdAt: Time
...@@ -5582,7 +6054,7 @@ type Epic implements CurrentUserTodos & Noteable { ...@@ -5582,7 +6054,7 @@ type Epic implements CurrentUserTodos & Noteable {
title: String title: String
""" """
Timestamp of the epic's last activity Timestamp of when the epic was updated
""" """
updatedAt: Time updatedAt: Time
......
...@@ -2786,6 +2786,479 @@ ...@@ -2786,6 +2786,479 @@
"defaultValue": null "defaultValue": null
} }
], ],
"type": {
"kind": "OBJECT",
"name": "BoardEpicConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hideBacklogList",
"description": "Whether or not backlog list is hidden.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hideClosedList",
"description": "Whether or not closed list is hidden.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID (global ID) of the board",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lists",
"description": "Lists of the board",
"args": [
{
"name": "id",
"description": "Find a list by its global ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"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": "BoardListConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "milestone",
"description": "The board milestone.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Milestone",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the board",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "weight",
"description": "Weight of the board.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardConnection",
"description": "The connection type for Board.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BoardEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardEpic",
"description": "Represents an epic on an issue board",
"fields": [
{
"name": "author",
"description": "Author of the epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "children",
"description": "Children (sub-epics) of the epic",
"args": [
{
"name": "startDate",
"description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "endDate",
"description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "iid",
"description": "IID of the epic, e.g., \"1\"",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "List of IIDs of epics, e.g., [1, 2]",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter epics by state",
"type": {
"kind": "ENUM",
"name": "EpicState",
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Search query for epic title or description",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sort",
"description": "List epics by sort order",
"type": {
"kind": "ENUM",
"name": "EpicSort",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Filter epics by author",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "labelName",
"description": "Filter epics by labels",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Filter epics by milestone title, computed from epic's issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
"type": {
"kind": "SCALAR",
"name": "String",
"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": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "EpicConnection", "name": "EpicConnection",
...@@ -2795,152 +3268,896 @@ ...@@ -2795,152 +3268,896 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "hideBacklogList", "name": "closedAt",
"description": "Whether or not backlog list is hidden.", "description": "Timestamp of when the epic was closed",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "confidential",
"description": "Indicates if the epic is confidential",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of when the epic was created",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"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",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EpicDescendantCount",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "descendantWeightSum",
"description": "Total weight of open and closed issues in the epic and its descendants",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EpicDescendantWeights",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the epic",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "discussions",
"description": "All discussions on this noteable",
"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
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiscussionConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "downvotes",
"description": "Number of downvotes the epic has received",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dueDate",
"description": "Due date of the epic",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dueDateFixed",
"description": "Fixed due date of the epic",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dueDateFromMilestones",
"description": "Inherited due date of the epic from milestones",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dueDateIsFixed",
"description": "Indicates if the due date has been manually set",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "group",
"description": "Group to which the epic belongs",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasChildren",
"description": "Indicates if the epic has children",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasIssues",
"description": "Indicates if the epic has direct issues",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasParent",
"description": "Indicates if the epic has a parent epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "healthStatus",
"description": "Current health status of the epic",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EpicHealthStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iid",
"description": "Internal ID of the epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issues",
"description": "A list of issues associated with the epic",
"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
}
],
"type": {
"kind": "OBJECT",
"name": "EpicIssueConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "labels",
"description": "Labels assigned to the epic",
"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
}
],
"type": {
"kind": "OBJECT",
"name": "LabelConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "notes",
"description": "All notes on this noteable",
"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
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "NoteConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "parent",
"description": "Parent epic of the epic",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Epic",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "participants",
"description": "List of participants for the epic",
"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
}
],
"type": {
"kind": "OBJECT",
"name": "UserConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reference",
"description": "Internal reference of the epic. Returned in shortened format by default",
"args": [
{
"name": "full",
"description": "Indicates if the reference should be returned in full",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "relationPath",
"description": "URI path of the epic-issue relationship",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "String",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "hideClosedList", "name": "relativePosition",
"description": "Whether or not closed list is hidden.", "description": "The relative position of the epic in the epic tree",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "Int",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "id", "name": "startDate",
"description": "ID (global ID) of the board", "description": "Start date of the epic",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "Time",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "lists", "name": "startDateFixed",
"description": "Lists of the board", "description": "Fixed start date of the epic",
"args": [ "args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "id", "name": "startDateFromMilestones",
"description": "Find a list by its global ID", "description": "Inherited start date of the epic from milestones",
"args": [
],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "Time",
"ofType": null "ofType": null
}, },
"defaultValue": null "isDeprecated": false,
"deprecationReason": null
}, },
{ {
"name": "after", "name": "startDateIsFixed",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Indicates if the start date has been manually set",
"args": [
],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "Boolean",
"ofType": null "ofType": null
}, },
"defaultValue": null "isDeprecated": false,
"deprecationReason": null
}, },
{ {
"name": "before", "name": "state",
"description": "Returns the elements in the list that come before the specified cursor.", "description": "State of the epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EpicState",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subscribed",
"description": "Indicates the currently logged in user is subscribed to the epic",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "title",
"description": "Title of the epic",
"args": [
],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
"ofType": null "ofType": null
}, },
"defaultValue": null "isDeprecated": false,
"deprecationReason": null
}, },
{ {
"name": "first", "name": "updatedAt",
"description": "Returns the first _n_ elements from the list.", "description": "Timestamp of when the epic was updated",
"args": [
],
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Int", "name": "Time",
"ofType": null "ofType": null
}, },
"defaultValue": null "isDeprecated": false,
"deprecationReason": null
}, },
{ {
"name": "last", "name": "upvotes",
"description": "Returns the last _n_ elements from the list.", "description": "Number of upvotes the epic has received",
"args": [
],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Int", "name": "Int",
"ofType": null "ofType": null
},
"defaultValue": null
} }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userPermissions",
"description": "Permissions for the current user on the resource",
"args": [
], ],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "BoardListConnection", "name": "EpicPermissions",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "milestone", "name": "userPreferences",
"description": "The board milestone.", "description": "User preferences for the epic on the issue board",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Milestone", "name": "BoardEpicUserPreferences",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "name", "name": "webPath",
"description": "Name of the board", "description": "Web path of the epic",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "weight", "name": "webUrl",
"description": "Weight of the board.", "description": "Web URL of the epic",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Int", "name": "String",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
...@@ -2948,15 +4165,24 @@ ...@@ -2948,15 +4165,24 @@
], ],
"inputFields": null, "inputFields": null,
"interfaces": [ "interfaces": [
{
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
}
], ],
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "BoardConnection", "name": "BoardEpicConnection",
"description": "The connection type for Board.", "description": "The connection type for BoardEpic.",
"fields": [ "fields": [
{ {
"name": "edges", "name": "edges",
...@@ -2969,7 +4195,7 @@ ...@@ -2969,7 +4195,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "BoardEdge", "name": "BoardEpicEdge",
"ofType": null "ofType": null
} }
}, },
...@@ -2987,7 +4213,7 @@ ...@@ -2987,7 +4213,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Board", "name": "BoardEpic",
"ofType": null "ofType": null
} }
}, },
...@@ -3022,7 +4248,7 @@ ...@@ -3022,7 +4248,7 @@
}, },
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "BoardEdge", "name": "BoardEpicEdge",
"description": "An edge in a connection.", "description": "An edge in a connection.",
"fields": [ "fields": [
{ {
...@@ -3051,8 +4277,39 @@ ...@@ -3051,8 +4277,39 @@
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Board", "name": "BoardEpic",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BoardEpicUserPreferences",
"description": "Represents user preferences for a board epic",
"fields": [
{
"name": "collapsed",
"description": "Indicates epic should be displayed as collapsed",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null "ofType": null
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
...@@ -8395,6 +9652,11 @@ ...@@ -8395,6 +9652,11 @@
"interfaces": null, "interfaces": null,
"enumValues": null, "enumValues": null,
"possibleTypes": [ "possibleTypes": [
{
"kind": "OBJECT",
"name": "BoardEpic",
"ofType": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Design", "name": "Design",
...@@ -14728,7 +15990,7 @@ ...@@ -14728,7 +15990,7 @@
}, },
{ {
"name": "closedAt", "name": "closedAt",
"description": "Timestamp of the epic's closure", "description": "Timestamp of when the epic was closed",
"args": [ "args": [
], ],
...@@ -14756,7 +16018,7 @@ ...@@ -14756,7 +16018,7 @@
}, },
{ {
"name": "createdAt", "name": "createdAt",
"description": "Timestamp of the epic's creation", "description": "Timestamp of when the epic was created",
"args": [ "args": [
], ],
...@@ -15523,7 +16785,7 @@ ...@@ -15523,7 +16785,7 @@
}, },
{ {
"name": "updatedAt", "name": "updatedAt",
"description": "Timestamp of the epic's last activity", "description": "Timestamp of when the epic was updated",
"args": [ "args": [
], ],
...@@ -34203,6 +35465,11 @@ ...@@ -34203,6 +35465,11 @@
"name": "AlertManagementAlert", "name": "AlertManagementAlert",
"ofType": null "ofType": null
}, },
{
"kind": "OBJECT",
"name": "BoardEpic",
"ofType": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Design", "name": "Design",
...@@ -209,6 +209,57 @@ Represents a project or group board. ...@@ -209,6 +209,57 @@ Represents a project or group board.
| `name` | String | Name of the board | | `name` | String | Name of the board |
| `weight` | Int | Weight of the board. | | `weight` | Int | Weight of the board. |
### BoardEpic
Represents an epic on an issue board.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `author` | User! | Author of the epic |
| `closedAt` | Time | Timestamp of when the epic was closed |
| `confidential` | Boolean | Indicates if the epic is confidential |
| `createdAt` | Time | Timestamp of when the epic was created |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants |
| `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
| `dueDateFixed` | Time | Fixed due date of the epic |
| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones |
| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set |
| `group` | Group! | Group to which the epic belongs |
| `hasChildren` | Boolean! | Indicates if the epic has children |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues |
| `hasParent` | Boolean! | Indicates if the epic has a parent epic |
| `healthStatus` | EpicHealthStatus | Current health status of the epic |
| `id` | ID! | ID of the epic |
| `iid` | ID! | Internal ID of the epic |
| `parent` | Epic | Parent epic of the epic |
| `reference` | String! | Internal reference of the epic. Returned in shortened format by default |
| `relationPath` | String | URI path of the epic-issue relationship |
| `relativePosition` | Int | The relative position of the epic in the epic tree |
| `startDate` | Time | Start date of the epic |
| `startDateFixed` | Time | Fixed start date of the epic |
| `startDateFromMilestones` | Time | Inherited start date of the epic from milestones |
| `startDateIsFixed` | Boolean | Indicates if the start date has been manually set |
| `state` | EpicState! | State of the epic |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic |
| `title` | String | Title of the epic |
| `updatedAt` | Time | Timestamp of when the epic was updated |
| `upvotes` | Int! | Number of upvotes the epic has received |
| `userPermissions` | EpicPermissions! | Permissions for the current user on the resource |
| `userPreferences` | BoardEpicUserPreferences | User preferences for the epic on the issue board |
| `webPath` | String! | Web path of the epic |
| `webUrl` | String! | Web URL of the epic |
### BoardEpicUserPreferences
Represents user preferences for a board epic.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `collapsed` | Boolean! | Indicates epic should be displayed as collapsed |
### BoardList ### BoardList
Represents a list for an issue board. Represents a list for an issue board.
...@@ -889,9 +940,9 @@ Represents an epic. ...@@ -889,9 +940,9 @@ Represents an epic.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `author` | User! | Author of the epic | | `author` | User! | Author of the epic |
| `closedAt` | Time | Timestamp of the epic's closure | | `closedAt` | Time | Timestamp of when the epic was closed |
| `confidential` | Boolean | Indicates if the epic is confidential | | `confidential` | Boolean | Indicates if the epic is confidential |
| `createdAt` | Time | Timestamp of the epic's creation | | `createdAt` | Time | Timestamp of when the epic was created |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues | | `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants | | `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants |
| `description` | String | Description of the epic | | `description` | String | Description of the epic |
...@@ -918,7 +969,7 @@ Represents an epic. ...@@ -918,7 +969,7 @@ Represents an epic.
| `state` | EpicState! | State of the epic | | `state` | EpicState! | State of the epic |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic | | `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic |
| `title` | String | Title of the epic | | `title` | String | Title of the epic |
| `updatedAt` | Time | Timestamp of the epic's last activity | | `updatedAt` | Time | Timestamp of when the epic was updated |
| `upvotes` | Int! | Number of upvotes the epic has received | | `upvotes` | Int! | Number of upvotes the epic has received |
| `userPermissions` | EpicPermissions! | Permissions for the current user on the resource | | `userPermissions` | EpicPermissions! | Permissions for the current user on the resource |
| `webPath` | String! | Web path of the epic | | `webPath` | String! | Web path of the epic |
......
fragment BoardEpicNode on BoardEpic {
id
iid
title
state
reference
webUrl
createdAt
closedAt
}
#import "ee_else_ce/boards/queries/board_list.fragment.graphql" #import "ee_else_ce/boards/queries/board_list.fragment.graphql"
#import "~/graphql_shared/fragments/epic.fragment.graphql" #import "./board_epic.fragment.graphql"
query BoardEE( query BoardEE(
$fullPath: ID! $fullPath: ID!
...@@ -18,7 +18,7 @@ query BoardEE( ...@@ -18,7 +18,7 @@ query BoardEE(
} }
epics(issueFilters: $issueFilters) { epics(issueFilters: $issueFilters) {
nodes { nodes {
...EpicNode ...BoardEpicNode
} }
} }
} }
...@@ -32,7 +32,7 @@ query BoardEE( ...@@ -32,7 +32,7 @@ query BoardEE(
} }
epics(issueFilters: $issueFilters) { epics(issueFilters: $issueFilters) {
nodes { nodes {
...EpicNode ...BoardEpicNode
} }
} }
} }
......
...@@ -21,7 +21,7 @@ module EE ...@@ -21,7 +21,7 @@ module EE
field :weight, type: GraphQL::INT_TYPE, null: true, field :weight, type: GraphQL::INT_TYPE, null: true,
description: 'Weight of the board.' description: 'Weight of the board.'
field :epics, ::Types::EpicType.connection_type, null: true, field :epics, ::Types::Boards::BoardEpicType.connection_type, null: true,
description: 'Epics associated with board issues.', description: 'Epics associated with board issues.',
resolver: ::Resolvers::BoardGroupings::EpicsResolver, resolver: ::Resolvers::BoardGroupings::EpicsResolver,
complexity: 5 complexity: 5
......
...@@ -11,13 +11,15 @@ module Resolvers ...@@ -11,13 +11,15 @@ module Resolvers
required: false, required: false,
description: 'Filters applied when selecting issues on the board' description: 'Filters applied when selecting issues on the board'
type Types::EpicType, null: true type Types::Boards::BoardEpicType, null: true
def resolve(**args) def resolve(**args)
return Epic.none unless board.present? return Epic.none unless board.present?
return Epic.none unless group.present? return Epic.none unless group.present?
return unless ::Feature.enabled?(:boards_with_swimlanes, group) return unless ::Feature.enabled?(:boards_with_swimlanes, group)
context.scoped_set!(:board, board)
Epic.for_ids(board_epic_ids(args[:issue_filters])) Epic.for_ids(board_epic_ids(args[:issue_filters]))
end end
......
# frozen_string_literal: true
module Types
module Boards
# rubocop: disable Graphql/AuthorizeTypes
class BoardEpicType < EpicType
graphql_name 'BoardEpic'
description 'Represents an epic on an issue board'
field :user_preferences, Types::Boards::EpicUserPreferencesType, null: true,
description: 'User preferences for the epic on the issue board'
def user_preferences
return unless current_user
board = context[:board]
raise ::Gitlab::Graphql::Errors::BaseError 'Board is not set' unless board
BatchLoader::GraphQL.for(object.id).batch(key: board) do |epic_ids, loader, args|
current_user
.boards_epic_user_preferences
.for_boards_and_epics(args[:key].id, epic_ids)
.each { |user_pref| loader.call(user_pref.epic_id, user_pref) }
end
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module Boards
# rubocop: disable Graphql/AuthorizeTypes
class EpicUserPreferencesType < BaseObject
graphql_name 'BoardEpicUserPreferences'
description 'Represents user preferences for a board epic'
field :collapsed, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates epic should be displayed as collapsed'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
...@@ -69,11 +69,11 @@ module Types ...@@ -69,11 +69,11 @@ module Types
description: 'Number of downvotes the epic has received' description: 'Number of downvotes the epic has received'
field :closed_at, Types::TimeType, null: true, field :closed_at, Types::TimeType, null: true,
description: "Timestamp of the epic's closure" description: 'Timestamp of when the epic was closed'
field :created_at, Types::TimeType, null: true, field :created_at, Types::TimeType, null: true,
description: "Timestamp of the epic's creation" description: 'Timestamp of when the epic was created'
field :updated_at, Types::TimeType, null: true, field :updated_at, Types::TimeType, null: true,
description: "Timestamp of the epic's last activity" description: 'Timestamp of when the epic was updated'
field :children, ::Types::EpicType.connection_type, null: true, field :children, ::Types::EpicType.connection_type, null: true,
description: 'Children (sub-epics) of the epic', description: 'Children (sub-epics) of the epic',
......
...@@ -9,5 +9,9 @@ module Boards ...@@ -9,5 +9,9 @@ module Boards
belongs_to :epic, inverse_of: :boards_epic_user_preferences belongs_to :epic, inverse_of: :boards_epic_user_preferences
validates :user, uniqueness: { scope: [:board_id, :epic_id] } validates :user, uniqueness: { scope: [:board_id, :epic_id] }
scope :for_boards_and_epics, -> (board_ids, epic_ids) do
where(board_id: board_ids, epic_id: epic_ids)
end
end end
end end
---
title: Expose board epic user preferences in GraphQL
merge_request: 42569
author:
type: added
...@@ -29,6 +29,14 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do ...@@ -29,6 +29,14 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) } let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
let_it_be(:epic_issue3) { create(:epic_issue, epic: epic3, issue: issue3) } let_it_be(:epic_issue3) { create(:epic_issue, epic: epic3, issue: issue3) }
let_it_be(:context) do
GraphQL::Query::Context.new(
query: OpenStruct.new(schema: nil),
values: { current_user: current_user },
object: nil
)
end
describe '#resolve' do describe '#resolve' do
before do before do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
...@@ -117,7 +125,7 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do ...@@ -117,7 +125,7 @@ RSpec.describe Resolvers::BoardGroupings::EpicsResolver do
end end
end end
def resolve_board_epics(object, args = {}, context = { current_user: current_user }) def resolve_board_epics(object, args = {})
resolve(described_class, obj: object, args: args, ctx: context) resolve(described_class, obj: object, args: args, ctx: context)
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardEpic'] do
it { expect(described_class.graphql_name).to eq('BoardEpic') }
it 'has specific fields' do
expect(described_class).to have_graphql_field(:user_preferences)
end
describe '#user_preferences' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:board) { create(:board, group: group) }
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:epic_issue) { create(:epic_issue, epic: epic, issue: issue) }
let(:query) do
%(
{
group(fullPath: "#{group.full_path}") {
board(id: "#{board.to_global_id}") {
epics {
nodes {
id
userPreferences {
collapsed
}
}
}
}
}
}
)
end
subject(:result) { GitlabSchema.execute(query, context: context).as_json }
let(:epics) { result['data']['group']['board']['epics']['nodes'] }
let(:epic_preferences) { epics.first['userPreferences'] }
before do
stub_licensed_features(epics: true)
group.add_developer(user)
end
context 'when user is not set' do
let(:context) { { board: board } }
it 'does not return any epics' do
expect(epics).to be_empty
end
end
context 'when user and board is set' do
let(:context) { { board: board, current_user: user } }
it 'returns nil if there are not preferences' do
expect(epics).not_to be_empty
expect(epic_preferences).to be_nil
end
context 'when user preferences are set' do
let_it_be(:epic_user_preference) { create(:epic_user_preference, board: board, epic: epic, user: user) }
it 'returns user preferences' do
expect(epics).not_to be_empty
expect(epic_preferences['collapsed']).to eq(false)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardEpicUserPreferences'] do
it { expect(described_class.graphql_name).to eq('BoardEpicUserPreferences') }
it 'has specific fields' do
expect(described_class).to have_graphql_field(:collapsed)
end
end
...@@ -14,4 +14,20 @@ RSpec.describe Boards::EpicUserPreference do ...@@ -14,4 +14,20 @@ RSpec.describe Boards::EpicUserPreference do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_uniqueness_of(:user).scoped_to([:board_id, :epic_id]) } it { is_expected.to validate_uniqueness_of(:user).scoped_to([:board_id, :epic_id]) }
end end
describe 'scopes' do
describe '.for_boards_and_epics' do
it 'returns user board epic preferences for the given boards and epics' do
user = create(:user)
board = create(:board)
user_pref1 = create(:epic_user_preference, user: user, board: board)
user_pref2 = create(:epic_user_preference, user: user, board: board)
user_pref3 = create(:epic_user_preference, board: board, epic: user_pref1.epic)
create(:epic_user_preference, user: user, board: board)
result = described_class.for_boards_and_epics(board.id, [user_pref1.epic_id, user_pref2.epic_id])
expect(result).to match_array([user_pref1, user_pref2, user_pref3])
end
end
end
end end
...@@ -25,6 +25,9 @@ RSpec.describe 'get list of boards' do ...@@ -25,6 +25,9 @@ RSpec.describe 'get list of boards' do
nodes { nodes {
id id
title title
userPreferences {
collapsed
}
} }
} }
EPIC EPIC
...@@ -42,24 +45,38 @@ RSpec.describe 'get list of boards' do ...@@ -42,24 +45,38 @@ RSpec.describe 'get list of boards' do
it 'returns open epics referenced by issues in the board' do it 'returns open epics referenced by issues in the board' do
board = create(:board, resource_parent: board_parent) board = create(:board, resource_parent: board_parent)
issue_project = board_parent.is_a?(Project) ? board_parent : create(:project, group: board_parent) issue_project = board_parent.is_a?(Project) ? board_parent : create(:project, group: board_parent)
# matches filters:
issue1 = create(:issue, project: issue_project, labels: [label]) issue1 = create(:issue, project: issue_project, labels: [label])
# matches filters, but is assigned to the same epic as issue1:
issue2 = create(:issue, project: issue_project, labels: [label]) issue2 = create(:issue, project: issue_project, labels: [label])
# doesn't match labelName filter:
issue3 = create(:issue, project: issue_project) issue3 = create(:issue, project: issue_project)
# matches filters, but its epic is closed:
issue4 = create(:issue, project: issue_project, labels: [label]) issue4 = create(:issue, project: issue_project, labels: [label])
# doesn't match negated authorUsername filter:
issue5 = create(:issue, project: issue_project, labels: [label], author: current_user) issue5 = create(:issue, project: issue_project, labels: [label], author: current_user)
epic1 = create(:epic, group: parent_group) epic1 = create(:epic, group: parent_group)
epic2 = create(:epic, group: parent_group) epic2 = create(:epic, group: parent_group)
epic3 = create(:epic, :closed, group: parent_group) epic3 = create(:epic, :closed, group: parent_group)
create(:epic_issue, issue: issue1, epic: epic1) create(:epic_issue, issue: issue1, epic: epic1)
create(:epic_issue, issue: issue2, epic: epic1) create(:epic_issue, issue: issue2, epic: epic1)
create(:epic_issue, issue: issue3, epic: epic2) create(:epic_issue, issue: issue3, epic: epic2)
create(:epic_issue, issue: issue4, epic: epic3) create(:epic_issue, issue: issue4, epic: epic3)
create(:epic_issue, issue: issue5, epic: epic2) create(:epic_issue, issue: issue5, epic: epic2)
create(:epic_user_preference, board: board, epic: epic1, user: current_user, collapsed: true)
post_graphql(board_epic_query(board), current_user: current_user) post_graphql(board_epic_query(board), current_user: current_user)
board_titles = board_data['epics']['nodes'].map { |node| node['title'] } aggregate_failures 'board epics response' do
expect(board_titles).to match_array [epic1.title] epics = board_data['epics']['nodes']
expect(epics.size).to eq(1)
expect(epics.first['title']).to eq(epic1.title)
expect(epics.first['userPreferences']['collapsed']).to eq(true)
end
end 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