Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
b16a98d7
Commit
b16a98d7
authored
Sep 04, 2017
by
Eric Eastwood
Committed by
Phil Hughes
Sep 04, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move "Move issue" to sidebar - EE merge edition
parent
4f71dc12
Changes
54
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
54 changed files
with
839 additions
and
430 deletions
+839
-430
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/services/issuable_base_service.rb
app/services/issuable_base_service.rb
+1
-0
app/services/issues/update_service.rb
app/services/issues/update_service.rb
+12
-1
app/services/quick_actions/interpret_service.rb
app/services/quick_actions/interpret_service.rb
+18
-0
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
changelogs/unreleased/move-action.yml
changelogs/unreleased/move-action.yml
+4
-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
doc/user/project/quick_actions.md
doc/user/project/quick_actions.md
+1
-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/features/issues/user_uses_slash_commands_spec.rb
spec/features/issues/user_uses_slash_commands_spec.rb
+109
-0
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
spec/services/issues/update_service_spec.rb
spec/services/issues/update_service_spec.rb
+20
-0
spec/services/quick_actions/interpret_service_spec.rb
spec/services/quick_actions/interpret_service_spec.rb
+10
-0
No files found.
app/assets/javascripts/gl_dropdown.js
View file @
b16a98d7
...
...
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
GitLabDropdown
.
prototype
.
shouldPropagate
=
function
(
e
)
{
var
$target
;
if
(
this
.
options
.
multiSelect
)
{
if
(
this
.
options
.
multiSelect
||
this
.
options
.
shouldPropagate
===
false
)
{
$target
=
$
(
e
.
target
);
if
(
$target
&&
!
$target
.
hasClass
(
'
dropdown-menu-close
'
)
&&
!
$target
.
hasClass
(
'
dropdown-menu-close-icon
'
)
&&
...
...
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
};
GitLabDropdown
.
prototype
.
positionMenuAbove
=
function
()
{
var
$button
=
$
(
this
.
el
);
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
)
{
...
...
@@ -713,7 +713,7 @@ GitLabDropdown = (function() {
GitLabDropdown
.
prototype
.
noResults
=
function
()
{
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
)
{
...
...
app/assets/javascripts/issuable_form.js
View file @
b16a98d7
...
...
@@ -11,8 +11,6 @@ import ZenMode from './zen_mode';
(
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
;
function
IssuableForm
(
form
)
{
...
...
@@ -28,7 +26,6 @@ import ZenMode from './zen_mode';
new
ZenMode
();
this
.
titleField
=
this
.
form
.
find
(
"
input[name*='[title]']
"
);
this
.
descriptionField
=
this
.
form
.
find
(
"
textarea[name*='[description]']
"
);
this
.
issueMoveField
=
this
.
form
.
find
(
"
#move_to_project_id
"
);
if
(
!
(
this
.
titleField
.
length
&&
this
.
descriptionField
.
length
))
{
return
;
}
...
...
@@ -36,7 +33,6 @@ import ZenMode from './zen_mode';
this
.
form
.
on
(
"
submit
"
,
this
.
handleSubmit
);
this
.
form
.
on
(
"
click
"
,
"
.btn-cancel
"
,
this
.
resetAutosave
);
this
.
initWip
();
this
.
initMoveDropdown
();
$issuableDueDate
=
$
(
'
#issuable-due-date
'
);
if
(
$issuableDueDate
.
length
)
{
calendar
=
new
Pikaday
({
...
...
@@ -58,12 +54,6 @@ import ZenMode from './zen_mode';
};
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
();
};
...
...
@@ -115,48 +105,6 @@ import ZenMode from './zen_mode';
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
;
})();
}).
call
(
window
);
app/assets/javascripts/issue_show/components/app.vue
View file @
b16a98d7
...
...
@@ -17,10 +17,6 @@ export default {
required
:
true
,
type
:
String
,
},
canMove
:
{
required
:
true
,
type
:
Boolean
,
},
canUpdate
:
{
required
:
true
,
type
:
Boolean
,
...
...
@@ -96,10 +92,6 @@ export default {
type
:
String
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
const
store
=
new
Store
({
...
...
@@ -142,7 +134,6 @@ export default {
confidential
:
this
.
isConfidential
,
description
:
this
.
state
.
descriptionText
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
});
}
...
...
@@ -151,16 +142,6 @@ export default {
this
.
showForm
=
false
;
},
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
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
...
...
@@ -239,14 +220,12 @@ export default {
<form-component
v-if=
"canUpdate && showForm"
:form-state=
"formState"
:can-move=
"canMove"
:can-destroy=
"canDestroy"
:issuable-templates=
"issuableTemplates"
:markdown-docs=
"markdownDocs"
:markdown-preview-url=
"markdownPreviewUrl"
:project-path=
"projectPath"
:project-namespace=
"projectNamespace"
:projects-autocomplete-url=
"projectsAutocompleteUrl"
/>
<div
v-else
>
<title-component
...
...
app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted
100644 → 0
View file @
4f71dc12
<
script
>
import
tooltip
from
'
../../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
},
props
:
{
formState
:
{
type
:
Object
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
mounted
()
{
const
$moveDropdown
=
$
(
this
.
$refs
[
'
move-dropdown
'
]);
$moveDropdown
.
select2
({
ajax
:
{
url
:
this
.
projectsAutocompleteUrl
,
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 @
b16a98d7
...
...
@@ -4,15 +4,10 @@
import
descriptionField
from
'
./fields/description.vue
'
;
import
editActions
from
'
./edit_actions.vue
'
;
import
descriptionTemplate
from
'
./fields/description_template.vue
'
;
import
projectMove
from
'
./fields/project_move.vue
'
;
import
confidentialCheckbox
from
'
./fields/confidential_checkbox.vue
'
;
export
default
{
props
:
{
canMove
:
{
type
:
Boolean
,
required
:
true
,
},
canDestroy
:
{
type
:
Boolean
,
required
:
true
,
...
...
@@ -42,10 +37,6 @@
type
:
String
,
required
:
true
,
},
projectsAutocompleteUrl
:
{
type
:
String
,
required
:
true
,
},
},
components
:
{
lockedWarning
,
...
...
@@ -53,7 +44,6 @@
descriptionField
,
descriptionTemplate
,
editActions
,
projectMove
,
confidentialCheckbox
,
},
computed
:
{
...
...
@@ -93,10 +83,6 @@
:markdown-docs=
"markdownDocs"
/>
<confidential-checkbox
:form-state=
"formState"
/>
<project-move
v-if=
"canMove"
:form-state=
"formState"
:projects-autocomplete-url=
"projectsAutocompleteUrl"
/>
<edit-actions
:form-state=
"formState"
:can-destroy=
"canDestroy"
/>
...
...
app/assets/javascripts/issue_show/index.js
View file @
b16a98d7
...
...
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props
:
{
canUpdate
:
this
.
canUpdate
,
canDestroy
:
this
.
canDestroy
,
canMove
:
this
.
canMove
,
endpoint
:
this
.
endpoint
,
issuableRef
:
this
.
issuableRef
,
initialTitleHtml
:
this
.
initialTitleHtml
,
...
...
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
markdownDocs
:
this
.
markdownDocs
,
projectPath
:
this
.
projectPath
,
projectNamespace
:
this
.
projectNamespace
,
projectsAutocompleteUrl
:
this
.
projectsAutocompleteUrl
,
updatedAt
:
this
.
updatedAt
,
updatedByName
:
this
.
updatedByName
,
updatedByPath
:
this
.
updatedByPath
,
...
...
app/assets/javascripts/issue_show/stores/index.js
View file @
b16a98d7
...
...
@@ -6,7 +6,6 @@ export default class Store {
confidential
:
false
,
description
:
''
,
lockedWarningVisible
:
false
,
move_to_project_id
:
0
,
updateLoading
:
false
,
};
}
...
...
app/assets/javascripts/right_sidebar.js
View file @
b16a98d7
...
...
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar
.
prototype
.
openDropdown
=
function
(
blockOrName
)
{
var
$block
;
$block
=
_
.
isString
(
blockOrName
)
?
this
.
getBlock
(
blockOrName
)
:
blockOrName
;
$block
.
find
(
'
.edit-link
'
).
trigger
(
'
click
'
);
if
(
!
this
.
isOpen
())
{
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
)
{
...
...
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
View file @
b16a98d7
...
...
@@ -36,7 +36,7 @@ export default {
/>
<a
v-if="editable"
class="edit-link pull-right"
class="
js-sidebar-dropdown-toggle
edit-link pull-right"
href="#"
>
Edit
...
...
app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
0 → 100644
View file @
b16a98d7
/* 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 @
b16a98d7
...
...
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue
.
use
(
VueResource
);
export
default
class
SidebarService
{
constructor
(
endpoint
)
{
constructor
(
endpoint
Map
)
{
if
(
!
SidebarService
.
singleton
)
{
this
.
endpoint
=
endpoint
;
this
.
endpoint
=
endpointMap
.
endpoint
;
this
.
moveIssueEndpoint
=
endpointMap
.
moveIssueEndpoint
;
this
.
projectsAutocompleteEndpoint
=
endpointMap
.
projectsAutocompleteEndpoint
;
SidebarService
.
singleton
=
this
;
}
...
...
@@ -25,4 +27,18 @@ export default class SidebarService {
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 @
b16a98d7
...
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
import
sidebarTimeTracking
from
'
./components/time_tracking/sidebar_time_tracking
'
;
import
sidebarAssignees
from
'
./components/assignees/sidebar_assignees
'
;
import
confidential
from
'
./components/confidential/confidential_issue_sidebar.vue
'
;
import
SidebarMoveIssue
from
'
./lib/sidebar_move_issue
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
...
...
@@ -31,6 +32,12 @@ function domContentLoaded() {
service
:
mediator
.
service
,
},
}).
$mount
(
confidentialEl
);
new
SidebarMoveIssue
(
mediator
,
$
(
'
.js-move-issue
'
),
$
(
'
.js-move-issue-confirmation-button
'
),
).
init
();
}
new
Vue
(
sidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
...
...
app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
b16a98d7
...
...
@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor
(
options
)
{
if
(
!
SidebarMediator
.
singleton
)
{
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
;
}
...
...
@@ -26,6 +30,10 @@ export default class SidebarMediator {
return
this
.
service
.
update
(
field
,
selected
.
length
===
0
?
[
0
]
:
selected
);
}
setMoveToProjectId
(
projectId
)
{
this
.
store
.
setMoveToProjectId
(
projectId
);
}
fetch
()
{
this
.
service
.
get
()
.
then
(
response
=>
response
.
json
())
...
...
@@ -35,4 +43,23 @@ export default class SidebarMediator {
})
.
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 @
b16a98d7
...
...
@@ -13,6 +13,8 @@ export default class SidebarStore {
this
.
isFetching
=
{
assignees
:
true
,
};
this
.
autocompleteProjects
=
[];
this
.
moveToProjectId
=
0
;
SidebarStore
.
singleton
=
this
;
}
...
...
@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees
()
{
this
.
assignees
=
[];
}
setAutocompleteProjects
(
projects
)
{
this
.
autocompleteProjects
=
projects
;
}
setMoveToProjectId
(
moveToProjectId
)
{
this
.
moveToProjectId
=
moveToProjectId
;
}
}
app/assets/stylesheets/framework/dropdowns.scss
View file @
b16a98d7
...
...
@@ -193,7 +193,7 @@
min-width
:
240px
;
max-width
:
500px
;
margin-top
:
2px
;
margin-bottom
:
0
;
margin-bottom
:
2px
;
font-size
:
14px
;
font-weight
:
$gl-font-weight-normal
;
padding
:
8px
0
;
...
...
@@ -618,6 +618,11 @@
border-top
:
1px
solid
$dropdown-divider-color
;
}
.dropdown-footer-content
{
padding-left
:
10px
;
padding-right
:
10px
;
}
.dropdown-due-date-footer
{
padding-top
:
0
;
margin-left
:
10px
;
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
b16a98d7
...
...
@@ -473,7 +473,7 @@
padding-top
:
6px
;
}
.
open
.
dropdown-menu
{
.dropdown-menu
{
width
:
100%
;
}
}
...
...
@@ -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
{
padding
:
16px
0
;
...
...
app/controllers/autocomplete_controller.rb
View file @
b16a98d7
...
...
@@ -45,12 +45,6 @@ class AutocompleteController < ApplicationController
project
=
Project
.
find_by_id
(
params
[
:project_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
)
end
...
...
app/controllers/projects/issues_controller.rb
View file @
b16a98d7
...
...
@@ -17,7 +17,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action
:authorize_create_issue!
,
only:
[
:new
,
:create
]
# 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
before_action
:authorize_create_merge_request!
,
only:
[
:create_merge_request
]
...
...
@@ -131,25 +131,33 @@ class Projects::IssuesController < Projects::ApplicationController
@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
new_project
=
Project
.
find
(
params
[
:move_to_project_id
])
return
render_404
unless
issue
.
can_move?
(
current_user
,
new_project
)
move_service
=
Issues
::
MoveService
.
new
(
project
,
current_user
)
@issue
=
move_service
.
execute
(
@issue
,
new_project
)
@issue
=
Issues
::
UpdateService
.
new
(
project
,
current_user
,
target_project:
new_project
).
execute
(
issue
)
end
respond_to
do
|
format
|
format
.
html
do
recaptcha_check_with_fallback
{
render
:edit
}
end
format
.
json
do
if
@issue
.
valid?
render
json:
IssueSerializer
.
new
.
represent
(
@issue
)
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
render_issue_json
end
end
...
...
@@ -260,6 +268,14 @@ class Projects::IssuesController < Projects::ApplicationController
return
render_404
unless
@project
.
feature_available?
(
:issues
,
current_user
)
end
def
render_issue_json
if
@issue
.
valid?
render
json:
IssueSerializer
.
new
.
represent
(
@issue
)
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
end
def
issue_params
params
.
require
(
:issue
).
permit
(
*
issue_params_attributes
)
end
...
...
app/helpers/dropdowns_helper.rb
View file @
b16a98d7
...
...
@@ -106,9 +106,11 @@ module DropdownsHelper
end
end
def
dropdown_footer
(
&
block
)
def
dropdown_footer
(
add_content_class:
false
,
&
block
)
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
)
end
end
...
...
app/helpers/issuables_helper.rb
View file @
b16a98d7
...
...
@@ -215,12 +215,10 @@ module IssuablesHelper
endpoint:
project_issue_path
(
@project
,
issuable
),
canUpdate:
can?
(
current_user
,
:update_issue
,
issuable
),
canDestroy:
can?
(
current_user
,
:destroy_issue
,
issuable
),
canMove:
current_user
?
issuable
.
can_move?
(
current_user
)
:
false
,
issuableRef:
issuable
.
to_reference
,
isConfidential:
issuable
.
confidential
,
markdownPreviewUrl:
preview_markdown_path
(
@project
),
markdownDocs:
help_page_path
(
'user/markdown'
),
projectsAutocompleteUrl:
autocomplete_projects_path
(
project_id:
@project
.
id
),
issuableTemplates:
issuable_templates
(
issuable
),
projectPath:
ref_project
.
path
,
projectNamespace:
ref_project
.
namespace
.
full_path
,
...
...
@@ -369,6 +367,8 @@ module IssuablesHelper
def
issuable_sidebar_options
(
issuable
,
can_edit_issuable
)
{
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
,
currentUser:
current_user
.
as_json
(
only:
[
:username
,
:id
,
:name
],
methods: :avatar_url
),
rootPath:
root_path
,
...
...
app/services/issuable_base_service.rb
View file @
b16a98d7
...
...
@@ -58,6 +58,7 @@ class IssuableBaseService < BaseService
params
.
delete
(
:assignee_id
)
params
.
delete
(
:due_date
)
params
.
delete
(
:canonical_issue_id
)
params
.
delete
(
:project
)
end
filter_assignee
(
issuable
)
...
...
app/services/issues/update_service.rb
View file @
b16a98d7
...
...
@@ -6,7 +6,7 @@ module Issues
handle_move_between_iids
(
issue
)
filter_spam_check_params
change_issue_duplicate
(
issue
)
update
(
issue
)
move_issue_to_new_project
(
issue
)
||
update
(
issue
)
end
def
before_update
(
issue
)
...
...
@@ -74,6 +74,17 @@ module Issues
end
end
def
move_issue_to_new_project
(
issue
)
target_project
=
params
.
delete
(
:target_project
)
return
unless
target_project
&&
issue
.
can_move?
(
current_user
,
target_project
)
&&
target_project
!=
issue
.
project
update
(
issue
)
Issues
::
MoveService
.
new
(
project
,
current_user
).
execute
(
issue
,
target_project
)
end
private
def
get_issue_if_allowed
(
project
,
iid
)
...
...
app/services/quick_actions/interpret_service.rb
View file @
b16a98d7
...
...
@@ -506,6 +506,24 @@ module QuickActions
end
end
desc
'Move this issue to another project.'
explanation
do
|
path_to_project
|
"Moves this issue to
#{
path_to_project
}
."
end
params
'path/to/project'
condition
do
issuable
.
is_a?
(
Issue
)
&&
issuable
.
persisted?
&&
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
command
:move
do
|
target_project_path
|
target_project
=
Project
.
find_by_full_path
(
target_project_path
)
if
target_project
.
present?
@updates
[
:target_project
]
=
target_project
end
end
def
extract_users
(
params
)
return
[]
if
params
.
nil?
...
...
app/views/projects/boards/components/sidebar/_due_date.html.haml
View file @
b16a98d7
...
...
@@ -3,7 +3,7 @@
Due date
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
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-content
%span
.no-value
{
"v-if"
=>
"!issue.dueDate"
}
...
...
app/views/projects/boards/components/sidebar/_labels.html.haml
View file @
b16a98d7
...
...
@@ -3,7 +3,7 @@
Labels
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
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
%span
.no-value
{
"v-if"
=>
"issue.labels && issue.labels.length === 0"
}
None
...
...
app/views/projects/boards/components/sidebar/_milestone.html.haml
View file @
b16a98d7
...
...
@@ -3,7 +3,7 @@
Milestone
-
if
can?
(
current_user
,
:admin_issue
,
@project
)
=
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
%span
.no-value
{
"v-if"
=>
"!issue.milestone"
}
None
...
...
app/views/shared/icons/_icon_arrow_right.svg.erb
0 → 100644
View file @
b16a98d7
<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 @
b16a98d7
...
...
@@ -29,18 +29,6 @@
=
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/approvals'
,
issuable:
issuable
,
form:
form
=
render
'shared/issuable/form/branch_chooser'
,
issuable:
issuable
,
form:
form
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
b16a98d7
...
...
@@ -34,7 +34,7 @@
Milestone
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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
-
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
}
...
...
@@ -60,7 +60,7 @@
Due date
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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
%span
.value-content
-
if
issuable
.
due_date
...
...
@@ -95,7 +95,7 @@
Labels
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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?
)
}
-
if
selected_labels
.
any?
-
selected_labels
.
each
do
|
label
|
...
...
@@ -168,5 +168,22 @@
%cite
{
title:
project_ref
}
=
project_ref
=
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
app/views/shared/issuable/_sidebar_assignees.html.haml
View file @
b16a98d7
...
...
@@ -13,7 +13,7 @@
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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
%a
.gutter-toggle.pull-right.js-sidebar-toggle
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
}
=
sidebar_gutter_toggle_icon
...
...
app/views/shared/issuable/form/_issue_assignee.html.haml
View file @
b16a98d7
...
...
@@ -11,7 +11,7 @@
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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
-
if
assignees
.
any?
-
assignees
.
each
do
|
assignee
|
...
...
app/views/shared/issuable/form/_merge_request_assignee.html.haml
View file @
b16a98d7
...
...
@@ -9,7 +9,7 @@
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
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
-
if
merge_request
.
assignee
=
link_to_member
(
@project
,
merge_request
.
assignee
,
size:
32
,
extra_class:
'bold'
)
do
...
...
app/views/shared/milestones/_sidebar.html.haml
View file @
b16a98d7
...
...
@@ -21,7 +21,7 @@
.title
Start date
-
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
%span
.value-content
-
if
milestone
.
start_date
...
...
@@ -51,7 +51,7 @@
.title.hide-collapsed
Due date
-
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
%span
.value-content
-
if
milestone
.
due_date
...
...
changelogs/unreleased/34261-move-move-to-sidebar.yml
0 → 100644
View file @
b16a98d7
---
title
:
Move "Move issue" controls to right-sidebar
merge_request
:
author
:
type
:
changed
changelogs/unreleased/move-action.yml
0 → 100644
View file @
b16a98d7
---
title
:
Allow users to move issues to other projects using a / command
merge_request
:
13436
author
:
Manolis Mavrofidis
config/routes/project.rb
View file @
b16a98d7
...
...
@@ -339,6 +339,7 @@ constraints(ProjectUrlConstrainer.new) do
member
do
post
:toggle_subscription
post
:mark_as_spam
post
:move
get
:referenced_merge_requests
get
:related_branches
get
:can_create_branch
...
...
doc/user/project/issues/img/sidebar_move_issue.png
0 → 100644
View file @
b16a98d7
53.2 KB
doc/user/project/issues/index.md
View file @
b16a98d7
...
...
@@ -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.
## Moving issues
Read through the
[
documentation on moving issues
](
moving_issues.md
)
.
## Create a merge request from an issue
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 @
b16a98d7
# 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
)
doc/user/project/quick_actions.md
View file @
b16a98d7
...
...
@@ -41,6 +41,7 @@ do.
|
`/clear_weight`
| Clears the issue weight |
|
`/board_move ~column`
| Move issue to column on the board |
|
`/duplicate #issue`
| Closes this issue and marks it as a duplicate of another issue |
|
`/move path/to/project`
| Moves issue to another project |
Note: In GitLab EES every issue can have more than one assignee, so commands
`/assign`
,
`/unassign`
and
`/reassign`
support multiple assignees.
spec/controllers/autocomplete_controller_spec.rb
View file @
b16a98d7
...
...
@@ -241,13 +241,10 @@ describe AutocompleteController do
it
'returns projects'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
(
2
)
expect
(
json_response
.
first
[
'id'
]).
to
eq
(
0
)
expect
(
json_response
.
first
[
'name_with_namespace'
]).
to
eq
'No project'
expect
(
json_response
.
size
).
to
eq
(
1
)
expect
(
json_response
.
la
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
[
'id'
]).
to
eq
authorized_project
.
id
expect
(
json_response
.
fir
st
[
'name_with_namespace'
]).
to
eq
authorized_project
.
name_with_namespace
end
end
end
...
...
@@ -265,10 +262,10 @@ describe AutocompleteController do
it
'returns projects'
do
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
.
la
st
[
'name_with_namespace'
]).
to
eq
authorized_search_project
.
name_with_namespace
expect
(
json_response
.
fir
st
[
'id'
]).
to
eq
authorized_search_project
.
id
expect
(
json_response
.
fir
st
[
'name_with_namespace'
]).
to
eq
authorized_search_project
.
name_with_namespace
end
end
end
...
...
@@ -292,7 +289,7 @@ describe AutocompleteController do
it
'returns projects'
do
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
...
...
@@ -312,9 +309,9 @@ describe AutocompleteController do
get
(
:projects
,
project_id:
project
.
id
,
offset_id:
authorized_project
.
id
)
end
it
'returns
"No project"
'
do
expect
(
json_response
.
detect
{
|
item
|
item
[
'id'
]
==
0
}).
to
be_nil
# 'No project' is not there
expect
(
json_response
.
detect
{
|
item
|
item
[
'id'
]
==
authorized_project
.
id
}).
to
be_nil
# Offset project is not there either
it
'returns
projects
'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
2
# Of a total of 3
end
end
end
...
...
@@ -331,10 +328,9 @@ describe AutocompleteController do
get
(
:projects
,
project_id:
project
.
id
)
end
it
'returns
a single "No project"
'
do
it
'returns
no projects
'
do
expect
(
json_response
).
to
be_kind_of
(
Array
)
expect
(
json_response
.
size
).
to
eq
(
1
)
# 'No project'
expect
(
json_response
.
first
[
'id'
]).
to
eq
0
expect
(
json_response
.
size
).
to
eq
(
0
)
end
end
end
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
b16a98d7
This diff is collapsed.
Click to expand it.
spec/features/issues/move_spec.rb
View file @
b16a98d7
...
...
@@ -15,11 +15,11 @@ feature 'issue move to another project' do
background
do
old_project
.
team
<<
[
user
,
:guest
]
edit_issue
(
issue
)
visit
issue_path
(
issue
)
end
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
...
...
@@ -34,12 +34,14 @@ feature 'issue move to another project' do
old_project
.
team
<<
[
user
,
:reporter
]
new_project
.
team
<<
[
user
,
:reporter
]
edit_issue
(
issue
)
visit
issue_path
(
issue
)
end
scenario
'moving issue to another project'
,
js:
true
do
find
(
'#issuable-move'
,
visible:
false
).
set
(
new_project
.
id
)
click_button
(
'Save changes'
)
find
(
'.js-move-issue'
).
trigger
(
'click'
)
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
(
"moved from
#{
cross_reference
}#{
issue
.
to_reference
}
"
)
...
...
@@ -50,13 +52,12 @@ feature 'issue move to another project' do
scenario
'searching project dropdown'
,
js:
true
do
new_project_search
.
team
<<
[
user
,
:reporter
]
page
.
within
'.detail-page-description'
do
first
(
'.select2-choice'
).
click
end
find
(
'.js-move-issue'
).
trigger
(
'click'
)
wait_for_requests
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
).
not_to
have_content
(
new_project
.
name
)
end
...
...
@@ -68,10 +69,10 @@ feature 'issue move to another project' do
background
{
another_project
.
team
<<
[
user
,
:guest
]
}
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
expect
(
page
).
to
have_content
'No project'
page
.
within
'.js-sidebar-move-issue-block'
do
expect
(
page
).
to
have_content
new_project
.
name_with_namespace
end
end
...
...
@@ -89,11 +90,6 @@ feature 'issue move to another project' do
end
end
def
edit_issue
(
issue
)
visit
issue_path
(
issue
)
page
.
within
(
'.issuable-actions'
)
{
first
(
:link
,
'Edit'
).
click
}
end
def
issue_path
(
issue
)
project_issue_path
(
issue
.
project
,
issue
)
end
...
...
spec/features/issues/user_uses_slash_commands_spec.rb
View file @
b16a98d7
...
...
@@ -231,5 +231,114 @@ feature 'Issues > User uses quick actions', js: true do
end
end
end
describe
'move the issue to another project'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
context
'when the project is valid'
,
js:
true
do
let
(
:target_project
)
{
create
(
:project
,
:public
)
}
before
do
target_project
.
team
<<
[
user
,
:master
]
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'moves the issue'
do
write_note
(
"/move
#{
target_project
.
full_path
}
"
)
expect
(
page
).
to
have_content
'Commands applied'
expect
(
issue
.
reload
).
to
be_closed
visit
project_issue_path
(
target_project
,
issue
)
expect
(
page
).
to
have_content
'Issues 1'
end
end
context
'when the project is valid but the user not authorized'
,
js:
true
do
let
(
:project_unauthorized
)
{
create
(
:project
,
:public
)}
before
do
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'does not move the issue'
do
write_note
(
"/move
#{
project_unauthorized
.
full_path
}
"
)
expect
(
page
).
not_to
have_content
'Commands applied'
expect
(
issue
.
reload
).
to
be_open
end
end
context
'when the project is invalid'
,
js:
true
do
before
do
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'does not move the issue'
do
write_note
(
"/move not/valid"
)
expect
(
page
).
not_to
have_content
'Commands applied'
expect
(
issue
.
reload
).
to
be_open
end
end
context
'when the user issues multiple commands'
,
js:
true
do
let
(
:target_project
)
{
create
(
:project
,
:public
)
}
let
(
:milestone
)
{
create
(
:milestone
,
title:
'1.0'
,
project:
project
)
}
let
(
:target_milestone
)
{
create
(
:milestone
,
title:
'1.0'
,
project:
target_project
)
}
let
(
:bug
)
{
create
(
:label
,
project:
project
,
title:
'bug'
)
}
let
(
:wontfix
)
{
create
(
:label
,
project:
project
,
title:
'wontfix'
)
}
let
(
:bug_target
)
{
create
(
:label
,
project:
target_project
,
title:
'bug'
)
}
let
(
:wontfix_target
)
{
create
(
:label
,
project:
target_project
,
title:
'wontfix'
)
}
before
do
target_project
.
team
<<
[
user
,
:master
]
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'applies the commands to both issues and moves the issue'
do
write_note
(
"/label ~
#{
bug
.
title
}
~
#{
wontfix
.
title
}
\n
/milestone %
\"
#{
milestone
.
title
}
\"\n
/move
#{
target_project
.
full_path
}
"
)
expect
(
page
).
to
have_content
'Commands applied'
expect
(
issue
.
reload
).
to
be_closed
visit
project_issue_path
(
target_project
,
issue
)
expect
(
page
).
to
have_content
'bug'
expect
(
page
).
to
have_content
'wontfix'
expect
(
page
).
to
have_content
'1.0'
visit
project_issue_path
(
project
,
issue
)
expect
(
page
).
to
have_content
'Closed'
expect
(
page
).
to
have_content
'bug'
expect
(
page
).
to
have_content
'wontfix'
expect
(
page
).
to
have_content
'1.0'
end
it
'moves the issue and applies the commands to both issues'
do
write_note
(
"/move
#{
target_project
.
full_path
}
\n
/label ~
#{
bug
.
title
}
~
#{
wontfix
.
title
}
\n
/milestone %
\"
#{
milestone
.
title
}
\"
"
)
expect
(
page
).
to
have_content
'Commands applied'
expect
(
issue
.
reload
).
to
be_closed
visit
project_issue_path
(
target_project
,
issue
)
expect
(
page
).
to
have_content
'bug'
expect
(
page
).
to
have_content
'wontfix'
expect
(
page
).
to
have_content
'1.0'
visit
project_issue_path
(
project
,
issue
)
expect
(
page
).
to
have_content
'Closed'
expect
(
page
).
to
have_content
'bug'
expect
(
page
).
to
have_content
'wontfix'
expect
(
page
).
to
have_content
'1.0'
end
end
end
end
end
spec/javascripts/issue_show/components/app_spec.js
View file @
b16a98d7
...
...
@@ -34,7 +34,6 @@ describe('Issuable output', () => {
propsData
:
{
canUpdate
:
true
,
canDestroy
:
true
,
canMove
:
true
,
endpoint
:
'
/gitlab-org/gitlab-shell/issues/9/realtime_changes
'
,
issuableRef
:
'
#1
'
,
initialTitleHtml
:
''
,
...
...
@@ -43,7 +42,6 @@ describe('Issuable output', () => {
initialDescriptionText
:
''
,
markdownPreviewUrl
:
'
/
'
,
markdownDocs
:
'
/
'
,
projectsAutocompleteUrl
:
'
/
'
,
isConfidential
:
false
,
projectNamespace
:
'
/
'
,
projectPath
:
'
/
'
,
...
...
@@ -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
(
vm
.
service
,
'
updateIssuable
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
)
=>
{
resolve
({
...
...
@@ -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
)
=>
{
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
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 @
4f71dc12
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
,
projectsAutocompleteUrl
:
'
/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 @
b16a98d7
...
...
@@ -12,7 +12,6 @@ describe('Inline edit form component', () => {
vm
=
new
Component
({
propsData
:
{
canDestroy
:
true
,
canMove
:
true
,
formState
:
{
title
:
'
b
'
,
description
:
'
a
'
,
...
...
@@ -20,7 +19,6 @@ describe('Inline edit form component', () => {
},
markdownPreviewUrl
:
'
/
'
,
markdownDocs
:
'
/
'
,
projectsAutocompleteUrl
:
'
/
'
,
projectPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
},
...
...
spec/javascripts/sidebar/mock_data.js
View file @
b16a98d7
...
...
@@ -66,17 +66,57 @@ const sidebarMockData = {
},
labels
:
[],
},
'
/autocomplete/projects?project_id=15
'
:
[
{
'
id
'
:
0
,
'
name_with_namespace
'
:
'
No project
'
,
},
{
'
id
'
:
20
,
'
name_with_namespace
'
:
'
foo / bar
'
,
},
],
},
'
PUT
'
:
{
'
/gitlab-org/gitlab-shell/issues/5.json
'
:
{
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
{
mediator
:
{
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
moveIssueEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/move
'
,
projectsAutocompleteEndpoint
:
'
/autocomplete/projects?project_id=15
'
,
editable
:
true
,
currentUser
:
{
id
:
1
,
...
...
@@ -85,6 +125,7 @@ export default {
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
},
rootPath
:
'
/
'
,
fullPath
:
'
/gitlab-org/gitlab-shell
'
,
},
time
:
{
time_estimate
:
3600
,
...
...
spec/javascripts/sidebar/sidebar_mediator_spec.js
View file @
b16a98d7
...
...
@@ -30,7 +30,7 @@ describe('Sidebar mediator', () => {
expect
(
resp
.
status
).
toEqual
(
200
);
done
();
})
.
catch
(
()
=>
{}
);
.
catch
(
done
.
fail
);
});
it
(
'
fetches the data
'
,
()
=>
{
...
...
@@ -38,4 +38,42 @@ describe('Sidebar mediator', () => {
this
.
mediator
.
fetch
();
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 @
b16a98d7
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 @
b16a98d7
...
...
@@ -5,7 +5,11 @@ import Mock from './mock_data';
describe
(
'
Sidebar service
'
,
()
=>
{
beforeEach
(()
=>
{
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
(()
=>
{
...
...
@@ -19,7 +23,7 @@ describe('Sidebar service', () => {
expect
(
resp
).
toBeDefined
();
done
();
})
.
catch
(
()
=>
{}
);
.
catch
(
done
.
fail
);
});
it
(
'
updates the data
'
,
(
done
)
=>
{
...
...
@@ -28,6 +32,24 @@ describe('Sidebar service', () => {
expect
(
resp
).
toBeDefined
();
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 @
b16a98d7
...
...
@@ -82,4 +82,18 @@ describe('Sidebar store', () => {
expect
(
this
.
store
.
humanTimeEstimate
).
toEqual
(
Mock
.
time
.
human_time_estimate
);
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
);
});
});
spec/services/issues/update_service_spec.rb
View file @
b16a98d7
...
...
@@ -510,6 +510,26 @@ describe Issues::UpdateService, :mailer do
end
end
context
'move issue to another project'
do
let
(
:target_project
)
{
create
(
:project
)
}
context
'valid project'
do
before
do
target_project
.
team
<<
[
user
,
:master
]
end
it
'calls the move service with the proper issue and project'
do
move_stub
=
instance_double
(
Issues
::
MoveService
)
allow
(
Issues
::
MoveService
).
to
receive
(
:new
).
and_return
(
move_stub
)
allow
(
move_stub
).
to
receive
(
:execute
).
with
(
issue
,
target_project
).
and_return
(
issue
)
expect
(
move_stub
).
to
receive
(
:execute
).
with
(
issue
,
target_project
)
update_issue
(
target_project:
target_project
)
end
end
end
include_examples
'issuable update service'
do
let
(
:open_issuable
)
{
issue
}
let
(
:closed_issuable
)
{
create
(
:closed_issue
,
project:
project
)
}
...
...
spec/services/quick_actions/interpret_service_spec.rb
View file @
b16a98d7
...
...
@@ -1224,6 +1224,16 @@ describe QuickActions::InterpretService do
end
end
describe
'move issue to another project command'
do
let
(
:content
)
{
'/move test/project'
}
it
'includes the project name'
do
_
,
explanations
=
service
.
explain
(
content
,
issue
)
expect
(
explanations
).
to
eq
([
"Moves this issue to test/project."
])
end
end
# EE-specific tests
describe
'weight command'
do
...
...
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