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
b4778a58
Commit
b4778a58
authored
Sep 07, 2017
by
Sean McGivern
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'ee_issue_928_backport' into 'master'
Group boards CE backport See merge request !13883
parents
cb555da1
a1a839c9
Changes
78
Hide whitespace changes
Inline
Side-by-side
Showing
78 changed files
with
774 additions
and
527 deletions
+774
-527
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+12
-4
app/assets/javascripts/boards/boards_bundle.js
app/assets/javascripts/boards/boards_bundle.js
+33
-18
app/assets/javascripts/boards/components/board_list.js
app/assets/javascripts/boards/components/board_list.js
+4
-6
app/assets/javascripts/boards/components/board_new_issue.js
app/assets/javascripts/boards/components/board_new_issue.js
+4
-1
app/assets/javascripts/boards/components/issue_card_inner.js
app/assets/javascripts/boards/components/issue_card_inner.js
+6
-3
app/assets/javascripts/boards/components/modal/footer.js
app/assets/javascripts/boards/components/modal/footer.js
+1
-1
app/assets/javascripts/boards/components/new_list_dropdown.js
...assets/javascripts/boards/components/new_list_dropdown.js
+1
-1
app/assets/javascripts/boards/components/sidebar/remove_issue.js
...ets/javascripts/boards/components/sidebar/remove_issue.js
+22
-6
app/assets/javascripts/boards/models/issue.js
app/assets/javascripts/boards/models/issue.js
+2
-2
app/assets/javascripts/boards/models/label.js
app/assets/javascripts/boards/models/label.js
+1
-0
app/assets/javascripts/boards/models/list.js
app/assets/javascripts/boards/models/list.js
+15
-13
app/assets/javascripts/boards/services/board_service.js
app/assets/javascripts/boards/services/board_service.js
+10
-10
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+1
-1
app/assets/stylesheets/pages/boards.scss
app/assets/stylesheets/pages/boards.scss
+20
-43
app/controllers/boards/application_controller.rb
app/controllers/boards/application_controller.rb
+21
-0
app/controllers/boards/issues_controller.rb
app/controllers/boards/issues_controller.rb
+90
-0
app/controllers/boards/lists_controller.rb
app/controllers/boards/lists_controller.rb
+75
-0
app/controllers/concerns/boards_responses.rb
app/controllers/concerns/boards_responses.rb
+42
-0
app/controllers/projects/boards/application_controller.rb
app/controllers/projects/boards/application_controller.rb
+0
-15
app/controllers/projects/boards/issues_controller.rb
app/controllers/projects/boards/issues_controller.rb
+0
-94
app/controllers/projects/boards/lists_controller.rb
app/controllers/projects/boards/lists_controller.rb
+0
-86
app/controllers/projects/boards_controller.rb
app/controllers/projects/boards_controller.rb
+13
-14
app/helpers/boards_helper.rb
app/helpers/boards_helper.rb
+71
-6
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+8
-0
app/helpers/labels_helper.rb
app/helpers/labels_helper.rb
+4
-3
app/helpers/search_helper.rb
app/helpers/search_helper.rb
+8
-6
app/models/board.rb
app/models/board.rb
+13
-1
app/models/concerns/relative_positioning.rb
app/models/concerns/relative_positioning.rb
+9
-5
app/models/label.rb
app/models/label.rb
+3
-1
app/models/project.rb
app/models/project.rb
+8
-0
app/services/boards/base_service.rb
app/services/boards/base_service.rb
+10
-0
app/services/boards/create_service.rb
app/services/boards/create_service.rb
+3
-3
app/services/boards/issues/create_service.rb
app/services/boards/issues/create_service.rb
+10
-2
app/services/boards/issues/list_service.rb
app/services/boards/issues/list_service.rb
+5
-5
app/services/boards/issues/move_service.rb
app/services/boards/issues/move_service.rb
+10
-10
app/services/boards/list_service.rb
app/services/boards/list_service.rb
+4
-4
app/services/boards/lists/create_service.rb
app/services/boards/lists/create_service.rb
+4
-5
app/services/boards/lists/destroy_service.rb
app/services/boards/lists/destroy_service.rb
+1
-1
app/services/boards/lists/generate_service.rb
app/services/boards/lists/generate_service.rb
+3
-3
app/services/boards/lists/list_service.rb
app/services/boards/lists/list_service.rb
+1
-1
app/services/boards/lists/move_service.rb
app/services/boards/lists/move_service.rb
+1
-1
app/services/issues/update_service.rb
app/services/issues/update_service.rb
+8
-8
app/views/projects/boards/index.html.haml
app/views/projects/boards/index.html.haml
+1
-1
app/views/projects/boards/show.html.haml
app/views/projects/boards/show.html.haml
+1
-1
app/views/shared/boards/_show.html.haml
app/views/shared/boards/_show.html.haml
+2
-2
app/views/shared/boards/components/_board.html.haml
app/views/shared/boards/components/_board.html.haml
+12
-11
app/views/shared/boards/components/_sidebar.html.haml
app/views/shared/boards/components/_sidebar.html.haml
+7
-6
app/views/shared/boards/components/sidebar/_assignee.html.haml
...iews/shared/boards/components/sidebar/_assignee.html.haml
+6
-6
app/views/shared/boards/components/sidebar/_due_date.html.haml
...iews/shared/boards/components/sidebar/_due_date.html.haml
+4
-4
app/views/shared/boards/components/sidebar/_labels.html.haml
app/views/shared/boards/components/sidebar/_labels.html.haml
+12
-5
app/views/shared/boards/components/sidebar/_milestone.html.haml
...ews/shared/boards/components/sidebar/_milestone.html.haml
+5
-5
app/views/shared/boards/components/sidebar/_notifications.html.haml
...shared/boards/components/sidebar/_notifications.html.haml
+1
-1
app/views/shared/boards/index.html.haml
app/views/shared/boards/index.html.haml
+1
-0
app/views/shared/boards/show.html.haml
app/views/shared/boards/show.html.haml
+1
-0
app/views/shared/issuable/_label_page_default.html.haml
app/views/shared/issuable/_label_page_default.html.haml
+5
-6
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+3
-3
config/routes.rb
config/routes.rb
+13
-0
config/routes/project.rb
config/routes/project.rb
+1
-13
doc/README.md
doc/README.md
+1
-1
lib/gitlab/path_regex.rb
lib/gitlab/path_regex.rb
+1
-0
spec/controllers/boards/issues_controller_spec.rb
spec/controllers/boards/issues_controller_spec.rb
+23
-9
spec/controllers/boards/lists_controller_spec.rb
spec/controllers/boards/lists_controller_spec.rb
+1
-1
spec/factories/milestones.rb
spec/factories/milestones.rb
+4
-0
spec/fixtures/api/schemas/issue.json
spec/fixtures/api/schemas/issue.json
+6
-0
spec/javascripts/boards/board_blank_state_spec.js
spec/javascripts/boards/board_blank_state_spec.js
+2
-1
spec/javascripts/boards/board_card_spec.js
spec/javascripts/boards/board_card_spec.js
+3
-2
spec/javascripts/boards/board_list_spec.js
spec/javascripts/boards/board_list_spec.js
+3
-1
spec/javascripts/boards/board_new_issue_spec.js
spec/javascripts/boards/board_new_issue_spec.js
+2
-1
spec/javascripts/boards/boards_store_spec.js
spec/javascripts/boards/boards_store_spec.js
+4
-2
spec/javascripts/boards/components/board_spec.js
spec/javascripts/boards/components/board_spec.js
+8
-2
spec/javascripts/boards/issue_card_spec.js
spec/javascripts/boards/issue_card_spec.js
+47
-48
spec/javascripts/boards/issue_spec.js
spec/javascripts/boards/issue_spec.js
+3
-1
spec/javascripts/boards/list_spec.js
spec/javascripts/boards/list_spec.js
+10
-5
spec/javascripts/boards/mock_data.js
spec/javascripts/boards/mock_data.js
+19
-3
spec/javascripts/boards/modal_store_spec.js
spec/javascripts/boards/modal_store_spec.js
+2
-0
spec/services/boards/issues/create_service_spec.rb
spec/services/boards/issues/create_service_spec.rb
+1
-1
spec/services/boards/issues/move_service_spec.rb
spec/services/boards/issues/move_service_spec.rb
+1
-1
spec/services/issues/update_service_spec.rb
spec/services/issues/update_service_spec.rb
+1
-1
No files found.
app/assets/javascripts/api.js
View file @
b4778a58
...
...
@@ -6,7 +6,8 @@ const Api = {
namespacesPath
:
'
/api/:version/namespaces.json
'
,
groupProjectsPath
:
'
/api/:version/groups/:id/projects.json
'
,
projectsPath
:
'
/api/:version/projects.json
'
,
labelsPath
:
'
/:namespace_path/:project_path/labels
'
,
projectLabelsPath
:
'
/:namespace_path/:project_path/labels
'
,
groupLabelsPath
:
'
/groups/:namespace_path/labels
'
,
licensePath
:
'
/api/:version/templates/licenses/:key
'
,
gitignorePath
:
'
/api/:version/templates/gitignores/:key
'
,
gitlabCiYmlPath
:
'
/api/:version/templates/gitlab_ci_ymls/:key
'
,
...
...
@@ -74,9 +75,16 @@ const Api = {
},
newLabel
(
namespacePath
,
projectPath
,
data
,
callback
)
{
const
url
=
Api
.
buildUrl
(
Api
.
labelsPath
)
.
replace
(
'
:namespace_path
'
,
namespacePath
)
.
replace
(
'
:project_path
'
,
projectPath
);
let
url
;
if
(
projectPath
)
{
url
=
Api
.
buildUrl
(
Api
.
projectLabelsPath
)
.
replace
(
'
:namespace_path
'
,
namespacePath
)
.
replace
(
'
:project_path
'
,
projectPath
);
}
else
{
url
=
Api
.
buildUrl
(
Api
.
groupLabelsPath
).
replace
(
'
:namespace_path
'
,
namespacePath
);
}
return
$
.
ajax
({
url
,
type
:
'
POST
'
,
...
...
app/assets/javascripts/boards/boards_bundle.js
View file @
b4778a58
...
...
@@ -53,7 +53,8 @@ $(() => {
data
:
{
state
:
Store
.
state
,
loading
:
true
,
endpoint
:
$boardApp
.
dataset
.
endpoint
,
boardsEndpoint
:
$boardApp
.
dataset
.
boardsEndpoint
,
listsEndpoint
:
$boardApp
.
dataset
.
listsEndpoint
,
boardId
:
$boardApp
.
dataset
.
boardId
,
disabled
:
$boardApp
.
dataset
.
disabled
===
'
true
'
,
issueLinkBase
:
$boardApp
.
dataset
.
issueLinkBase
,
...
...
@@ -68,7 +69,13 @@ $(() => {
},
},
created
()
{
gl
.
boardService
=
new
BoardService
(
this
.
endpoint
,
this
.
bulkUpdatePath
,
this
.
boardId
);
gl
.
boardService
=
new
BoardService
({
boardsEndpoint
:
this
.
boardsEndpoint
,
listsEndpoint
:
this
.
listsEndpoint
,
bulkUpdatePath
:
this
.
bulkUpdatePath
,
boardId
:
this
.
boardId
,
});
Store
.
rootPath
=
this
.
boardsEndpoint
;
this
.
filterManager
=
new
FilteredSearchBoards
(
Store
.
filter
,
true
);
this
.
filterManager
.
setup
();
...
...
@@ -112,19 +119,21 @@ $(() => {
gl
.
IssueBoardsSearch
=
new
Vue
({
el
:
document
.
getElementById
(
'
js-add-list
'
),
data
:
{
filters
:
Store
.
state
.
filters
filters
:
Store
.
state
.
filters
,
},
mounted
()
{
gl
.
issueBoards
.
newListDropdownInit
();
}
}
,
});
gl
.
IssueBoardsModalAddBtn
=
new
Vue
({
mixins
:
[
gl
.
issueBoards
.
ModalMixins
],
el
:
document
.
getElementById
(
'
js-add-issues-btn
'
),
data
:
{
modal
:
ModalStore
.
store
,
store
:
Store
.
state
,
data
()
{
return
{
modal
:
ModalStore
.
store
,
store
:
Store
.
state
,
};
},
watch
:
{
disabled
()
{
...
...
@@ -133,6 +142,9 @@ $(() => {
},
computed
:
{
disabled
()
{
if
(
!
this
.
store
)
{
return
true
;
}
return
!
this
.
store
.
lists
.
filter
(
list
=>
!
list
.
preset
).
length
;
},
tooltipTitle
()
{
...
...
@@ -145,7 +157,7 @@ $(() => {
},
methods
:
{
updateTooltip
()
{
const
$tooltip
=
$
(
this
.
$
el
);
const
$tooltip
=
$
(
this
.
$
refs
.
addIssuesButton
);
this
.
$nextTick
(()
=>
{
if
(
this
.
disabled
)
{
...
...
@@ -165,16 +177,19 @@ $(() => {
this
.
updateTooltip
();
},
template
:
`
<button
class="btn btn-create pull-right prepend-left-10"
type="button"
data-placement="bottom"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
@click="openModal">
Add issues
</button>
<div class="board-extra-actions">
<button
class="btn btn-create prepend-left-10"
type="button"
data-placement="bottom"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
@click="openModal">
Add issues
</button>
</div>
`
,
});
});
app/assets/javascripts/boards/components/board_list.js
View file @
b4778a58
...
...
@@ -77,7 +77,7 @@ export default {
this
.
showIssueForm
=
!
this
.
showIssueForm
;
},
onScroll
()
{
if
(
(
this
.
scrollTop
()
>
this
.
scrollHeight
()
-
this
.
scrollOffset
)
&&
!
this
.
list
.
loadingMore
)
{
if
(
!
this
.
loadingMore
&&
(
this
.
scrollTop
()
>
this
.
scrollHeight
()
-
this
.
scrollOffset
)
)
{
this
.
loadNextPage
();
}
},
...
...
@@ -165,11 +165,9 @@ export default {
v-if="loading">
<loading-icon />
</div>
<transition name="slide-down">
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
...
...
app/assets/javascripts/boards/components/board_new_issue.js
View file @
b4778a58
...
...
@@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
export
default
{
name
:
'
BoardNewIssue
'
,
props
:
{
list
:
Object
,
list
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
...
...
app/assets/javascripts/boards/components/issue_card_inner.js
View file @
b4778a58
...
...
@@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return
this
.
issue
.
assignees
.
length
>
this
.
numberOverLimit
;
},
cardUrl
()
{
return
`
${
this
.
issueLinkBase
}
/
${
this
.
issue
.
id
}
`
;
return
`
${
this
.
issueLinkBase
}
/
${
this
.
issue
.
i
i
d
}
`
;
},
issueId
()
{
return
`#
${
this
.
issue
.
id
}
`
;
if
(
this
.
issue
.
iid
)
{
return
`#
${
this
.
issue
.
iid
}
`
;
}
return
false
;
},
showLabelFooter
()
{
return
this
.
issue
.
labels
.
find
(
l
=>
this
.
showLabel
(
l
))
!==
undefined
;
...
...
@@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:title="issue.title">{{ issue.title }}</a>
<span
class="card-number"
v-if="issue
.i
d"
v-if="issue
I
d"
>
{{ issueId }}
</span>
...
...
app/assets/javascripts/boards/components/modal/footer.js
View file @
b4778a58
...
...
@@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
const
firstListIndex
=
1
;
const
list
=
this
.
modal
.
selectedList
||
this
.
state
.
lists
[
firstListIndex
];
const
selectedIssues
=
ModalStore
.
getSelectedIssues
();
const
issueIds
=
selectedIssues
.
map
(
issue
=>
issue
.
globalI
d
);
const
issueIds
=
selectedIssues
.
map
(
issue
=>
issue
.
i
d
);
// Post the data to the backend
gl
.
boardService
.
bulkUpdate
(
issueIds
,
{
...
...
app/assets/javascripts/boards/components/new_list_dropdown.js
View file @
b4778a58
...
...
@@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
$this
.
glDropdown
({
data
(
term
,
callback
)
{
$
.
get
(
$this
.
attr
(
'
data-l
abels
'
))
$
.
get
(
$this
.
attr
(
'
data-l
ist-labels-path
'
))
.
then
((
resp
)
=>
{
callback
(
resp
);
});
...
...
app/assets/javascripts/boards/components/sidebar/remove_issue.js
View file @
b4778a58
...
...
@@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type
:
Object
,
required
:
true
,
},
issueUpdate
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
updateUrl
()
{
return
this
.
issueUpdate
;
},
},
methods
:
{
removeIssue
()
{
const
issue
=
this
.
issue
;
const
lists
=
issue
.
getLists
();
const
labelIds
=
lists
.
map
(
list
=>
list
.
label
.
id
);
// Post the remove data
gl
.
boardService
.
bulkUpdate
([
issue
.
globalId
],
{
remove_label_ids
:
labelIds
,
}).
catch
(()
=>
{
const
listLabelIds
=
lists
.
map
(
list
=>
list
.
label
.
id
);
let
labelIds
=
this
.
issue
.
labels
.
map
(
label
=>
label
.
id
)
.
filter
(
id
=>
!
listLabelIds
.
includes
(
id
));
if
(
labelIds
.
length
===
0
)
{
labelIds
=
[
''
];
}
const
data
=
{
issue
:
{
label_ids
:
labelIds
,
},
};
Vue
.
http
.
patch
(
this
.
updateUrl
,
data
).
catch
(()
=>
{
new
Flash
(
'
Failed to remove issue from board, please try again.
'
,
'
alert
'
);
lists
.
forEach
((
list
)
=>
{
...
...
app/assets/javascripts/boards/models/issue.js
View file @
b4778a58
...
...
@@ -7,8 +7,8 @@ import Vue from 'vue';
class
ListIssue
{
constructor
(
obj
,
defaultAvatar
)
{
this
.
globalI
d
=
obj
.
id
;
this
.
id
=
obj
.
iid
;
this
.
i
d
=
obj
.
id
;
this
.
i
i
d
=
obj
.
iid
;
this
.
title
=
obj
.
title
;
this
.
confidential
=
obj
.
confidential
;
this
.
dueDate
=
obj
.
due_date
;
...
...
app/assets/javascripts/boards/models/label.js
View file @
b4778a58
...
...
@@ -4,6 +4,7 @@ class ListLabel {
constructor
(
obj
)
{
this
.
id
=
obj
.
id
;
this
.
title
=
obj
.
title
;
this
.
type
=
obj
.
type
;
this
.
color
=
obj
.
color
;
this
.
textColor
=
obj
.
text_color
;
this
.
description
=
obj
.
description
;
...
...
app/assets/javascripts/boards/models/list.js
View file @
b4778a58
...
...
@@ -110,11 +110,13 @@ class List {
return
gl
.
boardService
.
newIssue
(
this
.
id
,
issue
)
.
then
(
resp
=>
resp
.
json
())
.
then
((
data
)
=>
{
issue
.
id
=
data
.
iid
;
issue
.
id
=
data
.
id
;
issue
.
iid
=
data
.
iid
;
issue
.
project
=
data
.
project
;
if
(
this
.
issuesSize
>
1
)
{
const
moveBeforeI
i
d
=
this
.
issues
[
1
].
id
;
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
null
,
moveBeforeI
i
d
);
const
moveBeforeId
=
this
.
issues
[
1
].
id
;
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
null
,
moveBeforeId
);
}
});
}
...
...
@@ -126,19 +128,19 @@ class List {
}
addIssue
(
issue
,
listFrom
,
newIndex
)
{
let
moveBeforeI
i
d
=
null
;
let
moveAfterI
i
d
=
null
;
let
moveBeforeId
=
null
;
let
moveAfterId
=
null
;
if
(
!
this
.
findIssue
(
issue
.
id
))
{
if
(
newIndex
!==
undefined
)
{
this
.
issues
.
splice
(
newIndex
,
0
,
issue
);
if
(
this
.
issues
[
newIndex
-
1
])
{
moveBeforeI
i
d
=
this
.
issues
[
newIndex
-
1
].
id
;
moveBeforeId
=
this
.
issues
[
newIndex
-
1
].
id
;
}
if
(
this
.
issues
[
newIndex
+
1
])
{
moveAfterI
i
d
=
this
.
issues
[
newIndex
+
1
].
id
;
moveAfterId
=
this
.
issues
[
newIndex
+
1
].
id
;
}
}
else
{
this
.
issues
.
push
(
issue
);
...
...
@@ -151,30 +153,30 @@ class List {
if
(
listFrom
)
{
this
.
issuesSize
+=
1
;
this
.
updateIssueLabel
(
issue
,
listFrom
,
moveBeforeI
id
,
moveAfterIi
d
);
this
.
updateIssueLabel
(
issue
,
listFrom
,
moveBeforeI
d
,
moveAfterI
d
);
}
}
}
moveIssue
(
issue
,
oldIndex
,
newIndex
,
moveBeforeI
id
,
moveAfterIi
d
)
{
moveIssue
(
issue
,
oldIndex
,
newIndex
,
moveBeforeI
d
,
moveAfterI
d
)
{
this
.
issues
.
splice
(
oldIndex
,
1
);
this
.
issues
.
splice
(
newIndex
,
0
,
issue
);
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
moveBeforeI
id
,
moveAfterIi
d
)
gl
.
boardService
.
moveIssue
(
issue
.
id
,
null
,
null
,
moveBeforeI
d
,
moveAfterI
d
)
.
catch
(()
=>
{
// TODO: handle request error
});
}
updateIssueLabel
(
issue
,
listFrom
,
moveBeforeI
id
,
moveAfterIi
d
)
{
gl
.
boardService
.
moveIssue
(
issue
.
id
,
listFrom
.
id
,
this
.
id
,
moveBeforeI
id
,
moveAfterIi
d
)
updateIssueLabel
(
issue
,
listFrom
,
moveBeforeI
d
,
moveAfterI
d
)
{
gl
.
boardService
.
moveIssue
(
issue
.
id
,
listFrom
.
id
,
this
.
id
,
moveBeforeI
d
,
moveAfterI
d
)
.
catch
(()
=>
{
// TODO: handle request error
});
}
findIssue
(
id
)
{
return
this
.
issues
.
fi
lter
(
issue
=>
issue
.
id
===
id
)[
0
]
;
return
this
.
issues
.
fi
nd
(
issue
=>
issue
.
id
===
id
)
;
}
removeIssue
(
removeIssue
)
{
...
...
app/assets/javascripts/boards/services/board_service.js
View file @
b4778a58
...
...
@@ -3,21 +3,21 @@
import
Vue
from
'
vue
'
;
class
BoardService
{
constructor
(
root
,
bulkUpdatePath
,
boardId
)
{
this
.
boards
=
Vue
.
resource
(
`
${
roo
t
}
{/id}.json`
,
{},
{
constructor
(
{
boardsEndpoint
,
listsEndpoint
,
bulkUpdatePath
,
boardId
}
)
{
this
.
boards
=
Vue
.
resource
(
`
${
boardsEndpoin
t
}
{/id}.json`
,
{},
{
issues
:
{
method
:
'
GET
'
,
url
:
`
${
root
}
/
${
boardId
}
/issues.json`
url
:
`
${
gon
.
relative_url_root
}
/boards/
${
boardId
}
/issues.json`
,
}
});
this
.
lists
=
Vue
.
resource
(
`
${
root
}
/
${
boardId
}
/lists
{/id}`
,
{},
{
this
.
lists
=
Vue
.
resource
(
`
${
listsEndpoint
}
{/id}`
,
{},
{
generate
:
{
method
:
'
POST
'
,
url
:
`
${
root
}
/
${
boardId
}
/lists
/generate.json`
url
:
`
${
listsEndpoint
}
/generate.json`
}
});
this
.
issue
=
Vue
.
resource
(
`
${
root
}
/
${
boardId
}
/issues{/id}`
,
{});
this
.
issues
=
Vue
.
resource
(
`
${
root
}
/
${
boardId
}
/lists
{/id}/issues`
,
{},
{
this
.
issue
=
Vue
.
resource
(
`
${
gon
.
relative_url_root
}
/boards
/
${
boardId
}
/issues{/id}`
,
{});
this
.
issues
=
Vue
.
resource
(
`
${
listsEndpoint
}
{/id}/issues`
,
{},
{
bulkUpdate
:
{
method
:
'
POST
'
,
url
:
bulkUpdatePath
,
...
...
@@ -60,12 +60,12 @@ class BoardService {
return
this
.
issues
.
get
(
data
);
}
moveIssue
(
id
,
from_list_id
=
null
,
to_list_id
=
null
,
move_before_i
id
=
null
,
move_after_i
id
=
null
)
{
moveIssue
(
id
,
from_list_id
=
null
,
to_list_id
=
null
,
move_before_i
d
=
null
,
move_after_
id
=
null
)
{
return
this
.
issue
.
update
({
id
},
{
from_list_id
,
to_list_id
,
move_before_i
i
d
,
move_after_i
i
d
,
move_before_id
,
move_after_id
,
});
}
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
b4778a58
...
...
@@ -183,7 +183,7 @@
width
:
auto
;
top
:
100%
;
left
:
0
;
z-index
:
2
00
;
z-index
:
3
00
;
min-width
:
240px
;
max-width
:
500px
;
margin-top
:
2px
;
...
...
app/assets/stylesheets/pages/boards.scss
View file @
b4778a58
...
...
@@ -117,13 +117,12 @@
}
.board-title
{
position
:
initial
;
padding
:
0
;
border-bottom
:
0
;
>
span
{
display
:
block
;
transform
:
rotate
(
90deg
)
translate
(
25px
,
0
);
transform
:
rotate
(
90deg
)
translate
(
35px
,
10px
);
}
}
...
...
@@ -151,11 +150,18 @@
}
.board-header
{
border-top-left-radius
:
$border-radius-default
;
border-top-right-radius
:
$border-radius-default
;
position
:
relative
;
&
.has-border
{
&.
has-border
:
:
before
{
border-top
:
3px
solid
;
border-color
:
inherit
;
border-top-left-radius
:
$border-radius-default
;
border-top-right-radius
:
$border-radius-default
;
content
:
''
;
position
:
absolute
;
width
:
calc
(
100%
+
2px
);
top
:
0
;
left
:
0
;
margin-top
:
-1px
;
margin-right
:
-1px
;
margin-left
:
-1px
;
...
...
@@ -176,12 +182,16 @@
}
.board-title
{
position
:
relative
;
margin
:
0
;
padding
:
$gl-padding
;
padding-bottom
:
(
$gl-padding
+
3px
);
padding
:
12px
$gl-padding
;
font-size
:
1em
;
border-bottom
:
1px
solid
$border-color
;
display
:
flex
;
align-items
:
center
;
}
.board-title-text
{
margin-right
:
auto
;
}
.board-delete
{
...
...
@@ -221,43 +231,10 @@
}
}
.slide-down-enter
{
transform
:
translateY
(
-100%
);
}
.slide-down-enter-active
{
transition
:
transform
$fade-in-duration
;
+
.board-list
{
transform
:
translateY
(
-136px
);
transition
:
none
;
}
}
.slide-down-enter-to
{
+
.board-list
{
transform
:
translateY
(
0
);
transition
:
transform
$fade-in-duration
ease
;
}
}
.slide-down-leave
{
transform
:
translateY
(
0
);
}
.slide-down-leave-active
{
transition
:
all
$fade-in-duration
;
transform
:
translateY
(
-136px
);
+
.board-list
{
transition
:
transform
$fade-in-duration
ease
;
transform
:
translateY
(
-136px
);
}
}
.board-list-component
{
height
:
calc
(
100%
-
49px
);
overflow
:
hidden
;
position
:
relative
;
}
.board-list
{
...
...
@@ -429,7 +406,7 @@
}
.board-new-issue-form
{
z-index
:
1
;
z-index
:
4
;
margin
:
5px
;
}
...
...
app/controllers/boards/application_controller.rb
0 → 100644
View file @
b4778a58
module
Boards
class
ApplicationController
<
::
ApplicationController
respond_to
:json
rescue_from
ActiveRecord
::
RecordNotFound
,
with: :record_not_found
private
def
board
@board
||=
Board
.
find
(
params
[
:board_id
])
end
def
board_parent
@board_parent
||=
board
.
parent
end
def
record_not_found
(
exception
)
render
json:
{
error:
exception
.
message
},
status: :not_found
end
end
end
app/controllers/boards/issues_controller.rb
0 → 100644
View file @
b4778a58
module
Boards
class
IssuesController
<
Boards
::
ApplicationController
include
BoardsResponses
before_action
:authorize_read_issue
,
only:
[
:index
]
before_action
:authorize_create_issue
,
only:
[
:create
]
before_action
:authorize_update_issue
,
only:
[
:update
]
skip_before_action
:authenticate_user!
,
only:
[
:index
]
def
index
issues
=
Boards
::
Issues
::
ListService
.
new
(
board_parent
,
current_user
,
filter_params
).
execute
issues
=
issues
.
page
(
params
[
:page
]).
per
(
params
[
:per
]
||
20
)
make_sure_position_is_set
(
issues
)
render
json:
{
issues:
serialize_as_json
(
issues
.
preload
(
:project
)),
size:
issues
.
total_count
}
end
def
create
service
=
Boards
::
Issues
::
CreateService
.
new
(
board_parent
,
project
,
current_user
,
issue_params
)
issue
=
service
.
execute
if
issue
.
valid?
render
json:
serialize_as_json
(
issue
)
else
render
json:
issue
.
errors
,
status: :unprocessable_entity
end
end
def
update
service
=
Boards
::
Issues
::
MoveService
.
new
(
board_parent
,
current_user
,
move_params
)
if
service
.
execute
(
issue
)
head
:ok
else
head
:unprocessable_entity
end
end
private
def
make_sure_position_is_set
(
issues
)
issues
.
each
do
|
issue
|
issue
.
move_to_end
&&
issue
.
save
unless
issue
.
relative_position
end
end
def
issue
@issue
||=
issues_finder
.
execute
.
find
(
params
[
:id
])
end
def
filter_params
params
.
merge
(
board_id:
params
[
:board_id
],
id:
params
[
:list_id
])
.
reject
{
|
_
,
value
|
value
.
nil?
}
end
def
issues_finder
IssuesFinder
.
new
(
current_user
,
project_id:
board_parent
.
id
)
end
def
project
board_parent
end
def
move_params
params
.
permit
(
:board_id
,
:id
,
:from_list_id
,
:to_list_id
,
:move_before_id
,
:move_after_id
)
end
def
issue_params
params
.
require
(
:issue
)
.
permit
(
:title
,
:milestone_id
,
:project_id
)
.
merge
(
board_id:
params
[
:board_id
],
list_id:
params
[
:list_id
],
request:
request
)
end
def
serialize_as_json
(
resource
)
resource
.
as_json
(
labels:
true
,
only:
[
:id
,
:iid
,
:project_id
,
:title
,
:confidential
,
:due_date
,
:relative_position
],
include:
{
project:
{
only:
[
:id
,
:path
]
},
assignees:
{
only:
[
:id
,
:name
,
:username
],
methods:
[
:avatar_url
]
},
milestone:
{
only:
[
:id
,
:title
]
}
},
user:
current_user
)
end
end
end
app/controllers/boards/lists_controller.rb
0 → 100644
View file @
b4778a58
module
Boards
class
ListsController
<
Boards
::
ApplicationController
include
BoardsResponses
before_action
:authorize_admin_list
,
only:
[
:create
,
:update
,
:destroy
,
:generate
]
before_action
:authorize_read_list
,
only:
[
:index
]
skip_before_action
:authenticate_user!
,
only:
[
:index
]
def
index
lists
=
Boards
::
Lists
::
ListService
.
new
(
board
.
parent
,
current_user
).
execute
(
board
)
render
json:
serialize_as_json
(
lists
)
end
def
create
list
=
Boards
::
Lists
::
CreateService
.
new
(
board
.
parent
,
current_user
,
list_params
).
execute
(
board
)
if
list
.
valid?
render
json:
serialize_as_json
(
list
)
else
render
json:
list
.
errors
,
status: :unprocessable_entity
end
end
def
update
list
=
board
.
lists
.
movable
.
find
(
params
[
:id
])
service
=
Boards
::
Lists
::
MoveService
.
new
(
board_parent
,
current_user
,
move_params
)
if
service
.
execute
(
list
)
head
:ok
else
head
:unprocessable_entity
end
end
def
destroy
list
=
board
.
lists
.
destroyable
.
find
(
params
[
:id
])
service
=
Boards
::
Lists
::
DestroyService
.
new
(
board_parent
,
current_user
)
if
service
.
execute
(
list
)
head
:ok
else
head
:unprocessable_entity
end
end
def
generate
service
=
Boards
::
Lists
::
GenerateService
.
new
(
board_parent
,
current_user
)
if
service
.
execute
(
board
)
render
json:
serialize_as_json
(
board
.
lists
.
movable
)
else
head
:unprocessable_entity
end
end
private
def
list_params
params
.
require
(
:list
).
permit
(
:label_id
)
end
def
move_params
params
.
require
(
:list
).
permit
(
:position
)
end
def
serialize_as_json
(
resource
)
resource
.
as_json
(
only:
[
:id
,
:list_type
,
:position
],
methods:
[
:title
],
label:
true
)
end
end
end
app/controllers/concerns/boards_responses.rb
0 → 100644
View file @
b4778a58
module
BoardsResponses
def
authorize_read_list
authorize_action_for!
(
board
.
parent
,
:read_list
)
end
def
authorize_read_issue
authorize_action_for!
(
board
.
parent
,
:read_issue
)
end
def
authorize_update_issue
authorize_action_for!
(
issue
,
:admin_issue
)
end
def
authorize_create_issue
authorize_action_for!
(
project
,
:admin_issue
)
end
def
authorize_admin_list
authorize_action_for!
(
board
.
parent
,
:admin_list
)
end
def
authorize_action_for!
(
resource
,
ability
)
return
render_403
unless
can?
(
current_user
,
ability
,
resource
)
end
def
respond_with_boards
respond_with
(
@boards
)
end
def
respond_with_board
respond_with
(
@board
)
end
def
respond_with
(
resource
)
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
serialize_as_json
(
resource
)
end
end
end
end
app/controllers/projects/boards/application_controller.rb
deleted
100644 → 0
View file @
cb555da1
module
Projects
module
Boards
class
ApplicationController
<
Projects
::
ApplicationController
respond_to
:json
rescue_from
ActiveRecord
::
RecordNotFound
,
with: :record_not_found
private
def
record_not_found
(
exception
)
render
json:
{
error:
exception
.
message
},
status: :not_found
end
end
end
end
app/controllers/projects/boards/issues_controller.rb
deleted
100644 → 0
View file @
cb555da1
module
Projects
module
Boards
class
IssuesController
<
Boards
::
ApplicationController
before_action
:authorize_read_issue!
,
only:
[
:index
]
before_action
:authorize_create_issue!
,
only:
[
:create
]
before_action
:authorize_update_issue!
,
only:
[
:update
]
def
index
issues
=
::
Boards
::
Issues
::
ListService
.
new
(
project
,
current_user
,
filter_params
).
execute
issues
=
issues
.
page
(
params
[
:page
]).
per
(
params
[
:per
]
||
20
)
make_sure_position_is_set
(
issues
)
render
json:
{
issues:
serialize_as_json
(
issues
),
size:
issues
.
total_count
}
end
def
create
service
=
::
Boards
::
Issues
::
CreateService
.
new
(
project
,
current_user
,
issue_params
)
issue
=
service
.
execute
if
issue
.
valid?
render
json:
serialize_as_json
(
issue
)
else
render
json:
issue
.
errors
,
status: :unprocessable_entity
end
end
def
update
service
=
::
Boards
::
Issues
::
MoveService
.
new
(
project
,
current_user
,
move_params
)
if
service
.
execute
(
issue
)
head
:ok
else
head
:unprocessable_entity
end
end
private
def
make_sure_position_is_set
(
issues
)
issues
.
each
do
|
issue
|
issue
.
move_to_end
&&
issue
.
save
unless
issue
.
relative_position
end
end
def
issue
@issue
||=
IssuesFinder
.
new
(
current_user
,
project_id:
project
.
id
)
.
execute
.
where
(
iid:
params
[
:id
])
.
first!
end
def
authorize_read_issue!
return
render_403
unless
can?
(
current_user
,
:read_issue
,
project
)
end
def
authorize_create_issue!
return
render_403
unless
can?
(
current_user
,
:admin_issue
,
project
)
end
def
authorize_update_issue!
return
render_403
unless
can?
(
current_user
,
:update_issue
,
issue
)
end
def
filter_params
params
.
merge
(
board_id:
params
[
:board_id
],
id:
params
[
:list_id
])
.
reject
{
|
_
,
value
|
value
.
nil?
}
end
def
move_params
params
.
permit
(
:board_id
,
:id
,
:from_list_id
,
:to_list_id
,
:move_before_iid
,
:move_after_iid
)
end
def
issue_params
params
.
require
(
:issue
).
permit
(
:title
).
merge
(
board_id:
params
[
:board_id
],
list_id:
params
[
:list_id
],
request:
request
)
end
def
serialize_as_json
(
resource
)
resource
.
as_json
(
labels:
true
,
only:
[
:id
,
:iid
,
:title
,
:confidential
,
:due_date
,
:relative_position
],
include:
{
assignees:
{
only:
[
:id
,
:name
,
:username
],
methods:
[
:avatar_url
]
},
milestone:
{
only:
[
:id
,
:title
]
}
},
user:
current_user
)
end
end
end
end
app/controllers/projects/boards/lists_controller.rb
deleted
100644 → 0
View file @
cb555da1
module
Projects
module
Boards
class
ListsController
<
Boards
::
ApplicationController
before_action
:authorize_admin_list!
,
only:
[
:create
,
:update
,
:destroy
,
:generate
]
before_action
:authorize_read_list!
,
only:
[
:index
]
def
index
lists
=
::
Boards
::
Lists
::
ListService
.
new
(
project
,
current_user
).
execute
(
board
)
render
json:
serialize_as_json
(
lists
)
end
def
create
list
=
::
Boards
::
Lists
::
CreateService
.
new
(
project
,
current_user
,
list_params
).
execute
(
board
)
if
list
.
valid?
render
json:
serialize_as_json
(
list
)
else
render
json:
list
.
errors
,
status: :unprocessable_entity
end
end
def
update
list
=
board
.
lists
.
movable
.
find
(
params
[
:id
])
service
=
::
Boards
::
Lists
::
MoveService
.
new
(
project
,
current_user
,
move_params
)
if
service
.
execute
(
list
)
head
:ok
else
head
:unprocessable_entity
end
end
def
destroy
list
=
board
.
lists
.
destroyable
.
find
(
params
[
:id
])
service
=
::
Boards
::
Lists
::
DestroyService
.
new
(
project
,
current_user
)
if
service
.
execute
(
list
)
head
:ok
else
head
:unprocessable_entity
end
end
def
generate
service
=
::
Boards
::
Lists
::
GenerateService
.
new
(
project
,
current_user
)
if
service
.
execute
(
board
)
render
json:
serialize_as_json
(
board
.
lists
.
movable
)
else
head
:unprocessable_entity
end
end
private
def
authorize_admin_list!
return
render_403
unless
can?
(
current_user
,
:admin_list
,
project
)
end
def
authorize_read_list!
return
render_403
unless
can?
(
current_user
,
:read_list
,
project
)
end
def
board
@board
||=
project
.
boards
.
find
(
params
[
:board_id
])
end
def
list_params
params
.
require
(
:list
).
permit
(
:label_id
)
end
def
move_params
params
.
require
(
:list
).
permit
(
:position
)
end
def
serialize_as_json
(
resource
)
resource
.
as_json
(
only:
[
:id
,
:list_type
,
:position
],
methods:
[
:title
],
label:
true
)
end
end
end
end
app/controllers/projects/boards_controller.rb
View file @
b4778a58
class
Projects::BoardsController
<
Projects
::
ApplicationController
include
BoardsResponses
include
IssuableCollections
before_action
:authorize_read_board!
,
only:
[
:index
,
:show
]
before_action
:assign_endpoint_vars
def
index
@boards
=
::
Boards
::
ListService
.
new
(
project
,
current_user
).
execute
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
serialize_as_json
(
@boards
)
end
end
@boards
=
Boards
::
ListService
.
new
(
project
,
current_user
).
execute
respond_with_boards
end
def
show
@board
=
project
.
boards
.
find
(
params
[
:id
])
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
serialize_as_json
(
@board
)
end
end
respond_with_board
end
private
def
assign_endpoint_vars
@boards_endpoint
=
project_boards_url
(
project
)
@bulk_issues_path
=
bulk_update_project_issues_path
(
project
)
@namespace_path
=
project
.
namespace
.
full_path
@labels_endpoint
=
project_labels_path
(
project
)
end
def
authorize_read_board!
return
access_denied!
unless
can?
(
current_user
,
:read_board
,
project
)
end
...
...
app/helpers/boards_helper.rb
View file @
b4778a58
module
BoardsHelper
def
board_data
board
=
@board
||
@boards
.
first
def
board
@board
||=
@board
||
@boards
.
first
end
def
board_data
{
endpoint:
project_boards_path
(
@project
),
boards_endpoint:
@boards_endpoint
,
lists_endpoint:
board_lists_url
(
board
),
board_id:
board
.
id
,
disabled:
"
#{
!
can?
(
current_user
,
:admin_list
,
@projec
t
)
}
"
,
issue_link_base:
project_issues_path
(
@project
)
,
disabled:
"
#{
!
can?
(
current_user
,
:admin_list
,
current_board_paren
t
)
}
"
,
issue_link_base:
build_issue_link_base
,
root_path:
root_path
,
bulk_update_path:
bulk_update_project_issues_path
(
@project
)
,
bulk_update_path:
@bulk_issues_path
,
default_avatar:
image_path
(
default_avatar
)
}
end
def
build_issue_link_base
project_issues_path
(
@project
)
end
def
current_board_json
board
=
@board
||
@boards
.
first
board
.
to_json
(
only:
[
:id
,
:name
,
:milestone_id
],
include:
{
milestone:
{
only:
[
:title
]
}
}
)
end
def
board_base_url
project_boards_path
(
@project
)
end
def
multiple_boards_available?
current_board_parent
.
multiple_issue_boards_available?
(
current_user
)
end
def
current_board_path
(
board
)
@current_board_path
||=
project_board_path
(
current_board_parent
,
board
)
end
def
current_board_parent
@current_board_parent
||=
@project
end
def
can_admin_issue?
can?
(
current_user
,
:admin_issue
,
current_board_parent
)
end
def
board_list_data
{
toggle:
"dropdown"
,
list_labels_path:
labels_filter_path
(
true
),
labels:
labels_filter_path
(
true
),
labels_endpoint:
@labels_endpoint
,
namespace_path:
@namespace_path
,
project_path:
@project
&
.
try
(
:path
)
}
end
def
board_sidebar_user_data
dropdown_options
=
issue_assignees_dropdown_options
{
toggle:
'dropdown'
,
field_name:
'issue[assignee_ids][]'
,
first_user:
current_user
&
.
username
,
current_user:
'true'
,
project_id:
@project
&
.
try
(
:id
),
null_user:
'true'
,
multi_select:
'true'
,
'dropdown-header'
:
dropdown_options
[
:data
][
:'dropdown-header'
],
'max-select'
:
dropdown_options
[
:data
][
:'max-select'
]
}
end
end
app/helpers/issuables_helper.rb
View file @
b4778a58
...
...
@@ -347,6 +347,14 @@ module IssuablesHelper
end
end
def
labels_path
if
@project
project_labels_path
(
@project
)
elsif
@group
group_labels_path
(
@group
)
end
end
def
issuable_sidebar_options
(
issuable
,
can_edit_issuable
)
{
endpoint:
"
#{
issuable_json_path
(
issuable
)
}
?basic=true"
,
...
...
app/helpers/labels_helper.rb
View file @
b4778a58
...
...
@@ -121,13 +121,14 @@ module LabelsHelper
end
end
def
labels_filter_path
return
group_labels_path
(
@group
,
:json
)
if
@group
def
labels_filter_path
(
only_group_labels
=
false
)
project
=
@target_project
||
@project
if
project
project_labels_path
(
project
,
:json
)
elsif
@group
options
=
{
only_group_labels:
only_group_labels
}
if
only_group_labels
group_labels_path
(
@group
,
:json
,
options
)
else
dashboard_labels_path
(
:json
)
end
...
...
app/helpers/search_helper.rb
View file @
b4778a58
...
...
@@ -134,19 +134,21 @@ module SearchHelper
end
def
search_filter_input_options
(
type
)
opts
=
{
id:
"filtered-search-
#{
type
}
"
,
placeholder:
'Search or filter results...'
,
data:
{
'username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
])
opts
=
{
id:
"filtered-search-
#{
type
}
"
,
placeholder:
'Search or filter results...'
,
data:
{
'username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
])
}
}
}
if
@project
.
present?
opts
[
:data
][
'project-id'
]
=
@project
.
id
opts
[
:data
][
'base-endpoint'
]
=
project_path
(
@project
)
else
# Group context
opts
[
:data
][
'group-id'
]
=
@group
.
id
opts
[
:data
][
'base-endpoint'
]
=
group_canonical_path
(
@group
)
end
...
...
app/models/board.rb
View file @
b4778a58
...
...
@@ -3,7 +3,19 @@ class Board < ActiveRecord::Base
has_many
:lists
,
->
{
order
(
:list_type
,
:position
)
},
dependent: :delete_all
# rubocop:disable Cop/ActiveRecordDependent
validates
:project
,
presence:
true
validates
:project
,
presence:
true
,
if: :project_needed?
def
project_needed?
true
end
def
parent
project
end
def
group_board?
false
end
def
backlog_list
lists
.
merge
(
List
.
backlog
).
take
...
...
app/models/concerns/relative_positioning.rb
View file @
b4778a58
...
...
@@ -10,8 +10,12 @@ module RelativePositioning
after_save
:save_positionable_neighbours
end
def
project_ids
[
project
.
id
]
end
def
max_relative_position
self
.
class
.
in_projects
(
project
.
id
).
maximum
(
:relative_position
)
self
.
class
.
in_projects
(
project
_ids
).
maximum
(
:relative_position
)
end
def
prev_relative_position
...
...
@@ -19,7 +23,7 @@ module RelativePositioning
if
self
.
relative_position
prev_pos
=
self
.
class
.
in_projects
(
project
.
id
)
.
in_projects
(
project
_ids
)
.
where
(
'relative_position < ?'
,
self
.
relative_position
)
.
maximum
(
:relative_position
)
end
...
...
@@ -32,7 +36,7 @@ module RelativePositioning
if
self
.
relative_position
next_pos
=
self
.
class
.
in_projects
(
project
.
id
)
.
in_projects
(
project
_ids
)
.
where
(
'relative_position > ?'
,
self
.
relative_position
)
.
minimum
(
:relative_position
)
end
...
...
@@ -59,7 +63,7 @@ module RelativePositioning
pos_after
=
before
.
next_relative_position
if
before
.
shift_after?
issue_to_move
=
self
.
class
.
in_projects
(
project
.
id
).
find_by!
(
relative_position:
pos_after
)
issue_to_move
=
self
.
class
.
in_projects
(
project
_ids
).
find_by!
(
relative_position:
pos_after
)
issue_to_move
.
move_after
@positionable_neighbours
=
[
issue_to_move
]
...
...
@@ -74,7 +78,7 @@ module RelativePositioning
pos_before
=
after
.
prev_relative_position
if
after
.
shift_before?
issue_to_move
=
self
.
class
.
in_projects
(
project
.
id
).
find_by!
(
relative_position:
pos_before
)
issue_to_move
=
self
.
class
.
in_projects
(
project
_ids
).
find_by!
(
relative_position:
pos_before
)
issue_to_move
.
move_before
@positionable_neighbours
=
[
issue_to_move
]
...
...
app/models/label.rb
View file @
b4778a58
...
...
@@ -34,7 +34,8 @@ class Label < ActiveRecord::Base
scope
:templates
,
->
{
where
(
template:
true
)
}
scope
:with_title
,
->
(
title
)
{
where
(
title:
title
)
}
scope
:on_project_boards
,
->
(
project_id
)
{
joins
(
lists: :board
).
merge
(
List
.
movable
).
where
(
boards:
{
project_id:
project_id
})
}
scope
:with_lists_and_board
,
->
{
joins
(
lists: :board
).
merge
(
List
.
movable
)
}
scope
:on_project_boards
,
->
(
project_id
)
{
with_lists_and_board
.
where
(
boards:
{
project_id:
project_id
})
}
def
self
.
prioritized
(
project
)
joins
(
:priorities
)
...
...
@@ -172,6 +173,7 @@ class Label < ActiveRecord::Base
def
as_json
(
options
=
{})
super
(
options
).
tap
do
|
json
|
json
[
:type
]
=
self
.
try
(
:type
)
json
[
:priority
]
=
priority
(
options
[
:project
])
if
options
.
key?
(
:project
)
end
end
...
...
app/models/project.rb
View file @
b4778a58
...
...
@@ -1486,6 +1486,14 @@ class Project < ActiveRecord::Base
end
end
def
multiple_issue_boards_available?
(
user
)
feature_available?
(
:multiple_issue_boards
,
user
)
end
def
issue_board_milestone_available?
(
user
=
nil
)
feature_available?
(
:issue_board_milestone
,
user
)
end
def
full_path_was
File
.
join
(
namespace
.
full_path
,
previous_changes
[
'path'
].
first
)
end
...
...
app/services/boards/base_service.rb
0 → 100644
View file @
b4778a58
module
Boards
class
BaseService
<
::
BaseService
# Parent can either a group or a project
attr_accessor
:parent
,
:current_user
,
:params
def
initialize
(
parent
,
user
,
params
=
{})
@parent
,
@current_user
,
@params
=
parent
,
user
,
params
.
dup
end
end
end
app/services/boards/create_service.rb
View file @
b4778a58
module
Boards
class
CreateService
<
BaseService
class
CreateService
<
B
oards
::
B
aseService
def
execute
create_board!
if
can_create_board?
end
...
...
@@ -7,11 +7,11 @@ module Boards
private
def
can_create_board?
p
rojec
t
.
boards
.
size
==
0
p
aren
t
.
boards
.
size
==
0
end
def
create_board!
board
=
p
rojec
t
.
boards
.
create
(
params
)
board
=
p
aren
t
.
boards
.
create
(
params
)
if
board
.
persisted?
board
.
lists
.
create
(
list_type: :backlog
)
...
...
app/services/boards/issues/create_service.rb
View file @
b4778a58
module
Boards
module
Issues
class
CreateService
<
BaseService
class
CreateService
<
Boards
::
BaseService
attr_accessor
:project
def
initialize
(
parent
,
project
,
user
,
params
=
{})
@project
=
project
super
(
parent
,
user
,
params
)
end
def
execute
create_issue
(
params
.
merge
(
label_ids:
[
list
.
label_id
]))
end
...
...
@@ -8,7 +16,7 @@ module Boards
private
def
board
@board
||=
p
rojec
t
.
boards
.
find
(
params
.
delete
(
:board_id
))
@board
||=
p
aren
t
.
boards
.
find
(
params
.
delete
(
:board_id
))
end
def
list
...
...
app/services/boards/issues/list_service.rb
View file @
b4778a58
module
Boards
module
Issues
class
ListService
<
BaseService
class
ListService
<
B
oards
::
B
aseService
def
execute
issues
=
IssuesFinder
.
new
(
current_user
,
filter_params
).
execute
issues
=
without_board_labels
(
issues
)
unless
movable_list?
||
closed_list?
...
...
@@ -11,7 +11,7 @@ module Boards
private
def
board
@board
||=
p
rojec
t
.
boards
.
find
(
params
[
:board_id
])
@board
||=
p
aren
t
.
boards
.
find
(
params
[
:board_id
])
end
def
list
...
...
@@ -33,14 +33,14 @@ module Boards
end
def
filter_params
set_p
rojec
t
set_p
aren
t
set_state
params
end
def
set_p
rojec
t
params
[
:project_id
]
=
p
rojec
t
.
id
def
set_p
aren
t
params
[
:project_id
]
=
p
aren
t
.
id
end
def
set_state
...
...
app/services/boards/issues/move_service.rb
View file @
b4778a58
module
Boards
module
Issues
class
MoveService
<
BaseService
class
MoveService
<
B
oards
::
B
aseService
def
execute
(
issue
)
return
false
unless
can?
(
current_user
,
:update_issue
,
issue
)
return
false
if
issue_params
.
empty?
update
_service
.
execute
(
issue
)
update
(
issue
)
end
private
def
board
@board
||=
p
rojec
t
.
boards
.
find
(
params
[
:board_id
])
@board
||=
p
aren
t
.
boards
.
find
(
params
[
:board_id
])
end
def
move_between_lists?
...
...
@@ -27,8 +27,8 @@ module Boards
@moving_to_list
||=
board
.
lists
.
find_by
(
id:
params
[
:to_list_id
])
end
def
update
_service
::
Issues
::
UpdateService
.
new
(
project
,
current_user
,
issue_params
)
def
update
(
issue
)
::
Issues
::
UpdateService
.
new
(
issue
.
project
,
current_user
,
issue_params
).
execute
(
issue
)
end
def
issue_params
...
...
@@ -42,7 +42,7 @@ module Boards
)
end
attrs
[
:move_between_i
ids
]
=
move_between_iids
if
move_between_i
ids
attrs
[
:move_between_i
ds
]
=
move_between_ids
if
move_between_
ids
attrs
end
...
...
@@ -61,16 +61,16 @@ module Boards
if
moving_to_list
.
movable?
moving_from_list
.
label_id
else
Label
.
on_project_boards
(
p
rojec
t
.
id
).
pluck
(
:label_id
)
Label
.
on_project_boards
(
p
aren
t
.
id
).
pluck
(
:label_id
)
end
Array
(
label_ids
).
compact
end
def
move_between_i
i
ds
return
unless
params
[
:move_after_i
id
]
||
params
[
:move_before_i
id
]
def
move_between_ids
return
unless
params
[
:move_after_i
d
]
||
params
[
:move_before_
id
]
[
params
[
:move_after_i
id
],
params
[
:move_before_i
id
]]
[
params
[
:move_after_i
d
],
params
[
:move_before_
id
]]
end
end
end
...
...
app/services/boards/list_service.rb
View file @
b4778a58
module
Boards
class
ListService
<
BaseService
class
ListService
<
B
oards
::
B
aseService
def
execute
create_board!
if
p
rojec
t
.
boards
.
empty?
p
rojec
t
.
boards
create_board!
if
p
aren
t
.
boards
.
empty?
p
aren
t
.
boards
end
private
def
create_board!
Boards
::
CreateService
.
new
(
p
rojec
t
,
current_user
).
execute
Boards
::
CreateService
.
new
(
p
aren
t
,
current_user
).
execute
end
end
end
app/services/boards/lists/create_service.rb
View file @
b4778a58
module
Boards
module
Lists
class
CreateService
<
BaseService
class
CreateService
<
B
oards
::
B
aseService
def
execute
(
board
)
List
.
transaction
do
label
=
available_labels
.
find
(
params
[
:label_id
])
label
=
available_labels
_for
(
board
)
.
find
(
params
[
:label_id
])
position
=
next_position
(
board
)
create_list
(
board
,
label
,
position
)
end
end
private
def
available_labels
LabelsFinder
.
new
(
current_user
,
project_id:
p
rojec
t
.
id
).
execute
def
available_labels
_for
(
board
)
LabelsFinder
.
new
(
current_user
,
project_id:
p
aren
t
.
id
).
execute
end
def
next_position
(
board
)
...
...
app/services/boards/lists/destroy_service.rb
View file @
b4778a58
module
Boards
module
Lists
class
DestroyService
<
BaseService
class
DestroyService
<
B
oards
::
B
aseService
def
execute
(
list
)
return
false
unless
list
.
destroyable?
...
...
app/services/boards/lists/generate_service.rb
View file @
b4778a58
module
Boards
module
Lists
class
GenerateService
<
BaseService
class
GenerateService
<
B
oards
::
B
aseService
def
execute
(
board
)
return
false
unless
board
.
lists
.
movable
.
empty?
...
...
@@ -15,11 +15,11 @@ module Boards
def
create_list
(
board
,
params
)
label
=
find_or_create_label
(
params
)
Lists
::
CreateService
.
new
(
p
rojec
t
,
current_user
,
label_id:
label
.
id
).
execute
(
board
)
Lists
::
CreateService
.
new
(
p
aren
t
,
current_user
,
label_id:
label
.
id
).
execute
(
board
)
end
def
find_or_create_label
(
params
)
::
Labels
::
FindOrCreateService
.
new
(
current_user
,
p
rojec
t
,
params
).
execute
::
Labels
::
FindOrCreateService
.
new
(
current_user
,
p
aren
t
,
params
).
execute
end
def
label_params
...
...
app/services/boards/lists/list_service.rb
View file @
b4778a58
module
Boards
module
Lists
class
ListService
<
BaseService
class
ListService
<
B
oards
::
B
aseService
def
execute
(
board
)
board
.
lists
.
create
(
list_type: :backlog
)
unless
board
.
lists
.
backlog
.
exists?
...
...
app/services/boards/lists/move_service.rb
View file @
b4778a58
module
Boards
module
Lists
class
MoveService
<
BaseService
class
MoveService
<
B
oards
::
B
aseService
def
execute
(
list
)
@board
=
list
.
board
@old_position
=
list
.
position
...
...
app/services/issues/update_service.rb
View file @
b4778a58
...
...
@@ -3,7 +3,7 @@ module Issues
include
SpamCheckService
def
execute
(
issue
)
handle_move_between_i
i
ds
(
issue
)
handle_move_between_ids
(
issue
)
filter_spam_check_params
change_issue_duplicate
(
issue
)
move_issue_to_new_project
(
issue
)
||
update
(
issue
)
...
...
@@ -54,13 +54,13 @@ module Issues
end
end
def
handle_move_between_i
i
ds
(
issue
)
return
unless
params
[
:move_between_i
i
ds
]
def
handle_move_between_ids
(
issue
)
return
unless
params
[
:move_between_ids
]
after_i
id
,
before_iid
=
params
.
delete
(
:move_between_i
ids
)
after_i
d
,
before_id
=
params
.
delete
(
:move_between_
ids
)
issue_before
=
get_issue_if_allowed
(
issue
.
project
,
before_i
id
)
if
before_i
id
issue_after
=
get_issue_if_allowed
(
issue
.
project
,
after_i
id
)
if
after_i
id
issue_before
=
get_issue_if_allowed
(
issue
.
project
,
before_i
d
)
if
before_
id
issue_after
=
get_issue_if_allowed
(
issue
.
project
,
after_i
d
)
if
after_
id
issue
.
move_between
(
issue_before
,
issue_after
)
end
...
...
@@ -87,8 +87,8 @@ module Issues
private
def
get_issue_if_allowed
(
project
,
i
i
d
)
issue
=
project
.
issues
.
find
_by
(
iid:
i
id
)
def
get_issue_if_allowed
(
project
,
id
)
issue
=
project
.
issues
.
find
(
id
)
issue
if
can?
(
current_user
,
:update_issue
,
issue
)
end
...
...
app/views/projects/boards/index.html.haml
View file @
b4778a58
=
render
"sh
ow"
=
render
"sh
ared/boards/show"
,
board:
@boards
.
first
app/views/projects/boards/show.html.haml
View file @
b4778a58
=
render
"sh
ow"
=
render
"sh
ared/boards/show"
,
board:
@board
app/views/
projects
/boards/_show.html.haml
→
app/views/
shared
/boards/_show.html.haml
View file @
b4778a58
...
...
@@ -9,7 +9,7 @@
=
webpack_bundle_tag
'filtered_search'
=
webpack_bundle_tag
'boards'
%script
#js-board-template
{
type:
"text/x-template"
}=
render
"
projects
/boards/components/board"
%script
#js-board-template
{
type:
"text/x-template"
}=
render
"
shared
/boards/components/board"
%script
#js-board-modal-filter
{
type:
"text/x-template"
}=
render
"shared/issuable/search_bar"
,
type: :boards_modal
=
render
"projects/issues/head"
...
...
@@ -30,7 +30,7 @@
":root-path"
=>
"rootPath"
,
":board-id"
=>
"boardId"
,
":key"
=>
"_uid"
}
=
render
"
projects
/boards/components/sidebar"
=
render
"
shared
/boards/components/sidebar"
%board-add-issues-modal
{
"blank-state-image"
=>
render
(
'shared/empty_states/icons/issues.svg'
),
"new-issue-path"
=>
new_project_issue_path
(
@project
),
"milestone-path"
=>
milestones_filter_dropdown_path
,
...
...
app/views/
projects
/boards/components/_board.html.haml
→
app/views/
shared
/boards/components/_board.html.haml
View file @
b4778a58
...
...
@@ -7,20 +7,26 @@
":class"
:
"{
\"
fa-caret-down
\"
: list.isExpanded,
\"
fa-caret-right
\"
: !list.isExpanded && list.position === -1,
\"
fa-caret-left
\"
: !list.isExpanded && list.position !== -1 }"
,
"aria-hidden"
:
"true"
}
%span
.has-tooltip
{
"v-if"
:
"list.type !==
\"
label
\"
"
,
%span
.
board-title-text.
has-tooltip
{
"v-if"
:
"list.type !==
\"
label
\"
"
,
":title"
=>
'(list.label ? list.label.description : "")'
}
{{ list.title }}
%span
.has-tooltip
{
"v-if"
:
"list.type ===
\"
label
\"
"
,
":title"
=>
'(list.label ? list.label.description : "")'
,
data:
{
container:
"body"
,
placement:
"bottom"
},
class:
"label color-label title"
,
class:
"label color-label title
board-title-text
"
,
":style"
=>
"{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color :
\"
#2e2e2e
\"
) }"
}
{{ list.title }}
.issue-count-badge.pull-right.clearfix
{
"v-if"
=>
'list.type !== "blank"'
}
-
if
can?
(
current_user
,
:admin_list
,
current_board_parent
)
%board-delete
{
"inline-template"
=>
true
,
":list"
=>
"list"
,
"v-if"
=>
"!list.preset && list.id"
}
%button
.board-delete.has-tooltip.pull-right
{
type:
"button"
,
title:
"Delete list"
,
"aria-label"
=>
"Delete list"
,
data:
{
placement:
"bottom"
},
"@click.stop"
=>
"deleteBoard"
}
=
icon
(
"trash"
)
.issue-count-badge.clearfix
{
"v-if"
=>
'list.type !== "blank"'
}
%span
.issue-count-badge-count.pull-left
{
":class"
=>
'
{
"has-btn"
:
list
.
type
!==
"closed"
&&
!
disabled
}
'
}
{{ list.issuesSize }}
-
if
can?
(
current_user
,
:admin_
issue
,
@projec
t
)
-
if
can?
(
current_user
,
:admin_
list
,
current_board_paren
t
)
%button
.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse
{
type:
"button"
,
"@click"
=>
"showNewIssueForm"
,
"v-if"
=>
'list.type !== "closed"'
,
...
...
@@ -28,12 +34,7 @@
"title"
=>
"New issue"
,
data:
{
placement:
"top"
,
container:
"body"
}
}
=
icon
(
"plus"
,
class:
"js-no-trigger-collapse"
)
-
if
can?
(
current_user
,
:admin_list
,
@project
)
%board-delete
{
"inline-template"
=>
true
,
":list"
=>
"list"
,
"v-if"
=>
"!list.preset && list.id"
}
%button
.board-delete.has-tooltip.pull-right
{
type:
"button"
,
title:
"Delete list"
,
"aria-label"
=>
"Delete list"
,
data:
{
placement:
"bottom"
},
"@click.stop"
=>
"deleteBoard"
}
=
icon
(
"trash"
)
%board-list
{
"v-if"
=>
'list.type !== "blank"'
,
":list"
=>
"list"
,
":issues"
=>
"list.issues"
,
...
...
@@ -42,5 +43,5 @@
":issue-link-base"
=>
"issueLinkBase"
,
":root-path"
=>
"rootPath"
,
"ref"
=>
"board-list"
}
-
if
can?
(
current_user
,
:admin_list
,
@projec
t
)
-
if
can?
(
current_user
,
:admin_list
,
current_board_paren
t
)
%board-blank-state
{
"v-if"
=>
'list.id == "blank"'
}
app/views/
projects
/boards/components/_sidebar.html.haml
→
app/views/
shared
/boards/components/_sidebar.html.haml
View file @
b4778a58
...
...
@@ -10,18 +10,19 @@
%br
/
%span
=
precede
"#"
do
{{ issue.id }}
{{ issue.i
i
d }}
%a
.gutter-toggle.pull-right
{
role:
"button"
,
href:
"#"
,
"@click.prevent"
=>
"closeSidebar"
,
"aria-label"
=>
"Toggle sidebar"
}
=
custom_icon
(
"icon_close"
,
size:
15
)
.js-issuable-update
=
render
"
projects
/boards/components/sidebar/assignee"
=
render
"
projects
/boards/components/sidebar/milestone"
=
render
"
projects
/boards/components/sidebar/due_date"
=
render
"
projects
/boards/components/sidebar/labels"
=
render
"
projects
/boards/components/sidebar/notifications"
=
render
"
shared
/boards/components/sidebar/assignee"
=
render
"
shared
/boards/components/sidebar/milestone"
=
render
"
shared
/boards/components/sidebar/due_date"
=
render
"
shared
/boards/components/sidebar/labels"
=
render
"
shared
/boards/components/sidebar/notifications"
%remove-btn
{
":issue"
=>
"issue"
,
":issue-update"
=>
"'#{build_issue_link_base}/' + issue.iid + '.json'"
,
":list"
=>
"list"
,
"v-if"
=>
"canRemove"
}
app/views/
projects
/boards/components/sidebar/_assignee.html.haml
→
app/views/
shared
/boards/components/sidebar/_assignee.html.haml
View file @
b4778a58
...
...
@@ -2,13 +2,13 @@
%template
{
"v-if"
=>
"issue.assignees"
}
%assignee-title
{
":number-of-assignees"
=>
"issue.assignees.length"
,
":loading"
=>
"loadingAssignees"
,
":editable"
=>
can
?
(
current_user
,
:admin_issue
,
@project
)
}
":editable"
=>
can
_admin_issue?
}
%assignees
.value
{
"root-path"
=>
"#{root_url}"
,
":users"
=>
"issue.assignees"
,
":editable"
=>
can
?
(
current_user
,
:admin_issue
,
@project
)
,
":editable"
=>
can
_admin_issue?
,
"@assign-self"
=>
"assignSelf"
}
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
.selectbox.hide-collapsed
%input
.js-vue
{
type:
"hidden"
,
name:
"issue[assignee_ids][]"
,
...
...
@@ -20,9 +20,9 @@
":data-username"
=>
"assignee.username"
}
.dropdown
-
dropdown_options
=
issue_assignees_dropdown_options
%button
.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar
{
type:
'button'
,
ref:
'assigneeDropdown'
,
data:
{
toggle:
'dropdown'
,
field_name:
'issue[assignee_ids][]'
,
first_user:
current_user
&
.
username
,
current_user:
'true'
,
project_id:
@project
.
id
,
null_user:
'true'
,
multi_select:
'true'
,
'dropdown-header'
:
dropdown_options
[
:data
][
:'dropdown-header'
],
'max-select'
:
dropdown_options
[
:data
][
:'max-select'
]
}
,
":data-issuable-id"
=>
"issue.id"
,
":data-issue-update"
=>
"'#{
project_issues_path(@project)}/' + issue.
id + '.json'"
}
%button
.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar
{
type:
'button'
,
ref:
'assigneeDropdown'
,
data:
board_sidebar_user_data
,
":data-issuable-id"
=>
"issue.i
i
d"
,
":data-issue-update"
=>
"'#{
build_issue_link_base}/' + issue.i
id + '.json'"
}
=
dropdown_options
[
:title
]
=
icon
(
"chevron-down"
)
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
...
...
app/views/
projects
/boards/components/sidebar/_due_date.html.haml
→
app/views/
shared
/boards/components/sidebar/_due_date.html.haml
View file @
b4778a58
.block.due_date
.title
Due date
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"js-sidebar-dropdown-toggle edit-link pull-right"
.value
...
...
@@ -10,12 +10,12 @@
No due date
%span
.bold
{
"v-if"
=>
"issue.dueDate"
}
{{ issue.dueDate | due-date }}
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
%span
.no-value.js-remove-due-date-holder
{
"v-if"
=>
"issue.dueDate"
}
\-
%a
.js-remove-due-date
{
href:
"#"
,
role:
"button"
}
remove due date
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
.selectbox
%input
{
type:
"hidden"
,
name:
"issue[due_date]"
,
...
...
@@ -23,7 +23,7 @@
.dropdown
%button
.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date
{
type:
'button'
,
data:
{
toggle:
'dropdown'
,
field_name:
"issue[due_date]"
,
ability_name:
"issue"
},
":data-issue-update"
=>
"'#{
project_issues_path(@project)}/' + issue.
id + '.json'"
}
":data-issue-update"
=>
"'#{
build_issue_link_base}/' + issue.i
id + '.json'"
}
%span
.dropdown-toggle-text
Due date
=
icon
(
'chevron-down'
)
.dropdown-menu.dropdown-menu-due-date
...
...
app/views/
projects
/boards/components/sidebar/_labels.html.haml
→
app/views/
shared
/boards/components/sidebar/_labels.html.haml
View file @
b4778a58
.block.labels
.title
Labels
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels
...
...
@@ -11,7 +11,7 @@
"v-for"
=>
"label in issue.labels"
}
%span
.label.color-label.has-tooltip
{
":style"
=>
"{ backgroundColor: label.color, color: label.textColor }"
}
{{ label.title }}
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
.selectbox
%input
{
type:
"hidden"
,
name:
"issue[label_names][]"
,
...
...
@@ -19,12 +19,19 @@
":value"
=>
"label.id"
}
.dropdown
%button
.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
field_name:
"issue[label_names][]"
,
show_no:
"true"
,
show_any:
"true"
,
project_id:
@project
.
id
,
labels:
project_labels_path
(
@project
,
:json
),
namespace_path:
@project
.
try
(
:namespace
).
try
(
:full_path
),
project_path:
@project
.
try
(
:path
)
},
":data-issue-update"
=>
"'#{project_issues_path(@project)}/' + issue.id + '.json'"
}
data:
{
toggle:
"dropdown"
,
field_name:
"issue[label_names][]"
,
show_no:
"true"
,
show_any:
"true"
,
project_id:
@project
&
.
try
(
:id
),
labels:
labels_filter_path
(
false
),
namespace_path:
@project
.
try
(
:namespace
).
try
(
:full_path
),
project_path:
@project
.
try
(
:path
)
},
":data-issue-update"
=>
"'#{build_issue_link_base}/' + issue.iid + '.json'"
}
%span
.dropdown-toggle-text
Label
=
icon
(
'chevron-down'
)
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
=
render
partial:
"shared/issuable/label_page_default"
-
if
can?
current_user
,
:admin_label
,
@project
and
@project
-
if
can?
(
current_user
,
:admin_label
,
current_board_parent
)
=
render
partial:
"shared/issuable/label_page_create"
app/views/
projects
/boards/components/sidebar/_milestone.html.haml
→
app/views/
shared
/boards/components/sidebar/_milestone.html.haml
View file @
b4778a58
.block.milestone
.title
Milestone
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"js-sidebar-dropdown-toggle edit-link pull-right"
.value
...
...
@@ -9,17 +9,17 @@
None
%span
.bold.has-tooltip
{
"v-if"
=>
"issue.milestone"
}
{{ issue.milestone.title }}
-
if
can
?
(
current_user
,
:admin_issue
,
@project
)
-
if
can
_admin_issue?
.selectbox
%input
{
type:
"hidden"
,
":value"
=>
"issue.milestone.id"
,
name:
"issue[milestone_id]"
,
"v-if"
=>
"issue.milestone"
}
.dropdown
%button
.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
show_no:
"true"
,
field_name:
"issue[milestone_id]"
,
project_id:
@project
.
id
,
milestones:
project_milestones_path
(
@project
,
:json
),
ability_name:
"issue"
,
use_id:
"true"
,
default_no:
"true"
},
%button
.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
show_no:
"true"
,
field_name:
"issue[milestone_id]"
,
milestones:
milestones_filter_path
(
format:
:json
),
ability_name:
"issue"
,
use_id:
"true"
,
default_no:
"true"
},
":data-selected"
=>
"milestoneTitle"
,
":data-issuable-id"
=>
"issue.id"
,
":data-issue-update"
=>
"'#{
project_issues_path(@project)}/' + issue.
id + '.json'"
}
":data-issuable-id"
=>
"issue.i
i
d"
,
":data-issue-update"
=>
"'#{
build_issue_link_base}/' + issue.i
id + '.json'"
}
Milestone
=
icon
(
"chevron-down"
)
.dropdown-menu.dropdown-select.dropdown-menu-selectable
...
...
app/views/
projects
/boards/components/sidebar/_notifications.html.haml
→
app/views/
shared
/boards/components/sidebar/_notifications.html.haml
View file @
b4778a58
-
if
current_user
.block.light.subscription
{
":data-url"
=>
"'#{
project_issues_path(@project)}/' + issue.
id + '/toggle_subscription'"
}
.block.light.subscription
{
":data-url"
=>
"'#{
build_issue_link_base}/' + issue.i
id + '/toggle_subscription'"
}
%span
.issuable-header-text.hide-collapsed.pull-left
Notifications
%button
.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed
{
type:
"button"
}
...
...
app/views/shared/boards/index.html.haml
0 → 100644
View file @
b4778a58
=
render
"show"
app/views/shared/boards/show.html.haml
0 → 100644
View file @
b4778a58
=
render
"show"
app/views/shared/issuable/_label_page_default.html.haml
View file @
b4778a58
...
...
@@ -8,20 +8,19 @@
-
if
show_boards_content
.issue-board-dropdown-content
%p
Create lists from the labels you use in your project. Issues with that
label will automatically be added to the list.
Create lists from labels. Issues with that label appear in that list.
=
dropdown_filter
(
filter_placeholder
)
=
dropdown_content
-
if
@projec
t
&&
show_footer
-
if
current_board_paren
t
&&
show_footer
=
dropdown_footer
do
%ul
.dropdown-footer-list
-
if
can?
(
current_user
,
:admin_label
,
@projec
t
)
-
if
can?
(
current_user
,
:admin_label
,
current_board_paren
t
)
%li
%a
.dropdown-toggle-page
{
href:
"#"
}
Create new label
%li
=
link_to
project_labels_path
(
@project
)
,
:"data-is-link"
=>
true
do
-
if
show_create
&&
@project
&&
can?
(
current_user
,
:admin_label
,
@projec
t
)
=
link_to
labels_path
,
:"data-is-link"
=>
true
do
-
if
show_create
&&
can?
(
current_user
,
:admin_label
,
current_board_paren
t
)
Manage labels
-
else
View labels
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
b4778a58
...
...
@@ -104,13 +104,13 @@
=
icon
(
'times'
)
.filter-dropdown-container
-
if
type
==
:boards
-
if
can?
(
current_user
,
:admin_list
,
@projec
t
)
-
if
can?
(
current_user
,
:admin_list
,
board
.
paren
t
)
.dropdown.prepend-left-10
#js-add-list
%button
.btn.btn-create.btn-inverted.js-new-board-list
{
type:
"button"
,
data:
{
toggle:
"dropdown"
,
labels:
labels_filter_path
,
namespace_path:
@project
.
try
(
:namespace
).
try
(
:full_path
),
project_path:
@project
.
try
(
:path
)
}
}
%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-align-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
,
@projec
t
)
-
if
can?
(
current_user
,
:admin_label
,
board
.
paren
t
)
=
render
partial:
"shared/issuable/label_page_create"
=
dropdown_loading
#js-add-issues-btn
.prepend-left-10
...
...
config/routes.rb
View file @
b4778a58
...
...
@@ -74,6 +74,19 @@ Rails.application.routes.draw do
# Notification settings
resources
:notification_settings
,
only:
[
:create
,
:update
]
# Boards resources shared between group and projects
resources
:boards
do
resources
:lists
,
module: :boards
,
only:
[
:index
,
:create
,
:update
,
:destroy
]
do
collection
do
post
:generate
end
resources
:issues
,
only:
[
:index
,
:create
,
:update
]
end
resources
:issues
,
module: :boards
,
only:
[
:index
,
:update
]
end
draw
:import
draw
:uploads
draw
:explore
...
...
config/routes/project.rb
View file @
b4778a58
...
...
@@ -343,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do
get
'noteable/:target_type/:target_id/notes'
=>
'notes#index'
,
as:
'noteable_notes'
resources
:boards
,
only:
[
:index
,
:show
]
do
scope
module: :boards
do
resources
:issues
,
only:
[
:index
,
:update
]
resources
:lists
,
only:
[
:index
,
:create
,
:update
,
:destroy
]
do
collection
do
post
:generate
end
resources
:issues
,
only:
[
:index
,
:create
]
end
end
end
resources
:boards
,
only:
[
:index
,
:show
,
:create
,
:update
,
:destroy
]
resources
:todos
,
only:
[
:create
]
...
...
doc/README.md
View file @
b4778a58
...
...
@@ -84,7 +84,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
-
[
Discussions
](
user/discussions/index.md
)
Threads, comments, and resolvable discussions in issues, commits, and merge requests.
-
[
Issues
](
user/project/issues/index.md
)
-
[
I
ssue Board
](
user/project/issue_board.md
)
-
[
Project i
ssue Board
](
user/project/issue_board.md
)
-
[
Issues and merge requests templates
](
user/project/description_templates.md
)
: Create templates for submitting new issues and merge requests.
-
[
Labels
](
user/project/labels.md
)
: Categorize your issues or merge requests based on descriptive titles.
-
[
Merge Requests
](
user/project/merge_requests/index.md
)
...
...
lib/gitlab/path_regex.rb
View file @
b4778a58
...
...
@@ -26,6 +26,7 @@ module Gitlab
apple-touch-icon.png
assets
autocomplete
boards
ci
dashboard
deploy.html
...
...
spec/controllers/
projects/
boards/issues_controller_spec.rb
→
spec/controllers/boards/issues_controller_spec.rb
View file @
b4778a58
require
'spec_helper'
describe
Projects
::
Boards
::
IssuesController
do
describe
Boards
::
IssuesController
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
...
...
@@ -133,6 +133,22 @@ describe Projects::Boards::IssuesController do
expect
(
response
).
to
have_http_status
(
404
)
end
end
context
'with invalid board id'
do
it
'returns a not found 404 response'
do
create_issue
user:
user
,
board:
999
,
list:
list1
,
title:
'New issue'
expect
(
response
).
to
have_http_status
(
404
)
end
end
context
'with invalid list id'
do
it
'returns a not found 404 response'
do
create_issue
user:
user
,
board:
board
,
list:
999
,
title:
'New issue'
expect
(
response
).
to
have_http_status
(
404
)
end
end
end
context
'with unauthorized user'
do
...
...
@@ -146,17 +162,15 @@ describe Projects::Boards::IssuesController do
def
create_issue
(
user
:,
board
:,
list
:,
title
:)
sign_in
(
user
)
post
:create
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
,
board_id:
board
.
to_param
,
post
:create
,
board_id:
board
.
to_param
,
list_id:
list
.
to_param
,
issue:
{
title:
title
},
issue:
{
title:
title
,
project_id:
project
.
id
},
format: :json
end
end
describe
'PATCH update'
do
let
(
:issue
)
{
create
(
:labeled_issue
,
project:
project
,
labels:
[
planning
])
}
let
!
(
:issue
)
{
create
(
:labeled_issue
,
project:
project
,
labels:
[
planning
])
}
context
'with valid params'
do
it
'returns a successful 200 response'
do
...
...
@@ -186,7 +200,7 @@ describe Projects::Boards::IssuesController do
end
it
'returns a not found 404 response for invalid issue id'
do
move
user:
user
,
board:
board
,
issue:
999
,
from_list_id:
list1
.
id
,
to_list_id:
list2
.
id
move
user:
user
,
board:
board
,
issue:
double
(
id:
999
)
,
from_list_id:
list1
.
id
,
to_list_id:
list2
.
id
expect
(
response
).
to
have_http_status
(
404
)
end
...
...
@@ -210,9 +224,9 @@ describe Projects::Boards::IssuesController do
sign_in
(
user
)
patch
:update
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
,
project_id:
project
.
id
,
board_id:
board
.
to_param
,
id:
issue
.
to_param
,
id:
issue
.
id
,
from_list_id:
from_list_id
,
to_list_id:
to_list_id
,
format: :json
...
...
spec/controllers/
projects/
boards/lists_controller_spec.rb
→
spec/controllers/boards/lists_controller_spec.rb
View file @
b4778a58
require
'spec_helper'
describe
Projects
::
Boards
::
ListsController
do
describe
Boards
::
ListsController
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:board
)
{
create
(
:board
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
...
...
spec/factories/milestones.rb
View file @
b4778a58
...
...
@@ -7,6 +7,7 @@ FactoryGirl.define do
group
nil
project_id
nil
group_id
nil
parent
nil
end
trait
:active
do
...
...
@@ -26,6 +27,9 @@ FactoryGirl.define do
milestone
.
project
=
evaluator
.
project
elsif
evaluator
.
project_id
milestone
.
project_id
=
evaluator
.
project_id
elsif
evaluator
.
parent
id
=
evaluator
.
parent
.
id
evaluator
.
parent
.
is_a?
(
Group
)
?
board
.
group_id
=
id
:
evaluator
.
project_id
=
id
else
milestone
.
project
=
create
(
:project
)
end
...
...
spec/fixtures/api/schemas/issue.json
View file @
b4778a58
...
...
@@ -8,10 +8,15 @@
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"iid"
:
{
"type"
:
"integer"
},
"project_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"title"
:
{
"type"
:
"string"
},
"confidential"
:
{
"type"
:
"boolean"
},
"due_date"
:
{
"type"
:
[
"date"
,
"null"
]
},
"relative_position"
:
{
"type"
:
"integer"
},
"project"
:
{
"id"
:
{
"type"
:
"integer"
},
"path"
:
{
"type"
:
"string"
}
},
"labels"
:
{
"type"
:
"array"
,
"items"
:
{
...
...
@@ -34,6 +39,7 @@
"type"
:
"string"
,
"pattern"
:
"^#[0-9A-Fa-f]{3}{1,2}+$"
},
"type"
:
{
"type"
:
"string"
},
"title"
:
{
"type"
:
"string"
},
"priority"
:
{
"type"
:
[
"integer"
,
"null"
]
}
},
...
...
spec/javascripts/boards/board_blank_state_spec.js
View file @
b4778a58
/* global BoardService */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
'
~/boards/stores/boards_store
'
;
import
boardBlankState
from
'
~/boards/components/board_blank_state
'
;
...
...
@@ -12,7 +13,7 @@ describe('Boards blank state', () => {
const
Comp
=
Vue
.
extend
(
boardBlankState
);
gl
.
issueBoards
.
BoardsStore
.
create
();
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
spyOn
(
gl
.
boardService
,
'
generateDefaultLists
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
{
if
(
fail
)
{
...
...
spec/javascripts/boards/board_card_spec.js
View file @
b4778a58
...
...
@@ -4,6 +4,7 @@
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
'
~/boards/models/assignee
'
;
...
...
@@ -14,13 +15,13 @@ import '~/boards/stores/boards_store';
import
boardCard
from
'
~/boards/components/board_card
'
;
import
'
./mock_data
'
;
describe
(
'
Issue
card
'
,
()
=>
{
describe
(
'
Board
card
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
Vue
.
http
.
interceptors
.
push
(
boardsMockInterceptor
);
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
gl
.
issueBoards
.
BoardsStore
.
create
();
gl
.
issueBoards
.
BoardsStore
.
detail
.
issue
=
{};
...
...
spec/javascripts/boards/board_list_spec.js
View file @
b4778a58
...
...
@@ -3,6 +3,7 @@
/* global List */
/* global listObj */
/* global ListIssue */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
_
from
'
underscore
'
;
import
Sortable
from
'
vendor/Sortable
'
;
...
...
@@ -24,7 +25,7 @@ describe('Board list component', () => {
document
.
body
.
appendChild
(
el
);
Vue
.
http
.
interceptors
.
push
(
boardsMockInterceptor
);
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
gl
.
issueBoards
.
BoardsStore
.
create
();
gl
.
IssueBoardsApp
=
new
Vue
();
...
...
@@ -32,6 +33,7 @@ describe('Board list component', () => {
const
list
=
new
List
(
listObj
);
const
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[],
...
...
spec/javascripts/boards/board_new_issue_spec.js
View file @
b4778a58
...
...
@@ -2,6 +2,7 @@
/* global BoardService */
/* global List */
/* global listObj */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
boardNewIssue
from
'
~/boards/components/board_new_issue
'
;
...
...
@@ -35,7 +36,7 @@ describe('Issue boards new issue form', () => {
const
BoardNewIssueComp
=
Vue
.
extend
(
boardNewIssue
);
Vue
.
http
.
interceptors
.
push
(
boardsMockInterceptor
);
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
gl
.
issueBoards
.
BoardsStore
.
create
();
gl
.
IssueBoardsApp
=
new
Vue
();
...
...
spec/javascripts/boards/boards_store_spec.js
View file @
b4778a58
...
...
@@ -4,6 +4,7 @@
/* global listObj */
/* global listObjDuplicate */
/* global ListIssue */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
Cookies
from
'
js-cookie
'
;
...
...
@@ -20,7 +21,7 @@ import './mock_data';
describe
(
'
Store
'
,
()
=>
{
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
boardsMockInterceptor
);
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
gl
.
issueBoards
.
BoardsStore
.
create
();
spyOn
(
gl
.
boardService
,
'
moveIssue
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
...
...
@@ -78,7 +79,7 @@ describe('Store', () => {
it
(
'
persists new list
'
,
(
done
)
=>
{
gl
.
issueBoards
.
BoardsStore
.
new
({
title
:
'
Test
'
,
type
:
'
label
'
,
list_
type
:
'
label
'
,
label
:
{
id
:
1
,
title
:
'
Testing
'
,
...
...
@@ -210,6 +211,7 @@ describe('Store', () => {
it
(
'
moves issue in list
'
,
(
done
)
=>
{
const
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
2
,
iid
:
2
,
confidential
:
false
,
labels
:
[],
...
...
spec/javascripts/boards/components/board_spec.js
View file @
b4778a58
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
'
~/boards/services/board_service
'
;
import
'
~/boards/components/board
'
;
import
'
~/boards/models/list
'
;
import
'
../mock_data
'
;
describe
(
'
Board component
'
,
()
=>
{
let
vm
;
...
...
@@ -13,8 +15,12 @@ describe('Board component', () => {
el
=
document
.
createElement
(
'
div
'
);
document
.
body
.
appendChild
(
el
);
// eslint-disable-next-line no-undef
gl
.
boardService
=
new
BoardService
(
'
/
'
,
'
/
'
,
1
);
gl
.
boardService
=
mockBoardService
({
boardsEndpoint
:
'
/
'
,
listsEndpoint
:
'
/
'
,
bulkUpdatePath
:
'
/
'
,
boardId
:
1
,
});
vm
=
new
gl
.
issueBoards
.
Board
({
propsData
:
{
...
...
spec/javascripts/boards/issue_card_spec.js
View file @
b4778a58
...
...
@@ -37,6 +37,7 @@ describe('Issue card component', () => {
list
=
listObj
;
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[
list
.
label
],
...
...
@@ -238,65 +239,63 @@ describe('Issue card component', () => {
});
describe
(
'
labels
'
,
()
=>
{
describe
(
'
exists
'
,
()
=>
{
beforeEach
((
done
)
=>
{
component
.
issue
.
addLabel
(
label1
);
beforeEach
((
done
)
=>
{
component
.
issue
.
addLabel
(
label1
);
Vue
.
nextTick
(()
=>
done
());
});
Vue
.
nextTick
(()
=>
done
());
});
it
(
'
renders list label
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.label
'
).
length
,
).
toBe
(
2
);
it
(
'
renders list label
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.label
'
).
length
,
).
toBe
(
2
);
});
it
(
'
renders label
'
,
()
=>
{
const
nodes
=
[];
component
.
$el
.
querySelectorAll
(
'
.label
'
).
forEach
((
label
)
=>
{
nodes
.
push
(
label
.
title
);
});
it
(
'
renders label
'
,
()
=>
{
const
nodes
=
[];
component
.
$el
.
querySelectorAll
(
'
.label
'
).
forEach
((
label
)
=>
{
nodes
.
push
(
label
.
title
);
});
expect
(
nodes
.
includes
(
label1
.
description
),
).
toBe
(
true
);
});
expect
(
nodes
.
includes
(
label1
.
description
),
).
toBe
(
true
);
});
it
(
'
sets label description as title
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.label
'
).
getAttribute
(
'
title
'
),
).
toContain
(
label1
.
description
);
});
it
(
'
sets label description as title
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.label
'
).
getAttribute
(
'
title
'
),
).
toContain
(
label1
.
description
);
it
(
'
sets background color of button
'
,
()
=>
{
const
nodes
=
[];
component
.
$el
.
querySelectorAll
(
'
.label
'
).
forEach
((
label
)
=>
{
nodes
.
push
(
label
.
style
.
backgroundColor
);
});
it
(
'
sets background color of button
'
,
()
=>
{
const
nodes
=
[];
component
.
$el
.
querySelectorAll
(
'
.label
'
).
forEach
((
label
)
=>
{
nodes
.
push
(
label
.
style
.
backgroundColor
);
});
expect
(
nodes
.
includes
(
label1
.
color
),
).
toBe
(
true
);
});
expect
(
nodes
.
includes
(
label1
.
color
),
).
toBe
(
true
);
});
it
(
'
does not render label if label does not have an ID
'
,
(
done
)
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
title
:
'
closed
'
,
})
)
;
it
(
'
does not render label if label does not have an ID
'
,
(
done
)
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
title
:
'
closed
'
,
}));
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.label
'
).
length
,
).
toBe
(
2
);
expect
(
component
.
$el
.
textContent
,
).
not
.
toContain
(
'
closed
'
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.label
'
).
length
,
).
toBe
(
2
);
expect
(
component
.
$el
.
textContent
,
).
not
.
toContain
(
'
closed
'
);
done
();
})
.
catch
(
done
.
fail
);
});
done
();
})
.
catch
(
done
.
fail
);
});
});
});
spec/javascripts/boards/issue_spec.js
View file @
b4778a58
/* eslint-disable comma-dangle */
/* global BoardService */
/* global ListIssue */
/* global mockBoardService */
import
Vue
from
'
vue
'
;
import
'
~/lib/utils/url_utility
'
;
...
...
@@ -16,11 +17,12 @@ describe('Issue model', () => {
let
issue
;
beforeEach
(()
=>
{
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
(
);
gl
.
issueBoards
.
BoardsStore
.
create
();
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[{
...
...
spec/javascripts/boards/list_spec.js
View file @
b4778a58
/* eslint-disable comma-dangle */
/* global boardsMockInterceptor */
/* global BoardService */
/* global mockBoardService */
/* global List */
/* global ListIssue */
/* global listObj */
...
...
@@ -22,7 +23,9 @@ describe('List model', () => {
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
boardsMockInterceptor
);
gl
.
boardService
=
new
BoardService
(
'
/test/issue-boards/board
'
,
''
,
'
1
'
);
gl
.
boardService
=
mockBoardService
({
bulkUpdatePath
:
'
/test/issue-boards/board/1/lists
'
,
});
gl
.
issueBoards
.
BoardsStore
.
create
();
list
=
new
List
(
listObj
);
...
...
@@ -92,6 +95,7 @@ describe('List model', () => {
const
listDup
=
new
List
(
listObjDuplicate
);
const
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
_
.
random
(
10000
),
iid
:
_
.
random
(
10000
),
confidential
:
false
,
labels
:
[
list
.
label
,
listDup
.
label
],
...
...
@@ -118,6 +122,7 @@ describe('List model', () => {
for
(
let
i
=
0
;
i
<
30
;
i
+=
1
)
{
list
.
issues
.
push
(
new
ListIssue
({
title
:
'
Testing
'
,
id
:
_
.
random
(
10000
)
+
i
,
iid
:
_
.
random
(
10000
)
+
i
,
confidential
:
false
,
labels
:
[
list
.
label
],
...
...
@@ -137,7 +142,7 @@ describe('List model', () => {
it
(
'
does not increase page number if issue count is less than the page size
'
,
()
=>
{
list
.
issues
.
push
(
new
ListIssue
({
title
:
'
Testing
'
,
i
i
d
:
_
.
random
(
10000
),
id
:
_
.
random
(
10000
),
confidential
:
false
,
labels
:
[
list
.
label
],
assignees
:
[],
...
...
@@ -156,7 +161,7 @@ describe('List model', () => {
spyOn
(
gl
.
boardService
,
'
newIssue
'
).
and
.
returnValue
(
Promise
.
resolve
({
json
()
{
return
{
i
i
d
:
42
,
id
:
42
,
};
},
}));
...
...
@@ -165,14 +170,14 @@ describe('List model', () => {
it
(
'
adds new issue to top of list
'
,
(
done
)
=>
{
list
.
issues
.
push
(
new
ListIssue
({
title
:
'
Testing
'
,
i
i
d
:
_
.
random
(
10000
),
id
:
_
.
random
(
10000
),
confidential
:
false
,
labels
:
[
list
.
label
],
assignees
:
[],
}));
const
dummyIssue
=
new
ListIssue
({
title
:
'
new issue
'
,
i
i
d
:
_
.
random
(
10000
),
id
:
_
.
random
(
10000
),
confidential
:
false
,
labels
:
[
list
.
label
],
assignees
:
[],
...
...
spec/javascripts/boards/mock_data.js
View file @
b4778a58
/* global BoardService */
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
const
listObj
=
{
...
...
@@ -28,19 +29,19 @@ const listObjDuplicate = {
const
BoardsMockData
=
{
'
GET
'
:
{
'
/test/
issue-boards/board/1/lists
{/id}/issues
'
:
{
'
/test/
boards/1
{/id}/issues
'
:
{
issues
:
[{
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[],
assignees
:
[],
}],
size
:
1
}
},
'
POST
'
:
{
'
/test/
issue-boards/board/1/lists
{/id}
'
:
listObj
'
/test/
boards/1
{/id}
'
:
listObj
},
'
PUT
'
:
{
'
/test/issue-boards/board/1/lists{/id}
'
:
{}
...
...
@@ -58,7 +59,22 @@ const boardsMockInterceptor = (request, next) => {
}));
};
const
mockBoardService
=
(
opts
=
{})
=>
{
const
boardsEndpoint
=
opts
.
boardsEndpoint
||
'
/test/issue-boards/board
'
;
const
listsEndpoint
=
opts
.
listsEndpoint
||
'
/test/boards/1
'
;
const
bulkUpdatePath
=
opts
.
bulkUpdatePath
||
''
;
const
boardId
=
opts
.
boardId
||
'
1
'
;
return
new
BoardService
({
boardsEndpoint
,
listsEndpoint
,
bulkUpdatePath
,
boardId
,
});
};
window
.
listObj
=
listObj
;
window
.
listObjDuplicate
=
listObjDuplicate
;
window
.
BoardsMockData
=
BoardsMockData
;
window
.
boardsMockInterceptor
=
boardsMockInterceptor
;
window
.
mockBoardService
=
mockBoardService
;
spec/javascripts/boards/modal_store_spec.js
View file @
b4778a58
...
...
@@ -18,6 +18,7 @@ describe('Modal store', () => {
issue
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[],
...
...
@@ -25,6 +26,7 @@ describe('Modal store', () => {
});
issue2
=
new
ListIssue
({
title
:
'
Testing
'
,
id
:
1
,
iid
:
2
,
confidential
:
false
,
labels
:
[],
...
...
spec/services/boards/issues/create_service_spec.rb
View file @
b4778a58
...
...
@@ -8,7 +8,7 @@ describe Boards::Issues::CreateService do
let
(
:label
)
{
create
(
:label
,
project:
project
,
name:
'in-progress'
)
}
let!
(
:list
)
{
create
(
:list
,
board:
board
,
label:
label
,
position:
0
)
}
subject
(
:service
)
{
described_class
.
new
(
project
,
user
,
board_id:
board
.
id
,
list_id:
list
.
id
,
title:
'New issue'
)
}
subject
(
:service
)
{
described_class
.
new
(
board
.
parent
,
project
,
user
,
board_id:
board
.
id
,
list_id:
list
.
id
,
title:
'New issue'
)
}
before
do
project
.
team
<<
[
user
,
:developer
]
...
...
spec/services/boards/issues/move_service_spec.rb
View file @
b4778a58
...
...
@@ -98,7 +98,7 @@ describe Boards::Issues::MoveService do
issue
.
move_to_end
&&
issue
.
save!
end
params
.
merge!
(
move_after_i
id:
issue1
.
iid
,
move_before_iid:
issue2
.
i
id
)
params
.
merge!
(
move_after_i
d:
issue1
.
id
,
move_before_id:
issue2
.
id
)
described_class
.
new
(
project
,
user
,
params
).
execute
(
issue
)
...
...
spec/services/issues/update_service_spec.rb
View file @
b4778a58
...
...
@@ -80,7 +80,7 @@ describe Issues::UpdateService, :mailer do
issue
.
save
end
opts
[
:move_between_i
ids
]
=
[
issue1
.
iid
,
issue2
.
i
id
]
opts
[
:move_between_i
ds
]
=
[
issue1
.
id
,
issue2
.
id
]
update_issue
(
opts
)
...
...
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