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
Léo-Paul Géneau
gitlab-ce
Commits
9e820ae3
Commit
9e820ae3
authored
Sep 04, 2017
by
Grzegorz Bizon
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '34261-move-move-to-sidebar' into 'master'
Move "Move issue" to sidebar Closes #34261 See merge request !13616
parents
e30adf60
90c60138
Changes
46
Hide whitespace changes
Inline
Side-by-side
Showing
46 changed files
with
664 additions
and
429 deletions
+664
-429
app/assets/javascripts/gl_dropdown.js
app/assets/javascripts/gl_dropdown.js
+4
-4
app/assets/javascripts/issuable_form.js
app/assets/javascripts/issuable_form.js
+0
-52
app/assets/javascripts/issue_show/components/app.vue
app/assets/javascripts/issue_show/components/app.vue
+0
-21
app/assets/javascripts/issue_show/components/fields/project_move.vue
...javascripts/issue_show/components/fields/project_move.vue
+0
-83
app/assets/javascripts/issue_show/components/form.vue
app/assets/javascripts/issue_show/components/form.vue
+0
-14
app/assets/javascripts/issue_show/index.js
app/assets/javascripts/issue_show/index.js
+0
-2
app/assets/javascripts/issue_show/stores/index.js
app/assets/javascripts/issue_show/stores/index.js
+0
-1
app/assets/javascripts/right_sidebar.js
app/assets/javascripts/right_sidebar.js
+7
-2
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
...avascripts/sidebar/components/assignees/assignee_title.js
+1
-1
app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+85
-0
app/assets/javascripts/sidebar/services/sidebar_service.js
app/assets/javascripts/sidebar/services/sidebar_service.js
+18
-2
app/assets/javascripts/sidebar/sidebar_bundle.js
app/assets/javascripts/sidebar/sidebar_bundle.js
+7
-0
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+28
-1
app/assets/javascripts/sidebar/stores/sidebar_store.js
app/assets/javascripts/sidebar/stores/sidebar_store.js
+10
-0
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+6
-1
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+19
-1
app/controllers/autocomplete_controller.rb
app/controllers/autocomplete_controller.rb
+0
-6
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+28
-12
app/helpers/dropdowns_helper.rb
app/helpers/dropdowns_helper.rb
+4
-2
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+2
-2
app/views/projects/boards/components/sidebar/_due_date.html.haml
...ws/projects/boards/components/sidebar/_due_date.html.haml
+1
-1
app/views/projects/boards/components/sidebar/_labels.html.haml
...iews/projects/boards/components/sidebar/_labels.html.haml
+1
-1
app/views/projects/boards/components/sidebar/_milestone.html.haml
...s/projects/boards/components/sidebar/_milestone.html.haml
+1
-1
app/views/shared/icons/_icon_arrow_right.svg.erb
app/views/shared/icons/_icon_arrow_right.svg.erb
+1
-0
app/views/shared/issuable/_form.html.haml
app/views/shared/issuable/_form.html.haml
+0
-12
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+20
-3
app/views/shared/issuable/_sidebar_assignees.html.haml
app/views/shared/issuable/_sidebar_assignees.html.haml
+1
-1
app/views/shared/issuable/form/_issue_assignee.html.haml
app/views/shared/issuable/form/_issue_assignee.html.haml
+1
-1
app/views/shared/issuable/form/_merge_request_assignee.html.haml
...ws/shared/issuable/form/_merge_request_assignee.html.haml
+1
-1
app/views/shared/milestones/_sidebar.html.haml
app/views/shared/milestones/_sidebar.html.haml
+2
-2
changelogs/unreleased/34261-move-move-to-sidebar.yml
changelogs/unreleased/34261-move-move-to-sidebar.yml
+5
-0
config/routes/project.rb
config/routes/project.rb
+1
-0
doc/user/project/issues/img/sidebar_move_issue.png
doc/user/project/issues/img/sidebar_move_issue.png
+0
-0
doc/user/project/issues/index.md
doc/user/project/issues/index.md
+4
-0
doc/user/project/issues/moving_issues.md
doc/user/project/issues/moving_issues.md
+10
-0
spec/controllers/autocomplete_controller_spec.rb
spec/controllers/autocomplete_controller_spec.rb
+12
-16
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+108
-101
spec/features/issues/move_spec.rb
spec/features/issues/move_spec.rb
+14
-18
spec/javascripts/issue_show/components/app_spec.js
spec/javascripts/issue_show/components/app_spec.js
+1
-20
spec/javascripts/issue_show/components/fields/project_move_spec.js
...scripts/issue_show/components/fields/project_move_spec.js
+0
-38
spec/javascripts/issue_show/components/form_spec.js
spec/javascripts/issue_show/components/form_spec.js
+0
-2
spec/javascripts/sidebar/mock_data.js
spec/javascripts/sidebar/mock_data.js
+41
-0
spec/javascripts/sidebar/sidebar_mediator_spec.js
spec/javascripts/sidebar/sidebar_mediator_spec.js
+39
-1
spec/javascripts/sidebar/sidebar_move_issue_spec.js
spec/javascripts/sidebar/sidebar_move_issue_spec.js
+142
-0
spec/javascripts/sidebar/sidebar_service_spec.js
spec/javascripts/sidebar/sidebar_service_spec.js
+25
-3
spec/javascripts/sidebar/sidebar_store_spec.js
spec/javascripts/sidebar/sidebar_store_spec.js
+14
-0
No files found.
app/assets/javascripts/gl_dropdown.js
View file @
9e820ae3
...
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
...
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
GitLabDropdown
.
prototype
.
shouldPropagate
=
function
(
e
)
{
GitLabDropdown
.
prototype
.
shouldPropagate
=
function
(
e
)
{
var
$target
;
var
$target
;
if
(
this
.
options
.
multiSelect
)
{
if
(
this
.
options
.
multiSelect
||
this
.
options
.
shouldPropagate
===
false
)
{
$target
=
$
(
e
.
target
);
$target
=
$
(
e
.
target
);
if
(
$target
&&
!
$target
.
hasClass
(
'
dropdown-menu-close
'
)
&&
if
(
$target
&&
!
$target
.
hasClass
(
'
dropdown-menu-close
'
)
&&
!
$target
.
hasClass
(
'
dropdown-menu-close-icon
'
)
&&
!
$target
.
hasClass
(
'
dropdown-menu-close-icon
'
)
&&
...
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
...
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
};
};
GitLabDropdown
.
prototype
.
positionMenuAbove
=
function
()
{
GitLabDropdown
.
prototype
.
positionMenuAbove
=
function
()
{
var
$button
=
$
(
this
.
el
);
var
$menu
=
this
.
dropdown
.
find
(
'
.dropdown-menu
'
);
var
$menu
=
this
.
dropdown
.
find
(
'
.dropdown-menu
'
);
$menu
.
css
(
'
top
'
,
(
$button
.
height
()
+
$menu
.
height
())
*
-
1
);
$menu
.
css
(
'
top
'
,
'
initial
'
);
$menu
.
css
(
'
bottom
'
,
'
100%
'
);
};
};
GitLabDropdown
.
prototype
.
hidden
=
function
(
e
)
{
GitLabDropdown
.
prototype
.
hidden
=
function
(
e
)
{
...
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
...
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
GitLabDropdown
.
prototype
.
noResults
=
function
()
{
GitLabDropdown
.
prototype
.
noResults
=
function
()
{
var
html
;
var
html
;
return
html
=
"
<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>
"
;
return
html
=
'
<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>
'
;
};
};
GitLabDropdown
.
prototype
.
rowClicked
=
function
(
el
)
{
GitLabDropdown
.
prototype
.
rowClicked
=
function
(
el
)
{
...
...
app/assets/javascripts/issuable_form.js
View file @
9e820ae3
...
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
...
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
(
function
()
{
(
function
()
{
this
.
IssuableForm
=
(
function
()
{
this
.
IssuableForm
=
(
function
()
{
IssuableForm
.
prototype
.
issueMoveConfirmMsg
=
'
Are you sure you want to move this issue to another project?
'
;
IssuableForm
.
prototype
.
wipRegex
=
/^
\s
*
(\[
WIP
\]\s
*|WIP:
\s
*|WIP
\s
+
)
+
\s
*/i
;
IssuableForm
.
prototype
.
wipRegex
=
/^
\s
*
(\[
WIP
\]\s
*|WIP:
\s
*|WIP
\s
+
)
+
\s
*/i
;
function
IssuableForm
(
form
)
{
function
IssuableForm
(
form
)
{
...
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
...
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
new
ZenMode
();
new
ZenMode
();
this
.
titleField
=
this
.
form
.
find
(
"
input[name*='[title]']
"
);
this
.
titleField
=
this
.
form
.
find
(
"
input[name*='[title]']
"
);
this
.
descriptionField
=
this
.
form
.
find
(
"
textarea[name*='[description]']
"
);
this
.
descriptionField
=
this
.
form
.
find
(
"
textarea[name*='[description]']
"
);
this
.
issueMoveField
=
this
.
form
.
find
(
"
#move_to_project_id
"
);
if
(
!
(
this
.
titleField
.
length
&&
this
.
descriptionField
.
length
))
{
if
(
!
(
this
.
titleField
.
length
&&
this
.
descriptionField
.
length
))
{
return
;
return
;
}
}
...
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
...
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
this
.
form
.
on
(
"
submit
"
,
this
.
handleSubmit
);
this
.
form
.
on
(
"
submit
"
,
this
.
handleSubmit
);
this
.
form
.
on
(
"
click
"
,
"
.btn-cancel
"
,
this
.
resetAutosave
);
this
.
form
.
on
(
"
click
"
,
"
.btn-cancel
"
,
this
.
resetAutosave
);
this
.
initWip
();
this
.
initWip
();
this
.
initMoveDropdown
();
$issuableDueDate
=
$
(
'
#issuable-due-date
'
);
$issuableDueDate
=
$
(
'
#issuable-due-date
'
);
if
(
$issuableDueDate
.
length
)
{
if
(
$issuableDueDate
.
length
)
{
calendar
=
new
Pikaday
({
calendar
=
new
Pikaday
({
...
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
...
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
};
};
IssuableForm
.
prototype
.
handleSubmit
=
function
()
{
IssuableForm
.
prototype
.
handleSubmit
=
function
()
{
var
fieldId
=
(
this
.
issueMoveField
!=
null
)
?
this
.
issueMoveField
.
val
()
:
null
;
if
((
parseInt
(
fieldId
,
10
)
||
0
)
>
0
)
{
if
(
!
confirm
(
this
.
issueMoveConfirmMsg
))
{
return
false
;
}
}
return
this
.
resetAutosave
();
return
this
.
resetAutosave
();
};
};
...
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
...
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
return
this
.
titleField
.
val
(
"
WIP:
"
+
(
this
.
titleField
.
val
()));
return
this
.
titleField
.
val
(
"
WIP:
"
+
(
this
.
titleField
.
val
()));
};
};
IssuableForm
.
prototype
.
initMoveDropdown
=
function
()
{
var
$moveDropdown
,
pageSize
;
$moveDropdown
=
$
(
'
.js-move-dropdown
'
);
if
(
$moveDropdown
.
length
)
{
pageSize
=
$moveDropdown
.
data
(
'
page-size
'
);
return
$
(
'
.js-move-dropdown
'
).
select2
({
ajax
:
{
url
:
$moveDropdown
.
data
(
'
projects-url
'
),
quietMillis
:
125
,
data
:
function
(
term
,
page
,
context
)
{
return
{
search
:
term
,
offset_id
:
context
};
},
results
:
function
(
data
)
{
var
context
,
more
;
if
(
data
.
length
>=
pageSize
)
more
=
true
;
if
(
data
[
data
.
length
-
1
])
context
=
data
[
data
.
length
-
1
].
id
;
return
{
results
:
data
,
more
:
more
,
context
:
context
};
}
},
formatResult
:
function
(
project
)
{
return
project
.
name_with_namespace
;
},
formatSelection
:
function
(
project
)
{
return
project
.
name_with_namespace
;
}
});
}
};
return
IssuableForm
;
return
IssuableForm
;
})();
})();
}).
call
(
window
);
}).
call
(
window
);
app/assets/javascripts/issue_show/components/app.vue
View file @
9e820ae3
...
@@ -17,10 +17,6 @@ export default {
...
@@ -17,10 +17,6 @@ export default {
required
:
true
,
required
:
true
,
type
:
String
,
type
:
String
,
},
},
canMove
:
{
required
:
true
,
type
:
Boolean
,
},
canUpdate
:
{
canUpdate
:
{
required
:
true
,
required
:
true
,
type
:
Boolean
,
type
:
Boolean
,
...
@@ -96,10 +92,6 @@ export default {
...
@@ -96,10 +92,6 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
projectsAutocompletePath
:
{
type
:
String
,
required
:
true
,
},
},
},
data
()
{
data
()
{
const
store
=
new
Store
({
const
store
=
new
Store
({
...
@@ -142,7 +134,6 @@ export default {
...
@@ -142,7 +134,6 @@ export default {
confidential
:
this
.
isConfidential
,
confidential
:
this
.
isConfidential
,
description
:
this
.
state
.
descriptionText
,
description
:
this
.
state
.
descriptionText
,
lockedWarningVisible
:
false
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
updateLoading
:
false
,
});
});
}
}
...
@@ -151,16 +142,6 @@ export default {
...
@@ -151,16 +142,6 @@ export default {
this
.
showForm
=
false
;
this
.
showForm
=
false
;
},
},
updateIssuable
()
{
updateIssuable
()
{
const
canPostUpdate
=
this
.
store
.
formState
.
move_to_project_id
!==
0
?
confirm
(
'
Are you sure you want to move this issue to another project?
'
)
:
true
;
// eslint-disable-line no-alert
if
(
!
canPostUpdate
)
{
this
.
store
.
setFormState
({
updateLoading
:
false
,
});
return
;
}
this
.
service
.
updateIssuable
(
this
.
store
.
formState
)
this
.
service
.
updateIssuable
(
this
.
store
.
formState
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
.
then
((
data
)
=>
{
...
@@ -239,14 +220,12 @@ export default {
...
@@ -239,14 +220,12 @@ export default {
<form-component
<form-component
v-if=
"canUpdate && showForm"
v-if=
"canUpdate && showForm"
:form-state=
"formState"
:form-state=
"formState"
:can-move=
"canMove"
:can-destroy=
"canDestroy"
:can-destroy=
"canDestroy"
:issuable-templates=
"issuableTemplates"
:issuable-templates=
"issuableTemplates"
:markdown-docs-path=
"markdownDocsPath"
:markdown-docs-path=
"markdownDocsPath"
:markdown-preview-path=
"markdownPreviewPath"
:markdown-preview-path=
"markdownPreviewPath"
:project-path=
"projectPath"
:project-path=
"projectPath"
:project-namespace=
"projectNamespace"
:project-namespace=
"projectNamespace"
:projects-autocomplete-path=
"projectsAutocompletePath"
/>
/>
<div
v-else
>
<div
v-else
>
<title-component
<title-component
...
...
app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted
100644 → 0
View file @
e30adf60
<
script
>
import
tooltip
from
'
../../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
},
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
projectsAutocompletePath
:
{
type
:
String
,
required
:
true
,
},
},
mounted
()
{
const
$moveDropdown
=
$
(
this
.
$refs
[
'
move-dropdown
'
]);
$moveDropdown
.
select2
({
ajax
:
{
url
:
this
.
projectsAutocompletePath
,
quietMillis
:
125
,
data
(
term
,
page
,
context
)
{
return
{
search
:
term
,
offset_id
:
context
,
};
},
results
(
data
)
{
const
more
=
data
.
length
>=
50
;
const
context
=
data
[
data
.
length
-
1
]
?
data
[
data
.
length
-
1
].
id
:
null
;
return
{
results
:
data
,
more
,
context
,
};
},
},
formatResult
(
project
)
{
return
project
.
name_with_namespace
;
},
formatSelection
(
project
)
{
return
project
.
name_with_namespace
;
},
})
.
on
(
'
change
'
,
(
e
)
=>
{
this
.
formState
.
move_to_project_id
=
parseInt
(
e
.
target
.
value
,
10
);
});
},
beforeDestroy
()
{
$
(
this
.
$refs
[
'
move-dropdown
'
]).
select2
(
'
destroy
'
);
},
};
</
script
>
<
template
>
<fieldset>
<label
for=
"issuable-move"
class=
"sr-only"
>
Move
</label>
<div
class=
"issuable-form-select-holder append-right-5"
>
<input
ref=
"move-dropdown"
type=
"hidden"
id=
"issuable-move"
data-placeholder=
"Move to a different project"
/>
</div>
<span
v-tooltip
data-placement=
"auto top"
title=
"Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
>
<i
class=
"fa fa-question-circle"
aria-hidden=
"true"
>
</i>
</span>
</fieldset>
</
template
>
app/assets/javascripts/issue_show/components/form.vue
View file @
9e820ae3
...
@@ -4,15 +4,10 @@
...
@@ -4,15 +4,10 @@
import
descriptionField
from
'
./fields/description.vue
'
;
import
descriptionField
from
'
./fields/description.vue
'
;
import
editActions
from
'
./edit_actions.vue
'
;
import
editActions
from
'
./edit_actions.vue
'
;
import
descriptionTemplate
from
'
./fields/description_template.vue
'
;
import
descriptionTemplate
from
'
./fields/description_template.vue
'
;
import
projectMove
from
'
./fields/project_move.vue
'
;
import
confidentialCheckbox
from
'
./fields/confidential_checkbox.vue
'
;
import
confidentialCheckbox
from
'
./fields/confidential_checkbox.vue
'
;
export
default
{
export
default
{
props
:
{
props
:
{
canMove
:
{
type
:
Boolean
,
required
:
true
,
},
canDestroy
:
{
canDestroy
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
...
@@ -42,10 +37,6 @@
...
@@ -42,10 +37,6 @@
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
projectsAutocompletePath
:
{
type
:
String
,
required
:
true
,
},
},
},
components
:
{
components
:
{
lockedWarning
,
lockedWarning
,
...
@@ -53,7 +44,6 @@
...
@@ -53,7 +44,6 @@
descriptionField
,
descriptionField
,
descriptionTemplate
,
descriptionTemplate
,
editActions
,
editActions
,
projectMove
,
confidentialCheckbox
,
confidentialCheckbox
,
},
},
computed
:
{
computed
:
{
...
@@ -93,10 +83,6 @@
...
@@ -93,10 +83,6 @@
:markdown-docs-path=
"markdownDocsPath"
/>
:markdown-docs-path=
"markdownDocsPath"
/>
<confidential-checkbox
<confidential-checkbox
:form-state=
"formState"
/>
:form-state=
"formState"
/>
<project-move
v-if=
"canMove"
:form-state=
"formState"
:projects-autocomplete-path=
"projectsAutocompletePath"
/>
<edit-actions
<edit-actions
:form-state=
"formState"
:form-state=
"formState"
:can-destroy=
"canDestroy"
/>
:can-destroy=
"canDestroy"
/>
...
...
app/assets/javascripts/issue_show/index.js
View file @
9e820ae3
...
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
...
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props
:
{
props
:
{
canUpdate
:
this
.
canUpdate
,
canUpdate
:
this
.
canUpdate
,
canDestroy
:
this
.
canDestroy
,
canDestroy
:
this
.
canDestroy
,
canMove
:
this
.
canMove
,
endpoint
:
this
.
endpoint
,
endpoint
:
this
.
endpoint
,
issuableRef
:
this
.
issuableRef
,
issuableRef
:
this
.
issuableRef
,
initialTitleHtml
:
this
.
initialTitleHtml
,
initialTitleHtml
:
this
.
initialTitleHtml
,
...
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
...
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
markdownDocsPath
:
this
.
markdownDocsPath
,
markdownDocsPath
:
this
.
markdownDocsPath
,
projectPath
:
this
.
projectPath
,
projectPath
:
this
.
projectPath
,
projectNamespace
:
this
.
projectNamespace
,
projectNamespace
:
this
.
projectNamespace
,
projectsAutocompletePath
:
this
.
projectsAutocompletePath
,
updatedAt
:
this
.
updatedAt
,
updatedAt
:
this
.
updatedAt
,
updatedByName
:
this
.
updatedByName
,
updatedByName
:
this
.
updatedByName
,
updatedByPath
:
this
.
updatedByPath
,
updatedByPath
:
this
.
updatedByPath
,
...
...
app/assets/javascripts/issue_show/stores/index.js
View file @
9e820ae3
...
@@ -6,7 +6,6 @@ export default class Store {
...
@@ -6,7 +6,6 @@ export default class Store {
confidential
:
false
,
confidential
:
false
,
description
:
''
,
description
:
''
,
lockedWarningVisible
:
false
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
updateLoading
:
false
,
};
};
}
}
...
...
app/assets/javascripts/right_sidebar.js
View file @
9e820ae3
...
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
...
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar
.
prototype
.
openDropdown
=
function
(
blockOrName
)
{
Sidebar
.
prototype
.
openDropdown
=
function
(
blockOrName
)
{
var
$block
;
var
$block
;
$block
=
_
.
isString
(
blockOrName
)
?
this
.
getBlock
(
blockOrName
)
:
blockOrName
;
$block
=
_
.
isString
(
blockOrName
)
?
this
.
getBlock
(
blockOrName
)
:
blockOrName
;
$block
.
find
(
'
.edit-link
'
).
trigger
(
'
click
'
);
if
(
!
this
.
isOpen
())
{
if
(
!
this
.
isOpen
())
{
this
.
setCollapseAfterUpdate
(
$block
);
this
.
setCollapseAfterUpdate
(
$block
);
return
this
.
toggleSidebar
(
'
open
'
);
this
.
toggleSidebar
(
'
open
'
);
}
}
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout
(()
=>
{
$block
.
find
(
'
.js-sidebar-dropdown-toggle
'
).
trigger
(
'
click
'
);
});
};
};
Sidebar
.
prototype
.
setCollapseAfterUpdate
=
function
(
$block
)
{
Sidebar
.
prototype
.
setCollapseAfterUpdate
=
function
(
$block
)
{
...
...
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
View file @
9e820ae3
...
@@ -36,7 +36,7 @@ export default {
...
@@ -36,7 +36,7 @@ export default {
/>
/>
<a
<a
v-if="editable"
v-if="editable"
class="edit-link pull-right"
class="
js-sidebar-dropdown-toggle
edit-link pull-right"
href="#"
href="#"
>
>
Edit
Edit
...
...
app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
0 → 100644
View file @
9e820ae3
/* global Flash */
function
isValidProjectId
(
id
)
{
return
id
>
0
;
}
class
SidebarMoveIssue
{
constructor
(
mediator
,
dropdownToggle
,
confirmButton
)
{
this
.
mediator
=
mediator
;
this
.
$dropdownToggle
=
$
(
dropdownToggle
);
this
.
$confirmButton
=
$
(
confirmButton
);
this
.
onConfirmClickedWrapper
=
this
.
onConfirmClicked
.
bind
(
this
);
}
init
()
{
this
.
initDropdown
();
this
.
addEventListeners
();
}
destroy
()
{
this
.
removeEventListeners
();
}
initDropdown
()
{
this
.
$dropdownToggle
.
glDropdown
({
search
:
{
fields
:
[
'
name_with_namespace
'
],
},
showMenuAbove
:
true
,
selectable
:
true
,
filterable
:
true
,
filterRemote
:
true
,
multiSelect
:
false
,
// Keep the dropdown open after selecting an option
shouldPropagate
:
false
,
data
:
(
searchTerm
,
callback
)
=>
{
this
.
mediator
.
fetchAutocompleteProjects
(
searchTerm
)
.
then
(
callback
)
.
catch
(()
=>
new
Flash
(
'
An error occured while fetching projects autocomplete.
'
));
},
renderRow
:
project
=>
`
<li>
<a href="#" class="js-move-issue-dropdown-item">
${
project
.
name_with_namespace
}
</a>
</li>
`
,
clicked
:
(
options
)
=>
{
const
project
=
options
.
selectedObj
;
const
selectedProjectId
=
options
.
isMarking
?
project
.
id
:
0
;
this
.
mediator
.
setMoveToProjectId
(
selectedProjectId
);
this
.
$confirmButton
.
attr
(
'
disabled
'
,
!
isValidProjectId
(
selectedProjectId
));
},
});
}
addEventListeners
()
{
this
.
$confirmButton
.
on
(
'
click
'
,
this
.
onConfirmClickedWrapper
);
}
removeEventListeners
()
{
this
.
$confirmButton
.
off
(
'
click
'
,
this
.
onConfirmClickedWrapper
);
}
onConfirmClicked
()
{
if
(
isValidProjectId
(
this
.
mediator
.
store
.
moveToProjectId
))
{
this
.
$confirmButton
.
disable
()
.
addClass
(
'
is-loading
'
);
this
.
mediator
.
moveIssue
()
.
catch
(()
=>
{
Flash
(
'
An error occured while moving the issue.
'
);
this
.
$confirmButton
.
enable
()
.
removeClass
(
'
is-loading
'
);
});
}
}
}
export
default
SidebarMoveIssue
;
app/assets/javascripts/sidebar/services/sidebar_service.js
View file @
9e820ae3
...
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
...
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue
.
use
(
VueResource
);
Vue
.
use
(
VueResource
);
export
default
class
SidebarService
{
export
default
class
SidebarService
{
constructor
(
endpoint
)
{
constructor
(
endpoint
Map
)
{
if
(
!
SidebarService
.
singleton
)
{
if
(
!
SidebarService
.
singleton
)
{
this
.
endpoint
=
endpoint
;
this
.
endpoint
=
endpointMap
.
endpoint
;
this
.
moveIssueEndpoint
=
endpointMap
.
moveIssueEndpoint
;
this
.
projectsAutocompleteEndpoint
=
endpointMap
.
projectsAutocompleteEndpoint
;
SidebarService
.
singleton
=
this
;
SidebarService
.
singleton
=
this
;
}
}
...
@@ -25,4 +27,18 @@ export default class SidebarService {
...
@@ -25,4 +27,18 @@ export default class SidebarService {
emulateJSON
:
true
,
emulateJSON
:
true
,
});
});
}
}
getProjectsAutocomplete
(
searchTerm
)
{
return
Vue
.
http
.
get
(
this
.
projectsAutocompleteEndpoint
,
{
params
:
{
search
:
searchTerm
,
},
});
}
moveIssue
(
moveToProjectId
)
{
return
Vue
.
http
.
post
(
this
.
moveIssueEndpoint
,
{
move_to_project_id
:
moveToProjectId
,
});
}
}
}
app/assets/javascripts/sidebar/sidebar_bundle.js
View file @
9e820ae3
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
import
sidebarTimeTracking
from
'
./components/time_tracking/sidebar_time_tracking
'
;
import
sidebarTimeTracking
from
'
./components/time_tracking/sidebar_time_tracking
'
;
import
sidebarAssignees
from
'
./components/assignees/sidebar_assignees
'
;
import
sidebarAssignees
from
'
./components/assignees/sidebar_assignees
'
;
import
confidential
from
'
./components/confidential/confidential_issue_sidebar.vue
'
;
import
confidential
from
'
./components/confidential/confidential_issue_sidebar.vue
'
;
import
SidebarMoveIssue
from
'
./lib/sidebar_move_issue
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
...
@@ -31,6 +32,12 @@ function domContentLoaded() {
...
@@ -31,6 +32,12 @@ function domContentLoaded() {
service
:
mediator
.
service
,
service
:
mediator
.
service
,
},
},
}).
$mount
(
confidentialEl
);
}).
$mount
(
confidentialEl
);
new
SidebarMoveIssue
(
mediator
,
$
(
'
.js-move-issue
'
),
$
(
'
.js-move-issue-confirmation-button
'
),
).
init
();
}
}
new
Vue
(
sidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
new
Vue
(
sidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
...
...
app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
9e820ae3
...
@@ -7,7 +7,11 @@ export default class SidebarMediator {
...
@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor
(
options
)
{
constructor
(
options
)
{
if
(
!
SidebarMediator
.
singleton
)
{
if
(
!
SidebarMediator
.
singleton
)
{
this
.
store
=
new
Store
(
options
);
this
.
store
=
new
Store
(
options
);
this
.
service
=
new
Service
(
options
.
endpoint
);
this
.
service
=
new
Service
({
endpoint
:
options
.
endpoint
,
moveIssueEndpoint
:
options
.
moveIssueEndpoint
,
projectsAutocompleteEndpoint
:
options
.
projectsAutocompleteEndpoint
,
});
SidebarMediator
.
singleton
=
this
;
SidebarMediator
.
singleton
=
this
;
}
}
...
@@ -26,6 +30,10 @@ export default class SidebarMediator {
...
@@ -26,6 +30,10 @@ export default class SidebarMediator {
return
this
.
service
.
update
(
field
,
selected
.
length
===
0
?
[
0
]
:
selected
);
return
this
.
service
.
update
(
field
,
selected
.
length
===
0
?
[
0
]
:
selected
);
}
}
setMoveToProjectId
(
projectId
)
{
this
.
store
.
setMoveToProjectId
(
projectId
);
}
fetch
()
{
fetch
()
{
this
.
service
.
get
()
this
.
service
.
get
()
.
then
(
response
=>
response
.
json
())
.
then
(
response
=>
response
.
json
())
...
@@ -35,4 +43,23 @@ export default class SidebarMediator {
...
@@ -35,4 +43,23 @@ export default class SidebarMediator {
})
})
.
catch
(()
=>
new
Flash
(
'
Error occured when fetching sidebar data
'
));
.
catch
(()
=>
new
Flash
(
'
Error occured when fetching sidebar data
'
));
}
}
fetchAutocompleteProjects
(
searchTerm
)
{
return
this
.
service
.
getProjectsAutocomplete
(
searchTerm
)
.
then
(
response
=>
response
.
json
())
.
then
((
data
)
=>
{
this
.
store
.
setAutocompleteProjects
(
data
);
return
this
.
store
.
autocompleteProjects
;
});
}
moveIssue
()
{
return
this
.
service
.
moveIssue
(
this
.
store
.
moveToProjectId
)
.
then
(
response
=>
response
.
json
())
.
then
((
data
)
=>
{
if
(
location
.
pathname
!==
data
.
web_url
)
{
gl
.
utils
.
visitUrl
(
data
.
web_url
);
}
});
}
}
}
app/assets/javascripts/sidebar/stores/sidebar_store.js
View file @
9e820ae3
...
@@ -13,6 +13,8 @@ export default class SidebarStore {
...
@@ -13,6 +13,8 @@ export default class SidebarStore {
this
.
isFetching
=
{
this
.
isFetching
=
{
assignees
:
true
,
assignees
:
true
,
};
};
this
.
autocompleteProjects
=
[];
this
.
moveToProjectId
=
0
;
SidebarStore
.
singleton
=
this
;
SidebarStore
.
singleton
=
this
;
}
}
...
@@ -53,4 +55,12 @@ export default class SidebarStore {
...
@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees
()
{
removeAllAssignees
()
{
this
.
assignees
=
[];
this
.
assignees
=
[];
}
}
setAutocompleteProjects
(
projects
)
{
this
.
autocompleteProjects
=
projects
;
}
setMoveToProjectId
(
moveToProjectId
)
{
this
.
moveToProjectId
=
moveToProjectId
;
}
}
}
app/assets/stylesheets/framework/dropdowns.scss
View file @
9e820ae3
...
@@ -193,7 +193,7 @@
...
@@ -193,7 +193,7 @@
min-width
:
240px
;
min-width
:
240px
;
max-width
:
500px
;
max-width
:
500px
;
margin-top
:
2px
;
margin-top
:
2px
;
margin-bottom
:
0
;
margin-bottom
:
2px
;
font-size
:
14px
;
font-size
:
14px
;
font-weight
:
$gl-font-weight-normal
;
font-weight
:
$gl-font-weight-normal
;
padding
:
8px
0
;
padding
:
8px
0
;
...
@@ -622,6 +622,11 @@
...
@@ -622,6 +622,11 @@
border-top
:
1px
solid
$dropdown-divider-color
;
border-top
:
1px
solid
$dropdown-divider-color
;
}
}
.dropdown-footer-content
{
padding-left
:
10px
;
padding-right
:
10px
;
}
.dropdown-due-date-footer
{
.dropdown-due-date-footer
{
padding-top
:
0
;
padding-top
:
0
;
margin-left
:
10px
;
margin-left
:
10px
;
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
9e820ae3
...
@@ -473,7 +473,7 @@
...
@@ -473,7 +473,7 @@
padding-top
:
6px
;
padding-top
:
6px
;
}
}
.
open
.
dropdown-menu
{
.dropdown-menu
{
width
:
100%
;
width
:
100%
;
}
}
}
}
...
@@ -486,6 +486,24 @@
...
@@ -486,6 +486,24 @@
}
}
}
}
.sidebar-move-issue-dropdown
{
@include
new-style-dropdown
;
}
.sidebar-move-issue-confirmation-button
{
width
:
100%
;
&
.is-loading
{
.sidebar-move-issue-confirmation-loading-icon
{
display
:
inline-block
;
}
}
}
.sidebar-move-issue-confirmation-loading-icon
{
display
:
none
;
}
.detail-page-description
{
.detail-page-description
{
padding
:
16px
0
;
padding
:
16px
0
;
...
...
app/controllers/autocomplete_controller.rb
View file @
9e820ae3
...
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
...
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
project
=
Project
.
find_by_id
(
params
[
:project_id
])
project
=
Project
.
find_by_id
(
params
[
:project_id
])
projects
=
projects_finder
.
execute
(
project
,
search:
params
[
:search
],
offset_id:
params
[
:offset_id
])
projects
=
projects_finder
.
execute
(
project
,
search:
params
[
:search
],
offset_id:
params
[
:offset_id
])
no_project
=
{
id:
0
,
name_with_namespace:
'No project'
}
projects
.
unshift
(
no_project
)
unless
params
[
:offset_id
].
present?
render
json:
projects
.
to_json
(
only:
[
:id
,
:name_with_namespace
],
methods: :name_with_namespace
)
render
json:
projects
.
to_json
(
only:
[
:id
,
:name_with_namespace
],
methods: :name_with_namespace
)
end
end
...
...
app/controllers/projects/issues_controller.rb
View file @
9e820ae3
...
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
# Allow modify issue
# Allow modify issue
before_action
:authorize_update_issue!
,
only:
[
:edit
,
:update
]
before_action
:authorize_update_issue!
,
only:
[
:edit
,
:update
,
:move
]
# Allow create a new branch and empty WIP merge request from current issue
# Allow create a new branch and empty WIP merge request from current issue
before_action
:authorize_create_merge_request!
,
only:
[
:create_merge_request
]
before_action
:authorize_create_merge_request!
,
only:
[
:create_merge_request
]
...
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
@issue
=
Issues
::
UpdateService
.
new
(
project
,
current_user
,
update_params
).
execute
(
issue
)
@issue
=
Issues
::
UpdateService
.
new
(
project
,
current_user
,
update_params
).
execute
(
issue
)
respond_to
do
|
format
|
format
.
html
do
recaptcha_check_with_fallback
{
render
:edit
}
end
format
.
json
do
render_issue_json
end
end
rescue
ActiveRecord
::
StaleObjectError
render_conflict_response
end
def
move
params
.
require
(
:move_to_project_id
)
if
params
[
:move_to_project_id
].
to_i
>
0
if
params
[
:move_to_project_id
].
to_i
>
0
new_project
=
Project
.
find
(
params
[
:move_to_project_id
])
new_project
=
Project
.
find
(
params
[
:move_to_project_id
])
return
render_404
unless
issue
.
can_move?
(
current_user
,
new_project
)
return
render_404
unless
issue
.
can_move?
(
current_user
,
new_project
)
move_service
=
Issues
::
MoveService
.
new
(
project
,
current_user
)
@issue
=
Issues
::
UpdateService
.
new
(
project
,
current_user
,
target_project:
new_project
).
execute
(
issue
)
@issue
=
move_service
.
execute
(
@issue
,
new_project
)
end
end
respond_to
do
|
format
|
respond_to
do
|
format
|
format
.
html
do
recaptcha_check_with_fallback
{
render
:edit
}
end
format
.
json
do
format
.
json
do
if
@issue
.
valid?
render_issue_json
render
json:
serializer
.
represent
(
@issue
)
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
end
end
end
end
...
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
return
render_404
unless
@project
.
feature_available?
(
:issues
,
current_user
)
return
render_404
unless
@project
.
feature_available?
(
:issues
,
current_user
)
end
end
def
render_issue_json
if
@issue
.
valid?
render
json:
serializer
.
represent
(
@issue
)
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
end
def
issue_params
def
issue_params
params
.
require
(
:issue
).
permit
(
*
issue_params_attributes
)
params
.
require
(
:issue
).
permit
(
*
issue_params_attributes
)
end
end
...
...
app/helpers/dropdowns_helper.rb
View file @
9e820ae3
...
@@ -97,9 +97,11 @@ module DropdownsHelper
...
@@ -97,9 +97,11 @@ module DropdownsHelper
end
end
end
end
def
dropdown_footer
(
&
block
)
def
dropdown_footer
(
add_content_class:
false
,
&
block
)
content_tag
(
:div
,
class:
"dropdown-footer"
)
do
content_tag
(
:div
,
class:
"dropdown-footer"
)
do
if
block
if
add_content_class
content_tag
(
:div
,
capture
(
&
block
),
class:
"dropdown-footer-content"
)
else
capture
(
&
block
)
capture
(
&
block
)
end
end
end
end
...
...
app/helpers/issuables_helper.rb
View file @
9e820ae3
...
@@ -207,12 +207,10 @@ module IssuablesHelper
...
@@ -207,12 +207,10 @@ module IssuablesHelper
endpoint:
project_issue_path
(
@project
,
issuable
),
endpoint:
project_issue_path
(
@project
,
issuable
),
canUpdate:
can?
(
current_user
,
:update_issue
,
issuable
),
canUpdate:
can?
(
current_user
,
:update_issue
,
issuable
),
canDestroy:
can?
(
current_user
,
:destroy_issue
,
issuable
),
canDestroy:
can?
(
current_user
,
:destroy_issue
,
issuable
),
canMove:
current_user
?
issuable
.
can_move?
(
current_user
)
:
false
,
issuableRef:
issuable
.
to_reference
,
issuableRef:
issuable
.
to_reference
,
isConfidential:
issuable
.
confidential
,
isConfidential:
issuable
.
confidential
,
markdownPreviewPath:
preview_markdown_path
(
@project
),
markdownPreviewPath:
preview_markdown_path
(
@project
),
markdownDocsPath:
help_page_path
(
'user/markdown'
),
markdownDocsPath:
help_page_path
(
'user/markdown'
),
projectsAutocompletePath:
autocomplete_projects_path
(
project_id:
@project
.
id
),
issuableTemplates:
issuable_templates
(
issuable
),
issuableTemplates:
issuable_templates
(
issuable
),
projectPath:
ref_project
.
path
,
projectPath:
ref_project
.
path
,
projectNamespace:
ref_project
.
namespace
.
full_path
,
projectNamespace:
ref_project
.
namespace
.
full_path
,
...
@@ -354,6 +352,8 @@ module IssuablesHelper
...
@@ -354,6 +352,8 @@ module IssuablesHelper
def
issuable_sidebar_options
(
issuable
,
can_edit_issuable
)
def
issuable_sidebar_options
(
issuable
,
can_edit_issuable
)
{
{
endpoint:
"
#{
issuable_json_path
(
issuable
)
}
?basic=true"
,
endpoint:
"
#{
issuable_json_path
(
issuable
)
}
?basic=true"
,
moveIssueEndpoint:
move_namespace_project_issue_path
(
namespace_id:
issuable
.
project
.
namespace
.
to_param
,
project_id:
issuable
.
project
,
id:
issuable
),
projectsAutocompleteEndpoint:
autocomplete_projects_path
(
project_id:
@project
.
id
),
editable:
can_edit_issuable
,
editable:
can_edit_issuable
,
currentUser:
current_user
.
as_json
(
only:
[
:username
,
:id
,
:name
],
methods: :avatar_url
),
currentUser:
current_user
.
as_json
(
only:
[
:username
,
:id
,
:name
],
methods: :avatar_url
),
rootPath:
root_path
,
rootPath:
root_path
,
...
...
app/views/projects/boards/components/sidebar/_due_date.html.haml
View file @
9e820ae3
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
Due date
Due date
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"edit-link pull-right"
=
link_to
"Edit"
,
"#"
,
class:
"
js-sidebar-dropdown-toggle
edit-link pull-right"
.value
.value
.value-content
.value-content
%span
.no-value
{
"v-if"
=>
"!issue.dueDate"
}
%span
.no-value
{
"v-if"
=>
"!issue.dueDate"
}
...
...
app/views/projects/boards/components/sidebar/_labels.html.haml
View file @
9e820ae3
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
Labels
Labels
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"edit-link pull-right"
=
link_to
"Edit"
,
"#"
,
class:
"
js-sidebar-dropdown-toggle
edit-link pull-right"
.value.issuable-show-labels
.value.issuable-show-labels
%span
.no-value
{
"v-if"
=>
"issue.labels && issue.labels.length === 0"
}
%span
.no-value
{
"v-if"
=>
"issue.labels && issue.labels.length === 0"
}
None
None
...
...
app/views/projects/boards/components/sidebar/_milestone.html.haml
View file @
9e820ae3
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
Milestone
Milestone
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
icon
(
"spinner spin"
,
class:
"block-loading"
)
=
link_to
"Edit"
,
"#"
,
class:
"edit-link pull-right"
=
link_to
"Edit"
,
"#"
,
class:
"
js-sidebar-dropdown-toggle
edit-link pull-right"
.value
.value
%span
.no-value
{
"v-if"
=>
"!issue.milestone"
}
%span
.no-value
{
"v-if"
=>
"!issue.milestone"
}
None
None
...
...
app/views/shared/icons/_icon_arrow_right.svg.erb
0 → 100644
View file @
9e820ae3
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
><path
fill-rule=
"evenodd"
d=
"M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"
/></svg>
app/views/shared/issuable/_form.html.haml
View file @
9e820ae3
...
@@ -29,18 +29,6 @@
...
@@ -29,18 +29,6 @@
=
render
'shared/issuable/form/metadata'
,
issuable:
issuable
,
form:
form
=
render
'shared/issuable/form/metadata'
,
issuable:
issuable
,
form:
form
-
if
issuable
.
can_move?
(
current_user
)
%hr
.form-group
=
label_tag
:move_to_project_id
,
'Move'
,
class:
'control-label'
.col-sm-10
.issuable-form-select-holder
=
hidden_field_tag
:move_to_project_id
,
nil
,
class:
'js-move-dropdown'
,
data:
{
placeholder:
'Select project'
,
projects_url:
autocomplete_projects_path
(
project_id:
@project
.
id
),
page_size:
MoveToProjectFinder
::
PAGE_SIZE
}
%span
{
data:
{
toggle:
'tooltip'
,
placement:
'auto top'
},
style:
'cursor: default'
,
title:
'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.'
}
=
icon
(
'question-circle'
)
=
render
'shared/issuable/form/branch_chooser'
,
issuable:
issuable
,
form:
form
=
render
'shared/issuable/form/branch_chooser'
,
issuable:
issuable
,
form:
form
=
render
'shared/issuable/form/merge_params'
,
issuable:
issuable
=
render
'shared/issuable/form/merge_params'
,
issuable:
issuable
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
9e820ae3
...
@@ -34,7 +34,7 @@
...
@@ -34,7 +34,7 @@
Milestone
Milestone
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.hide-collapsed
.value.hide-collapsed
-
if
issuable
.
milestone
-
if
issuable
.
milestone
=
link_to
issuable
.
milestone
.
title
,
milestone_path
(
issuable
.
milestone
),
class:
"bold has-tooltip"
,
title:
milestone_remaining_days
(
issuable
.
milestone
),
data:
{
container:
"body"
,
html:
1
}
=
link_to
issuable
.
milestone
.
title
,
milestone_path
(
issuable
.
milestone
),
class:
"bold has-tooltip"
,
title:
milestone_remaining_days
(
issuable
.
milestone
),
data:
{
container:
"body"
,
html:
1
}
...
@@ -60,7 +60,7 @@
...
@@ -60,7 +60,7 @@
Due date
Due date
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
-
if
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.hide-collapsed
.value.hide-collapsed
%span
.value-content
%span
.value-content
-
if
issuable
.
due_date
-
if
issuable
.
due_date
...
@@ -95,7 +95,7 @@
...
@@ -95,7 +95,7 @@
Labels
Labels
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.issuable-show-labels.hide-collapsed
{
class:
(
"has-labels"
if
selected_labels
.
any?
)
}
.value.issuable-show-labels.hide-collapsed
{
class:
(
"has-labels"
if
selected_labels
.
any?
)
}
-
if
selected_labels
.
any?
-
if
selected_labels
.
any?
-
selected_labels
.
each
do
|
label
|
-
selected_labels
.
each
do
|
label
|
...
@@ -141,5 +141,22 @@
...
@@ -141,5 +141,22 @@
%cite
{
title:
project_ref
}
%cite
{
title:
project_ref
}
=
project_ref
=
project_ref
=
clipboard_button
(
text:
project_ref
,
title:
"Copy reference to clipboard"
,
placement:
"left"
)
=
clipboard_button
(
text:
project_ref
,
title:
"Copy reference to clipboard"
,
placement:
"left"
)
-
if
current_user
&&
issuable
.
can_move?
(
current_user
)
.block.js-sidebar-move-issue-block
.sidebar-collapsed-icon
{
data:
{
toggle:
'tooltip'
,
placement:
'left'
,
container:
'body'
},
title:
'Move issue'
}
=
custom_icon
(
'icon_arrow_right'
)
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button
.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue
{
type:
'button'
,
data:
{
toggle:
'dropdown'
}
}
Move issue
.dropdown-menu.dropdown-menu-selectable
=
dropdown_title
(
'Move issue'
)
=
dropdown_filter
(
'Search project'
,
search_id:
'sidebar-move-issue-dropdown-search'
)
=
dropdown_content
=
dropdown_loading
=
dropdown_footer
add_content_class:
true
do
%button
.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button
{
disabled:
true
}
Move
=
icon
(
'spinner spin'
,
class:
'sidebar-move-issue-confirmation-loading-icon'
)
%script
.js-sidebar-options
{
type:
"application/json"
}=
issuable_sidebar_options
(
issuable
,
can_edit_issuable
).
to_json
.
html_safe
%script
.js-sidebar-options
{
type:
"application/json"
}=
issuable_sidebar_options
(
issuable
,
can_edit_issuable
).
to_json
.
html_safe
app/views/shared/issuable/_sidebar_assignees.html.haml
View file @
9e820ae3
...
@@ -13,7 +13,7 @@
...
@@ -13,7 +13,7 @@
Assignee
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
-
if
!
signed_in
-
if
!
signed_in
%a
.gutter-toggle.pull-right.js-sidebar-toggle
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
}
%a
.gutter-toggle.pull-right.js-sidebar-toggle
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
}
=
sidebar_gutter_toggle_icon
=
sidebar_gutter_toggle_icon
...
...
app/views/shared/issuable/form/_issue_assignee.html.haml
View file @
9e820ae3
...
@@ -11,7 +11,7 @@
...
@@ -11,7 +11,7 @@
Assignee
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.hide-collapsed
.value.hide-collapsed
-
if
assignees
.
any?
-
if
assignees
.
any?
-
assignees
.
each
do
|
assignee
|
-
assignees
.
each
do
|
assignee
|
...
...
app/views/shared/issuable/form/_merge_request_assignee.html.haml
View file @
9e820ae3
...
@@ -9,7 +9,7 @@
...
@@ -9,7 +9,7 @@
Assignee
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
=
link_to
'Edit'
,
'#'
,
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.hide-collapsed
.value.hide-collapsed
-
if
merge_request
.
assignee
-
if
merge_request
.
assignee
=
link_to_member
(
@project
,
merge_request
.
assignee
,
size:
32
,
extra_class:
'bold'
)
do
=
link_to_member
(
@project
,
merge_request
.
assignee
,
size:
32
,
extra_class:
'bold'
)
do
...
...
app/views/shared/milestones/_sidebar.html.haml
View file @
9e820ae3
...
@@ -21,7 +21,7 @@
...
@@ -21,7 +21,7 @@
.title
.title
Start date
Start date
-
if
@project
&&
can?
(
current_user
,
:admin_milestone
,
@project
)
-
if
@project
&&
can?
(
current_user
,
:admin_milestone
,
@project
)
=
link_to
'Edit'
,
edit_project_milestone_path
(
@project
,
@milestone
),
class:
'edit-link pull-right'
=
link_to
'Edit'
,
edit_project_milestone_path
(
@project
,
@milestone
),
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value
.value
%span
.value-content
%span
.value-content
-
if
milestone
.
start_date
-
if
milestone
.
start_date
...
@@ -51,7 +51,7 @@
...
@@ -51,7 +51,7 @@
.title.hide-collapsed
.title.hide-collapsed
Due date
Due date
-
if
@project
&&
can?
(
current_user
,
:admin_milestone
,
@project
)
-
if
@project
&&
can?
(
current_user
,
:admin_milestone
,
@project
)
=
link_to
'Edit'
,
edit_project_milestone_path
(
@project
,
@milestone
),
class:
'edit-link pull-right'
=
link_to
'Edit'
,
edit_project_milestone_path
(
@project
,
@milestone
),
class:
'
js-sidebar-dropdown-toggle
edit-link pull-right'
.value.hide-collapsed
.value.hide-collapsed
%span
.value-content
%span
.value-content
-
if
milestone
.
due_date
-
if
milestone
.
due_date
...
...
changelogs/unreleased/34261-move-move-to-sidebar.yml
0 → 100644
View file @
9e820ae3
---
title
:
Move "Move issue" controls to right-sidebar
merge_request
:
author
:
type
:
changed
config/routes/project.rb
View file @
9e820ae3
...
@@ -303,6 +303,7 @@ constraints(ProjectUrlConstrainer.new) do
...
@@ -303,6 +303,7 @@ constraints(ProjectUrlConstrainer.new) do
member
do
member
do
post
:toggle_subscription
post
:toggle_subscription
post
:mark_as_spam
post
:mark_as_spam
post
:move
get
:referenced_merge_requests
get
:referenced_merge_requests
get
:related_branches
get
:related_branches
get
:can_create_branch
get
:can_create_branch
...
...
doc/user/project/issues/img/sidebar_move_issue.png
0 → 100644
View file @
9e820ae3
53.2 KB
doc/user/project/issues/index.md
View file @
9e820ae3
...
@@ -86,6 +86,10 @@ Read through the [documentation on creating issues](create_new_issue.md).
...
@@ -86,6 +86,10 @@ Read through the [documentation on creating issues](create_new_issue.md).
Learn distinct ways to
[
close issues
](
closing_issues.md
)
in GitLab.
Learn distinct ways to
[
close issues
](
closing_issues.md
)
in GitLab.
## Moving issues
Read through the
[
documentation on moving issues
](
moving_issues.md
)
.
## Create a merge request from an issue
## Create a merge request from an issue
Learn more about it on the
[
GitLab Issues Functionalities documentation
](
issues_functionalities.md#18-new-merge-request
)
.
Learn more about it on the
[
GitLab Issues Functionalities documentation
](
issues_functionalities.md#18-new-merge-request
)
.
...
...
doc/user/project/issues/moving_issues.md
0 → 100644
View file @
9e820ae3
# Moving Issues
Please read through the
[
GitLab Issue Documentation
](
index.md
)
for an overview on GitLab Issues.
Moving an issue will close it and duplicate it on the specified project.
There will also be a system note added to both issues indicating where it came from or went to.
You can move an issue with the "Move issue" button at the bottom of the right-sidebar when viewing the issue.
![
move issue - button
](
img/sidebar_move_issue.png
)
spec/controllers/autocomplete_controller_spec.rb
View file @
9e820ae3
...
@@ -241,13 +241,10 @@ describe AutocompleteController do
...
@@ -241,13 +241,10 @@ describe AutocompleteController do
it
'returns projects'
do
it
'returns projects'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
(
2
)
expect
(
json_response
.
size
).
to
eq
(
1
)
expect
(
json_response
.
first
[
'id'
]).
to
eq
(
0
)
expect
(
json_response
.
first
[
'name_with_namespace'
]).
to
eq
'No project'
expect
(
json_response
.
la
st
[
'id'
]).
to
eq
authorized_project
.
id
expect
(
json_response
.
fir
st
[
'id'
]).
to
eq
authorized_project
.
id
expect
(
json_response
.
la
st
[
'name_with_namespace'
]).
to
eq
authorized_project
.
name_with_namespace
expect
(
json_response
.
fir
st
[
'name_with_namespace'
]).
to
eq
authorized_project
.
name_with_namespace
end
end
end
end
end
end
...
@@ -265,10 +262,10 @@ describe AutocompleteController do
...
@@ -265,10 +262,10 @@ describe AutocompleteController do
it
'returns projects'
do
it
'returns projects'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
(
2
)
expect
(
json_response
.
size
).
to
eq
(
1
)
expect
(
json_response
.
la
st
[
'id'
]).
to
eq
authorized_search_project
.
id
expect
(
json_response
.
fir
st
[
'id'
]).
to
eq
authorized_search_project
.
id
expect
(
json_response
.
la
st
[
'name_with_namespace'
]).
to
eq
authorized_search_project
.
name_with_namespace
expect
(
json_response
.
fir
st
[
'name_with_namespace'
]).
to
eq
authorized_search_project
.
name_with_namespace
end
end
end
end
end
end
...
@@ -292,7 +289,7 @@ describe AutocompleteController do
...
@@ -292,7 +289,7 @@ describe AutocompleteController do
it
'returns projects'
do
it
'returns projects'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
3
# Of a total of 4
expect
(
json_response
.
size
).
to
eq
2
# Of a total of 3
end
end
end
end
end
end
...
@@ -312,9 +309,9 @@ describe AutocompleteController do
...
@@ -312,9 +309,9 @@ describe AutocompleteController do
get
(
:projects
,
project_id:
project
.
id
,
offset_id:
authorized_project
.
id
)
get
(
:projects
,
project_id:
project
.
id
,
offset_id:
authorized_project
.
id
)
end
end
it
'returns
"No project"
'
do
it
'returns
projects
'
do
expect
(
json_response
.
detect
{
|
item
|
item
[
'id'
]
==
0
}).
to
be_nil
# 'No project' is not there
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
detect
{
|
item
|
item
[
'id'
]
==
authorized_project
.
id
}).
to
be_nil
# Offset project is not there either
expect
(
json_response
.
size
).
to
eq
2
# Of a total of 3
end
end
end
end
end
end
...
@@ -331,10 +328,9 @@ describe AutocompleteController do
...
@@ -331,10 +328,9 @@ describe AutocompleteController do
get
(
:projects
,
project_id:
project
.
id
)
get
(
:projects
,
project_id:
project
.
id
)
end
end
it
'returns
a single "No project"
'
do
it
'returns
no projects
'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
(
1
)
# 'No project'
expect
(
json_response
.
size
).
to
eq
(
0
)
expect
(
json_response
.
first
[
'id'
]).
to
eq
0
end
end
end
end
end
end
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
9e820ae3
...
@@ -233,144 +233,119 @@ describe Projects::IssuesController do
...
@@ -233,144 +233,119 @@ describe Projects::IssuesController do
end
end
end
end
context
'when moving issue to another private project'
do
context
'Akismet is enabled'
do
let
(
:another_project
)
{
create
(
:project
,
:private
)
}
let
(
:project
)
{
create
(
:project_empty_repo
,
:public
)
}
context
'when user has access to move issue'
do
before
do
another_project
.
team
<<
[
user
,
:reporter
]
end
it
'moves issue to another project'
do
move_issue
expect
(
response
).
to
have_http_status
:found
before
do
expect
(
another_project
.
issues
).
not_to
be_empty
stub_application_setting
(
recaptcha_enabled:
true
)
end
allow_any_instance_of
(
SpamService
).
to
receive
(
:check_for_spam?
).
and_return
(
true
)
end
end
context
'when user does not have access to move issue'
do
context
'when an issue is not identified as spam'
do
it
'responds with 404'
do
before
do
move_issue
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
).
and_return
(
false
)
allow_any_instance_of
(
AkismetService
).
to
receive
(
:spam?
).
and_return
(
false
)
end
expect
(
response
).
to
have_http_status
:not_found
it
'normally updates the issue'
do
expect
{
update_issue
(
title:
'Foo'
)
}.
to
change
{
issue
.
reload
.
title
}.
to
(
'Foo'
)
end
end
end
end
context
'Akismet is enabled'
do
context
'when an issue is identified as spam'
do
let
(
:project
)
{
create
(
:project_empty_repo
,
:public
)
}
before
do
before
do
stub_application_setting
(
recaptcha_enabled:
true
)
allow_any_instance_of
(
AkismetService
).
to
receive
(
:spam?
).
and_return
(
true
)
allow_any_instance_of
(
SpamService
).
to
receive
(
:check_for_spam?
).
and_return
(
true
)
end
end
context
'when an issue is not identified as spam'
do
context
'when captcha is not verified'
do
before
do
def
update_spam_issue
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
).
and_return
(
false
)
update_issue
(
title:
'Spam Title'
,
description:
'Spam lives here'
)
allow_any_instance_of
(
AkismetService
).
to
receive
(
:spam?
).
and_return
(
false
)
end
end
it
'normally updates the issue'
do
before
do
expect
{
update_issue
(
title:
'Foo'
)
}.
to
change
{
issue
.
reload
.
title
}.
to
(
'Foo'
)
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
).
and_return
(
false
)
end
end
end
context
'when an issue is identified as
spam'
do
it
'rejects an issue recognized as a
spam'
do
before
do
expect
(
Gitlab
::
Recaptcha
).
to
receive
(
:load_configurations!
).
and_return
(
true
)
allow_any_instance_of
(
AkismetService
).
to
receive
(
:spam?
).
and_return
(
true
)
expect
{
update_spam_issue
}.
not_to
change
{
issue
.
reload
.
title
}
end
end
context
'when captcha is not verified'
do
it
'rejects an issue recognized as a spam when recaptcha disabled'
do
def
update_spam_issue
stub_application_setting
(
recaptcha_enabled:
false
)
update_issue
(
title:
'Spam Title'
,
description:
'Spam lives here'
)
end
before
do
expect
{
update_spam_issue
}.
not_to
change
{
issue
.
reload
.
title
}
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
).
and_return
(
false
)
end
end
it
'rejects an issue recognized as a spam'
do
it
'creates a spam log'
do
expect
(
Gitlab
::
Recaptcha
).
to
receive
(
:load_configurations!
).
and_return
(
true
)
update_spam_issue
expect
{
update_spam_issue
}.
not_to
change
{
issue
.
reload
.
title
}
end
it
'rejects an issue recognized as a spam when recaptcha disabled'
do
spam_logs
=
SpamLog
.
all
stub_application_setting
(
recaptcha_enabled:
false
)
expect
{
update_spam_issue
}.
not_to
change
{
issue
.
reload
.
title
}
expect
(
spam_logs
.
count
).
to
eq
(
1
)
end
expect
(
spam_logs
.
first
.
title
).
to
eq
(
'Spam Title'
)
expect
(
spam_logs
.
first
.
recaptcha_verified
).
to
be_falsey
end
it
'creates a spam log'
do
context
'as HTML'
do
it
'renders verify template'
do
update_spam_issue
update_spam_issue
spam_logs
=
SpamLog
.
all
expect
(
response
).
to
render_template
(
:verify
)
expect
(
spam_logs
.
count
).
to
eq
(
1
)
expect
(
spam_logs
.
first
.
title
).
to
eq
(
'Spam Title'
)
expect
(
spam_logs
.
first
.
recaptcha_verified
).
to
be_falsey
end
end
end
context
'as HTML'
do
context
'as JSON'
do
it
'renders verify template'
do
before
do
update_spam_issue
update_issue
({
title:
'Spam Title'
,
description:
'Spam lives here'
},
format: :json
)
expect
(
response
).
to
render_template
(
:verify
)
end
end
end
context
'as JSON'
do
it
'renders json errors'
do
before
do
expect
(
json_response
)
update_issue
({
title:
'Spam Title'
,
description:
'Spam lives here'
},
format: :json
)
.
to
eql
(
"errors"
=>
[
"Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."
])
end
end
it
'renders json errors'
do
expect
(
json_response
)
.
to
eql
(
"errors"
=>
[
"Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."
])
end
it
'returns 422 status'
do
it
'returns 422 status'
do
expect
(
response
).
to
have_http_status
(
422
)
expect
(
response
).
to
have_http_status
(
422
)
end
end
end
end
end
end
context
'when captcha is verified'
do
context
'when captcha is verified'
do
let
(
:spammy_title
)
{
'Whatever'
}
let
(
:spammy_title
)
{
'Whatever'
}
let!
(
:spam_logs
)
{
create_list
(
:spam_log
,
2
,
user:
user
,
title:
spammy_title
)
}
let!
(
:spam_logs
)
{
create_list
(
:spam_log
,
2
,
user:
user
,
title:
spammy_title
)
}
def
update_verified_issue
def
update_verified_issue
update_issue
({
title:
spammy_title
},
update_issue
({
title:
spammy_title
},
{
spam_log_id:
spam_logs
.
last
.
id
,
{
spam_log_id:
spam_logs
.
last
.
id
,
recaptcha_verification:
true
})
recaptcha_verification:
true
})
end
end
before
do
before
do
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
)
allow_any_instance_of
(
described_class
).
to
receive
(
:verify_recaptcha
)
.
and_return
(
true
)
.
and_return
(
true
)
end
end
it
'redirect to issue page'
do
it
'redirect to issue page'
do
update_verified_issue
update_verified_issue
expect
(
response
)
expect
(
response
)
.
to
redirect_to
(
project_issue_path
(
project
,
issue
))
.
to
redirect_to
(
project_issue_path
(
project
,
issue
))
end
end
it
'accepts an issue after recaptcha is verified'
do
it
'accepts an issue after recaptcha is verified'
do
expect
{
update_verified_issue
}.
to
change
{
issue
.
reload
.
title
}.
to
(
spammy_title
)
expect
{
update_verified_issue
}.
to
change
{
issue
.
reload
.
title
}.
to
(
spammy_title
)
end
end
it
'marks spam log as recaptcha_verified'
do
it
'marks spam log as recaptcha_verified'
do
expect
{
update_verified_issue
}.
to
change
{
SpamLog
.
last
.
recaptcha_verified
}.
from
(
false
).
to
(
true
)
expect
{
update_verified_issue
}.
to
change
{
SpamLog
.
last
.
recaptcha_verified
}.
from
(
false
).
to
(
true
)
end
end
it
'does not mark spam log as recaptcha_verified when it does not belong to current_user'
do
it
'does not mark spam log as recaptcha_verified when it does not belong to current_user'
do
spam_log
=
create
(
:spam_log
)
spam_log
=
create
(
:spam_log
)
expect
{
update_issue
(
spam_log_id:
spam_log
.
id
,
recaptcha_verification:
true
)
}
expect
{
update_issue
(
spam_log_id:
spam_log
.
id
,
recaptcha_verification:
true
)
}
.
not_to
change
{
SpamLog
.
last
.
recaptcha_verified
}
.
not_to
change
{
SpamLog
.
last
.
recaptcha_verified
}
end
end
end
end
end
end
end
...
@@ -385,13 +360,45 @@ describe Projects::IssuesController do
...
@@ -385,13 +360,45 @@ describe Projects::IssuesController do
put
:update
,
params
put
:update
,
params
end
end
end
end
describe
'POST #move'
do
before
do
sign_in
(
user
)
project
.
add_developer
(
user
)
end
context
'when moving issue to another private project'
do
let
(
:another_project
)
{
create
(
:project
,
:private
)
}
context
'when user has access to move issue'
do
before
do
another_project
.
add_reporter
(
user
)
end
it
'moves issue to another project'
do
move_issue
expect
(
response
).
to
have_http_status
:ok
expect
(
another_project
.
issues
).
not_to
be_empty
end
end
context
'when user does not have access to move issue'
do
it
'responds with 404'
do
move_issue
expect
(
response
).
to
have_http_status
:not_found
end
end
def
move_issue
def
move_issue
put
:update
,
post
:move
,
format: :json
,
namespace_id:
project
.
namespace
.
to_param
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
,
project_id:
project
,
id:
issue
.
iid
,
id:
issue
.
iid
,
issue:
{
title:
'New title'
},
move_to_project_id:
another_project
.
id
move_to_project_id:
another_project
.
id
end
end
end
end
...
...
spec/features/issues/move_spec.rb
View file @
9e820ae3
...
@@ -15,11 +15,11 @@ feature 'issue move to another project' do
...
@@ -15,11 +15,11 @@ feature 'issue move to another project' do
background
do
background
do
old_project
.
team
<<
[
user
,
:guest
]
old_project
.
team
<<
[
user
,
:guest
]
edit_issue
(
issue
)
visit
issue_path
(
issue
)
end
end
scenario
'moving issue to another project not allowed'
do
scenario
'moving issue to another project not allowed'
do
expect
(
page
).
to
have_no_selector
(
'
#move_to_project_id
'
)
expect
(
page
).
to
have_no_selector
(
'
.js-sidebar-move-issue-block
'
)
end
end
end
end
...
@@ -34,12 +34,14 @@ feature 'issue move to another project' do
...
@@ -34,12 +34,14 @@ feature 'issue move to another project' do
old_project
.
team
<<
[
user
,
:reporter
]
old_project
.
team
<<
[
user
,
:reporter
]
new_project
.
team
<<
[
user
,
:reporter
]
new_project
.
team
<<
[
user
,
:reporter
]
edit_issue
(
issue
)
visit
issue_path
(
issue
)
end
end
scenario
'moving issue to another project'
,
js:
true
do
scenario
'moving issue to another project'
,
js:
true
do
find
(
'#issuable-move'
,
visible:
false
).
set
(
new_project
.
id
)
find
(
'.js-move-issue'
).
trigger
(
'click'
)
click_button
(
'Save changes'
)
wait_for_requests
all
(
'.js-move-issue-dropdown-item'
)[
0
].
click
find
(
'.js-move-issue-confirmation-button'
).
click
expect
(
page
).
to
have_content
(
"Text with
#{
cross_reference
}#{
mr
.
to_reference
}
"
)
expect
(
page
).
to
have_content
(
"Text with
#{
cross_reference
}#{
mr
.
to_reference
}
"
)
expect
(
page
).
to
have_content
(
"moved from
#{
cross_reference
}#{
issue
.
to_reference
}
"
)
expect
(
page
).
to
have_content
(
"moved from
#{
cross_reference
}#{
issue
.
to_reference
}
"
)
...
@@ -50,13 +52,12 @@ feature 'issue move to another project' do
...
@@ -50,13 +52,12 @@ feature 'issue move to another project' do
scenario
'searching project dropdown'
,
js:
true
do
scenario
'searching project dropdown'
,
js:
true
do
new_project_search
.
team
<<
[
user
,
:reporter
]
new_project_search
.
team
<<
[
user
,
:reporter
]
page
.
within
'.detail-page-description'
do
find
(
'.js-move-issue'
).
trigger
(
'click'
)
first
(
'.select2-choice'
).
click
wait_for_requests
end
fill_in
(
's2id_autogen1_search'
,
with:
new_project_search
.
name
)
page
.
within
'.js-sidebar-move-issue-block'
do
fill_in
(
'sidebar-move-issue-dropdown-search'
,
with:
new_project_search
.
name
)
page
.
within
'.select2-drop'
do
expect
(
page
).
to
have_content
(
new_project_search
.
name
)
expect
(
page
).
to
have_content
(
new_project_search
.
name
)
expect
(
page
).
not_to
have_content
(
new_project
.
name
)
expect
(
page
).
not_to
have_content
(
new_project
.
name
)
end
end
...
@@ -68,10 +69,10 @@ feature 'issue move to another project' do
...
@@ -68,10 +69,10 @@ feature 'issue move to another project' do
background
{
another_project
.
team
<<
[
user
,
:guest
]
}
background
{
another_project
.
team
<<
[
user
,
:guest
]
}
scenario
'browsing projects in projects select'
do
scenario
'browsing projects in projects select'
do
click_link
'Move to a different project'
find
(
'.js-move-issue'
).
trigger
(
'click'
)
wait_for_requests
page
.
within
'.select2-results'
do
page
.
within
'.js-sidebar-move-issue-block'
do
expect
(
page
).
to
have_content
'No project'
expect
(
page
).
to
have_content
new_project
.
name_with_namespace
expect
(
page
).
to
have_content
new_project
.
name_with_namespace
end
end
end
end
...
@@ -89,11 +90,6 @@ feature 'issue move to another project' do
...
@@ -89,11 +90,6 @@ feature 'issue move to another project' do
end
end
end
end
def
edit_issue
(
issue
)
visit
issue_path
(
issue
)
page
.
within
(
'.issuable-actions'
)
{
first
(
:link
,
'Edit'
).
click
}
end
def
issue_path
(
issue
)
def
issue_path
(
issue
)
project_issue_path
(
issue
.
project
,
issue
)
project_issue_path
(
issue
.
project
,
issue
)
end
end
...
...
spec/javascripts/issue_show/components/app_spec.js
View file @
9e820ae3
...
@@ -34,7 +34,6 @@ describe('Issuable output', () => {
...
@@ -34,7 +34,6 @@ describe('Issuable output', () => {
propsData
:
{
propsData
:
{
canUpdate
:
true
,
canUpdate
:
true
,
canDestroy
:
true
,
canDestroy
:
true
,
canMove
:
true
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/realtime_changes
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/realtime_changes
'
,
issuableRef
:
'
#1
'
,
issuableRef
:
'
#1
'
,
initialTitleHtml
:
''
,
initialTitleHtml
:
''
,
...
@@ -43,7 +42,6 @@ describe('Issuable output', () => {
...
@@ -43,7 +42,6 @@ describe('Issuable output', () => {
initialDescriptionText
:
''
,
initialDescriptionText
:
''
,
markdownPreviewPath
:
'
/
'
,
markdownPreviewPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
projectsAutocompletePath
:
'
/
'
,
isConfidential
:
false
,
isConfidential
:
false
,
projectNamespace
:
'
/
'
,
projectNamespace
:
'
/
'
,
projectPath
:
'
/
'
,
projectPath
:
'
/
'
,
...
@@ -226,7 +224,7 @@ describe('Issuable output', () => {
...
@@ -226,7 +224,7 @@ describe('Issuable output', () => {
});
});
});
});
it
(
'
redirects if
issue is mov
ed
'
,
(
done
)
=>
{
it
(
'
redirects if
returned web_url has chang
ed
'
,
(
done
)
=>
{
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
resolve
({
...
@@ -250,23 +248,6 @@ describe('Issuable output', () => {
...
@@ -250,23 +248,6 @@ describe('Issuable output', () => {
});
});
});
});
it
(
'
does not update issuable if project move confirm is false
'
,
(
done
)
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
false
);
spyOn
(
vm
.
service
,
'
updateIssuable
'
);
vm
.
store
.
formState
.
move_to_project_id
=
1
;
vm
.
updateIssuable
();
setTimeout
(()
=>
{
expect
(
vm
.
service
.
updateIssuable
,
).
not
.
toHaveBeenCalled
();
done
();
});
});
it
(
'
closes form on error
'
,
(
done
)
=>
{
it
(
'
closes form on error
'
,
(
done
)
=>
{
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
{
spyOn
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
{
...
...
spec/javascripts/issue_show/components/fields/project_move_spec.js
deleted
100644 → 0
View file @
e30adf60
import
Vue
from
'
vue
'
;
import
projectMove
from
'
~/issue_show/components/fields/project_move.vue
'
;
describe
(
'
Project move field component
'
,
()
=>
{
let
vm
;
let
formState
;
beforeEach
((
done
)
=>
{
const
Component
=
Vue
.
extend
(
projectMove
);
formState
=
{
move_to_project_id
:
0
,
};
vm
=
new
Component
({
propsData
:
{
formState
,
projectsAutocompletePath
:
'
/autocomplete
'
,
},
}).
$mount
();
Vue
.
nextTick
(
done
);
});
it
(
'
mounts select2 element
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.select2-container
'
),
).
not
.
toBeNull
();
});
it
(
'
updates formState on change
'
,
()
=>
{
$
(
vm
.
$refs
[
'
move-dropdown
'
]).
val
(
2
).
trigger
(
'
change
'
);
expect
(
formState
.
move_to_project_id
,
).
toBe
(
2
);
});
});
spec/javascripts/issue_show/components/form_spec.js
View file @
9e820ae3
...
@@ -12,7 +12,6 @@ describe('Inline edit form component', () => {
...
@@ -12,7 +12,6 @@ describe('Inline edit form component', () => {
vm
=
new
Component
({
vm
=
new
Component
({
propsData
:
{
propsData
:
{
canDestroy
:
true
,
canDestroy
:
true
,
canMove
:
true
,
formState
:
{
formState
:
{
title
:
'
b
'
,
title
:
'
b
'
,
description
:
'
a
'
,
description
:
'
a
'
,
...
@@ -20,7 +19,6 @@ describe('Inline edit form component', () => {
...
@@ -20,7 +19,6 @@ describe('Inline edit form component', () => {
},
},
markdownPreviewPath
:
'
/
'
,
markdownPreviewPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
projectsAutocompletePath
:
'
/
'
,
projectPath
:
'
/
'
,
projectPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
projectNamespace
:
'
/
'
,
},
},
...
...
spec/javascripts/sidebar/mock_data.js
View file @
9e820ae3
...
@@ -66,17 +66,57 @@ const sidebarMockData = {
...
@@ -66,17 +66,57 @@ const sidebarMockData = {
},
},
labels
:
[],
labels
:
[],
},
},
'
/autocomplete/projects?project_id=15
'
:
[
{
'
id
'
:
0
,
'
name_with_namespace
'
:
'
No project
'
,
},
{
'
id
'
:
20
,
'
name_with_namespace
'
:
'
foo / bar
'
,
},
],
},
},
'
PUT
'
:
{
'
PUT
'
:
{
'
/gitlab-org/gitlab-shell/issues/5.json
'
:
{
'
/gitlab-org/gitlab-shell/issues/5.json
'
:
{
data
:
{},
data
:
{},
},
},
},
},
'
POST
'
:
{
'
/gitlab-org/gitlab-shell/issues/5/move
'
:
{
id
:
123
,
iid
:
5
,
author_id
:
1
,
description
:
'
some description
'
,
lock_version
:
5
,
milestone_id
:
null
,
state
:
'
opened
'
,
title
:
'
some title
'
,
updated_by_id
:
1
,
created_at
:
'
2017-06-27T19:54:42.437Z
'
,
updated_at
:
'
2017-08-18T03:39:49.222Z
'
,
deleted_at
:
null
,
time_estimate
:
0
,
total_time_spent
:
0
,
human_time_estimate
:
null
,
human_total_time_spent
:
null
,
branch_name
:
null
,
confidential
:
false
,
assignees
:
[],
due_date
:
null
,
moved_to_id
:
null
,
project_id
:
7
,
milestone
:
null
,
labels
:
[],
web_url
:
'
/root/some-project/issues/5
'
,
},
},
};
};
export
default
{
export
default
{
mediator
:
{
mediator
:
{
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
moveIssueEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/move
'
,
projectsAutocompleteEndpoint
:
'
/autocomplete/projects?project_id=15
'
,
editable
:
true
,
editable
:
true
,
currentUser
:
{
currentUser
:
{
id
:
1
,
id
:
1
,
...
@@ -85,6 +125,7 @@ export default {
...
@@ -85,6 +125,7 @@ export default {
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
},
},
rootPath
:
'
/
'
,
rootPath
:
'
/
'
,
fullPath
:
'
/gitlab-org/gitlab-shell
'
,
},
},
time
:
{
time
:
{
time_estimate
:
3600
,
time_estimate
:
3600
,
...
...
spec/javascripts/sidebar/sidebar_mediator_spec.js
View file @
9e820ae3
...
@@ -30,7 +30,7 @@ describe('Sidebar mediator', () => {
...
@@ -30,7 +30,7 @@ describe('Sidebar mediator', () => {
expect
(
resp
.
status
).
toEqual
(
200
);
expect
(
resp
.
status
).
toEqual
(
200
);
done
();
done
();
})
})
.
catch
(
()
=>
{}
);
.
catch
(
done
.
fail
);
});
});
it
(
'
fetches the data
'
,
()
=>
{
it
(
'
fetches the data
'
,
()
=>
{
...
@@ -38,4 +38,42 @@ describe('Sidebar mediator', () => {
...
@@ -38,4 +38,42 @@ describe('Sidebar mediator', () => {
this
.
mediator
.
fetch
();
this
.
mediator
.
fetch
();
expect
(
this
.
mediator
.
service
.
get
).
toHaveBeenCalled
();
expect
(
this
.
mediator
.
service
.
get
).
toHaveBeenCalled
();
});
});
it
(
'
sets moveToProjectId
'
,
()
=>
{
const
projectId
=
7
;
spyOn
(
this
.
mediator
.
store
,
'
setMoveToProjectId
'
).
and
.
callThrough
();
this
.
mediator
.
setMoveToProjectId
(
projectId
);
expect
(
this
.
mediator
.
store
.
setMoveToProjectId
).
toHaveBeenCalledWith
(
projectId
);
});
it
(
'
fetches autocomplete projects
'
,
(
done
)
=>
{
const
searchTerm
=
'
foo
'
;
spyOn
(
this
.
mediator
.
service
,
'
getProjectsAutocomplete
'
).
and
.
callThrough
();
spyOn
(
this
.
mediator
.
store
,
'
setAutocompleteProjects
'
).
and
.
callThrough
();
this
.
mediator
.
fetchAutocompleteProjects
(
searchTerm
)
.
then
(()
=>
{
expect
(
this
.
mediator
.
service
.
getProjectsAutocomplete
).
toHaveBeenCalledWith
(
searchTerm
);
expect
(
this
.
mediator
.
store
.
setAutocompleteProjects
).
toHaveBeenCalled
();
done
();
})
.
catch
(
done
.
fail
);
});
it
(
'
moves issue
'
,
(
done
)
=>
{
const
moveToProjectId
=
7
;
this
.
mediator
.
store
.
setMoveToProjectId
(
moveToProjectId
);
spyOn
(
this
.
mediator
.
service
,
'
moveIssue
'
).
and
.
callThrough
();
spyOn
(
gl
.
utils
,
'
visitUrl
'
);
this
.
mediator
.
moveIssue
()
.
then
(()
=>
{
expect
(
this
.
mediator
.
service
.
moveIssue
).
toHaveBeenCalledWith
(
moveToProjectId
);
expect
(
gl
.
utils
.
visitUrl
).
toHaveBeenCalledWith
(
'
/root/some-project/issues/5
'
);
done
();
})
.
catch
(
done
.
fail
);
});
});
});
spec/javascripts/sidebar/sidebar_move_issue_spec.js
0 → 100644
View file @
9e820ae3
import
Vue
from
'
vue
'
;
import
SidebarMediator
from
'
~/sidebar/sidebar_mediator
'
;
import
SidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
import
SidebarService
from
'
~/sidebar/services/sidebar_service
'
;
import
SidebarMoveIssue
from
'
~/sidebar/lib/sidebar_move_issue
'
;
import
Mock
from
'
./mock_data
'
;
describe
(
'
SidebarMoveIssue
'
,
()
=>
{
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
Mock
.
sidebarMockInterceptor
);
this
.
mediator
=
new
SidebarMediator
(
Mock
.
mediator
);
this
.
$content
=
$
(
`
<div class="dropdown">
<div class="js-toggle"></div>
<div class="dropdown-content"></div>
<div class="js-confirm-button"></div>
</div>
`
);
this
.
$toggleButton
=
this
.
$content
.
find
(
'
.js-toggle
'
);
this
.
$confirmButton
=
this
.
$content
.
find
(
'
.js-confirm-button
'
);
this
.
sidebarMoveIssue
=
new
SidebarMoveIssue
(
this
.
mediator
,
this
.
$toggleButton
,
this
.
$confirmButton
,
);
this
.
sidebarMoveIssue
.
init
();
});
afterEach
(()
=>
{
SidebarService
.
singleton
=
null
;
SidebarStore
.
singleton
=
null
;
SidebarMediator
.
singleton
=
null
;
this
.
sidebarMoveIssue
.
destroy
();
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
Mock
.
sidebarMockInterceptor
);
});
describe
(
'
init
'
,
()
=>
{
it
(
'
should initialize the dropdown and listeners
'
,
()
=>
{
spyOn
(
this
.
sidebarMoveIssue
,
'
initDropdown
'
);
spyOn
(
this
.
sidebarMoveIssue
,
'
addEventListeners
'
);
this
.
sidebarMoveIssue
.
init
();
expect
(
this
.
sidebarMoveIssue
.
initDropdown
).
toHaveBeenCalled
();
expect
(
this
.
sidebarMoveIssue
.
addEventListeners
).
toHaveBeenCalled
();
});
});
describe
(
'
destroy
'
,
()
=>
{
it
(
'
should remove the listeners
'
,
()
=>
{
spyOn
(
this
.
sidebarMoveIssue
,
'
removeEventListeners
'
);
this
.
sidebarMoveIssue
.
destroy
();
expect
(
this
.
sidebarMoveIssue
.
removeEventListeners
).
toHaveBeenCalled
();
});
});
describe
(
'
initDropdown
'
,
()
=>
{
it
(
'
should initialize the gl_dropdown
'
,
()
=>
{
spyOn
(
$
.
fn
,
'
glDropdown
'
);
this
.
sidebarMoveIssue
.
initDropdown
();
expect
(
$
.
fn
.
glDropdown
).
toHaveBeenCalled
();
});
});
describe
(
'
onConfirmClicked
'
,
()
=>
{
it
(
'
should move the issue with valid project ID
'
,
()
=>
{
spyOn
(
this
.
mediator
,
'
moveIssue
'
).
and
.
returnValue
(
Promise
.
resolve
());
this
.
mediator
.
setMoveToProjectId
(
7
);
this
.
sidebarMoveIssue
.
onConfirmClicked
();
expect
(
this
.
mediator
.
moveIssue
).
toHaveBeenCalled
();
expect
(
this
.
$confirmButton
.
attr
(
'
disabled
'
)).
toBe
(
'
disabled
'
);
expect
(
this
.
$confirmButton
.
hasClass
(
'
is-loading
'
)).
toBe
(
true
);
});
it
(
'
should remove loading state from confirm button on failure
'
,
(
done
)
=>
{
spyOn
(
window
,
'
Flash
'
);
spyOn
(
this
.
mediator
,
'
moveIssue
'
).
and
.
returnValue
(
Promise
.
reject
());
this
.
mediator
.
setMoveToProjectId
(
7
);
this
.
sidebarMoveIssue
.
onConfirmClicked
();
expect
(
this
.
mediator
.
moveIssue
).
toHaveBeenCalled
();
// Wait for the move issue request to fail
setTimeout
(()
=>
{
expect
(
window
.
Flash
).
toHaveBeenCalled
();
expect
(
this
.
$confirmButton
.
attr
(
'
disabled
'
)).
toBe
(
undefined
);
expect
(
this
.
$confirmButton
.
hasClass
(
'
is-loading
'
)).
toBe
(
false
);
done
();
});
});
it
(
'
should not move the issue with id=0
'
,
()
=>
{
spyOn
(
this
.
mediator
,
'
moveIssue
'
);
this
.
mediator
.
setMoveToProjectId
(
0
);
this
.
sidebarMoveIssue
.
onConfirmClicked
();
expect
(
this
.
mediator
.
moveIssue
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
should set moveToProjectId on dropdown item "No project" click
'
,
(
done
)
=>
{
spyOn
(
this
.
mediator
,
'
setMoveToProjectId
'
);
// Open the dropdown
this
.
$toggleButton
.
dropdown
(
'
toggle
'
);
// Wait for the autocomplete request to finish
setTimeout
(()
=>
{
this
.
$content
.
find
(
'
.js-move-issue-dropdown-item
'
).
eq
(
0
).
trigger
(
'
click
'
);
expect
(
this
.
mediator
.
setMoveToProjectId
).
toHaveBeenCalledWith
(
0
);
expect
(
this
.
$confirmButton
.
attr
(
'
disabled
'
)).
toBe
(
'
disabled
'
);
done
();
},
0
);
});
it
(
'
should set moveToProjectId on dropdown item click
'
,
(
done
)
=>
{
spyOn
(
this
.
mediator
,
'
setMoveToProjectId
'
);
// Open the dropdown
this
.
$toggleButton
.
dropdown
(
'
toggle
'
);
// Wait for the autocomplete request to finish
setTimeout
(()
=>
{
this
.
$content
.
find
(
'
.js-move-issue-dropdown-item
'
).
eq
(
1
).
trigger
(
'
click
'
);
expect
(
this
.
mediator
.
setMoveToProjectId
).
toHaveBeenCalledWith
(
20
);
expect
(
this
.
$confirmButton
.
attr
(
'
disabled
'
)).
toBe
(
undefined
);
done
();
},
0
);
});
});
spec/javascripts/sidebar/sidebar_service_spec.js
View file @
9e820ae3
...
@@ -5,7 +5,11 @@ import Mock from './mock_data';
...
@@ -5,7 +5,11 @@ import Mock from './mock_data';
describe
(
'
Sidebar service
'
,
()
=>
{
describe
(
'
Sidebar service
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
Mock
.
sidebarMockInterceptor
);
Vue
.
http
.
interceptors
.
push
(
Mock
.
sidebarMockInterceptor
);
this
.
service
=
new
SidebarService
(
'
/gitlab-org/gitlab-shell/issues/5.json
'
);
this
.
service
=
new
SidebarService
({
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
moveIssueEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/move
'
,
projectsAutocompleteEndpoint
:
'
/autocomplete/projects?project_id=15
'
,
});
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -19,7 +23,7 @@ describe('Sidebar service', () => {
...
@@ -19,7 +23,7 @@ describe('Sidebar service', () => {
expect
(
resp
).
toBeDefined
();
expect
(
resp
).
toBeDefined
();
done
();
done
();
})
})
.
catch
(
()
=>
{}
);
.
catch
(
done
.
fail
);
});
});
it
(
'
updates the data
'
,
(
done
)
=>
{
it
(
'
updates the data
'
,
(
done
)
=>
{
...
@@ -28,6 +32,24 @@ describe('Sidebar service', () => {
...
@@ -28,6 +32,24 @@ describe('Sidebar service', () => {
expect
(
resp
).
toBeDefined
();
expect
(
resp
).
toBeDefined
();
done
();
done
();
})
})
.
catch
(()
=>
{});
.
catch
(
done
.
fail
);
});
it
(
'
gets projects for autocomplete
'
,
(
done
)
=>
{
this
.
service
.
getProjectsAutocomplete
()
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
done
();
})
.
catch
(
done
.
fail
);
});
it
(
'
moves the issue to another project
'
,
(
done
)
=>
{
this
.
service
.
moveIssue
(
123
)
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
done
();
})
.
catch
(
done
.
fail
);
});
});
});
});
spec/javascripts/sidebar/sidebar_store_spec.js
View file @
9e820ae3
...
@@ -82,4 +82,18 @@ describe('Sidebar store', () => {
...
@@ -82,4 +82,18 @@ describe('Sidebar store', () => {
expect
(
this
.
store
.
humanTimeEstimate
).
toEqual
(
Mock
.
time
.
human_time_estimate
);
expect
(
this
.
store
.
humanTimeEstimate
).
toEqual
(
Mock
.
time
.
human_time_estimate
);
expect
(
this
.
store
.
humanTotalTimeSpent
).
toEqual
(
Mock
.
time
.
human_total_time_spent
);
expect
(
this
.
store
.
humanTotalTimeSpent
).
toEqual
(
Mock
.
time
.
human_total_time_spent
);
});
});
it
(
'
set autocomplete projects
'
,
()
=>
{
const
projects
=
[{
id
:
0
}];
this
.
store
.
setAutocompleteProjects
(
projects
);
expect
(
this
.
store
.
autocompleteProjects
).
toEqual
(
projects
);
});
it
(
'
set move to project ID
'
,
()
=>
{
const
projectId
=
7
;
this
.
store
.
setMoveToProjectId
(
projectId
);
expect
(
this
.
store
.
moveToProjectId
).
toEqual
(
projectId
);
});
});
});
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