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
e3c376a2
Commit
e3c376a2
authored
Aug 07, 2018
by
Constance Okoghenun
Committed by
Phil Hughes
Aug 07, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Issue board milestone lists"
parent
7463291b
Changes
66
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
66 changed files
with
1135 additions
and
219 deletions
+1135
-219
app/assets/javascripts/boards/components/board_list.vue
app/assets/javascripts/boards/components/board_list.vue
+10
-2
app/assets/javascripts/boards/components/board_new_issue.vue
app/assets/javascripts/boards/components/board_new_issue.vue
+3
-0
app/assets/javascripts/boards/models/issue.js
app/assets/javascripts/boards/models/issue.js
+14
-1
app/assets/javascripts/boards/models/list.js
app/assets/javascripts/boards/models/list.js
+15
-2
app/assets/javascripts/boards/models/milestone.js
app/assets/javascripts/boards/models/milestone.js
+5
-1
app/assets/javascripts/boards/stores/boards_store.js
app/assets/javascripts/boards/stores/boards_store.js
+14
-1
app/models/list.rb
app/models/list.rb
+3
-3
app/views/shared/boards/components/_board.html.haml
app/views/shared/boards/components/_board.html.haml
+2
-1
config/routes.rb
config/routes.rb
+1
-0
db/schema.rb
db/schema.rb
+35
-32
ee/app/assets/javascripts/boards/components/assignees_list_slector.js
...s/javascripts/boards/components/assignees_list_slector.js
+11
-0
ee/app/assets/javascripts/boards/components/boards_list_selector/assignees_list_item.vue
...s/components/boards_list_selector/assignees_list_item.vue
+7
-7
ee/app/assets/javascripts/boards/components/boards_list_selector/index.js
...vascripts/boards/components/boards_list_selector/index.js
+98
-0
ee/app/assets/javascripts/boards/components/boards_list_selector/list_container.vue
...boards/components/boards_list_selector/list_container.vue
+24
-19
ee/app/assets/javascripts/boards/components/boards_list_selector/list_content.vue
...s/boards/components/boards_list_selector/list_content.vue
+41
-0
ee/app/assets/javascripts/boards/components/boards_list_selector/list_filter.vue
...ts/boards/components/boards_list_selector/list_filter.vue
+0
-0
ee/app/assets/javascripts/boards/components/boards_list_selector/milestones_list_item.vue
.../components/boards_list_selector/milestones_list_item.vue
+31
-0
ee/app/assets/javascripts/boards/components/boards_selector.js
...p/assets/javascripts/boards/components/boards_selector.js
+11
-6
ee/app/assets/javascripts/boards/components/milestone_list_selector.js
.../javascripts/boards/components/milestone_list_selector.js
+11
-0
ee/app/assets/javascripts/boards/models/list.js
ee/app/assets/javascripts/boards/models/list.js
+2
-1
ee/app/assets/stylesheets/pages/boards.scss
ee/app/assets/stylesheets/pages/boards.scss
+1
-1
ee/app/controllers/boards/milestones_controller.rb
ee/app/controllers/boards/milestones_controller.rb
+9
-0
ee/app/controllers/ee/boards/lists_controller.rb
ee/app/controllers/ee/boards/lists_controller.rb
+2
-2
ee/app/finders/boards/milestones_finder.rb
ee/app/finders/boards/milestones_finder.rb
+32
-0
ee/app/helpers/ee/boards_helper.rb
ee/app/helpers/ee/boards_helper.rb
+2
-1
ee/app/models/ee/list.rb
ee/app/models/ee/list.rb
+21
-14
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/serializers/milestone_serializer.rb
ee/app/serializers/milestone_serializer.rb
+3
-0
ee/app/services/ee/boards/issues/create_service.rb
ee/app/services/ee/boards/issues/create_service.rb
+3
-1
ee/app/services/ee/boards/issues/list_service.rb
ee/app/services/ee/boards/issues/list_service.rb
+43
-16
ee/app/services/ee/boards/issues/move_service.rb
ee/app/services/ee/boards/issues/move_service.rb
+20
-1
ee/app/services/ee/boards/lists/create_service.rb
ee/app/services/ee/boards/lists/create_service.rb
+20
-3
ee/app/services/ee/boards/lists/list_service.rb
ee/app/services/ee/boards/lists/list_service.rb
+24
-2
ee/app/views/shared/boards/components/_list_milestone.html.haml
.../views/shared/boards/components/_list_milestone.html.haml
+1
-0
ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml
...ews/shared/issuable/_board_create_list_dropdown.html.haml
+23
-7
ee/app/views/shared/issuable/_milestone_page_default.html.haml
...p/views/shared/issuable/_milestone_page_default.html.haml
+5
-0
ee/changelogs/unreleased/6469-issue-board-milestone-lists.yml
...hangelogs/unreleased/6469-issue-board-milestone-lists.yml
+5
-0
ee/changelogs/unreleased/ccr-6397-add_board_assignee_list.yml
...hangelogs/unreleased/ccr-6397-add_board_assignee_list.yml
+5
-0
ee/db/migrate/20180724161450_add_milestone_to_lists.rb
ee/db/migrate/20180724161450_add_milestone_to_lists.rb
+7
-0
ee/lib/ee/api/boards_responses.rb
ee/lib/ee/api/boards_responses.rb
+35
-0
ee/lib/ee/api/entities.rb
ee/lib/ee/api/entities.rb
+9
-0
ee/spec/controllers/boards/milestones_controller_spec.rb
ee/spec/controllers/boards/milestones_controller_spec.rb
+27
-0
ee/spec/factories/lists.rb
ee/spec/factories/lists.rb
+7
-0
ee/spec/finders/boards/milestones_finder_spec.rb
ee/spec/finders/boards/milestones_finder_spec.rb
+48
-0
ee/spec/fixtures/api/schemas/entities/milestone.json
ee/spec/fixtures/api/schemas/entities/milestone.json
+18
-0
ee/spec/fixtures/api/schemas/list.json
ee/spec/fixtures/api/schemas/list.json
+1
-1
ee/spec/javascripts/boards/components/board_list_selector/assignees_list_item_spec.js
...omponents/board_list_selector/assignees_list_item_spec.js
+2
-2
ee/spec/javascripts/boards/components/board_list_selector/board_list_selector_spec.js
...omponents/board_list_selector/board_list_selector_spec.js
+9
-8
ee/spec/javascripts/boards/components/board_list_selector/list_container_spec.js
...rds/components/board_list_selector/list_container_spec.js
+11
-10
ee/spec/javascripts/boards/components/board_list_selector/list_content_spec.js
...oards/components/board_list_selector/list_content_spec.js
+5
-4
ee/spec/javascripts/boards/components/board_list_selector/list_filter_spec.js
...boards/components/board_list_selector/list_filter_spec.js
+3
-3
ee/spec/models/ee/list_spec.rb
ee/spec/models/ee/list_spec.rb
+33
-6
ee/spec/requests/api/boards_spec.rb
ee/spec/requests/api/boards_spec.rb
+7
-0
ee/spec/requests/api/group_boards_spec.rb
ee/spec/requests/api/group_boards_spec.rb
+7
-0
ee/spec/services/ee/boards/issues/create_service_spec.rb
ee/spec/services/ee/boards/issues/create_service_spec.rb
+33
-6
ee/spec/services/ee/boards/issues/list_service_spec.rb
ee/spec/services/ee/boards/issues/list_service_spec.rb
+31
-4
ee/spec/services/ee/boards/issues/move_service_spec.rb
ee/spec/services/ee/boards/issues/move_service_spec.rb
+134
-26
ee/spec/services/ee/boards/lists/create_service_spec.rb
ee/spec/services/ee/boards/lists/create_service_spec.rb
+33
-14
ee/spec/services/ee/boards/lists/list_service_spec.rb
ee/spec/services/ee/boards/lists/list_service_spec.rb
+27
-0
ee/spec/support/shared_examples/assignee_board_list.rb
ee/spec/support/shared_examples/assignee_board_list.rb
+31
-0
ee/spec/support/shared_examples/milestone_board_list.rb
ee/spec/support/shared_examples/milestone_board_list.rb
+30
-0
lib/api/boards.rb
lib/api/boards.rb
+3
-4
lib/api/boards_responses.rb
lib/api/boards_responses.rb
+15
-1
lib/api/entities.rb
lib/api/entities.rb
+1
-0
lib/api/group_boards.rb
lib/api/group_boards.rb
+2
-4
locale/gitlab.pot
locale/gitlab.pot
+8
-2
No files found.
app/assets/javascripts/boards/components/board_list.vue
View file @
e3c376a2
...
...
@@ -112,12 +112,20 @@ export default {
if
(
e
.
target
)
{
const
containerEl
=
e
.
target
.
closest
(
'
.js-board-list
'
)
||
e
.
target
.
querySelector
(
'
.js-board-list
'
);
const
toBoardType
=
containerEl
.
dataset
.
boardType
;
const
cloneActions
=
{
label
:
[
'
milestone
'
,
'
assignee
'
],
assignee
:
[
'
milestone
'
,
'
label
'
],
milestone
:
[
'
label
'
,
'
assignee
'
],
};
if
(
toBoardType
)
{
const
fromBoardType
=
this
.
list
.
type
;
// For each list we check if the destination list is
// a the list were we should clone the issue
const
shouldClone
=
Object
.
entries
(
cloneActions
).
some
(
entry
=>
(
fromBoardType
===
entry
[
0
]
&&
entry
[
1
].
includes
(
toBoardType
)));
if
((
fromBoardType
===
'
assignee
'
&&
toBoardType
===
'
label
'
)
||
(
fromBoardType
===
'
label
'
&&
toBoardType
===
'
assignee
'
))
{
if
(
shouldClone
)
{
return
'
clone
'
;
}
}
...
...
app/assets/javascripts/boards/components/board_new_issue.vue
View file @
e3c376a2
...
...
@@ -50,11 +50,14 @@ export default {
const
labels
=
this
.
list
.
label
?
[
this
.
list
.
label
]
:
[];
const
assignees
=
this
.
list
.
assignee
?
[
this
.
list
.
assignee
]
:
[];
const
milestone
=
this
.
list
.
milestone
?
this
.
list
.
milestone
:
null
;
const
issue
=
new
ListIssue
({
title
:
this
.
title
,
labels
,
subscribed
:
true
,
assignees
,
milestone
,
project_id
:
this
.
selectedProject
.
id
,
});
...
...
app/assets/javascripts/boards/models/issue.js
View file @
e3c376a2
...
...
@@ -27,7 +27,6 @@ class ListIssue {
this
.
referencePath
=
obj
.
reference_path
;
this
.
path
=
obj
.
real_path
;
this
.
toggleSubscriptionEndpoint
=
obj
.
toggle_subscription_endpoint
;
this
.
milestone_id
=
obj
.
milestone_id
;
this
.
project_id
=
obj
.
project_id
;
if
(
obj
.
project
)
{
...
...
@@ -36,6 +35,7 @@ class ListIssue {
if
(
obj
.
milestone
)
{
this
.
milestone
=
new
ListMilestone
(
obj
.
milestone
);
this
.
milestone_id
=
obj
.
milestone
.
id
;
}
obj
.
labels
.
forEach
((
label
)
=>
{
...
...
@@ -85,6 +85,19 @@ class ListIssue {
this
.
assignees
=
[];
}
addMilestone
(
milestone
)
{
const
miletoneId
=
this
.
milestone
?
this
.
milestone
.
id
:
null
;
if
(
milestone
.
id
!==
miletoneId
)
{
this
.
milestone
=
new
ListMilestone
(
milestone
);
}
}
removeMilestone
(
removeMilestone
)
{
if
(
removeMilestone
&&
removeMilestone
.
id
===
this
.
milestone
.
id
)
{
this
.
milestone
=
{};
}
}
getLists
()
{
return
gl
.
issueBoards
.
BoardsStore
.
state
.
lists
.
filter
(
list
=>
list
.
findIssue
(
this
.
id
));
}
...
...
app/assets/javascripts/boards/models/list.js
View file @
e3c376a2
...
...
@@ -4,6 +4,7 @@
import
ListLabel
from
'
~/vue_shared/models/label
'
;
import
ListAssignee
from
'
~/vue_shared/models/assignee
'
;
import
queryData
from
'
../utils/query_data
'
;
import
ListMilestone
from
'
./milestone
'
;
const
PER_PAGE
=
20
;
...
...
@@ -49,6 +50,9 @@ class List {
}
else
if
(
obj
.
user
)
{
this
.
assignee
=
new
ListAssignee
(
obj
.
user
);
this
.
title
=
this
.
assignee
.
name
;
}
else
if
(
obj
.
milestone
)
{
this
.
milestone
=
new
ListMilestone
(
obj
.
milestone
);
this
.
title
=
this
.
milestone
.
title
;
}
if
(
!
typeInfo
.
isBlank
&&
this
.
id
)
{
...
...
@@ -67,12 +71,14 @@ class List {
}
save
()
{
const
entity
=
this
.
label
||
this
.
assignee
;
const
entity
=
this
.
label
||
this
.
assignee
||
this
.
milestone
;
let
entityType
=
''
;
if
(
this
.
label
)
{
entityType
=
'
label_id
'
;
}
else
{
}
else
if
(
this
.
assignee
)
{
entityType
=
'
assignee_id
'
;
}
else
if
(
this
.
milestone
)
{
entityType
=
'
milestone_id
'
;
}
return
gl
.
boardService
...
...
@@ -187,6 +193,13 @@ class List {
issue
.
addAssignee
(
this
.
assignee
);
}
if
(
this
.
milestone
)
{
if
(
listFrom
&&
listFrom
.
type
===
'
milestone
'
)
{
issue
.
removeMilestone
(
listFrom
.
milestone
);
}
issue
.
addMilestone
(
this
.
milestone
);
}
if
(
listFrom
)
{
this
.
issuesSize
+=
1
;
...
...
app/assets/javascripts/boards/models/milestone.js
View file @
e3c376a2
class
ListMilestone
{
export
default
class
ListMilestone
{
constructor
(
obj
)
{
this
.
id
=
obj
.
id
;
this
.
title
=
obj
.
title
;
this
.
path
=
obj
.
path
;
this
.
state
=
obj
.
state
;
this
.
webUrl
=
obj
.
web_url
||
obj
.
webUrl
;
this
.
description
=
obj
.
description
;
}
}
...
...
app/assets/javascripts/boards/stores/boards_store.js
View file @
e3c376a2
...
...
@@ -126,6 +126,16 @@ gl.issueBoards.BoardsStore = {
issue
.
findAssignee
(
listTo
.
assignee
))
{
const
targetIssue
=
listTo
.
findIssue
(
issue
.
id
);
targetIssue
.
removeAssignee
(
listFrom
.
assignee
);
}
else
if
(
listTo
.
type
===
'
milestone
'
)
{
const
currentMilestone
=
issue
.
milestone
;
const
currentLists
=
this
.
state
.
lists
.
filter
(
list
=>
(
list
.
type
===
'
milestone
'
&&
list
.
id
!==
listTo
.
id
))
.
filter
(
list
=>
list
.
issues
.
some
(
listIssue
=>
issue
.
id
===
listIssue
.
id
));
issue
.
removeMilestone
(
currentMilestone
);
issue
.
addMilestone
(
listTo
.
milestone
);
currentLists
.
forEach
(
currentList
=>
currentList
.
removeIssue
(
issue
));
listTo
.
addIssue
(
issue
,
listFrom
,
newIndex
);
}
else
{
// Add to new lists issues if it doesn't already exist
listTo
.
addIssue
(
issue
,
listFrom
,
newIndex
);
...
...
@@ -143,6 +153,9 @@ gl.issueBoards.BoardsStore = {
}
else
if
(
listTo
.
type
===
'
backlog
'
&&
listFrom
.
type
===
'
assignee
'
)
{
issue
.
removeAssignee
(
listFrom
.
assignee
);
listFrom
.
removeIssue
(
issue
);
}
else
if
(
listTo
.
type
===
'
backlog
'
&&
listFrom
.
type
===
'
milestone
'
)
{
issue
.
removeMilestone
(
listFrom
.
milestone
);
listFrom
.
removeIssue
(
issue
);
}
else
if
(
this
.
shouldRemoveIssue
(
listFrom
,
listTo
))
{
listFrom
.
removeIssue
(
issue
);
}
...
...
@@ -162,7 +175,7 @@ gl.issueBoards.BoardsStore = {
},
findList
(
key
,
val
,
type
=
'
label
'
)
{
const
filteredList
=
this
.
state
.
lists
.
filter
((
list
)
=>
{
const
byType
=
type
?
(
list
.
type
===
type
)
||
(
list
.
type
===
'
assignee
'
)
:
true
;
const
byType
=
type
?
(
list
.
type
===
type
)
||
(
list
.
type
===
'
assignee
'
)
||
(
list
.
type
===
'
milestone
'
)
:
true
;
return
list
[
key
]
===
val
&&
byType
;
});
...
...
app/models/list.rb
View file @
e3c376a2
...
...
@@ -6,7 +6,7 @@ class List < ActiveRecord::Base
belongs_to
:board
belongs_to
:label
enum
list_type:
{
backlog:
0
,
label:
1
,
closed:
2
,
assignee:
3
}
enum
list_type:
{
backlog:
0
,
label:
1
,
closed:
2
,
assignee:
3
,
milestone:
4
}
validates
:board
,
:list_type
,
presence:
true
validates
:label
,
:position
,
presence:
true
,
if: :label?
...
...
@@ -29,11 +29,11 @@ class List < ActiveRecord::Base
end
def
destroyable?
label?
self
.
class
.
destroyable_types
.
include?
(
list_type
&
.
to_sym
)
end
def
movable?
label?
self
.
class
.
movable_types
.
include?
(
list_type
&
.
to_sym
)
end
def
title
...
...
app/views/shared/boards/components/_board.html.haml
View file @
e3c376a2
...
...
@@ -6,12 +6,13 @@
%i
.fa.fa-fw.board-title-expandable-toggle
{
"v-if"
:
"list.isExpandable"
,
":class"
:
"{
\"
fa-caret-down
\"
: list.isExpanded,
\"
fa-caret-right
\"
: !list.isExpanded }"
,
"aria-hidden"
:
"true"
}
=
render_if_exists
"shared/boards/components/list_milestone"
%a
.user-avatar-link.js-no-trigger
{
"v-if"
:
"list.type ===
\"
assignee
\"
"
,
":href"
:
"list.assignee.path"
}
-# haml-lint:disable AltText
%img
.avatar.s20.has-tooltip
{
height:
"20"
,
width:
"20"
,
":src"
:
"list.assignee.avatar"
,
":alt"
:
"list.assignee.name"
}
%span
.board-title-text.has-tooltip
{
"v-if"
:
"list.type !==
\"
label
\"
"
,
%span
.board-title-text.has-tooltip
.block-truncated
{
"v-if"
:
"list.type !==
\"
label
\"
"
,
":title"
=>
'((list.label && list.label.description) || list.title || "")'
,
data:
{
container:
"body"
}
}
{{ list.title }}
...
...
config/routes.rb
View file @
e3c376a2
...
...
@@ -81,6 +81,7 @@ Rails.application.routes.draw do
resources
:issues
,
module: :boards
,
only:
[
:index
,
:update
]
resources
:users
,
module: :boards
,
only:
[
:index
]
resources
:milestones
,
module: :boards
,
only:
[
:index
]
end
# UserCallouts
...
...
db/schema.rb
View file @
e3c376a2
This diff is collapsed.
Click to expand it.
ee/app/assets/javascripts/boards/components/assignees_list_slector.js
0 → 100644
View file @
e3c376a2
import
BoardsListSelector
from
'
./boards_list_selector/index
'
;
export
default
function
()
{
const
$addListEl
=
document
.
querySelector
(
'
#js-add-list
'
);
return
new
BoardsListSelector
({
propsData
:
{
listPath
:
$addListEl
.
querySelector
(
'
.js-new-board-list
'
).
dataset
.
listAssigneesPath
,
listType
:
'
assignees
'
,
},
}).
$mount
(
'
.js-assignees-list
'
);
}
ee/app/assets/javascripts/boards/components/
assignees_list
/assignees_list_item.vue
→
ee/app/assets/javascripts/boards/components/
boards_list_selector
/assignees_list_item.vue
View file @
e3c376a2
...
...
@@ -3,7 +3,7 @@ import { sprintf, __ } from '~/locale';
export
default
{
props
:
{
assignee
:
{
item
:
{
type
:
Object
,
required
:
true
,
},
...
...
@@ -11,13 +11,13 @@ export default {
computed
:
{
avatarAltText
()
{
return
sprintf
(
__
(
"
%{name}'s avatar
"
),
{
name
:
this
.
assignee
.
name
,
name
:
this
.
item
.
name
,
});
},
},
methods
:
{
handleItemClick
()
{
this
.
$emit
(
'
onItemSelect
'
,
this
.
assignee
);
this
.
$emit
(
'
onItemSelect
'
,
this
.
item
);
},
},
};
...
...
@@ -35,16 +35,16 @@ export default {
<div
class=
"avatar-container s32"
>
<img
:alt=
"avatarAltText"
:src=
"
assignee
.avatar_url"
:src=
"
item
.avatar_url"
class=
"avatar s32 lazy"
/>
</div>
<div
class=
"dropdown-user-details"
>
<div
:title=
"
assignee.name"
>
{{
assignee
.
name
}}
</div>
<div
:title=
"
item.name"
>
{{
item
.
name
}}
</div>
<div
:title=
"
assignee
.username"
:title=
"
item
.username"
class=
"dropdown-light-content"
>
@
{{
assignee
.
username
}}
</div>
>
@
{{
item
.
username
}}
</div>
</div>
</button>
</li>
...
...
ee/app/assets/javascripts/boards/components/
assignees_list
/index.js
→
ee/app/assets/javascripts/boards/components/
boards_list_selector
/index.js
View file @
e3c376a2
import
Vue
from
'
vue
'
;
import
_
from
'
underscore
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
AssigneesListContainer
from
'
./assignees_
list_container.vue
'
;
import
ListContainer
from
'
./
list_container.vue
'
;
export
default
Vue
.
extend
({
components
:
{
Assignees
ListContainer
,
ListContainer
,
},
props
:
{
listAssigneesPath
:
{
listPath
:
{
type
:
String
,
required
:
true
,
},
listType
:
{
type
:
String
,
required
:
true
,
},
...
...
@@ -23,43 +27,68 @@ export default Vue.extend({
};
},
mounted
()
{
this
.
load
Assignees
();
this
.
load
List
();
},
methods
:
{
load
Assignees
()
{
if
(
this
.
store
.
state
.
assignees
.
length
)
{
load
List
()
{
if
(
this
.
store
.
state
[
this
.
listType
]
.
length
)
{
return
Promise
.
resolve
();
}
return
axios
.
get
(
this
.
list
Assignees
Path
)
.
get
(
this
.
listPath
)
.
then
(({
data
})
=>
{
this
.
loading
=
false
;
this
.
store
.
state
.
assignees
=
data
;
this
.
store
.
state
[
this
.
listType
]
=
data
;
})
.
catch
(()
=>
{
this
.
loading
=
false
;
Flash
(
__
(
'
Something went wrong while fetching assignees list
'
));
Flash
(
sprintf
(
__
(
'
Something went wrong while fetching %{listType} list
'
),
{
listType
:
this
.
listType
,
}));
});
},
handleItemClick
(
assignee
)
{
if
(
!
this
.
store
.
findList
(
'
title
'
,
assignee
.
name
))
{
this
.
store
.
new
({
title
:
assignee
.
name
,
filterItems
(
term
,
items
)
{
const
query
=
term
.
toLowerCase
();
return
items
.
filter
((
item
)
=>
{
const
name
=
item
.
name
?
item
.
name
.
toLowerCase
()
:
item
.
title
.
toLowerCase
();
const
foundName
=
name
.
indexOf
(
query
)
>
-
1
;
if
(
this
.
listType
===
'
milestones
'
)
{
return
foundName
;
}
const
username
=
item
.
username
.
toLowerCase
();
return
foundName
||
username
.
indexOf
(
query
)
>
-
1
;
});
},
handleItemClick
(
item
)
{
if
(
!
this
.
store
.
findList
(
'
title
'
,
item
.
name
))
{
const
list
=
{
title
:
item
.
name
,
position
:
this
.
store
.
state
.
lists
.
length
-
2
,
list_type
:
'
assignee
'
,
user
:
assignee
,
});
list_type
:
this
.
listType
,
};
if
(
this
.
listType
===
'
milestones
'
)
{
list
.
milestone
=
item
;
}
else
if
(
this
.
listType
===
'
assignees
'
)
{
list
.
user
=
item
;
}
this
.
store
.
new
(
list
);
this
.
store
.
state
.
lists
=
_
.
sortBy
(
this
.
store
.
state
.
lists
,
'
position
'
);
}
},
},
render
(
createElement
)
{
return
createElement
(
'
assignees-
list-container
'
,
{
return
createElement
(
'
list-container
'
,
{
props
:
{
loading
:
this
.
loading
,
assignees
:
this
.
store
.
state
.
assignees
,
items
:
this
.
store
.
state
[
this
.
listType
],
listType
:
this
.
listType
,
},
on
:
{
onItemSelect
:
this
.
handleItemClick
,
...
...
ee/app/assets/javascripts/boards/components/
assignees_list/assignees_
list_container.vue
→
ee/app/assets/javascripts/boards/components/
boards_list_selector/
list_container.vue
View file @
e3c376a2
<
script
>
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
AssigneesListFilter
from
'
./assignees_
list_filter.vue
'
;
import
AssigneesListContent
from
'
./assignees_
list_content.vue
'
;
import
ListFilter
from
'
./
list_filter.vue
'
;
import
ListContent
from
'
./
list_content.vue
'
;
export
default
{
components
:
{
LoadingIcon
,
Assignees
ListFilter
,
Assignees
ListContent
,
ListFilter
,
ListContent
,
},
props
:
{
loading
:
{
type
:
Boolean
,
required
:
true
,
},
assignee
s
:
{
item
s
:
{
type
:
Array
,
required
:
true
,
},
listType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
...
...
@@ -26,18 +30,18 @@ export default {
};
},
computed
:
{
filteredAssignees
()
{
if
(
!
this
.
query
)
{
return
this
.
assignees
;
}
filteredItems
()
{
if
(
!
this
.
query
)
return
this
.
items
;
// fuzzaldrinPlus doesn't support filtering
// on multiple keys hence we're using plain JS.
const
query
=
this
.
query
.
toLowerCase
();
return
this
.
assignees
.
filter
((
assignee
)
=>
{
const
name
=
assignee
.
name
.
toLowerCase
();
const
username
=
assignee
.
username
.
toLowerCase
();
return
this
.
items
.
filter
((
item
)
=>
{
const
name
=
item
.
name
?
item
.
name
.
toLowerCase
()
:
item
.
title
.
toLowerCase
();
if
(
this
.
listType
===
'
milestones
'
)
{
return
name
.
indexOf
(
query
)
>
-
1
;
}
const
username
=
item
.
username
.
toLowerCase
();
return
name
.
indexOf
(
query
)
>
-
1
||
username
.
indexOf
(
query
)
>
-
1
;
});
},
...
...
@@ -46,8 +50,8 @@ export default {
handleSearch
(
query
)
{
this
.
query
=
query
;
},
handleItemClick
(
assignee
)
{
this
.
$emit
(
'
onItemSelect
'
,
assignee
);
handleItemClick
(
item
)
{
this
.
$emit
(
'
onItemSelect
'
,
item
);
},
},
};
...
...
@@ -61,12 +65,13 @@ export default {
>
<loading-icon
/>
</div>
<
assignees-
list-filter
<list-filter
@
onSearchInput=
"handleSearch"
/>
<
assignees-
list-content
<list-content
v-if=
"!loading"
:assignees=
"filteredAssignees"
:items=
"filteredItems"
:list-type=
"listType"
@
onItemSelect=
"handleItemClick"
/>
</div>
...
...
ee/app/assets/javascripts/boards/components/
assignees_list/assignees_
list_content.vue
→
ee/app/assets/javascripts/boards/components/
boards_list_selector/
list_content.vue
View file @
e3c376a2
<
script
>
import
AssigneesListItem
from
'
./assignees_list_item.vue
'
;
import
MilestoneListItem
from
'
./milestones_list_item.vue
'
;
export
default
{
components
:
{
AssigneesListItem
,
},
props
:
{
assignee
s
:
{
item
s
:
{
type
:
Array
,
required
:
true
,
},
listType
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
listContentComponent
()
{
return
this
.
listType
===
'
assignees
'
?
AssigneesListItem
:
MilestoneListItem
;
},
},
methods
:
{
handleItemClick
(
assignee
)
{
this
.
$emit
(
'
onItemSelect
'
,
assignee
);
handleItemClick
(
item
)
{
this
.
$emit
(
'
onItemSelect
'
,
item
);
},
},
};
...
...
@@ -22,10 +29,11 @@ export default {
<
template
>
<div
class=
"dropdown-content"
>
<ul>
<assignees-list-item
v-for=
"assignee in assignees"
:key=
"assignee.id"
:assignee=
"assignee"
<component
v-for=
"item in items"
:is=
"listContentComponent"
:key=
"item.id"
:item=
"item"
@
onItemSelect=
"handleItemClick"
/>
</ul>
...
...
ee/app/assets/javascripts/boards/components/
assignees_list/assignees_
list_filter.vue
→
ee/app/assets/javascripts/boards/components/
boards_list_selector/
list_filter.vue
View file @
e3c376a2
File moved
ee/app/assets/javascripts/boards/components/boards_list_selector/milestones_list_item.vue
0 → 100644
View file @
e3c376a2
<
script
>
export
default
{
props
:
{
item
:
{
type
:
Object
,
required
:
true
,
},
},
methods
:
{
handleItemClick
()
{
this
.
$emit
(
'
onItemSelect
'
,
this
.
item
);
},
},
};
</
script
>
<
template
>
<li
class=
"filter-dropdown-item"
@
click=
"handleItemClick"
>
<button
class=
"btn btn-link dropdown-user"
type=
"button"
>
<div
class=
"dropdown-user-details"
>
<div
:title=
"item.title"
>
{{
item
.
title
}}
</div>
</div>
</button>
</li>
</
template
>
ee/app/assets/javascripts/boards/components/boards_selector.js
View file @
e3c376a2
...
...
@@ -3,7 +3,8 @@ import $ from 'jquery';
import
{
throttle
}
from
'
underscore
'
;
import
'
~/boards/stores/boards_store
'
;
import
BoardForm
from
'
./board_form.vue
'
;
import
AssigneesList
from
'
./assignees_list
'
;
import
AssigneeList
from
'
./assignees_list_slector
'
;
import
MilestoneList
from
'
./milestone_list_selector
'
;
(()
=>
{
window
.
gl
=
window
.
gl
||
{};
...
...
@@ -38,12 +39,14 @@ import AssigneesList from './assignees_list';
loading
:
true
,
hasScrollFade
:
false
,
hasAssigneesListMounted
:
false
,
hasMilestoneListMounted
:
false
,
scrollFadeInitialized
:
false
,
boards
:
[],
state
:
Store
.
state
,
throttledSetScrollFade
:
throttle
(
this
.
setScrollFade
,
this
.
throttleDuration
),
contentClientHeight
:
0
,
maxPosition
:
0
,
store
:
gl
.
issueBoards
.
BoardsStore
,
};
},
computed
:
{
...
...
@@ -84,6 +87,7 @@ import AssigneesList from './assignees_list';
created
()
{
this
.
state
.
currentBoard
=
this
.
currentBoard
;
Store
.
state
.
assignees
=
[];
Store
.
state
.
milestones
=
[];
$
(
'
#js-add-list
'
).
on
(
'
hide.bs.dropdown
'
,
this
.
handleDropdownHide
);
$
(
'
.js-new-board-list-tabs
'
).
on
(
'
click
'
,
this
.
handleDropdownTabClick
);
},
...
...
@@ -146,13 +150,14 @@ import AssigneesList from './assignees_list';
$addListEl
.
data
(
'
preventClose
'
,
true
);
if
(
e
.
target
.
dataset
.
action
===
'
tab-assignees
'
&&
!
this
.
hasAssigneesListMounted
)
{
this
.
assigneeList
=
new
AssigneesList
({
propsData
:
{
listAssigneesPath
:
$addListEl
.
find
(
'
.js-new-board-list
'
).
data
(
'
listAssigneesPath
'
),
},
}).
$mount
(
'
.js-assignees-list
'
);
this
.
assigneeList
=
AssigneeList
();
this
.
hasAssigneesListMounted
=
true
;
}
if
(
e
.
target
.
dataset
.
action
===
'
tab-milestones
'
&&
!
this
.
hasMilestoneListMounted
)
{
this
.
milstoneList
=
MilestoneList
();
this
.
hasMilestoneListMounted
=
true
;
}
},
},
});
...
...
ee/app/assets/javascripts/boards/components/milestone_list_selector.js
0 → 100644
View file @
e3c376a2
import
BoardsListSelector
from
'
./boards_list_selector
'
;
export
default
function
()
{
const
$addListEl
=
document
.
querySelector
(
'
#js-add-list
'
);
return
new
BoardsListSelector
({
propsData
:
{
listPath
:
$addListEl
.
querySelector
(
'
.js-new-board-list
'
).
dataset
.
listMilestonePath
,
listType
:
'
milestones
'
,
},
}).
$mount
(
'
.js-milestone-list
'
);
}
ee/app/assets/javascripts/boards/models/list.js
View file @
e3c376a2
/* eslint-disable no-param-reassign */
import
List
from
'
~/boards/models/list
'
;
import
ListAssignee
from
'
~/vue_shared/models/assignee
'
;
import
ListMilestone
from
'
~/boards/models/milestone
'
;
const
EE_TYPES
=
{
promotion
:
{
...
...
@@ -47,7 +48,7 @@ class ListEE extends List {
}
onNewIssueResponse
(
issue
,
data
)
{
issue
.
milestone
=
data
.
milestone
;
issue
.
milestone
=
data
.
milestone
?
new
ListMilestone
(
data
.
milestone
)
:
data
.
milestone
;
issue
.
assignees
=
Array
.
isArray
(
data
.
assignees
)
?
data
.
assignees
.
map
(
assignee
=>
new
ListAssignee
(
assignee
))
:
data
.
assignees
;
...
...
ee/app/assets/stylesheets/pages/boards.scss
View file @
e3c376a2
...
...
@@ -12,7 +12,7 @@
.dropdown
.dropdown-menu.dropdown-menu-tabs
{
padding-top
:
0
;
width
:
240px
;
min-
width
:
240px
;
.dropdown-tabs-list
{
display
:
flex
;
...
...
ee/app/controllers/boards/milestones_controller.rb
0 → 100644
View file @
e3c376a2
module
Boards
class
MilestonesController
<
Boards
::
ApplicationController
def
index
milestones_finder
=
Boards
::
MilestonesFinder
.
new
(
board
,
current_user
)
render
json:
MilestoneSerializer
.
new
.
represent
(
milestones_finder
.
execute
)
end
end
end
ee/app/controllers/ee/boards/lists_controller.rb
View file @
e3c376a2
...
...
@@ -5,12 +5,12 @@ module EE
override
:list_creation_attrs
def
list_creation_attrs
super
+
%i[assignee_id]
super
+
%i[assignee_id
milestone_id
]
end
override
:serialization_attrs
def
serialization_attrs
super
.
merge
(
user:
true
)
super
.
merge
(
user:
true
,
milestone:
true
)
end
end
end
...
...
ee/app/finders/boards/milestones_finder.rb
0 → 100644
View file @
e3c376a2
module
Boards
class
MilestonesFinder
def
initialize
(
board
,
current_user
=
nil
)
@board
=
board
@current_user
=
current_user
end
def
execute
finder_service
.
execute
end
private
def
finder_service
parent
=
@board
.
parent
finder_params
=
if
parent
.
is_a?
(
Group
)
{
group_ids:
parent
.
self_and_ancestors
}
else
{
project_ids:
[
parent
.
id
],
group_ids:
parent
.
group
&
.
self_and_ancestors
}
end
::
MilestonesFinder
.
new
(
finder_params
)
end
end
end
ee/app/helpers/ee/boards_helper.rb
View file @
e3c376a2
...
...
@@ -8,7 +8,8 @@ module EE
override
:board_list_data
def
board_list_data
super
.
merge
(
list_assignees_path:
board_users_path
(
board
,
:json
))
super
.
merge
(
list_milestone_path:
board_milestones_path
(
board
,
:json
),
list_assignees_path:
board_users_path
(
board
,
:json
))
end
override
:board_data
...
...
ee/app/models/ee/list.rb
View file @
e3c376a2
...
...
@@ -10,31 +10,34 @@ module EE
end
base
.
belongs_to
:user
base
.
belongs_to
:milestone
base
.
validates
:user
,
presence:
true
,
if: :assignee?
base
.
validates
:milestone
,
presence:
true
,
if: :milestone?
base
.
validates
:user_id
,
uniqueness:
{
scope: :board_id
},
if: :assignee?
base
.
validates
:milestone_id
,
uniqueness:
{
scope: :board_id
},
if: :milestone?
base
.
validates
:list_type
,
exclusion:
{
in:
%w[assignee]
,
message:
_
(
'Assignee
board
s not available with your current license'
)
},
exclusion:
{
in:
%w[assignee]
,
message:
_
(
'Assignee
list
s not available with your current license'
)
},
unless:
->
{
board
&
.
parent
&
.
feature_available?
(
:board_assignee_lists
)
}
base
.
validates
:list_type
,
exclusion:
{
in:
%w[milestone]
,
message:
_
(
'Milestone lists not available with your current license'
)
},
unless:
->
{
board
&
.
parent
&
.
feature_available?
(
:board_milestone_lists
)
}
end
def
assignee
=
(
user
)
self
.
user
=
user
end
override
:destroyable?
def
destroyable?
assignee?
||
super
end
override
:movable?
def
movable?
assignee?
||
super
end
override
:title
def
title
assignee?
?
user
.
to_reference
:
super
case
list_type
when
'assignee'
user
.
to_reference
when
'milestone'
milestone
.
title
else
super
end
end
override
:as_json
...
...
@@ -43,16 +46,20 @@ module EE
if
options
.
key?
(
:user
)
json
[
:user
]
=
UserSerializer
.
new
.
represent
(
user
).
as_json
end
if
options
.
key?
(
:milestone
)
json
[
:milestone
]
=
MilestoneSerializer
.
new
.
represent
(
milestone
).
as_json
end
end
end
module
ClassMethods
def
destroyable_types
super
+
[
:assignee
]
super
+
[
:assignee
,
:milestone
]
end
def
movable_types
super
+
[
:assignee
]
super
+
[
:assignee
,
:milestone
]
end
end
end
...
...
ee/app/models/license.rb
View file @
e3c376a2
...
...
@@ -38,6 +38,7 @@ class License < ActiveRecord::Base
admin_audit_log
auditor_user
board_assignee_lists
board_milestone_lists
cross_project_pipelines
email_additional_text
db_load_balancing
...
...
ee/app/serializers/milestone_serializer.rb
0 → 100644
View file @
e3c376a2
class
MilestoneSerializer
<
BaseSerializer
entity
API
::
Entities
::
Milestone
end
ee/app/services/ee/boards/issues/create_service.rb
View file @
e3c376a2
...
...
@@ -7,10 +7,12 @@ module EE
override
:issue_params
def
issue_params
assignee_ids
=
Array
(
list
.
user_id
||
board
.
assignee
&
.
id
)
milestone_id
=
list
.
milestone_id
||
board
.
milestone_id
{
label_ids:
[
list
.
label_id
,
*
board
.
label_ids
],
weight:
board
.
weight
,
milestone_id:
board
.
milestone_id
,
milestone_id:
milestone_id
,
# This can be removed when boards have multiple assignee support.
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/3786
assignee_ids:
assignee_ids
...
...
ee/app/services/ee/boards/issues/list_service.rb
View file @
e3c376a2
...
...
@@ -6,11 +6,19 @@ module EE
override
:filter
def
filter
(
issues
)
issues
=
without_board_assignees
(
issues
)
unless
list
&
.
movable?
||
list
&
.
closed?
return
super
unless
list
&
.
assignee?
unless
list
&
.
movable?
||
list
&
.
closed?
issues
=
without_assignees_from_lists
(
issues
)
issues
=
without_milestones_from_lists
(
issues
)
end
with_assignee
(
super
)
case
list
&
.
list_type
when
'assignee'
with_assignee
(
super
)
when
'milestone'
with_milestone
(
super
)
else
super
end
end
override
:issues_label_links
...
...
@@ -24,30 +32,49 @@ module EE
private
def
all_assignee_lists
if
parent
.
feature_available?
(
:board_assignee_lists
)
board
.
lists
.
assignee
.
where
.
not
(
user_id:
nil
)
else
::
List
.
none
end
end
def
all_milestone_lists
if
parent
.
feature_available?
(
:board_milestone_lists
)
board
.
lists
.
milestone
.
where
.
not
(
milestone_id:
nil
)
else
::
List
.
none
end
end
def
without_assignees_from_lists
(
issues
)
return
issues
if
all_assignee_lists
.
empty?
issues
.
where
.
not
(
id:
issues
.
joins
(
:assignees
).
where
(
users:
{
id:
all_assignee_lists
.
select
(
:user_id
)
}))
end
override
:metadata_fields
def
metadata_fields
super
.
merge
(
total_weight:
'COALESCE(SUM(weight), 0)'
)
end
def
board_assignee_ids
@board_assignee_ids
||=
if
parent
.
feature_available?
(
:board_assignee_lists
)
board
.
lists
.
movable
.
pluck
(
:user_id
).
compact
else
[]
end
end
def
without_board_assignees
(
issues
)
return
issues
unless
board_assignee_ids
.
any?
def
without_milestones_from_lists
(
issues
)
return
issues
if
all_milestone_lists
.
empty?
issues
.
where
.
not
(
id:
issues
.
joins
(
:assignees
).
where
(
users:
{
id:
board_assignee_ids
}))
issues
.
where
(
"milestone_id NOT IN (?) OR milestone_id IS NULL"
,
all_milestone_lists
.
select
(
:milestone_id
))
end
def
with_assignee
(
issues
)
issues
.
assigned_to
(
list
.
user
)
end
def
with_milestone
(
issues
)
issues
.
where
(
milestone_id:
list
.
milestone_id
)
end
# Prevent filtering by milestone stubs
# like Milestone::Upcoming, Milestone::Started etc
def
has_valid_milestone?
...
...
ee/app/services/ee/boards/issues/move_service.rb
View file @
e3c376a2
...
...
@@ -14,7 +14,7 @@ module EE
args
.
delete
(
:remove_label_ids
)
end
args
.
merge
(
assignee_ids:
assignee_id
s
(
issue
))
args
.
merge
(
list_movement_arg
s
(
issue
))
end
def
both_are_list_type?
(
type
)
...
...
@@ -27,6 +27,25 @@ module EE
moving_from_list
.
list_type
==
moving_to_list
.
list_type
end
def
list_movement_args
(
issue
)
assignee_ids
=
assignee_ids
(
issue
)
milestone_id
=
milestone_id
(
issue
)
{
assignee_ids:
assignee_ids
,
milestone_id:
milestone_id
}
end
def
milestone_id
(
issue
)
# We want to nullify the issue milestone.
return
if
moving_to_list
.
backlog?
# Moving to a list which is not a 'milestone list' will keep
# the already existent milestone.
[
issue
.
milestone_id
,
moving_to_list
.
milestone_id
].
compact
.
last
end
def
assignee_ids
(
issue
)
assignees
=
(
issue
.
assignee_ids
+
[
moving_to_list
.
user_id
]).
compact
...
...
ee/app/services/ee/boards/lists/create_service.rb
View file @
e3c376a2
...
...
@@ -6,9 +6,15 @@ module EE
override
:type
def
type
return
:assignee
if
params
.
keys
.
include?
(
'assignee_id'
)
super
# We don't ever expect to have more than one list
# type param at once.
if
params
.
key?
(
'assignee_id'
)
:assignee
elsif
params
.
key?
(
'milestone_id'
)
:milestone
else
super
end
end
override
:target
...
...
@@ -17,17 +23,28 @@ module EE
case
type
when
:assignee
find_user
(
board
)
when
:milestone
find_milestone
(
board
)
else
super
end
end
end
def
find_milestone
(
board
)
milestones
=
milestone_finder
(
board
).
execute
milestones
.
find
(
params
[
'milestone_id'
])
end
def
find_user
(
board
)
user_ids
=
user_finder
(
board
).
execute
.
select
(
:user_id
)
::
User
.
where
(
id:
user_ids
).
find
(
params
[
'assignee_id'
])
end
def
milestone_finder
(
board
)
@milestone_finder
||=
::
Boards
::
MilestonesFinder
.
new
(
board
,
current_user
)
end
def
user_finder
(
board
)
@user_finder
||=
::
Boards
::
UsersFinder
.
new
(
board
,
current_user
)
end
...
...
ee/app/services/ee/boards/lists/list_service.rb
View file @
e3c376a2
...
...
@@ -2,13 +2,35 @@ module EE
module
Boards
module
Lists
module
ListService
# When adding a new licensed type, make sure to also add
# it on license.rb with the pattern "board_<list_type>_lists"
LICENSED_LIST_TYPES
=
[
:assignee
,
:milestone
].
freeze
extend
::
Gitlab
::
Utils
::
Override
override
:execute
def
execute
(
board
)
return
super
if
board
.
parent
.
feature_available?
(
:board_assignee_lists
)
not_available_lists
=
list_type_features_availability
(
board
).
select
{
|
_
,
available
|
!
available
}
if
not_available_lists
.
any?
super
.
where
.
not
(
list_type:
not_available_lists
.
keys
)
else
super
end
end
private
def
list_type_features_availability
(
board
)
parent
=
board
.
parent
super
.
where
.
not
(
list_type:
::
List
.
list_types
[
:assignee
])
{}.
tap
do
|
hash
|
LICENSED_LIST_TYPES
.
each
do
|
list_type
|
list_type_key
=
::
List
.
list_types
[
list_type
]
hash
[
list_type_key
]
=
parent
&
.
feature_available?
(
:"board_
#{
list_type
}
_lists"
)
end
end
end
end
end
...
...
ee/app/views/shared/boards/components/_list_milestone.html.haml
0 → 100644
View file @
e3c376a2
=
content_tag
(
:span
,
sprite_icon
(
'timer'
,
size:
16
),
{
"v-if"
:
"list.milestone"
,
"aria-hidden"
:
"true"
,
"class"
:
"append-right-5"
})
ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml
View file @
e3c376a2
-
if
board
.
parent
.
feature_available?
(
:board_assignee_lists
)
-
assignee_lists_available
=
board
.
parent
.
feature_available?
(
:board_assignee_lists
)
-
milestone_lists_available
=
board
.
parent
.
feature_available?
(
:board_milestone_lists
)
-
if
assignee_lists_available
||
milestone_lists_available
.dropdown.prepend-left-10
#js-add-list
%button
.btn.btn-create.btn-inverted.d-flex.js-new-board-list
{
type:
"button"
,
data:
board_list_data
}
%span
Add list
...
...
@@ -7,17 +10,30 @@
%ul
.nav.nav-tabs.dropdown-tabs-list.js-new-board-list-tabs
{
role:
'tablist'
}
%li
.nav-item.dropdown-tab-item.js-tab-button-labels
%a
.active
{
href:
'#'
,
role:
'tab'
,
data:
{
is_link:
'true'
,
toggle:
'tab'
,
action:
'tab-labels'
,
target:
'#tab-labels'
}
}
Label list
%li
.nav-item.dropdown-tab-item.js-tab-button-assignees
%a
{
href:
'#'
,
role:
'tab'
,
data:
{
is_link:
'true'
,
toggle:
'tab'
,
action:
'tab-assignees'
,
target:
'#tab-assignees'
}
}
Assignee list
Label
-
if
assignee_lists_available
%li
.nav-item.dropdown-tab-item.js-tab-button-assignees
%a
{
href:
'#'
,
role:
'tab'
,
data:
{
is_link:
'true'
,
toggle:
'tab'
,
action:
'tab-assignees'
,
target:
'#tab-assignees'
}
}
Assignee
-
if
milestone_lists_available
%li
.nav-item.dropdown-tab-item.js-tab-button-milestones
%a
{
href:
'#'
,
role:
'tab'
,
data:
{
is_link:
'true'
,
toggle:
'tab'
,
action:
'tab-milestones'
,
target:
'#tab-milestones'
}
}
Milestone
.tab-content
#tab-labels
.tab-pane.tab-pane-labels.active.js-tab-container-labels
{
role:
'tabpanel'
}
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
show_title:
false
,
show_footer:
true
,
show_create:
true
,
show_boards_content:
true
,
content_title:
_
(
'Label lists show all issues with the selected label.'
)
}
-
if
can?
(
current_user
,
:admin_label
,
board
.
parent
)
=
render
partial:
"shared/issuable/label_page_create"
,
locals:
{
show_close:
false
}
#tab-assignees
.tab-pane.tab-pane-assignees.js-tab-container-assignees
{
role:
'tabpanel'
}
=
render
partial:
"shared/issuable/assignee_page_default"
-
if
assignee_lists_available
#tab-assignees
.tab-pane.tab-pane-assignees.js-tab-container-assignees
{
role:
'tabpanel'
}
=
render
partial:
"shared/issuable/assignee_page_default"
-
if
milestone_lists_available
#tab-milestones
.tab-pane.tab-pane-milestones.js-tab-container-milestones
{
role:
'tabpanel'
}
=
render
partial:
"shared/issuable/milestone_page_default"
=
dropdown_loading
-
else
=
render_ce
'shared/issuable/board_create_list_dropdown'
,
board:
board
ee/app/views/shared/issuable/_milestone_page_default.html.haml
0 → 100644
View file @
e3c376a2
.dropdown-page-one
.issue-board-dropdown-content
%p
=
_
(
'Milestone lists show all issues from the selected milestone.'
)
.js-milestone-list
ee/changelogs/unreleased/6469-issue-board-milestone-lists.yml
0 → 100644
View file @
e3c376a2
---
title
:
Add support for milestones lists on the issue boards
merge_request
:
6615
author
:
type
:
added
ee/changelogs/unreleased/ccr-6397-add_board_assignee_list.yml
0 → 100644
View file @
e3c376a2
---
title
:
Allow creating assignee lists via API
merge_request
:
author
:
type
:
added
ee/db/migrate/20180724161450_add_milestone_to_lists.rb
0 → 100644
View file @
e3c376a2
class
AddMilestoneToLists
<
ActiveRecord
::
Migration
DOWNTIME
=
false
def
change
add_reference
:lists
,
:milestone
,
index:
true
,
foreign_key:
{
on_delete: :cascade
}
end
end
ee/lib/ee/api/boards_responses.rb
View file @
e3c376a2
...
...
@@ -34,6 +34,41 @@ module EE
end
end
def
create_list_params
params
.
slice
(
:label_id
,
:milestone_id
,
:assignee_id
)
end
# Overrides API::BoardsResponses authorize_list_type_resource!
def
authorize_list_type_resource!
if
params
[
:label_id
]
&&
!
available_labels_for
(
board_parent
).
exists?
(
params
[
:label_id
])
render_api_error!
({
error:
'Label not found!'
},
400
)
end
if
milestone_id
=
params
[
:milestone_id
]
milestones
=
::
Boards
::
MilestonesFinder
.
new
(
board
,
current_user
).
execute
unless
milestones
.
find_by
(
id:
milestone_id
)
render_api_error!
({
error:
'Milestone not found!'
},
400
)
end
end
if
assignee_id
=
params
[
:assignee_id
]
users
=
::
Boards
::
UsersFinder
.
new
(
board
,
current_user
).
execute
unless
users
.
find_by
(
user_id:
assignee_id
)
render_api_error!
({
error:
'User not found!'
},
400
)
end
end
end
# Overrides API::BoardsResponses list_creation_params
params
:list_creation_params
do
optional
:label_id
,
type:
Integer
,
desc:
'The ID of an existing label'
optional
:milestone_id
,
type:
Integer
,
desc:
'The ID of an existing milestone'
optional
:assignee_id
,
type:
Integer
,
desc:
'The ID of an assignee'
exactly_one_of
:label_id
,
:milestone_id
,
:assignee_id
end
params
:update_params
do
optional
:name
,
type:
String
,
desc:
'The board name'
optional
:assignee_id
,
type:
Integer
,
desc:
'The ID of a user to associate with board'
...
...
ee/lib/ee/api/entities.rb
View file @
e3c376a2
...
...
@@ -100,6 +100,15 @@ module EE
end
end
module
List
extend
ActiveSupport
::
Concern
prepended
do
expose
:milestone
,
using:
::
API
::
Entities
::
Milestone
,
if:
->
(
entity
,
_
)
{
entity
.
milestone?
}
expose
:user
,
as: :assignee
,
using:
::
API
::
Entities
::
UserSafe
,
if:
->
(
entity
,
_
)
{
entity
.
assignee?
}
end
end
module
ApplicationSetting
extend
ActiveSupport
::
Concern
...
...
ee/spec/controllers/boards/milestones_controller_spec.rb
0 → 100644
View file @
e3c376a2
require
'spec_helper'
describe
Boards
::
MilestonesController
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
before
do
create
(
:milestone
,
project:
project
)
project
.
add_maintainer
(
user
)
sign_in
(
user
)
end
describe
'GET index'
do
it
'returns a list of all milestones of board parent'
do
get
:index
,
board_id:
board
.
to_param
,
format: :json
parsed_response
=
JSON
.
parse
(
response
.
body
)
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
.
content_type
).
to
eq
(
'application/json'
)
expect
(
parsed_response
).
to
all
(
match_schema
(
'entities/milestone'
,
dir:
'ee'
))
expect
(
parsed_response
.
size
).
to
eq
(
1
)
end
end
end
ee/spec/factories/lists.rb
View file @
e3c376a2
...
...
@@ -4,4 +4,11 @@ FactoryBot.define do
label
nil
user
end
factory
:milestone_list
,
parent: :list
do
list_type
:milestone
label
nil
user
nil
milestone
end
end
ee/spec/finders/boards/milestones_finder_spec.rb
0 → 100644
View file @
e3c376a2
require
'spec_helper'
describe
Boards
::
MilestonesFinder
do
describe
'#execute'
do
let
(
:group
)
{
create
(
:group
)
}
let
(
:nested_group
)
{
create
(
:group
,
parent:
group
)
}
let
(
:deep_nested_group
)
{
create
(
:group
,
parent:
group
)
}
let
(
:group_project
)
{
create
(
:project
,
group:
group
)
}
let
(
:nested_group_project
)
{
create
(
:project
,
group:
nested_group
)
}
let!
(
:group_milestone
)
{
create
(
:milestone
,
group:
group
,
project:
nil
)
}
let!
(
:group_project_milestone
)
{
create
(
:milestone
,
project:
group_project
,
group:
nil
)
}
let!
(
:nested_group_project_milestone
)
{
create
(
:milestone
,
project:
nested_group_project
,
group:
nil
)
}
let!
(
:nested_group_milestone
)
{
create
(
:milestone
,
group:
nested_group
,
project:
nil
)
}
let!
(
:deep_nested_group_milestone
)
{
create
(
:milestone
,
group:
deep_nested_group
,
project:
nil
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:finder
)
{
described_class
.
new
(
board
,
user
)
}
context
'when project board'
,
:nested_groups
do
let
(
:board
)
{
create
(
:board
,
project:
nested_group_project
,
group:
nil
)
}
it
'returns milestones from board project and ancestors groups'
do
group
.
add_developer
(
user
)
results
=
finder
.
execute
expect
(
results
).
to
contain_exactly
(
nested_group_project_milestone
,
nested_group_milestone
,
group_milestone
)
end
end
context
'when group board'
,
:nested_groups
do
let
(
:board
)
{
create
(
:board
,
project:
nil
,
group:
nested_group
)
}
it
'returns milestones from board group and its ancestors'
do
group
.
add_developer
(
user
)
results
=
finder
.
execute
expect
(
results
).
to
contain_exactly
(
group_milestone
,
nested_group_milestone
)
end
end
end
end
ee/spec/fixtures/api/schemas/entities/milestone.json
0 → 100644
View file @
e3c376a2
{
"type"
:
"object"
,
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"iid"
:
{
"type"
:
"integer"
},
"project_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"group_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"title"
:
{
"type"
:
"string"
},
"description"
:
{
"type"
:
[
"string"
,
"null"
]
},
"state"
:
{
"type"
:
"string"
},
"created_at"
:
{
"type"
:
"string"
},
"updated_at"
:
{
"type"
:
"string"
},
"start_date"
:
{
"type"
:
[
"date"
,
"null"
]
},
"due_date"
:
{
"type"
:
[
"date"
,
"null"
]
},
"web_url"
:
{
"type"
:
"string"
}
},
"additionalProperties"
:
false
}
ee/spec/fixtures/api/schemas/list.json
View file @
e3c376a2
...
...
@@ -48,4 +48,4 @@
}
}
]
}
\ No newline at end of file
}
ee/spec/javascripts/boards/components/
assignees_list
/assignees_list_item_spec.js
→
ee/spec/javascripts/boards/components/
board_list_selector
/assignees_list_item_spec.js
View file @
e3c376a2
import
Vue
from
'
vue
'
;
import
AssigneesListItemComponent
from
'
ee/boards/components/
assignees_list
/assignees_list_item.vue
'
;
import
AssigneesListItemComponent
from
'
ee/boards/components/
boards_list_selector
/assignees_list_item.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockAssigneesList
}
from
'
spec/boards/mock_data
'
;
...
...
@@ -9,7 +9,7 @@ const createComponent = () => {
const
Component
=
Vue
.
extend
(
AssigneesListItemComponent
);
return
mountComponent
(
Component
,
{
assignee
:
mockAssigneesList
[
0
],
item
:
mockAssigneesList
[
0
],
});
};
...
...
ee/spec/javascripts/boards/components/
assignees_list/assignees_list
_spec.js
→
ee/spec/javascripts/boards/components/
board_list_selector/board_list_selector
_spec.js
View file @
e3c376a2
...
...
@@ -2,19 +2,20 @@ import '~/boards/stores/boards_store';
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
AssigneesListComponent
from
'
ee/boards/components/assignees_list
/
'
;
import
BoardListSelectorComponent
from
'
ee/boards/components/boards_list_selector
/
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockAssigneesList
}
from
'
spec/boards/mock_data
'
;
import
{
TEST_HOST
}
from
'
spec/test_constants
'
;
describe
(
'
AssigneesList
Component
'
,
()
=>
{
describe
(
'
BoardListSelector
Component
'
,
()
=>
{
const
dummyEndpoint
=
`
${
TEST_HOST
}
/users.json`
;
const
createComponent
=
()
=>
mountComponent
(
AssigneesListComponent
,
{
listAssigneesPath
:
dummyEndpoint
,
mountComponent
(
BoardListSelectorComponent
,
{
listPath
:
dummyEndpoint
,
listType
:
'
assignees
'
,
});
let
vm
;
...
...
@@ -43,13 +44,13 @@ describe('AssigneesListComponent', () => {
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
load
Assignees
'
,
()
=>
{
describe
(
'
load
List
'
,
()
=>
{
it
(
'
calls axios.get and sets response to store.state.assignees
'
,
done
=>
{
mock
.
onGet
(
dummyEndpoint
).
reply
(
200
,
mockAssigneesList
);
gl
.
issueBoards
.
BoardsStore
.
state
.
assignees
=
[];
vm
.
load
Assignees
()
.
load
List
()
.
then
(()
=>
{
expect
(
vm
.
loading
).
toBe
(
false
);
expect
(
vm
.
store
.
state
.
assignees
.
length
).
toBe
(
mockAssigneesList
.
length
);
...
...
@@ -63,7 +64,7 @@ describe('AssigneesListComponent', () => {
gl
.
issueBoards
.
BoardsStore
.
state
.
assignees
=
mockAssigneesList
;
vm
.
load
Assignees
()
.
load
List
()
.
then
(()
=>
{
expect
(
axios
.
get
).
not
.
toHaveBeenCalled
();
})
...
...
@@ -76,7 +77,7 @@ describe('AssigneesListComponent', () => {
gl
.
issueBoards
.
BoardsStore
.
state
.
assignees
=
[];
vm
.
load
Assignees
()
.
load
List
()
.
then
(()
=>
{
expect
(
vm
.
loading
).
toBe
(
false
);
expect
(
document
.
querySelector
(
'
.flash-text
'
).
innerText
.
trim
()).
toBe
(
...
...
ee/spec/javascripts/boards/components/
assignees_list/assignees_
list_container_spec.js
→
ee/spec/javascripts/boards/components/
board_list_selector/
list_container_spec.js
View file @
e3c376a2
import
Vue
from
'
vue
'
;
import
AssigneesListContainerComponent
from
'
ee/boards/components/assignees_list/assignees_
list_container.vue
'
;
import
ListContainerComponent
from
'
ee/boards/components/boards_list_selector/
list_container.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockAssigneesList
}
from
'
spec/boards/mock_data
'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
Assignees
ListContainerComponent
);
const
Component
=
Vue
.
extend
(
ListContainerComponent
);
return
mountComponent
(
Component
,
{
loading
:
false
,
assignees
:
mockAssigneesList
,
items
:
mockAssigneesList
,
listType
:
'
assignees
'
,
});
};
describe
(
'
Assignees
ListContainerComponent
'
,
()
=>
{
describe
(
'
ListContainerComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
...
...
@@ -26,26 +27,26 @@ describe('AssigneesListContainerComponent', () => {
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
filtered
Assignee
s
'
,
()
=>
{
describe
(
'
filtered
Item
s
'
,
()
=>
{
it
(
'
returns assignees list as it is when `query` is empty
'
,
()
=>
{
vm
.
query
=
''
;
expect
(
vm
.
filtered
Assignee
s
.
length
).
toBe
(
mockAssigneesList
.
length
);
expect
(
vm
.
filtered
Item
s
.
length
).
toBe
(
mockAssigneesList
.
length
);
});
it
(
'
returns filtered assignees list as it is when `query` has name
'
,
()
=>
{
const
assignee
=
mockAssigneesList
[
0
];
vm
.
query
=
assignee
.
name
;
expect
(
vm
.
filtered
Assignee
s
.
length
).
toBe
(
1
);
expect
(
vm
.
filtered
Assignee
s
[
0
].
name
).
toBe
(
assignee
.
name
);
expect
(
vm
.
filtered
Item
s
.
length
).
toBe
(
1
);
expect
(
vm
.
filtered
Item
s
[
0
].
name
).
toBe
(
assignee
.
name
);
});
it
(
'
returns filtered assignees list as it is when `query` has username
'
,
()
=>
{
const
assignee
=
mockAssigneesList
[
0
];
vm
.
query
=
assignee
.
username
;
expect
(
vm
.
filtered
Assignee
s
.
length
).
toBe
(
1
);
expect
(
vm
.
filtered
Assignee
s
[
0
].
username
).
toBe
(
assignee
.
username
);
expect
(
vm
.
filtered
Item
s
.
length
).
toBe
(
1
);
expect
(
vm
.
filtered
Item
s
[
0
].
username
).
toBe
(
assignee
.
username
);
});
});
});
...
...
ee/spec/javascripts/boards/components/
assignees_list/assignees_
list_content_spec.js
→
ee/spec/javascripts/boards/components/
board_list_selector/
list_content_spec.js
View file @
e3c376a2
import
Vue
from
'
vue
'
;
import
AssigneesListContentComponent
from
'
ee/boards/components/assignees_list/assignees_
list_content.vue
'
;
import
ListContentComponent
from
'
ee/boards/components/boards_list_selector/
list_content.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockAssigneesList
}
from
'
spec/boards/mock_data
'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
Assignees
ListContentComponent
);
const
Component
=
Vue
.
extend
(
ListContentComponent
);
return
mountComponent
(
Component
,
{
assignees
:
mockAssigneesList
,
items
:
mockAssigneesList
,
listType
:
'
assignees
'
,
});
};
describe
(
'
Assignees
ListContentComponent
'
,
()
=>
{
describe
(
'
ListContentComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
...
...
ee/spec/javascripts/boards/components/
assignees_list/assignees_
list_filter_spec.js
→
ee/spec/javascripts/boards/components/
board_list_selector/
list_filter_spec.js
View file @
e3c376a2
import
Vue
from
'
vue
'
;
import
AssigneesListFilterComponent
from
'
ee/boards/components/assignees_list/assignees_
list_filter.vue
'
;
import
ListFilterComponent
from
'
ee/boards/components/boards_list_selector/
list_filter.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
Assignees
ListFilterComponent
);
const
Component
=
Vue
.
extend
(
ListFilterComponent
);
return
mountComponent
(
Component
);
};
describe
(
'
Assignees
ListFilterComponent
'
,
()
=>
{
describe
(
'
ListFilterComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
...
...
ee/spec/models/ee/list_spec.rb
View file @
e3c376a2
require
'rails_helper'
describe
List
do
context
'when it is an assignee type'
do
let
(
:board
)
{
create
(
:board
)
}
let
(
:board
)
{
create
(
:board
)
}
describe
'relationships'
do
it
{
is_expected
.
to
belong_to
(
:user
)
}
it
{
is_expected
.
to
belong_to
(
:milestone
)
}
end
context
'when it is an assignee type'
do
subject
{
described_class
.
new
(
list_type: :assignee
,
board:
board
)
}
it
{
is_expected
.
to
be_destroyable
}
it
{
is_expected
.
to
be_movable
}
describe
'relationships'
do
it
{
is_expected
.
to
belong_to
(
:user
)
}
end
describe
'validations'
do
it
{
is_expected
.
to
validate_presence_of
(
:user
)
}
end
...
...
@@ -25,4 +26,30 @@ describe List do
end
end
end
context
'when it is a milestone type'
do
let
(
:milestone
)
{
build
(
:milestone
,
title:
'awesome-release'
)
}
subject
{
described_class
.
new
(
list_type: :milestone
,
milestone:
milestone
,
board:
board
)
}
it
{
is_expected
.
to
be_destroyable
}
it
{
is_expected
.
to
be_movable
}
describe
'validations'
do
it
{
is_expected
.
to
validate_presence_of
(
:milestone
)
}
it
'is invalid when feature is not available'
do
stub_licensed_features
(
board_milestone_lists:
false
)
expect
(
subject
).
to
be_invalid
expect
(
subject
.
errors
[
:list_type
])
.
to
contain_exactly
(
'Milestone lists not available with your current license'
)
end
end
describe
'#title'
do
it
'returns the milestone title'
do
expect
(
subject
.
title
).
to
eq
(
'awesome-release'
)
end
end
end
end
ee/spec/requests/api/boards_spec.rb
View file @
e3c376a2
...
...
@@ -7,4 +7,11 @@ describe API::Boards do
set
(
:board
)
{
create
(
:board
,
project:
board_parent
,
milestone:
milestone
)
}
it_behaves_like
'multiple and scoped issue boards'
,
"/projects/:id/boards"
describe
'POST /projects/:id/boards/:board_id/lists'
do
let
(
:url
)
{
"/projects/
#{
board_parent
.
id
}
/boards/
#{
board
.
id
}
/lists"
}
it_behaves_like
'milestone board list'
it_behaves_like
'assignee board list'
end
end
ee/spec/requests/api/group_boards_spec.rb
View file @
e3c376a2
...
...
@@ -46,4 +46,11 @@ describe API::GroupBoards do
it_behaves_like
'group and project boards'
,
"/groups/:id/boards"
,
true
it_behaves_like
'multiple and scoped issue boards'
,
"/groups/:id/boards"
describe
'POST /groups/:id/boards/:board_id/lists'
do
let
(
:url
)
{
"/groups/
#{
board_parent
.
id
}
/boards/
#{
board
.
id
}
/lists"
}
it_behaves_like
'milestone board list'
it_behaves_like
'assignee board list'
end
end
ee/spec/services/ee/boards/issues/create_service_spec.rb
View file @
e3c376a2
...
...
@@ -6,19 +6,46 @@ describe Boards::Issues::CreateService do
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:label
)
{
create
(
:label
,
project:
project
,
name:
'in-progress'
)
}
let
(
:list
)
{
create
(
:list
,
board:
board
,
user:
user
,
list_type:
List
.
list_types
[
:assignee
],
position:
0
)
}
subject
(
:service
)
{
described_class
.
new
(
board
.
parent
,
project
,
user
,
board_id:
board
.
id
,
list_id:
list
.
id
,
title:
'New issue'
)
}
subject
(
:service
)
do
described_class
.
new
(
board
.
parent
,
project
,
user
,
board_id:
board
.
id
,
list_id:
list
.
id
,
title:
'New issue'
)
end
before
do
stub_licensed_features
(
board_assignee_lists:
true
)
project
.
add_developer
(
user
)
end
it
'assigns the issue to the List assignee'
do
issue
=
service
.
execute
context
'assignees list'
do
before
do
stub_licensed_features
(
board_assignee_lists:
true
)
end
let
(
:list
)
do
create
(
:list
,
board:
board
,
user:
user
,
list_type:
List
.
list_types
[
:assignee
],
position:
0
)
end
it
'assigns the issue to the List assignee'
do
issue
=
service
.
execute
expect
(
issue
.
assignees
).
to
eq
([
user
])
end
end
context
'milestone list'
do
before
do
stub_licensed_features
(
board_milestone_lists:
true
)
end
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
let
(
:list
)
do
create
(
:list
,
board:
board
,
milestone:
milestone
,
list_type:
List
.
list_types
[
:milestone
],
position:
0
)
end
it
'assigns the issue to the list milestone'
do
issue
=
service
.
execute
expect
(
issue
.
assignees
).
to
eq
([
user
])
expect
(
issue
.
milestone
).
to
eq
(
milestone
)
end
end
end
end
ee/spec/services/ee/boards/issues/list_service_spec.rb
View file @
e3c376a2
...
...
@@ -20,6 +20,7 @@ describe Boards::Issues::ListService, services: true do
let
(
:p3
)
{
create
(
:group_label
,
title:
'P3'
,
group:
group
)
}
let
(
:user_list
)
{
create
(
:user_list
,
board:
board
,
position:
2
)
}
let
(
:milestone_list
)
{
create
(
:milestone_list
,
board:
board
,
position:
3
)
}
let
(
:backlog
)
{
create
(
:backlog_list
,
board:
board
)
}
let
(
:list1
)
{
create
(
:list
,
board:
board
,
label:
development
,
position:
0
)
}
let
(
:list2
)
{
create
(
:list
,
board:
board
,
label:
testing
,
position:
1
)
}
...
...
@@ -44,11 +45,36 @@ describe Boards::Issues::ListService, services: true do
let
(
:parent
)
{
group
}
before
do
stub_licensed_features
(
board_assignee_lists:
true
)
stub_licensed_features
(
board_assignee_lists:
true
,
board_milestone_lists:
true
)
parent
.
add_developer
(
user
)
opened_issue3
.
assignees
.
push
(
user_list
.
user
)
end
context
'milestone lists'
do
let!
(
:milestone_issue
)
{
create
(
:labeled_issue
,
project:
project
,
milestone:
milestone_list
.
milestone
,
labels:
[
p3
])
}
it
'returns issues from milestone persisted in the list'
do
params
=
{
board_id:
board
.
id
,
id:
milestone_list
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
contain_exactly
(
milestone_issue
)
end
context
'backlog list context'
do
it
'returns issues without milestones and without milestones from other lists'
do
params
=
{
board_id:
board
.
id
,
id:
backlog
.
id
}
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
contain_exactly
(
opened_issue1
,
# milestone from this issue is not in a list
opened_issue2
,
# milestone from this issue is not in a list
reopened_issue1
)
# has no milestone
end
end
end
context
'#metadata'
do
it
'returns issues count and weight for list'
do
params
=
{
board_id:
board
.
id
,
id:
backlog
.
id
}
...
...
@@ -72,7 +98,7 @@ describe Boards::Issues::ListService, services: true do
end
context
'when list_id is missing'
do
context
'when board
does not have a
milestone'
do
context
'when board
is not scoped by
milestone'
do
it
'returns opened issues without board labels and assignees applied'
do
params
=
{
board_id:
board
.
id
}
...
...
@@ -82,14 +108,15 @@ describe Boards::Issues::ListService, services: true do
end
end
context
'when board
have a
milestone'
do
context
'when board
is scoped by
milestone'
do
it
'returns opened issues without board labels, assignees, or milestone applied'
do
params
=
{
board_id:
board
.
id
}
board
.
update_attribute
(
:milestone
,
m1
)
issues
=
described_class
.
new
(
parent
,
user
,
params
).
execute
expect
(
issues
).
to
match_array
([
opened_issue2
,
list1_issue2
,
reopened_issue1
,
opened_issue1
])
expect
(
issues
)
.
to
match_array
([
opened_issue2
,
list1_issue2
,
reopened_issue1
,
opened_issue1
])
end
context
'when milestone is predefined'
do
...
...
ee/spec/services/ee/boards/issues/move_service_spec.rb
View file @
e3c376a2
This diff is collapsed.
Click to expand it.
ee/spec/services/ee/boards/lists/create_service_spec.rb
View file @
e3c376a2
...
...
@@ -2,27 +2,46 @@ require 'spec_helper'
describe
Boards
::
Lists
::
CreateService
do
describe
'#execute'
do
let
(
:parent
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
parent
)
}
let
(
:label
)
{
create
(
:label
,
project:
parent
,
name:
'in-progress'
)
}
let
(
:project
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:other_user
)
{
create
(
:user
)
}
context
'when assignee_id param is sent'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:other_user
)
{
create
(
:user
)
}
subject
(
:service
)
{
described_class
.
new
(
project
,
user
,
'assignee_id'
=>
other_user
.
id
)
}
subject
(
:service
)
{
described_class
.
new
(
parent
,
user
,
'assignee_id'
=>
other_user
.
id
)
}
before
do
project
.
add_developer
(
user
)
project
.
add_developer
(
other_user
)
before
do
parent
.
add_developer
(
user
)
parent
.
add_developer
(
other_user
)
stub_licensed_features
(
board_assignee_lists:
true
)
end
stub_licensed_features
(
board_assignee_lists:
true
)
it
'creates a new assignee list'
do
list
=
service
.
execute
(
board
)
expect
(
list
.
list_type
).
to
eq
(
'assignee'
)
expect
(
list
).
to
be_valid
end
end
it
'creates a new assignee list'
do
list
=
service
.
execute
(
board
)
context
'when milestone_id param is sent'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
subject
(
:service
)
{
described_class
.
new
(
project
,
user
,
'milestone_id'
=>
milestone
.
id
)
}
before
do
project
.
add_developer
(
user
)
stub_licensed_features
(
board_milestone_lists:
true
)
end
it
'creates a milestone list when param is valid'
do
list
=
service
.
execute
(
board
)
expect
(
list
.
list_type
).
to
eq
(
'assignee'
)
expect
(
list
).
to
be_valid
expect
(
list
.
list_type
).
to
eq
(
'milestone'
)
expect
(
list
).
to
be_valid
end
end
end
end
ee/spec/services/ee/boards/lists/list_service_spec.rb
View file @
e3c376a2
...
...
@@ -10,6 +10,7 @@ describe Boards::Lists::ListService do
context
'when the feature is enabled'
do
before
do
allow
(
board
.
parent
).
to
receive
(
:feature_available?
).
with
(
:board_assignee_lists
).
and_return
(
true
)
allow
(
board
.
parent
).
to
receive
(
:feature_available?
).
with
(
:board_milestone_lists
).
and_return
(
false
)
end
it
'returns all lists'
do
...
...
@@ -24,6 +25,30 @@ describe Boards::Lists::ListService do
end
end
shared_examples
'list service for board with milestone lists'
do
let!
(
:milestone_list
)
{
build
(
:milestone_list
,
board:
board
).
tap
{
|
l
|
l
.
save
(
validate:
false
)
}
}
let!
(
:backlog_list
)
{
create
(
:backlog_list
,
board:
board
)
}
let!
(
:list
)
{
create
(
:list
,
board:
board
,
label:
label
)
}
context
'when the feature is enabled'
do
before
do
allow
(
board
.
parent
).
to
receive
(
:feature_available?
).
with
(
:board_assignee_lists
).
and_return
(
false
)
allow
(
board
.
parent
).
to
receive
(
:feature_available?
).
with
(
:board_milestone_lists
).
and_return
(
true
)
end
it
'returns all lists'
do
expect
(
service
.
execute
(
board
))
.
to
match_array
([
backlog_list
,
list
,
milestone_list
,
board
.
closed_list
])
end
end
context
'when the feature is disabled'
do
it
'filters out assignee lists that might have been created while subscribed'
do
expect
(
service
.
execute
(
board
)).
to
match_array
[
backlog_list
,
list
,
board
.
closed_list
]
end
end
end
context
'when board parent is a project'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
...
...
@@ -31,6 +56,7 @@ describe Boards::Lists::ListService do
let
(
:service
)
{
described_class
.
new
(
project
,
double
)
}
it_behaves_like
'list service for board with assignee lists'
it_behaves_like
'list service for board with milestone lists'
end
context
'when board parent is a group'
do
...
...
@@ -40,6 +66,7 @@ describe Boards::Lists::ListService do
let
(
:service
)
{
described_class
.
new
(
group
,
double
)
}
it_behaves_like
'list service for board with assignee lists'
it_behaves_like
'list service for board with milestone lists'
end
end
end
ee/spec/support/shared_examples/assignee_board_list.rb
0 → 100644
View file @
e3c376a2
# frozen_string_literal: true
shared_examples_for
'assignee board list'
do
context
'when assignee_id is sent'
do
it
'returns 400 if user is not found'
do
other_user
=
create
(
:user
)
post
api
(
url
,
user
),
assignee_id:
other_user
.
id
expect
(
response
).
to
have_gitlab_http_status
(
400
)
expect
(
json_response
.
dig
(
'message'
,
'error'
)).
to
eq
(
'User not found!'
)
end
it
'returns 400 if assignee list feature is not available'
do
stub_licensed_features
(
board_assignee_lists:
false
)
post
api
(
url
,
user
),
assignee_id:
user
.
id
expect
(
response
).
to
have_gitlab_http_status
(
400
)
expect
(
json_response
.
dig
(
'message'
,
'list_type'
))
.
to
contain_exactly
(
'Assignee lists not available with your current license'
)
end
it
'creates an assignee list if user is found'
do
stub_licensed_features
(
board_assignee_lists:
true
)
post
api
(
url
,
user
),
assignee_id:
user
.
id
expect
(
response
).
to
have_gitlab_http_status
(
201
)
expect
(
json_response
.
dig
(
'assignee'
,
'id'
)).
to
eq
(
user
.
id
)
end
end
end
ee/spec/support/shared_examples/milestone_board_list.rb
0 → 100644
View file @
e3c376a2
shared_examples_for
'milestone board list'
do
context
'when milestone_id is sent'
do
it
'returns 400 if milestone is not found'
do
other_milestone
=
create
(
:milestone
)
post
api
(
url
,
user
),
milestone_id:
other_milestone
.
id
expect
(
response
).
to
have_gitlab_http_status
(
400
)
expect
(
json_response
.
dig
(
'message'
,
'error'
)).
to
eq
(
'Milestone not found!'
)
end
it
'returns 400 if milestone list feature is not available'
do
stub_licensed_features
(
board_milestone_lists:
false
)
post
api
(
url
,
user
),
milestone_id:
milestone
.
id
expect
(
response
).
to
have_gitlab_http_status
(
400
)
expect
(
json_response
.
dig
(
'message'
,
'list_type'
))
.
to
contain_exactly
(
'Milestone lists not available with your current license'
)
end
it
'creates a milestone list if milestone is found'
do
stub_licensed_features
(
board_milestone_lists:
true
)
post
api
(
url
,
user
),
milestone_id:
milestone
.
id
expect
(
response
).
to
have_gitlab_http_status
(
201
)
expect
(
json_response
.
dig
(
'milestone'
,
'id'
)).
to
eq
(
milestone
.
id
)
end
end
end
lib/api/boards.rb
View file @
e3c376a2
module
API
class
Boards
<
Grape
::
API
include
BoardsResponses
include
EE
::
API
::
BoardsResponses
include
PaginationParams
before
{
authenticate!
}
...
...
@@ -71,12 +72,10 @@ module API
success
Entities
::
List
end
params
do
requires
:label_id
,
type:
Integer
,
desc:
'The ID of an existing label'
use
:list_creation_params
end
post
'/lists'
do
unless
available_labels_for
(
user_project
).
exists?
(
params
[
:label_id
])
render_api_error!
({
error:
'Label not found!'
},
400
)
end
authorize_list_type_resource!
authorize!
(
:admin_list
,
user_project
)
...
...
lib/api/boards_responses.rb
View file @
e3c376a2
...
...
@@ -14,7 +14,7 @@ module API
def
create_list
create_list_service
=
::
Boards
::
Lists
::
CreateService
.
new
(
board_parent
,
current_user
,
{
label_id:
params
[
:label_id
]
}
)
::
Boards
::
Lists
::
CreateService
.
new
(
board_parent
,
current_user
,
create_list_params
)
list
=
create_list_service
.
execute
(
board
)
...
...
@@ -25,6 +25,10 @@ module API
end
end
def
create_list_params
params
.
slice
(
:label_id
)
end
def
move_list
(
list
)
move_list_service
=
::
Boards
::
Lists
::
MoveService
.
new
(
board_parent
,
current_user
,
{
position:
params
[
:position
].
to_i
})
...
...
@@ -44,6 +48,16 @@ module API
end
end
end
def
authorize_list_type_resource!
unless
available_labels_for
(
board_parent
).
exists?
(
params
[
:label_id
])
render_api_error!
({
error:
'Label not found!'
},
400
)
end
end
params
:list_creation_params
do
requires
:label_id
,
type:
Integer
,
desc:
'The ID of an existing label'
end
end
end
end
...
...
lib/api/entities.rb
View file @
e3c376a2
...
...
@@ -1453,6 +1453,7 @@ API::Entities.prepend_entity(::API::Entities::Board, with: EE::API::Entities::Bo
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
Group
,
with:
EE
::
API
::
Entities
::
Group
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
GroupDetail
,
with:
EE
::
API
::
Entities
::
GroupDetail
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
IssueBasic
,
with:
EE
::
API
::
Entities
::
IssueBasic
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
List
,
with:
EE
::
API
::
Entities
::
List
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
MergeRequestBasic
,
with:
EE
::
API
::
Entities
::
MergeRequestBasic
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
Namespace
,
with:
EE
::
API
::
Entities
::
Namespace
)
API
::
Entities
.
prepend_entity
(
::
API
::
Entities
::
Project
,
with:
EE
::
API
::
Entities
::
Project
)
...
...
lib/api/group_boards.rb
View file @
e3c376a2
...
...
@@ -71,12 +71,10 @@ module API
success
Entities
::
List
end
params
do
requires
:label_id
,
type:
Integer
,
desc:
'The ID of an existing label'
use
:list_creation_params
end
post
'/lists'
do
unless
available_labels_for
(
board_parent
).
exists?
(
params
[
:label_id
])
render_api_error!
({
error:
'Label not found!'
},
400
)
end
authorize_list_type_resource!
authorize!
(
:admin_list
,
user_group
)
...
...
locale/gitlab.pot
View file @
e3c376a2
...
...
@@ -723,7 +723,7 @@ msgstr ""
msgid "Assignee"
msgstr ""
msgid "Assignee
board
s not available with your current license"
msgid "Assignee
list
s not available with your current license"
msgstr ""
msgid "Assignee lists show all issues assigned to the selected user."
...
...
@@ -4357,6 +4357,12 @@ msgstr ""
msgid "Milestone"
msgstr ""
msgid "Milestone lists not available with your current license"
msgstr ""
msgid "Milestone lists show all issues from the selected milestone."
msgstr ""
msgid "Milestones"
msgstr ""
...
...
@@ -6046,7 +6052,7 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching
assignees
list"
msgid "Something went wrong while fetching
%{listType}
list"
msgstr ""
msgid "Something went wrong while fetching group member contributions"
...
...
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