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
90c60138
Commit
90c60138
authored
Aug 14, 2017
by
Eric Eastwood
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move "Move to different project" to sidebar
Fix
https://gitlab.com/gitlab-org/gitlab-ce/issues/34261
parent
a3af6830
Changes
46
Show 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 @
90c60138
...
...
@@ -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
)
{
...
...
@@ -698,7 +698,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 @
90c60138
...
...
@@ -10,8 +10,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
)
{
...
...
@@ -26,7 +24,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
;
}
...
...
@@ -34,7 +31,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
({
...
...
@@ -56,12 +52,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
();
};
...
...
@@ -113,48 +103,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 @
90c60138
...
...
@@ -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
,
},
projectsAutocompletePath
:
{
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-path=
"markdownDocsPath"
:markdown-preview-path=
"markdownPreviewPath"
:project-path=
"projectPath"
:project-namespace=
"projectNamespace"
:projects-autocomplete-path=
"projectsAutocompletePath"
/>
<div
v-else
>
<title-component
...
...
app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted
100644 → 0
View file @
a3af6830
<
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 @
90c60138
...
...
@@ -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
,
},
projectsAutocompletePath
:
{
type
:
String
,
required
:
true
,
},
},
components
:
{
lockedWarning
,
...
...
@@ -53,7 +44,6 @@
descriptionField
,
descriptionTemplate
,
editActions
,
projectMove
,
confidentialCheckbox
,
},
computed
:
{
...
...
@@ -93,10 +83,6 @@
:markdown-docs-path=
"markdownDocsPath"
/>
<confidential-checkbox
:form-state=
"formState"
/>
<project-move
v-if=
"canMove"
:form-state=
"formState"
:projects-autocomplete-path=
"projectsAutocompletePath"
/>
<edit-actions
:form-state=
"formState"
:can-destroy=
"canDestroy"
/>
...
...
app/assets/javascripts/issue_show/index.js
View file @
90c60138
...
...
@@ -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', () => {
markdownDocsPath
:
this
.
markdownDocsPath
,
projectPath
:
this
.
projectPath
,
projectNamespace
:
this
.
projectNamespace
,
projectsAutocompletePath
:
this
.
projectsAutocompletePath
,
updatedAt
:
this
.
updatedAt
,
updatedByName
:
this
.
updatedByName
,
updatedByPath
:
this
.
updatedByPath
,
...
...
app/assets/javascripts/issue_show/stores/index.js
View file @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
/* 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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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
;
...
...
@@ -622,6 +622,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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -41,12 +41,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 @
90c60138
...
...
@@ -15,7 +15,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
]
...
...
@@ -142,25 +142,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:
serializer
.
represent
(
@issue
)
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
end
render_issue_json
end
end
...
...
@@ -271,6 +279,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:
serializer
.
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 @
90c60138
...
...
@@ -97,9 +97,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 @
90c60138
...
...
@@ -207,12 +207,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
,
markdownPreviewPath:
preview_markdown_path
(
@project
),
markdownDocsPath:
help_page_path
(
'user/markdown'
),
projectsAutocompletePath:
autocomplete_projects_path
(
project_id:
@project
.
id
),
issuableTemplates:
issuable_templates
(
issuable
),
projectPath:
ref_project
.
path
,
projectNamespace:
ref_project
.
namespace
.
full_path
,
...
...
@@ -354,6 +352,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/views/projects/boards/components/sidebar/_due_date.html.haml
View file @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
<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 @
90c60138
...
...
@@ -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/form/branch_chooser'
,
issuable:
issuable
,
form:
form
=
render
'shared/issuable/form/merge_params'
,
issuable:
issuable
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
90c60138
...
...
@@ -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
|
...
...
@@ -141,5 +141,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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
---
title
:
Move "Move issue" controls to right-sidebar
merge_request
:
author
:
type
:
changed
config/routes/project.rb
View file @
90c60138
...
...
@@ -303,6 +303,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 @
90c60138
53.2 KB
doc/user/project/issues/index.md
View file @
90c60138
...
...
@@ -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 @
90c60138
# 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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -233,30 +233,6 @@ describe Projects::IssuesController do
end
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
.
team
<<
[
user
,
:reporter
]
end
it
'moves issue to another project'
do
move_issue
expect
(
response
).
to
have_http_status
:found
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
context
'Akismet is enabled'
do
let
(
:project
)
{
create
(
:project_empty_repo
,
:public
)
}
...
...
@@ -373,7 +349,6 @@ describe Projects::IssuesController do
end
end
end
end
def
update_issue
(
issue_params
=
{},
additional_params
=
{})
params
=
{
...
...
@@ -385,13 +360,45 @@ describe Projects::IssuesController do
put
:update
,
params
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
put
:update
,
post
:move
,
format: :json
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
,
id:
issue
.
iid
,
issue:
{
title:
'New title'
},
move_to_project_id:
another_project
.
id
end
end
...
...
spec/features/issues/move_spec.rb
View file @
90c60138
...
...
@@ -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/javascripts/issue_show/components/app_spec.js
View file @
90c60138
...
...
@@ -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
:
''
,
markdownPreviewPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
projectsAutocompletePath
:
'
/
'
,
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 @
a3af6830
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 @
90c60138
...
...
@@ -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', () => {
},
markdownPreviewPath
:
'
/
'
,
markdownDocsPath
:
'
/
'
,
projectsAutocompletePath
:
'
/
'
,
projectPath
:
'
/
'
,
projectNamespace
:
'
/
'
,
},
...
...
spec/javascripts/sidebar/mock_data.js
View file @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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 @
90c60138
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 @
90c60138
...
...
@@ -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 @
90c60138
...
...
@@ -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
);
});
});
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