Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
1c8deba0
Commit
1c8deba0
authored
Jan 08, 2021
by
Andreas Brandl
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'epic_boards_epic_list' into 'master'
Epic boards epic list See merge request gitlab-org/gitlab!50277
parents
60ea2659
b901713d
Changes
26
Show whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
579 additions
and
183 deletions
+579
-183
app/services/boards/base_items_list_service.rb
app/services/boards/base_items_list_service.rb
+128
-0
app/services/boards/issues/list_service.rb
app/services/boards/issues/list_service.rb
+11
-100
changelogs/unreleased/epic_boards_epic_list.yml
changelogs/unreleased/epic_boards_epic_list.yml
+5
-0
db/migrate/20210104163218_add_epic_board_position_index.rb
db/migrate/20210104163218_add_epic_board_position_index.rb
+18
-0
db/schema_migrations/20210104163218
db/schema_migrations/20210104163218
+1
-0
db/structure.sql
db/structure.sql
+2
-0
doc/api/graphql/reference/gitlab_schema.graphql
doc/api/graphql/reference/gitlab_schema.graphql
+30
-0
doc/api/graphql/reference/gitlab_schema.json
doc/api/graphql/reference/gitlab_schema.json
+63
-0
doc/api/graphql/reference/index.md
doc/api/graphql/reference/index.md
+1
-0
ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb
ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb
+18
-0
ee/app/graphql/resolvers/boards/epic_lists_resolver.rb
ee/app/graphql/resolvers/boards/epic_lists_resolver.rb
+3
-5
ee/app/graphql/types/boards/epic_list_type.rb
ee/app/graphql/types/boards/epic_list_type.rb
+4
-0
ee/app/models/boards/epic_board.rb
ee/app/models/boards/epic_board.rb
+4
-0
ee/app/models/boards/epic_list.rb
ee/app/models/boards/epic_list.rb
+11
-0
ee/app/models/ee/epic.rb
ee/app/models/ee/epic.rb
+9
-0
ee/app/services/boards/epics/list_service.rb
ee/app/services/boards/epics/list_service.rb
+25
-0
ee/app/services/ee/boards/issues/list_service.rb
ee/app/services/ee/boards/issues/list_service.rb
+2
-2
ee/spec/factories/boards/epic_lists.rb
ee/spec/factories/boards/epic_lists.rb
+1
-1
ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb
...raphql/resolvers/boards/board_list_epics_resolver_spec.rb
+42
-0
ee/spec/graphql/types/boards/epic_list_type_spec.rb
ee/spec/graphql/types/boards/epic_list_type_spec.rb
+1
-1
ee/spec/models/epic_spec.rb
ee/spec/models/epic_spec.rb
+17
-1
ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb
...ts/api/graphql/boards/epic_board_list_epics_query_spec.rb
+56
-0
ee/spec/services/boards/epics/list_service_spec.rb
ee/spec/services/boards/epics/list_service_spec.rb
+54
-0
spec/lib/gitlab/import_export/all_models.yml
spec/lib/gitlab/import_export/all_models.yml
+1
-0
spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb
...es/services/boards/issues_list_service_shared_examples.rb
+7
-73
spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
...les/services/boards/items_list_service_shared_examples.rb
+65
-0
No files found.
app/services/boards/base_items_list_service.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
module
Boards
class
BaseItemsListService
<
Boards
::
BaseService
include
Gitlab
::
Utils
::
StrongMemoize
include
ActiveRecord
::
ConnectionAdapters
::
Quoting
def
execute
return
items
.
order_closed_date_desc
if
list
&
.
closed?
ordered_items
end
private
def
ordered_items
raise
NotImplementedError
end
def
finder
raise
NotImplementedError
end
def
board
raise
NotImplementedError
end
def
item_model
raise
NotImplementedError
end
# We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query.
# rubocop: disable CodeReuse/ActiveRecord
def
items
strong_memoize
(
:items
)
do
filter
(
finder
.
execute
).
reorder
(
nil
)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def
filter
(
items
)
# when grouping board issues by epics (used in board swimlanes)
# we need to get all issues in the board
# TODO: ignore hidden columns -
# https://gitlab.com/gitlab-org/gitlab/-/issues/233870
return
items
if
params
[
:all_lists
]
items
=
without_board_labels
(
items
)
unless
list
&
.
movable?
||
list
&
.
closed?
items
=
with_list_label
(
items
)
if
list
&
.
label?
items
end
def
list
return
unless
params
.
key?
(
:id
)
strong_memoize
(
:list
)
do
id
=
params
[
:id
]
if
board
.
lists
.
loaded?
board
.
lists
.
find
{
|
l
|
l
.
id
==
id
}
else
board
.
lists
.
find
(
id
)
end
end
end
def
filter_params
set_parent
set_state
set_attempt_search_optimizations
params
end
def
set_parent
if
parent
.
is_a?
(
Group
)
params
[
:group_id
]
=
parent
.
id
else
params
[
:project_id
]
=
parent
.
id
end
end
def
set_state
return
if
params
[
:all_lists
]
params
[
:state
]
=
list
&&
list
.
closed?
?
'closed'
:
'opened'
end
def
set_attempt_search_optimizations
return
unless
params
[
:search
].
present?
if
board
.
group_board?
params
[
:attempt_group_search_optimizations
]
=
true
else
params
[
:attempt_project_search_optimizations
]
=
true
end
end
# rubocop: disable CodeReuse/ActiveRecord
def
board_label_ids
@board_label_ids
||=
board
.
lists
.
movable
.
pluck
(
:label_id
)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
without_board_labels
(
items
)
return
items
unless
board_label_ids
.
any?
items
.
where
.
not
(
'EXISTS (?)'
,
label_links
(
board_label_ids
).
limit
(
1
))
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
label_links
(
label_ids
)
LabelLink
.
where
(
'label_links.target_type = ?'
,
item_model
)
.
where
(
item_model
.
arel_table
[
:id
].
eq
(
LabelLink
.
arel_table
[
:target_id
]).
to_sql
)
.
where
(
label_id:
label_ids
)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
with_list_label
(
items
)
items
.
where
(
'EXISTS (?)'
,
label_links
(
list
.
label_id
).
limit
(
1
))
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
app/services/boards/issues/list_service.rb
View file @
1c8deba0
...
...
@@ -2,26 +2,20 @@
module
Boards
module
Issues
class
ListService
<
Boards
::
BaseService
class
ListService
<
Boards
::
Base
ItemsList
Service
include
Gitlab
::
Utils
::
StrongMemoize
def
self
.
valid_params
IssuesFinder
.
valid_params
end
def
execute
return
fetch_issues
.
order_closed_date_desc
if
list
&
.
closed?
fetch_issues
.
order_by_position_and_priority
(
with_cte:
params
[
:search
].
present?
)
end
# rubocop: disable CodeReuse/ActiveRecord
def
metadata
issues
=
Issue
.
arel_table
keys
=
metadata_fields
.
keys
# TODO: eliminate need for SQL literal fragment
columns
=
Arel
.
sql
(
metadata_fields
.
values_at
(
*
keys
).
join
(
', '
))
results
=
Issue
.
where
(
id:
fetch_issue
s
.
select
(
issues
[
:id
])).
pluck
(
columns
)
results
=
Issue
.
where
(
id:
item
s
.
select
(
issues
[
:id
])).
pluck
(
columns
)
Hash
[
keys
.
zip
(
results
.
flatten
)]
end
...
...
@@ -29,74 +23,28 @@ module Boards
private
def
metadata_fields
{
size:
'COUNT(*)'
}
end
# We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query.
# rubocop: disable CodeReuse/ActiveRecord
def
fetch_issues
strong_memoize
(
:fetch_issues
)
do
issues
=
IssuesFinder
.
new
(
current_user
,
filter_params
).
execute
filter
(
issues
).
reorder
(
nil
)
def
ordered_items
items
.
order_by_position_and_priority
(
with_cte:
params
[
:search
].
present?
)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def
filter
(
issues
)
# when grouping board issues by epics (used in board swimlanes)
# we need to get all issues in the board
# TODO: ignore hidden columns -
# https://gitlab.com/gitlab-org/gitlab/-/issues/233870
return
issues
if
params
[
:all_lists
]
issues
=
without_board_labels
(
issues
)
unless
list
&
.
movable?
||
list
&
.
closed?
issues
=
with_list_label
(
issues
)
if
list
&
.
label?
issues
def
finder
IssuesFinder
.
new
(
current_user
,
filter_params
)
end
def
board
@board
||=
parent
.
boards
.
find
(
params
[
:board_id
])
end
def
list
return
unless
params
.
key?
(
:id
)
strong_memoize
(
:list
)
do
id
=
params
[
:id
]
if
board
.
lists
.
loaded?
board
.
lists
.
find
{
|
l
|
l
.
id
==
id
}
else
board
.
lists
.
find
(
id
)
end
end
def
metadata_fields
{
size:
'COUNT(*)'
}
end
def
filter_params
set_parent
set_state
set_scope
set_non_archived
set_attempt_search_optimizations
set_issue_types
params
end
def
set_parent
if
parent
.
is_a?
(
Group
)
params
[
:group_id
]
=
parent
.
id
else
params
[
:project_id
]
=
parent
.
id
end
end
def
set_state
return
if
params
[
:all_lists
]
params
[
:state
]
=
list
&&
list
.
closed?
?
'closed'
:
'opened'
super
end
def
set_scope
...
...
@@ -107,49 +55,12 @@ module Boards
params
[
:non_archived
]
=
parent
.
is_a?
(
Group
)
end
def
set_attempt_search_optimizations
return
unless
params
[
:search
].
present?
if
board
.
group_board?
params
[
:attempt_group_search_optimizations
]
=
true
else
params
[
:attempt_project_search_optimizations
]
=
true
end
end
def
set_issue_types
params
[
:issue_types
]
=
Issue
::
TYPES_FOR_LIST
end
# rubocop: disable CodeReuse/ActiveRecord
def
board_label_ids
@board_label_ids
||=
board
.
lists
.
movable
.
pluck
(
:label_id
)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
without_board_labels
(
issues
)
return
issues
unless
board_label_ids
.
any?
issues
.
where
.
not
(
'EXISTS (?)'
,
issues_label_links
.
limit
(
1
))
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
issues_label_links
LabelLink
.
where
(
"label_links.target_type = 'Issue' AND label_links.target_id = issues.id"
).
where
(
label_id:
board_label_ids
)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
with_list_label
(
issues
)
issues
.
where
(
'EXISTS (?)'
,
LabelLink
.
where
(
"label_links.target_type = 'Issue' AND label_links.target_id = issues.id"
)
.
where
(
"label_links.label_id = ?"
,
list
.
label_id
).
limit
(
1
))
end
# rubocop: enable CodeReuse/ActiveRecord
def
board_group
board
.
group_board?
?
parent
:
parent
.
group
def
item_model
Issue
end
end
end
...
...
changelogs/unreleased/epic_boards_epic_list.yml
0 → 100644
View file @
1c8deba0
---
title
:
Added epic board position database index
merge_request
:
50277
author
:
type
:
added
db/migrate/20210104163218_add_epic_board_position_index.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
class
AddEpicBoardPositionIndex
<
ActiveRecord
::
Migration
[
6.0
]
include
Gitlab
::
Database
::
MigrationHelpers
DOWNTIME
=
false
INDEX_NAME
=
'index_boards_epic_board_positions_on_scoped_relative_position'
disable_ddl_transaction!
def
up
add_concurrent_index
:boards_epic_board_positions
,
[
:epic_board_id
,
:epic_id
,
:relative_position
],
name:
INDEX_NAME
end
def
down
remove_concurrent_index_by_name
:boards_epic_board_positions
,
INDEX_NAME
end
end
db/schema_migrations/20210104163218
0 → 100644
View file @
1c8deba0
37aa0564d2ade1cab56a669facccbaaf08e4d9856c7a4cc120968d33cff161bd
\ No newline at end of file
db/structure.sql
View file @
1c8deba0
...
...
@@ -20946,6 +20946,8 @@ CREATE UNIQUE INDEX index_boards_epic_board_positions_on_epic_board_id_and_epic_
CREATE
INDEX
index_boards_epic_board_positions_on_epic_id
ON
boards_epic_board_positions
USING
btree
(
epic_id
);
CREATE
INDEX
index_boards_epic_board_positions_on_scoped_relative_position
ON
boards_epic_board_positions
USING
btree
(
epic_board_id
,
epic_id
,
relative_position
);
CREATE
INDEX
index_boards_epic_boards_on_group_id
ON
boards_epic_boards
USING
btree
(
group_id
);
CREATE
INDEX
index_boards_epic_lists_on_epic_board_id
ON
boards_epic_lists
USING
btree
(
epic_board_id
);
...
...
doc/api/graphql/reference/gitlab_schema.graphql
View file @
1c8deba0
...
...
@@ -8511,6 +8511,11 @@ type EpicBoard {
"""
first
:
Int
"""
Find
an
epic
board
list
by
ID
.
"""
id
:
BoardsEpicListID
"""
Returns
the
last
_n_
elements
from
the
list
.
"""
...
...
@@ -9122,6 +9127,31 @@ type EpicIssueEdge {
Represents an epic board list
"""
type
EpicList
{
"""
List
epics
.
"""
epics
(
"""
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
):
EpicConnection
"""
Global
ID
of
the
board
list
.
"""
...
...
doc/api/graphql/reference/gitlab_schema.json
View file @
1c8deba0
...
...
@@ -23565,6 +23565,16 @@
"name": "lists",
"description": "Epic board lists.",
"args": [
{
"name": "id",
"description": "Find an epic board list by ID.",
"type": {
"kind": "SCALAR",
"name": "BoardsEpicListID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
...
...
@@ -25359,6 +25369,59 @@
"name": "EpicList",
"description": "Represents an epic board list",
"fields": [
{
"name": "epics",
"description": "List epics.",
"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": "EpicConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "Global ID of the board list.",
doc/api/graphql/reference/index.md
View file @
1c8deba0
...
...
@@ -1489,6 +1489,7 @@ Represents an epic board list.
| Field | Type | Description |
| ----- | ---- | ----------- |
|
`epics`
| EpicConnection | List epics. |
|
`id`
| BoardsEpicListID! | Global ID of the board list. |
|
`label`
| Label | Label of the list. |
|
`listType`
| String! | Type of the list. |
...
...
ee/app/graphql/resolvers/boards/board_list_epics_resolver.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
module
Resolvers
module
Boards
class
BoardListEpicsResolver
<
BaseResolver
type
Types
::
EpicType
.
connection_type
,
null:
true
alias_method
:list
,
:object
def
resolve
(
**
args
)
filter_params
=
{
board_id:
list
.
epic_board
.
id
,
id:
list
.
id
}
service
=
::
Boards
::
Epics
::
ListService
.
new
(
list
.
epic_board
.
group
,
context
[
:current_user
],
filter_params
)
offset_pagination
(
service
.
execute
)
end
end
end
end
ee/app/graphql/resolvers/boards/epic_lists_resolver.rb
View file @
1c8deba0
...
...
@@ -8,11 +8,9 @@ module Resolvers
type
Types
::
Boards
::
EpicListType
.
connection_type
,
null:
true
when_single
do
argument
:id
,
::
Types
::
GlobalIDType
[
::
Boards
::
EpicList
],
required:
tru
e
,
required:
fals
e
,
description:
'Find an epic board list by ID.'
end
alias_method
:epic_board
,
:object
...
...
ee/app/graphql/types/boards/epic_list_type.rb
View file @
1c8deba0
...
...
@@ -23,6 +23,10 @@ module Types
field
:label
,
Types
::
LabelType
,
null:
true
,
description:
'Label of the list.'
field
:epics
,
Types
::
EpicType
.
connection_type
,
null:
true
,
resolver:
Resolvers
::
Boards
::
BoardListEpicsResolver
,
description:
'List epics.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
...
...
ee/app/models/boards/epic_board.rb
View file @
1c8deba0
...
...
@@ -10,5 +10,9 @@ module Boards
validates
:name
,
length:
{
maximum:
255
}
scope
:order_by_name_asc
,
->
{
order
(
arel_table
[
:name
].
lower
.
asc
).
order
(
id: :asc
)
}
def
lists
epic_lists
end
end
end
ee/app/models/boards/epic_list.rb
View file @
1c8deba0
...
...
@@ -2,6 +2,8 @@
module
Boards
class
EpicList
<
ApplicationRecord
# TODO: we can move logic shared with with List model to
# a module. https://gitlab.com/gitlab-org/gitlab/-/issues/296559
belongs_to
:epic_board
,
optional:
false
,
inverse_of: :epic_lists
belongs_to
:label
,
inverse_of: :epic_lists
...
...
@@ -12,9 +14,18 @@ module Boards
validates
:position
,
numericality:
{
only_integer:
true
,
greater_than_or_equal_to:
0
},
if: :label?
scope
:ordered
,
->
{
order
(
:list_type
,
:position
)
}
scope
:movable
,
->
{
where
(
list_type:
list_types
.
slice
(
*
movable_types
).
values
)
}
def
self
.
movable_types
[
:label
]
end
def
title
label?
?
label
.
name
:
list_type
.
humanize
end
def
movable?
label?
end
end
end
ee/app/models/ee/epic.rb
View file @
1c8deba0
...
...
@@ -57,6 +57,7 @@ module EE
has_many
:issues
,
through: :epic_issues
has_many
:user_mentions
,
class_name:
"EpicUserMention"
,
dependent: :delete_all
# rubocop:disable Cop/ActiveRecordDependent
has_many
:boards_epic_user_preferences
,
class_name:
'Boards::EpicUserPreference'
,
inverse_of: :epic
has_many
:epic_board_positions
,
class_name:
'Boards::EpicBoardPosition'
,
inverse_of: :epic_board
validates
:group
,
presence:
true
validate
:validate_parent
,
on: :create
...
...
@@ -103,10 +104,18 @@ module EE
reorder
(
::
Gitlab
::
Database
.
nulls_last_order
(
'start_date'
,
'DESC'
),
'id DESC'
)
end
scope
:order_closed_date_desc
,
->
{
reorder
(
closed_at: :desc
)
}
scope
:order_relative_position
,
->
do
reorder
(
'relative_position ASC'
,
'id DESC'
)
end
scope
:order_relative_position_on_board
,
->
(
board_id
)
do
left_joins
(
:epic_board_positions
)
.
where
(
boards_epic_board_positions:
{
epic_board_id:
[
nil
,
board_id
]
})
.
reorder
(
::
Gitlab
::
Database
.
nulls_last_order
(
'boards_epic_board_positions.relative_position'
,
'ASC'
),
'epics.id DESC'
)
end
scope
:with_api_entity_associations
,
->
{
preload
(
:author
,
:labels
,
group: :route
)
}
scope
:start_date_inherited
,
->
{
where
(
start_date_is_fixed:
[
nil
,
false
])
}
scope
:due_date_inherited
,
->
{
where
(
due_date_is_fixed:
[
nil
,
false
])
}
...
...
ee/app/services/boards/epics/list_service.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
module
Boards
module
Epics
class
ListService
<
Boards
::
BaseItemsListService
private
def
finder
EpicsFinder
.
new
(
current_user
,
filter_params
.
merge
(
group_id:
parent
.
id
))
end
def
board
@board
||=
parent
.
epic_boards
.
find
(
params
[
:board_id
])
end
def
ordered_items
items
.
order_relative_position_on_board
(
board
.
id
)
end
def
item_model
::
Epic
end
end
end
end
ee/app/services/ee/boards/issues/list_service.rb
View file @
1c8deba0
...
...
@@ -28,9 +28,9 @@ module EE
end
end
override
:
issues_
label_links
override
:label_links
# rubocop: disable CodeReuse/ActiveRecord
def
issues_label_links
def
label_links
(
label_ids
)
if
has_valid_milestone?
super
.
where
(
"issues.milestone_id = ?"
,
board
.
milestone_id
)
else
...
...
ee/spec/factories/boards/epic_lists.rb
View file @
1c8deba0
...
...
@@ -3,7 +3,7 @@
FactoryBot
.
define
do
factory
:epic_list
,
class:
'Boards::EpicList'
do
epic_board
label
association
:label
,
factory: :group_
label
list_type
{
:label
}
sequence
(
:position
)
end
...
...
ee/spec/graphql/resolvers/boards/board_list_epics_resolver_spec.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
Resolvers
::
Boards
::
BoardListEpicsResolver
do
include
GraphqlHelpers
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:development
)
{
create
(
:group_label
,
group:
group
,
name:
'Development'
)
}
let_it_be
(
:testing
)
{
create
(
:group_label
,
group:
group
,
name:
'Testing'
)
}
let_it_be
(
:board
)
{
create
(
:epic_board
,
group:
group
)
}
let_it_be
(
:list1
)
{
create
(
:epic_list
,
epic_board:
board
,
label:
development
,
position:
0
)
}
let_it_be
(
:list2
)
{
create
(
:epic_list
,
epic_board:
board
,
label:
testing
,
position:
0
)
}
let_it_be
(
:list1_epic1
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:list1_epic2
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:list2_epic1
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
testing
])
}
let_it_be
(
:epic_pos1
)
{
create
(
:epic_board_position
,
epic:
list1_epic1
,
epic_board:
board
,
relative_position:
20
)
}
let_it_be
(
:epic_pos2
)
{
create
(
:epic_board_position
,
epic:
list1_epic2
,
epic_board:
board
,
relative_position:
10
)
}
let_it_be
(
:epic_pos3
)
{
create
(
:epic_board_position
,
epic:
list1_epic1
,
relative_position:
30
)
}
specify
do
expect
(
described_class
).
to
have_nullable_graphql_type
(
Types
::
EpicType
.
connection_type
)
end
describe
'#resolve'
do
let
(
:args
)
{
{}
}
subject
(
:result
)
{
resolve
(
described_class
,
ctx:
{
current_user:
user
},
obj:
list1
,
args:
args
)
}
before
do
stub_licensed_features
(
epics:
true
)
group
.
add_reporter
(
user
)
end
it
'returns epics on the board list ordered by position on the board'
do
expect
(
result
.
items
).
to
eq
([
list1_epic2
,
list1_epic1
])
end
end
end
ee/spec/graphql/types/boards/epic_list_type_spec.rb
View file @
1c8deba0
...
...
@@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['EpicList'] do
specify
{
expect
(
described_class
.
graphql_name
).
to
eq
(
'EpicList'
)
}
it
'has specific fields'
do
expected_fields
=
%w[id title list_type position label]
expected_fields
=
%w[id title list_type position label
epics
]
expect
(
described_class
).
to
include_graphql_fields
(
*
expected_fields
)
end
...
...
ee/spec/models/epic_spec.rb
View file @
1c8deba0
...
...
@@ -19,7 +19,8 @@ RSpec.describe Epic do
it
{
is_expected
.
to
have_many
(
:epic_issues
)
}
it
{
is_expected
.
to
have_many
(
:children
)
}
it
{
is_expected
.
to
have_many
(
:user_mentions
).
class_name
(
'EpicUserMention'
)
}
it
{
is_expected
.
to
have_many
(
:boards_epic_user_preferences
).
class_name
(
'Boards::EpicUserPreference'
)
}
it
{
is_expected
.
to
have_many
(
:boards_epic_user_preferences
).
class_name
(
'Boards::EpicUserPreference'
).
inverse_of
(
:epic
)
}
it
{
is_expected
.
to
have_many
(
:epic_board_positions
).
class_name
(
'Boards::EpicBoardPosition'
).
inverse_of
(
:epic_board
)
}
end
describe
'scopes'
do
...
...
@@ -45,6 +46,21 @@ RSpec.describe Epic do
expect
(
described_class
.
not_confidential_or_in_groups
(
group
)).
to
match_array
([
confidential_epic
,
public_epic
])
end
end
describe
'.order_relative_position_on_board'
do
let_it_be
(
:board
)
{
create
(
:epic_board
)
}
let_it_be
(
:epic1
)
{
create
(
:epic
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
)
}
let_it_be
(
:epic3
)
{
create
(
:epic
)
}
it
'returns epics ordered by position on the board, null last'
do
create
(
:epic_board_position
,
epic:
epic2
,
epic_board:
board
,
relative_position:
10
)
create
(
:epic_board_position
,
epic:
epic1
,
epic_board:
board
,
relative_position:
20
)
create
(
:epic_board_position
,
epic:
epic3
,
epic_board:
board
,
relative_position:
20
)
expect
(
described_class
.
order_relative_position_on_board
(
board
.
id
)).
to
eq
([
epic2
,
epic3
,
epic1
,
public_epic
,
confidential_epic
])
end
end
end
describe
'validations'
do
...
...
ee/spec/requests/api/graphql/boards/epic_board_list_epics_query_spec.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
'get list of epics for an epic board list'
do
include
GraphqlHelpers
let_it_be
(
:current_user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
,
:private
)
}
let_it_be
(
:development
)
{
create
(
:group_label
,
group:
group
,
name:
'Development'
)
}
let_it_be
(
:board
)
{
create
(
:epic_board
,
group:
group
)
}
let_it_be
(
:list
)
{
create
(
:epic_list
,
epic_board:
board
,
label:
development
)
}
let_it_be
(
:epic1
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:epic2
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:epic3
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:epic4
)
{
create
(
:labeled_epic
,
group:
group
)
}
let_it_be
(
:epic_pos1
)
{
create
(
:epic_board_position
,
epic:
epic1
,
epic_board:
board
,
relative_position:
20
)
}
let_it_be
(
:epic_pos2
)
{
create
(
:epic_board_position
,
epic:
epic2
,
epic_board:
board
,
relative_position:
10
)
}
def
pagination_query
(
params
=
{})
graphql_query_for
(
:group
,
{
full_path:
group
.
full_path
},
<<~
BOARDS
epicBoard(id: "
#{
board
.
to_global_id
}
") {
lists(id: "
#{
list
.
to_global_id
}
") {
nodes {
#{
query_nodes
(
:epics
,
all_graphql_fields_for
(
'epics'
.
classify
),
include_pagination_info:
true
,
args:
params
)
}
}
}
}
BOARDS
)
end
before
do
stub_licensed_features
(
epics:
true
)
group
.
add_developer
(
current_user
)
end
describe
'sorting and pagination'
do
let
(
:data_path
)
{
[
:group
,
:epicBoard
,
:lists
,
:nodes
,
0
,
:epics
]
}
let
(
:expected_results
)
{
[
epic2
.
to_global_id
.
to_s
,
epic1
.
to_global_id
.
to_s
,
epic3
.
to_global_id
.
to_s
]
}
def
pagination_results_data
(
nodes
)
nodes
.
map
{
|
list
|
list
[
'id'
]
}
end
it_behaves_like
'sorted paginated query'
do
# currently we don't support custom sorting for epic lists,
# nil value will be ignored by ::Graphql::Arguments
let
(
:sort_param
)
{
nil
}
let
(
:first_param
)
{
2
}
end
end
end
ee/spec/services/boards/epics/list_service_spec.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
Boards
::
Epics
::
ListService
do
describe
'#execute'
do
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:board
)
{
create
(
:epic_board
,
group:
group
)
}
let_it_be
(
:development
)
{
create
(
:group_label
,
group:
group
,
name:
'Development'
)
}
let_it_be
(
:testing
)
{
create
(
:group_label
,
group:
group
,
name:
'Testing'
)
}
let_it_be
(
:backlog
)
{
create
(
:epic_list
,
epic_board:
board
,
list_type: :backlog
)
}
let_it_be
(
:list1
)
{
create
(
:epic_list
,
epic_board:
board
,
label:
development
,
position:
0
)
}
let_it_be
(
:list2
)
{
create
(
:epic_list
,
epic_board:
board
,
label:
testing
,
position:
1
)
}
let_it_be
(
:closed
)
{
create
(
:epic_list
,
epic_board:
board
,
list_type: :closed
)
}
let_it_be
(
:backlog_epic1
)
{
create
(
:epic
,
group:
group
)
}
let_it_be
(
:list1_epic1
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:list1_epic2
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:list1_epic3
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
development
])
}
let_it_be
(
:list2_epic1
)
{
create
(
:labeled_epic
,
group:
group
,
labels:
[
testing
])
}
let_it_be
(
:closed_epic1
)
{
create
(
:labeled_epic
,
:closed
,
group:
group
,
labels:
[
development
],
closed_at:
1
.
day
.
ago
)
}
let_it_be
(
:closed_epic2
)
{
create
(
:labeled_epic
,
:closed
,
group:
group
,
labels:
[
testing
],
closed_at:
2
.
days
.
ago
)
}
let_it_be
(
:closed_epic3
)
{
create
(
:epic
,
:closed
,
group:
group
,
closed_at:
1
.
week
.
ago
)
}
before
do
stub_licensed_features
(
epics:
true
)
group
.
add_developer
(
user
)
end
it_behaves_like
'items list service'
do
let
(
:parent
)
{
group
}
let
(
:backlog_items
)
{
[
backlog_epic1
]
}
let
(
:list1_items
)
{
[
list1_epic1
,
list1_epic2
,
list1_epic3
]
}
let
(
:closed_items
)
{
[
closed_epic1
,
closed_epic2
,
closed_epic3
]
}
let
(
:all_items
)
{
backlog_items
+
list1_items
+
closed_items
+
[
list2_epic1
]
}
let
(
:list_factory
)
{
:epic_list
}
let
(
:new_list
)
{
create
(
:epic_list
,
epic_board:
board
)
}
end
it
'returns epics sorted by position on the board'
do
create
(
:epic_board_position
,
epic:
list1_epic1
,
epic_board:
board
,
relative_position:
20
)
create
(
:epic_board_position
,
epic:
list1_epic2
,
epic_board:
board
,
relative_position:
10
)
create
(
:epic_board_position
,
epic:
list1_epic1
,
relative_position:
30
)
epics
=
described_class
.
new
(
group
,
user
,
{
board_id:
board
.
id
,
id:
list1
.
id
}).
execute
expect
(
epics
).
to
eq
([
list1_epic2
,
list1_epic1
,
list1_epic3
])
end
end
end
spec/lib/gitlab/import_export/all_models.yml
View file @
1c8deba0
...
...
@@ -723,6 +723,7 @@ epic:
-
user_mentions
-
note_authors
-
boards_epic_user_preferences
-
epic_board_positions
epic_issue
:
-
epic
-
issue
...
...
spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb
View file @
1c8deba0
...
...
@@ -19,78 +19,12 @@ RSpec.shared_examples 'issues list service' do
end
end
it
'avoids N+1'
do
params
=
{
board_id:
board
.
id
}
control
=
ActiveRecord
::
QueryRecorder
.
new
{
described_class
.
new
(
parent
,
user
,
params
).
execute
}
create
(
:list
,
board:
board
)
expect
{
described_class
.
new
(
parent
,
user
,
params
).
execute
}.
not_to
exceed_query_limit
(
control
)
end
context
'issues are ordered by priority'
do
it
'returns opened issues when list_id is missing'
do
params
=
{
board_id:
board
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
eq
[
opened_issue2
,
reopened_issue1
,
opened_issue1
]
end
it
'returns opened issues when listing issues from Backlog'
do
params
=
{
board_id:
board
.
id
,
id:
backlog
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
eq
[
opened_issue2
,
reopened_issue1
,
opened_issue1
]
end
it
'returns opened issues that have label list applied when listing issues from a label list'
do
params
=
{
board_id:
board
.
id
,
id:
list1
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
eq
[
list1_issue3
,
list1_issue1
,
list1_issue2
]
end
end
context
'issues are ordered by date of closing'
do
it
'returns closed issues when listing issues from Closed'
do
params
=
{
board_id:
board
.
id
,
id:
closed
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
eq
[
closed_issue1
,
closed_issue2
,
closed_issue3
,
closed_issue4
,
closed_issue5
]
end
end
context
'with list that does not belong to the board'
do
it
'raises an error'
do
list
=
create
(
:list
)
service
=
described_class
.
new
(
parent
,
user
,
board_id:
board
.
id
,
id:
list
.
id
)
expect
{
service
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordNotFound
)
end
end
context
'with invalid list id'
do
it
'raises an error'
do
service
=
described_class
.
new
(
parent
,
user
,
board_id:
board
.
id
,
id:
nil
)
expect
{
service
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordNotFound
)
end
end
context
'when :all_lists is used'
do
it
'returns issues from all lists'
do
params
=
{
board_id:
board
.
id
,
all_lists:
true
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expected
=
[
opened_issue2
,
reopened_issue1
,
opened_issue1
,
list1_issue1
,
list1_issue2
,
list1_issue3
,
list2_issue1
,
closed_issue1
,
closed_issue2
,
closed_issue3
,
closed_issue4
,
closed_issue5
]
expect
(
issues
).
to
match_array
(
expected
)
end
it_behaves_like
'items list service'
do
let
(
:backlog_items
)
{
[
opened_issue2
,
reopened_issue1
,
opened_issue1
]
}
let
(
:list1_items
)
{
[
list1_issue3
,
list1_issue1
,
list1_issue2
]
}
let
(
:closed_items
)
{
[
closed_issue1
,
closed_issue2
,
closed_issue3
,
closed_issue4
,
closed_issue5
]
}
let
(
:all_items
)
{
backlog_items
+
list1_items
+
closed_items
+
[
list2_issue1
]
}
let
(
:list_factory
)
{
:list
}
let
(
:new_list
)
{
create
(
:list
,
board:
board
)
}
end
end
spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
0 → 100644
View file @
1c8deba0
# frozen_string_literal: true
RSpec
.
shared_examples
'items list service'
do
it
'avoids N+1'
do
params
=
{
board_id:
board
.
id
}
control
=
ActiveRecord
::
QueryRecorder
.
new
{
described_class
.
new
(
parent
,
user
,
params
).
execute
}
new_list
expect
{
described_class
.
new
(
parent
,
user
,
params
).
execute
}.
not_to
exceed_query_limit
(
control
)
end
it
'returns opened items when list_id is missing'
do
params
=
{
board_id:
board
.
id
}
items
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
items
).
to
match_array
(
backlog_items
)
end
it
'returns opened items when listing items from Backlog'
do
params
=
{
board_id:
board
.
id
,
id:
backlog
.
id
}
items
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
items
).
to
match_array
(
backlog_items
)
end
it
'returns opened items that have label list applied when listing items from a label list'
do
params
=
{
board_id:
board
.
id
,
id:
list1
.
id
}
items
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
items
).
to
match_array
(
list1_items
)
end
it
'returns closed items when listing items from Closed sorted by closed_at in descending order'
do
params
=
{
board_id:
board
.
id
,
id:
closed
.
id
}
items
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
items
).
to
eq
(
closed_items
)
end
it
'raises an error if the list does not belong to the board'
do
list
=
create
(
list_factory
)
# rubocop:disable Rails/SaveBang
service
=
described_class
.
new
(
parent
,
user
,
board_id:
board
.
id
,
id:
list
.
id
)
expect
{
service
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordNotFound
)
end
it
'raises an error if list id is invalid'
do
service
=
described_class
.
new
(
parent
,
user
,
board_id:
board
.
id
,
id:
nil
)
expect
{
service
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordNotFound
)
end
it
'returns items from all lists if :all_list is used'
do
params
=
{
board_id:
board
.
id
,
all_lists:
true
}
items
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
items
).
to
match_array
(
all_items
)
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment