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
0
Merge Requests
0
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
Jérome Perrin
gitlab-ce
Commits
cf41aaba
Commit
cf41aaba
authored
Jun 07, 2018
by
Mario de la Ossa
Committed by
Sean McGivern
Jun 07, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Backport of "Add assignee lists to boards"
parent
d4357afd
Changes
33
Show whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
273 additions
and
118 deletions
+273
-118
app/assets/javascripts/boards/components/board_list.vue
app/assets/javascripts/boards/components/board_list.vue
+39
-2
app/assets/javascripts/boards/components/board_new_issue.vue
app/assets/javascripts/boards/components/board_new_issue.vue
+2
-2
app/assets/javascripts/boards/components/new_list_dropdown.js
...assets/javascripts/boards/components/new_list_dropdown.js
+1
-0
app/assets/javascripts/boards/index.js
app/assets/javascripts/boards/index.js
+1
-1
app/assets/javascripts/boards/models/assignee.js
app/assets/javascripts/boards/models/assignee.js
+0
-12
app/assets/javascripts/boards/models/list.js
app/assets/javascripts/boards/models/list.js
+55
-32
app/assets/javascripts/boards/services/board_service.js
app/assets/javascripts/boards/services/board_service.js
+6
-4
app/assets/javascripts/boards/stores/boards_store.js
app/assets/javascripts/boards/stores/boards_store.js
+18
-6
app/assets/javascripts/gl_dropdown.js
app/assets/javascripts/gl_dropdown.js
+5
-1
app/assets/javascripts/vue_shared/models/assignee.js
app/assets/javascripts/vue_shared/models/assignee.js
+13
-0
app/controllers/boards/lists_controller.rb
app/controllers/boards/lists_controller.rb
+11
-3
app/finders/group_members_finder.rb
app/finders/group_members_finder.rb
+19
-7
app/finders/members_finder.rb
app/finders/members_finder.rb
+2
-2
app/models/list.rb
app/models/list.rb
+14
-4
app/services/boards/issues/list_service.rb
app/services/boards/issues/list_service.rb
+7
-14
app/services/boards/issues/move_service.rb
app/services/boards/issues/move_service.rb
+3
-3
app/services/boards/lists/create_service.rb
app/services/boards/lists/create_service.rb
+16
-4
app/views/shared/boards/components/_board.html.haml
app/views/shared/boards/components/_board.html.haml
+10
-2
app/views/shared/issuable/_board_create_list_dropdown.html.haml
...ews/shared/issuable/_board_create_list_dropdown.html.haml
+8
-0
app/views/shared/issuable/_label_page_create.html.haml
app/views/shared/issuable/_label_page_create.html.haml
+2
-1
app/views/shared/issuable/_label_page_default.html.haml
app/views/shared/issuable/_label_page_default.html.haml
+5
-2
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+1
-8
spec/features/boards/boards_spec.rb
spec/features/boards/boards_spec.rb
+1
-1
spec/features/issues/form_spec.rb
spec/features/issues/form_spec.rb
+3
-0
spec/finders/group_members_finder_spec.rb
spec/finders/group_members_finder_spec.rb
+12
-0
spec/finders/members_finder_spec.rb
spec/finders/members_finder_spec.rb
+12
-0
spec/fixtures/api/schemas/list.json
spec/fixtures/api/schemas/list.json
+1
-1
spec/javascripts/boards/board_card_spec.js
spec/javascripts/boards/board_card_spec.js
+1
-1
spec/javascripts/boards/boards_store_spec.js
spec/javascripts/boards/boards_store_spec.js
+1
-1
spec/javascripts/boards/issue_card_spec.js
spec/javascripts/boards/issue_card_spec.js
+1
-1
spec/javascripts/boards/issue_spec.js
spec/javascripts/boards/issue_spec.js
+1
-1
spec/javascripts/boards/list_spec.js
spec/javascripts/boards/list_spec.js
+1
-1
spec/javascripts/boards/modal_store_spec.js
spec/javascripts/boards/modal_store_spec.js
+1
-1
No files found.
app/assets/javascripts/boards/components/board_list.vue
View file @
cf41aaba
...
...
@@ -87,10 +87,46 @@ export default {
mounted
()
{
const
options
=
gl
.
issueBoards
.
getBoardSortableDefaultOptions
({
scroll
:
true
,
group
:
'
issues
'
,
disabled
:
this
.
disabled
,
filter
:
'
.board-list-count, .is-disabled
'
,
dataIdAttr
:
'
data-issue-id
'
,
group
:
{
name
:
'
issues
'
,
/**
* Dynamically determine between which containers
* items can be moved or copied as
* Assignee lists (EE feature) require this behavior
*/
pull
:
(
to
,
from
,
dragEl
,
e
)
=>
{
// As per Sortable's docs, `to` should provide
// reference to exact sortable container on which
// we're trying to drag element, but either it is
// a library's bug or our markup structure is too complex
// that `to` never points to correct container
// See https://github.com/RubaXa/Sortable/issues/1037
//
// So we use `e.target` which is always accurate about
// which element we're currently dragging our card upon
// So from there, we can get reference to actual container
// and thus the container type to enable Copy or Move
if
(
e
.
target
)
{
const
containerEl
=
e
.
target
.
closest
(
'
.js-board-list
'
)
||
e
.
target
.
querySelector
(
'
.js-board-list
'
);
const
toBoardType
=
containerEl
.
dataset
.
boardType
;
if
(
toBoardType
)
{
const
fromBoardType
=
this
.
list
.
type
;
if
((
fromBoardType
===
'
assignee
'
&&
toBoardType
===
'
label
'
)
||
(
fromBoardType
===
'
label
'
&&
toBoardType
===
'
assignee
'
))
{
return
'
clone
'
;
}
}
}
return
true
;
},
revertClone
:
true
,
},
onStart
:
(
e
)
=>
{
const
card
=
this
.
$refs
.
issue
[
e
.
oldIndex
];
...
...
@@ -179,10 +215,11 @@ export default {
:list=
"list"
v-if=
"list.type !== 'closed' && showIssueForm"
/>
<ul
class=
"board-list"
class=
"board-list
js-board-list
"
v-show=
"!loading"
ref=
"list"
:data-board=
"list.id"
:data-board-type=
"list.type"
:class=
"
{ 'is-smaller': showIssueForm }">
<board-card
v-for=
"(issue, index) in issues"
...
...
app/assets/javascripts/boards/components/board_new_issue.vue
View file @
cf41aaba
...
...
@@ -49,11 +49,12 @@ export default {
this
.
error
=
false
;
const
labels
=
this
.
list
.
label
?
[
this
.
list
.
label
]
:
[];
const
assignees
=
this
.
list
.
assignee
?
[
this
.
list
.
assignee
]
:
[];
const
issue
=
new
ListIssue
({
title
:
this
.
title
,
labels
,
subscribed
:
true
,
assignees
:
[]
,
assignees
,
project_id
:
this
.
selectedProject
.
id
,
});
...
...
@@ -141,4 +142,3 @@ export default {
</div>
</div>
</
template
>
app/assets/javascripts/boards/components/new_list_dropdown.js
View file @
cf41aaba
...
...
@@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => {
filterable
:
true
,
selectable
:
true
,
multiSelect
:
true
,
containerSelector
:
'
.js-tab-container-labels .dropdown-page-one .dropdown-content
'
,
clicked
(
options
)
{
const
{
e
}
=
options
;
const
label
=
options
.
selectedObj
;
...
...
app/assets/javascripts/boards/index.js
View file @
cf41aaba
...
...
@@ -7,6 +7,7 @@ import Vue from 'vue';
import
Flash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
FilteredSearchBoards
from
'
./filtered_search_boards
'
;
import
eventHub
from
'
./eventhub
'
;
...
...
@@ -15,7 +16,6 @@ import './models/issue';
import
'
./models/list
'
;
import
'
./models/milestone
'
;
import
'
./models/project
'
;
import
'
./models/assignee
'
;
import
'
./stores/boards_store
'
;
import
ModalStore
from
'
./stores/modal_store
'
;
import
BoardService
from
'
./services/board_service
'
;
...
...
app/assets/javascripts/boards/models/assignee.js
deleted
100644 → 0
View file @
d4357afd
/* eslint-disable no-unused-vars */
class
ListAssignee
{
constructor
(
user
,
defaultAvatar
)
{
this
.
id
=
user
.
id
;
this
.
name
=
user
.
name
;
this
.
username
=
user
.
username
;
this
.
avatar
=
user
.
avatar_url
||
defaultAvatar
;
}
}
window
.
ListAssignee
=
ListAssignee
;
app/assets/javascripts/boards/models/list.js
View file @
cf41aaba
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
import
ListLabel
from
'
~/vue_shared/models/label
'
;
import
ListAssignee
from
'
~/vue_shared/models/assignee
'
;
import
queryData
from
'
../utils/query_data
'
;
const
PER_PAGE
=
20
;
class
List
{
constructor
(
obj
,
defaultAvatar
)
{
constructor
(
obj
,
defaultAvatar
)
{
this
.
id
=
obj
.
id
;
this
.
_uid
=
this
.
guid
();
this
.
position
=
obj
.
position
;
...
...
@@ -24,6 +26,9 @@ class List {
if
(
obj
.
label
)
{
this
.
label
=
new
ListLabel
(
obj
.
label
);
}
else
if
(
obj
.
user
)
{
this
.
assignee
=
new
ListAssignee
(
obj
.
user
);
this
.
title
=
this
.
assignee
.
name
;
}
if
(
this
.
type
!==
'
blank
'
&&
this
.
id
)
{
...
...
@@ -34,14 +39,25 @@ class List {
}
guid
()
{
const
s4
=
()
=>
Math
.
floor
((
1
+
Math
.
random
())
*
0x10000
).
toString
(
16
).
substring
(
1
);
const
s4
=
()
=>
Math
.
floor
((
1
+
Math
.
random
())
*
0x10000
)
.
toString
(
16
)
.
substring
(
1
);
return
`
${
s4
()}${
s4
()}
-
${
s4
()}
-
${
s4
()}
-
${
s4
()}
-
${
s4
()}${
s4
()}${
s4
()}
`
;
}
save
()
{
save
()
{
const
entity
=
this
.
label
||
this
.
assignee
;
let
entityType
=
''
;
if
(
this
.
label
)
{
entityType
=
'
label_id
'
;
}
else
{
entityType
=
'
assignee_id
'
;
}
return
gl
.
boardService
.
createList
(
this
.
label
.
id
)
.
then
(
res
=>
res
.
data
)
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
this
.
id
=
data
.
id
;
this
.
type
=
data
.
list_type
;
this
.
position
=
data
.
position
;
...
...
@@ -50,25 +66,23 @@ class List {
});
}
destroy
()
{
destroy
()
{
const
index
=
gl
.
issueBoards
.
BoardsStore
.
state
.
lists
.
indexOf
(
this
);
gl
.
issueBoards
.
BoardsStore
.
state
.
lists
.
splice
(
index
,
1
);
gl
.
issueBoards
.
BoardsStore
.
updateNewListDropdown
(
this
.
id
);
gl
.
boardService
.
destroyList
(
this
.
id
)
.
catch
(()
=>
{
gl
.
boardService
.
destroyList
(
this
.
id
).
catch
(()
=>
{
// TODO: handle request error
});
}
update
()
{
gl
.
boardService
.
updateList
(
this
.
id
,
this
.
position
)
.
catch
(()
=>
{
update
()
{
gl
.
boardService
.
updateList
(
this
.
id
,
this
.
position
).
catch
(()
=>
{
// TODO: handle request error
});
}
nextPage
()
{
nextPage
()
{
if
(
this
.
issuesSize
>
this
.
issues
.
length
)
{
if
(
this
.
issues
.
length
/
PER_PAGE
>=
1
)
{
this
.
page
+=
1
;
...
...
@@ -78,7 +92,7 @@ class List {
}
}
getIssues
(
emptyIssues
=
true
)
{
getIssues
(
emptyIssues
=
true
)
{
const
data
=
queryData
(
gl
.
issueBoards
.
BoardsStore
.
filter
.
path
,
{
page
:
this
.
page
});
if
(
this
.
label
&&
data
.
label_name
)
{
...
...
@@ -89,7 +103,8 @@ class List {
this
.
loading
=
true
;
}
return
gl
.
boardService
.
getIssuesForList
(
this
.
id
,
data
)
return
gl
.
boardService
.
getIssuesForList
(
this
.
id
,
data
)
.
then
(
res
=>
res
.
data
)
.
then
((
data
)
=>
{
this
.
loading
=
false
;
...
...
@@ -103,11 +118,12 @@ class List {
});
}
newIssue
(
issue
)
{
newIssue
(
issue
)
{
this
.
addIssue
(
issue
,
null
,
0
);
this
.
issuesSize
+=
1
;
return
gl
.
boardService
.
newIssue
(
this
.
id
,
issue
)
return
gl
.
boardService
.
newIssue
(
this
.
id
,
issue
)
.
then
(
res
=>
res
.
data
)
.
then
((
data
)
=>
{
issue
.
id
=
data
.
id
;
...
...
@@ -123,13 +139,13 @@ class List {
});
}
createIssues
(
data
)
{
data
.
forEach
(
(
issueObj
)
=>
{
createIssues
(
data
)
{
data
.
forEach
(
issueObj
=>
{
this
.
addIssue
(
new
ListIssue
(
issueObj
,
this
.
defaultAvatar
));
});
}
addIssue
(
issue
,
listFrom
,
newIndex
)
{
addIssue
(
issue
,
listFrom
,
newIndex
)
{
let
moveBeforeId
=
null
;
let
moveAfterId
=
null
;
...
...
@@ -152,6 +168,13 @@ class List {
issue
.
addLabel
(
this
.
label
);
}
if
(
this
.
assignee
)
{
if
(
listFrom
&&
listFrom
.
type
===
'
assignee
'
)
{
issue
.
removeAssignee
(
listFrom
.
assignee
);
}
issue
.
addAssignee
(
this
.
assignee
);
}
if
(
listFrom
)
{
this
.
issuesSize
+=
1
;
...
...
@@ -160,29 +183,29 @@ class List {
}
}
moveIssue
(
issue
,
oldIndex
,
newIndex
,
moveBeforeId
,
moveAfterId
)
{
moveIssue
(
issue
,
oldIndex
,
newIndex
,
moveBeforeId
,
moveAfterId
)
{
this
.
issues
.
splice
(
oldIndex
,
1
);
this
.
issues
.
splice
(
newIndex
,
0
,
issue
);
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
moveBeforeId
,
moveAfterId
)
.
catch
(()
=>
{
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
moveBeforeId
,
moveAfterId
).
catch
(()
=>
{
// TODO: handle request error
});
}
updateIssueLabel
(
issue
,
listFrom
,
moveBeforeId
,
moveAfterId
)
{
gl
.
boardService
.
moveIssue
(
issue
.
id
,
listFrom
.
id
,
this
.
id
,
moveBeforeId
,
moveAfterId
)
gl
.
boardService
.
moveIssue
(
issue
.
id
,
listFrom
.
id
,
this
.
id
,
moveBeforeId
,
moveAfterId
)
.
catch
(()
=>
{
// TODO: handle request error
});
}
findIssue
(
id
)
{
findIssue
(
id
)
{
return
this
.
issues
.
find
(
issue
=>
issue
.
id
===
id
);
}
removeIssue
(
removeIssue
)
{
this
.
issues
=
this
.
issues
.
filter
(
(
issue
)
=>
{
removeIssue
(
removeIssue
)
{
this
.
issues
=
this
.
issues
.
filter
(
issue
=>
{
const
matchesRemove
=
removeIssue
.
id
===
issue
.
id
;
if
(
matchesRemove
)
{
...
...
app/assets/javascripts/boards/services/board_service.js
View file @
cf41aaba
...
...
@@ -30,11 +30,13 @@ export default class BoardService {
return
axios
.
post
(
this
.
listsEndpointGenerate
,
{});
}
createList
(
labelId
)
{
createList
(
entityId
,
entityType
)
{
const
list
=
{
[
entityType
]:
entityId
,
};
return
axios
.
post
(
this
.
listsEndpoint
,
{
list
:
{
label_id
:
labelId
,
},
list
,
});
}
...
...
app/assets/javascripts/boards/stores/boards_store.js
View file @
cf41aaba
...
...
@@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = {
const
listLabels
=
issueLists
.
map
(
listIssue
=>
listIssue
.
label
);
if
(
!
issueTo
)
{
// Check if target list assignee is already present in this issue
if
((
listTo
.
type
===
'
assignee
'
&&
listFrom
.
type
===
'
assignee
'
)
&&
issue
.
findAssignee
(
listTo
.
assignee
))
{
const
targetIssue
=
listTo
.
findIssue
(
issue
.
id
);
targetIssue
.
removeAssignee
(
listFrom
.
assignee
);
}
else
{
// Add to new lists issues if it doesn't already exist
listTo
.
addIssue
(
issue
,
listFrom
,
newIndex
);
}
}
else
{
listTo
.
updateIssueLabel
(
issue
,
listFrom
);
issueTo
.
removeLabel
(
listFrom
.
label
);
...
...
@@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = {
list
.
removeIssue
(
issue
);
});
issue
.
removeLabels
(
listLabels
);
}
else
{
}
else
if
(
listTo
.
type
===
'
backlog
'
&&
listFrom
.
type
===
'
assignee
'
)
{
issue
.
removeAssignee
(
listFrom
.
assignee
);
listFrom
.
removeIssue
(
issue
);
}
else
if
((
listTo
.
type
!==
'
label
'
&&
listFrom
.
type
===
'
assignee
'
)
||
(
listTo
.
type
!==
'
assignee
'
&&
listFrom
.
type
===
'
label
'
))
{
listFrom
.
removeIssue
(
issue
);
}
},
...
...
@@ -126,11 +137,12 @@ gl.issueBoards.BoardsStore = {
list
.
moveIssue
(
issue
,
oldIndex
,
newIndex
,
beforeId
,
afterId
);
},
findList
(
key
,
val
,
type
=
'
label
'
)
{
return
this
.
state
.
lists
.
filter
((
list
)
=>
{
const
byType
=
type
?
list
[
'
type
'
]
===
type
:
true
;
const
filteredList
=
this
.
state
.
lists
.
filter
((
list
)
=>
{
const
byType
=
type
?
(
list
.
type
===
type
)
||
(
list
.
type
===
'
assignee
'
)
:
true
;
return
list
[
key
]
===
val
&&
byType
;
})[
0
];
});
return
filteredList
[
0
];
},
updateFiltersUrl
()
{
history
.
pushState
(
null
,
null
,
`?
${
this
.
filter
.
path
}
`
);
...
...
app/assets/javascripts/gl_dropdown.js
View file @
cf41aaba
...
...
@@ -602,7 +602,11 @@ GitLabDropdown = (function() {
var
selector
;
selector
=
'
.dropdown-content
'
;
if
(
this
.
dropdown
.
find
(
"
.dropdown-toggle-page
"
).
length
)
{
selector
=
"
.dropdown-page-one .dropdown-content
"
;
if
(
this
.
options
.
containerSelector
)
{
selector
=
this
.
options
.
containerSelector
;
}
else
{
selector
=
'
.dropdown-page-one .dropdown-content
'
;
}
}
return
$
(
selector
,
this
.
dropdown
).
empty
();
...
...
app/assets/javascripts/vue_shared/models/assignee.js
0 → 100644
View file @
cf41aaba
export
default
class
ListAssignee
{
constructor
(
obj
,
defaultAvatar
)
{
this
.
id
=
obj
.
id
;
this
.
name
=
obj
.
name
;
this
.
username
=
obj
.
username
;
this
.
avatar
=
obj
.
avatar_url
||
obj
.
avatar
||
defaultAvatar
;
this
.
path
=
obj
.
path
;
this
.
state
=
obj
.
state
;
this
.
webUrl
=
obj
.
web_url
||
obj
.
webUrl
;
}
}
window
.
ListAssignee
=
ListAssignee
;
app/controllers/boards/lists_controller.rb
View file @
cf41aaba
...
...
@@ -56,8 +56,12 @@ module Boards
private
def
list_creation_attrs
%i[label_id]
end
def
list_params
params
.
require
(
:list
).
permit
(
:label_id
)
params
.
require
(
:list
).
permit
(
list_creation_attrs
)
end
def
move_params
...
...
@@ -65,11 +69,15 @@ module Boards
end
def
serialize_as_json
(
resource
)
resource
.
as_json
(
resource
.
as_json
(
serialization_attrs
)
end
def
serialization_attrs
{
only:
[
:id
,
:list_type
,
:position
],
methods:
[
:title
],
label:
true
)
}
end
end
end
app/finders/group_members_finder.rb
View file @
cf41aaba
...
...
@@ -3,17 +3,29 @@ class GroupMembersFinder
@group
=
group
end
def
execute
def
execute
(
include_descendants:
false
)
group_members
=
@group
.
members
wheres
=
[]
return
group_members
unless
@group
.
parent
return
group_members
unless
@group
.
parent
||
include_descendants
wheres
<<
"members.id IN (
#{
group_members
.
select
(
:id
).
to_sql
}
)"
if
@group
.
parent
parents_members
=
GroupMember
.
non_request
.
where
(
source_id:
@group
.
ancestors
.
select
(
:id
))
.
where
.
not
(
user_id:
@group
.
users
.
select
(
:id
))
wheres
=
[
"members.id IN (
#{
group_members
.
select
(
:id
).
to_sql
}
)"
]
wheres
<<
"members.id IN (
#{
parents_members
.
select
(
:id
).
to_sql
}
)"
end
if
include_descendants
descendant_members
=
GroupMember
.
non_request
.
where
(
source_id:
@group
.
descendants
.
select
(
:id
))
.
where
.
not
(
user_id:
@group
.
users
.
select
(
:id
))
wheres
<<
"members.id IN (
#{
descendant_members
.
select
(
:id
).
to_sql
}
)"
end
GroupMember
.
where
(
wheres
.
join
(
' OR '
))
end
...
...
app/finders/members_finder.rb
View file @
cf41aaba
...
...
@@ -7,12 +7,12 @@ class MembersFinder
@group
=
project
.
group
end
def
execute
def
execute
(
include_descendants:
false
)
project_members
=
project
.
project_members
project_members
=
project_members
.
non_invite
unless
can?
(
current_user
,
:admin_project
,
project
)
if
group
group_members
=
GroupMembersFinder
.
new
(
group
).
execute
group_members
=
GroupMembersFinder
.
new
(
group
).
execute
(
include_descendants:
include_descendants
)
group_members
=
group_members
.
non_invite
union
=
Gitlab
::
SQL
::
Union
.
new
([
project_members
,
group_members
],
remove_duplicates:
false
)
...
...
app/models/list.rb
View file @
cf41aaba
...
...
@@ -2,17 +2,27 @@ class List < ActiveRecord::Base
belongs_to
:board
belongs_to
:label
enum
list_type:
{
backlog:
0
,
label:
1
,
closed:
2
}
enum
list_type:
{
backlog:
0
,
label:
1
,
closed:
2
,
assignee:
3
}
validates
:board
,
:list_type
,
presence:
true
validates
:label
,
:position
,
presence:
true
,
if: :label?
validates
:label_id
,
uniqueness:
{
scope: :board_id
},
if: :label?
validates
:position
,
numericality:
{
only_integer:
true
,
greater_than_or_equal_to:
0
},
if: :
label
?
validates
:position
,
numericality:
{
only_integer:
true
,
greater_than_or_equal_to:
0
},
if: :
movable
?
before_destroy
:can_be_destroyed
scope
:destroyable
,
->
{
where
(
list_type:
list_types
[
:label
])
}
scope
:movable
,
->
{
where
(
list_type:
list_types
[
:label
])
}
scope
:destroyable
,
->
{
where
(
list_type:
list_types
.
slice
(
*
destroyable_types
).
values
)
}
scope
:movable
,
->
{
where
(
list_type:
list_types
.
slice
(
*
movable_types
).
values
)
}
class
<<
self
def
destroyable_types
[
:label
]
end
def
movable_types
[
:label
]
end
end
def
destroyable?
label?
...
...
app/services/boards/issues/list_service.rb
View file @
cf41aaba
...
...
@@ -3,13 +3,18 @@ module Boards
class
ListService
<
Boards
::
BaseService
def
execute
issues
=
IssuesFinder
.
new
(
current_user
,
filter_params
).
execute
issues
=
without_board_labels
(
issues
)
unless
movable_list?
||
closed_list?
issues
=
with_list_label
(
issues
)
if
movable_list?
issues
=
filter
(
issues
)
issues
.
order_by_position_and_priority
end
private
def
filter
(
issues
)
issues
=
without_board_labels
(
issues
)
unless
list
&
.
movable?
||
list
&
.
closed?
issues
=
with_list_label
(
issues
)
if
list
&
.
label?
issues
end
def
board
@board
||=
parent
.
boards
.
find
(
params
[
:board_id
])
end
...
...
@@ -20,18 +25,6 @@ module Boards
@list
=
board
.
lists
.
find
(
params
[
:id
])
if
params
.
key?
(
:id
)
end
def
movable_list?
return
@movable_list
if
defined?
(
@movable_list
)
@movable_list
=
list
.
present?
&&
list
.
movable?
end
def
closed_list?
return
@closed_list
if
defined?
(
@closed_list
)
@closed_list
=
list
.
present?
&&
list
.
closed?
end
def
filter_params
set_parent
set_state
...
...
app/services/boards/issues/move_service.rb
View file @
cf41aaba
...
...
@@ -3,7 +3,7 @@ module Boards
class
MoveService
<
Boards
::
BaseService
def
execute
(
issue
)
return
false
unless
can?
(
current_user
,
:update_issue
,
issue
)
return
false
if
issue_params
.
empty?
return
false
if
issue_params
(
issue
)
.
empty?
update
(
issue
)
end
...
...
@@ -28,10 +28,10 @@ module Boards
end
def
update
(
issue
)
::
Issues
::
UpdateService
.
new
(
issue
.
project
,
current_user
,
issue_params
).
execute
(
issue
)
::
Issues
::
UpdateService
.
new
(
issue
.
project
,
current_user
,
issue_params
(
issue
)
).
execute
(
issue
)
end
def
issue_params
def
issue_params
(
issue
)
attrs
=
{}
if
move_between_lists?
...
...
app/services/boards/lists/create_service.rb
View file @
cf41aaba
module
Boards
module
Lists
class
CreateService
<
Boards
::
BaseService
include
Gitlab
::
Utils
::
StrongMemoize
def
execute
(
board
)
List
.
transaction
do
label
=
available_labels_for
(
board
).
find
(
params
[
:label_id
]
)
target
=
target
(
board
)
position
=
next_position
(
board
)
create_list
(
board
,
label
,
position
)
create_list
(
board
,
type
,
target
,
position
)
end
end
private
def
type
:label
end
def
target
(
board
)
strong_memoize
(
:target
)
do
available_labels_for
(
board
).
find
(
params
[
:label_id
])
end
end
def
available_labels_for
(
board
)
options
=
{
include_ancestor_groups:
true
}
...
...
@@ -28,8 +40,8 @@ module Boards
max_position
.
nil?
?
0
:
max_position
.
succ
end
def
create_list
(
board
,
label
,
position
)
board
.
lists
.
create
(
label:
label
,
list_type: :label
,
position:
position
)
def
create_list
(
board
,
type
,
target
,
position
)
board
.
lists
.
create
(
type
=>
target
,
list_type:
type
,
position:
position
)
end
end
end
...
...
app/views/shared/boards/components/_board.html.haml
View file @
cf41aaba
.board
{
":class"
=>
'
{
"is-draggable"
:
!
list
.
preset
,
"is-expandable"
:
list
.
isExpandable
,
"is-collapsed"
:
!
list
.
isExpanded
}
'
,
.board
{
":class"
=>
'
{
"is-draggable"
:
!
list
.
preset
,
"is-expandable"
:
list
.
isExpandable
,
"is-collapsed"
:
!
list
.
isExpanded
,
"board-type-assignee"
:
list
.
type
===
"assignee"
}
'
,
":data-id"
=>
"list.id"
}
.board-inner
%header
.board-header
{
":class"
=>
'
{
"has-border"
:
list
.
label
&&
list
.
label
.
color
}
'
,
":style"
=>
"{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }"
,
"@click"
=>
"toggleExpanded($event)"
}
...
...
@@ -7,10 +7,18 @@
":class"
:
"{
\"
fa-caret-down
\"
: list.isExpanded,
\"
fa-caret-right
\"
: !list.isExpanded }"
,
"aria-hidden"
:
"true"
}
%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
\"
"
,
":title"
=>
'(
list.label ? list.label.description :
"")'
,
data:
{
container:
"body"
}
}
":title"
=>
'(
(list.label && list.label.description) || list.title ||
"")'
,
data:
{
container:
"body"
}
}
{{ list.title }}
%span
.board-title-sub-text.prepend-left-5.has-tooltip
{
"v-if"
:
"list.type ===
\"
assignee
\"
"
,
":title"
=>
'(list.assignee && list.assignee.username || "")'
}
@{{ list.assignee.username }}
%span
.has-tooltip
{
"v-if"
:
"list.type ===
\"
label
\"
"
,
":title"
=>
'(list.label ? list.label.description : "")'
,
data:
{
container:
"body"
,
placement:
"bottom"
},
...
...
app/views/shared/issuable/_board_create_list_dropdown.html.haml
0 → 100644
View file @
cf41aaba
.dropdown.prepend-left-10
#js-add-list
%button
.btn.btn-create.btn-inverted.js-new-board-list
{
type:
"button"
,
data:
board_list_data
}
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
show_footer:
true
,
show_create:
true
,
show_boards_content:
true
,
title:
"Add list"
}
-
if
can?
(
current_user
,
:admin_label
,
board
.
parent
)
=
render
partial:
"shared/issuable/label_page_create"
=
dropdown_loading
app/views/shared/issuable/_label_page_create.html.haml
View file @
cf41aaba
-
show_close
=
local_assigns
.
fetch
(
:show_close
,
true
)
-
subject
=
@project
||
@group
.dropdown-page-two.dropdown-new-label
=
dropdown_title
(
create_label_title
(
subject
),
options:
{
back:
true
})
=
dropdown_title
(
create_label_title
(
subject
),
options:
{
back:
true
,
close:
show_close
})
=
dropdown_content
do
.dropdown-labels-error.js-label-error
%input
#new_label_name
.default-dropdown-input
{
type:
"text"
,
placeholder:
_
(
'Name new label'
)
}
...
...
app/views/shared/issuable/_label_page_default.html.haml
View file @
cf41aaba
-
title
=
local_assigns
.
fetch
(
:title
,
_
(
'Assign labels'
))
-
content_title
=
local_assigns
.
fetch
(
:content_title
,
_
(
'Create lists from labels. Issues with that label appear in that list.'
))
-
show_title
=
local_assigns
.
fetch
(
:show_title
,
true
)
-
show_create
=
local_assigns
.
fetch
(
:show_create
,
true
)
-
show_footer
=
local_assigns
.
fetch
(
:show_footer
,
true
)
-
filter_placeholder
=
local_assigns
.
fetch
(
:filter_placeholder
,
'Search'
)
-
show_boards_content
=
local_assigns
.
fetch
(
:show_boards_content
,
false
)
-
subject
=
@project
||
@group
.dropdown-page-one
-
if
show_title
=
dropdown_title
(
title
)
-
if
show_boards_content
.issue-board-dropdown-content
%p
=
_
(
'Create lists from labels. Issues with that label appear in that list.'
)
=
content_title
=
dropdown_filter
(
filter_placeholder
)
=
dropdown_content
-
if
current_board_parent
&&
show_footer
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
cf41aaba
...
...
@@ -104,14 +104,7 @@
.filter-dropdown-container
-
if
type
==
:boards
-
if
can?
(
current_user
,
:admin_list
,
board
.
parent
)
.dropdown.prepend-left-10
#js-add-list
%button
.btn.btn-create.btn-inverted.js-new-board-list
{
type:
"button"
,
data:
board_list_data
}
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
show_footer:
true
,
show_create:
true
,
show_boards_content:
true
,
title:
"Add list"
}
-
if
can?
(
current_user
,
:admin_label
,
board
.
parent
)
=
render
partial:
"shared/issuable/label_page_create"
=
dropdown_loading
=
render_if_exists
'shared/issuable/board_create_list_dropdown'
,
board:
board
-
if
@project
#js-add-issues-btn
.prepend-left-10
{
data:
{
can_admin_list:
can?
(
current_user
,
:admin_list
,
@project
)
}
}
-
elsif
type
!=
:boards_modal
...
...
spec/features/boards/boards_spec.rb
View file @
cf41aaba
...
...
@@ -150,7 +150,7 @@ describe 'Issue Boards', :js do
click_button
'Add list'
wait_for_requests
find
(
'.
dropdown-menu-close
'
).
click
find
(
'.
js-new-board-list
'
).
click
page
.
within
(
find
(
'.board:nth-child(2)'
))
do
accept_confirm
{
find
(
'.board-delete'
).
click
}
...
...
spec/features/issues/form_spec.rb
View file @
cf41aaba
...
...
@@ -143,6 +143,9 @@ describe 'New/edit issue', :js do
click_link
label
.
title
click_link
label2
.
title
end
find
(
'.js-issuable-form-dropdown.js-label-select'
).
click
page
.
within
'.js-label-select'
do
expect
(
page
).
to
have_content
label
.
title
end
...
...
spec/finders/group_members_finder_spec.rb
View file @
cf41aaba
...
...
@@ -29,4 +29,16 @@ describe GroupMembersFinder, '#execute' do
expect
(
result
.
to_a
).
to
match_array
([
member1
,
member3
,
member4
])
end
it
'returns members for descendant groups if requested'
,
:nested_groups
do
member1
=
group
.
add_master
(
user2
)
member2
=
group
.
add_master
(
user1
)
nested_group
.
add_master
(
user2
)
member3
=
nested_group
.
add_master
(
user3
)
member4
=
nested_group
.
add_master
(
user4
)
result
=
described_class
.
new
(
group
).
execute
(
include_descendants:
true
)
expect
(
result
.
to_a
).
to
match_array
([
member1
,
member2
,
member3
,
member4
])
end
end
spec/finders/members_finder_spec.rb
View file @
cf41aaba
...
...
@@ -19,4 +19,16 @@ describe MembersFinder, '#execute' do
expect
(
result
.
to_a
).
to
match_array
([
member1
,
member2
,
member3
])
end
it
'includes nested group members if asked'
,
:nested_groups
do
project
=
create
(
:project
,
namespace:
group
)
nested_group
.
request_access
(
user1
)
member1
=
group
.
add_master
(
user2
)
member2
=
nested_group
.
add_master
(
user3
)
member3
=
project
.
add_master
(
user4
)
result
=
described_class
.
new
(
project
,
user2
).
execute
(
include_descendants:
true
)
expect
(
result
.
to_a
).
to
match_array
([
member1
,
member2
,
member3
])
end
end
spec/fixtures/api/schemas/list.json
View file @
cf41aaba
...
...
@@ -37,5 +37,5 @@
"title"
:
{
"type"
:
"string"
},
"position"
:
{
"type"
:
[
"integer"
,
"null"
]
}
},
"additionalProperties"
:
fals
e
"additionalProperties"
:
tru
e
}
spec/javascripts/boards/board_card_spec.js
View file @
cf41aaba
...
...
@@ -5,10 +5,10 @@
import
Vue
from
'
vue
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
'
~/boards/models/assignee
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/stores/boards_store
'
;
import
boardCard
from
'
~/boards/components/board_card.vue
'
;
...
...
spec/javascripts/boards/boards_store_spec.js
View file @
cf41aaba
...
...
@@ -7,9 +7,9 @@ import axios from '~/lib/utils/axios_utils';
import
Cookies
from
'
js-cookie
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/models/assignee
'
;
import
'
~/boards/services/board_service
'
;
import
'
~/boards/stores/boards_store
'
;
import
{
listObj
,
listObjDuplicate
,
boardsMockInterceptor
,
mockBoardService
}
from
'
./mock_data
'
;
...
...
spec/javascripts/boards/issue_card_spec.js
View file @
cf41aaba
...
...
@@ -5,9 +5,9 @@
import
Vue
from
'
vue
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/models/assignee
'
;
import
'
~/boards/stores/boards_store
'
;
import
'
~/boards/components/issue_card_inner
'
;
import
{
listObj
}
from
'
./mock_data
'
;
...
...
spec/javascripts/boards/issue_spec.js
View file @
cf41aaba
...
...
@@ -3,9 +3,9 @@
import
Vue
from
'
vue
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/models/assignee
'
;
import
'
~/boards/services/board_service
'
;
import
'
~/boards/stores/boards_store
'
;
import
{
mockBoardService
}
from
'
./mock_data
'
;
...
...
spec/javascripts/boards/list_spec.js
View file @
cf41aaba
...
...
@@ -6,9 +6,9 @@ import MockAdapter from 'axios-mock-adapter';
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
_
from
'
underscore
'
;
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/models/assignee
'
;
import
'
~/boards/services/board_service
'
;
import
'
~/boards/stores/boards_store
'
;
import
{
listObj
,
listObjDuplicate
,
boardsMockInterceptor
,
mockBoardService
}
from
'
./mock_data
'
;
...
...
spec/javascripts/boards/modal_store_spec.js
View file @
cf41aaba
/* global ListIssue */
import
'
~/vue_shared/models/label
'
;
import
'
~/vue_shared/models/assignee
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
'
~/boards/models/assignee
'
;
import
Store
from
'
~/boards/stores/modal_store
'
;
describe
(
'
Modal store
'
,
()
=>
{
...
...
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