Commit 55df9a70 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'functional-sidebar' into 'master'

Updates sidebar to use new dropdowns for issues and merge requests

Fixes #12935, #13164 

See merge request !3175
parents 3385696b 5449296f
...@@ -126,8 +126,10 @@ class GitLabDropdown ...@@ -126,8 +126,10 @@ class GitLabDropdown
@selectFirstRow() @selectFirstRow()
# Event listeners # Event listeners
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "hidden.bs.dropdown", @hidden
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
...@@ -143,10 +145,11 @@ class GitLabDropdown ...@@ -143,10 +145,11 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a" selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) -> @dropdown.on "click", selector, (e) ->
e.preventDefault()
self.rowClicked $(@) self.rowClicked $(@)
if self.options.clicked if self.options.clicked
self.options.clicked() self.options.clicked.call(@,e)
toggleLoading: -> toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
...@@ -176,6 +179,15 @@ class GitLabDropdown ...@@ -176,6 +179,15 @@ class GitLabDropdown
@appendMenu(full_html) @appendMenu(full_html)
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
e.stopPropagation()
return false
else
return true
opened: => opened: =>
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
...@@ -184,7 +196,7 @@ class GitLabDropdown ...@@ -184,7 +196,7 @@ class GitLabDropdown
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @dropdown.find(".dropdown-input-field").focus()
hidden: => hidden: (e) =>
if @options.filterable if @options.filterable
@dropdown @dropdown
.find(".dropdown-input-field") .find(".dropdown-input-field")
...@@ -195,6 +207,9 @@ class GitLabDropdown ...@@ -195,6 +207,9 @@ class GitLabDropdown
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
if @options.hidden
@options.hidden.call(@,e)
# Render the full menu # Render the full menu
renderMenu: (html) -> renderMenu: (html) ->
...@@ -226,6 +241,13 @@ class GitLabDropdown ...@@ -226,6 +241,13 @@ class GitLabDropdown
html = @options.renderRow(data) html = @options.renderRow(data)
else else
selected = if @options.isSelected then @options.isSelected(data) else false selected = if @options.isSelected then @options.isSelected(data) else false
if not selected
value = if @options.id then @options.id(data) else data.id
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if field.length
selected = true
url = if @options.url then @options.url(data) else "#" url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else "" text = if @options.text then @options.text(data) else ""
cssClass = ""; cssClass = "";
...@@ -258,26 +280,28 @@ class GitLabDropdown ...@@ -258,26 +280,28 @@ class GitLabDropdown
rowClicked: (el) -> rowClicked: (el) ->
fieldName = @options.fieldName fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']") selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS) if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove() field.remove()
else else
fieldName = @options.fieldName fieldName = @options.fieldName
selectedIndex = el.parent().index() selectedIndex = el.parent().index()
if @renderedData if @renderedData
selectedObject = @renderedData[selectedIndex] selectedObject = @renderedData[selectedIndex]
selectedObject.selected = true
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if !value? if !value?
field.remove() field.remove()
if @options.multiSelect if not @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.toggleClass "is-active"
...@@ -285,15 +309,15 @@ class GitLabDropdown ...@@ -285,15 +309,15 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length
# Create hidden input for form # Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />" input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input @dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: -> selectFirstRow: ->
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
......
class @IssuableContext class @IssuableContext
constructor: -> constructor: (currentUser) ->
@initParticipants() @initParticipants()
new UsersSelect(currentUser)
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".issuable-sidebar .inline-update").on "change", "select", -> $(".issuable-sidebar .inline-update").on "change", "select", ->
...@@ -11,10 +10,20 @@ class @IssuableContext ...@@ -11,10 +10,20 @@ class @IssuableContext
$(this).submit() $(this).submit()
$(document).on "click",".edit-link", (e) -> $(document).on "click",".edit-link", (e) ->
block = $(@).parents('.block') $block = $(@).parents('.block')
block.find('.selectbox').show() $selectbox = $block.find('.selectbox')
block.find('.value').hide() if $selectbox.is(':visible')
block.find('.js-select2').select2("open") $selectbox.hide()
$block.find('.value').show()
else
$selectbox.show()
$block.find('.value').hide()
if $selectbox.is(':visible')
setTimeout (->
$block.find('.dropdown-menu-toggle').trigger 'click'
), 0
$(".right-sidebar").niceScroll() $(".right-sidebar").niceScroll()
......
...@@ -4,14 +4,20 @@ class @LabelsSelect ...@@ -4,14 +4,20 @@ class @LabelsSelect
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel if selectedLabel?
selectedLabel = selectedLabel.toString().split(',') selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
if newLabelField.length if newLabelField.length
$newLabelCreateButton = $('.js-new-label-btn') $newLabelCreateButton = $('.js-new-label-btn')
...@@ -21,6 +27,22 @@ class @LabelsSelect ...@@ -21,6 +27,22 @@ class @LabelsSelect
# Suggested colors in the dropdown to chose from pre-chosen colors # Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on 'click', (e) -> $('.suggest-colors-dropdown a').on 'click', (e) ->
issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
<span class="label color-label" style="background-color: <%= label.color %>;">
<%= label.title %>
</span>
</a>
<% }); %>'
);
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
if newLabelField.length and $dropdown.hasClass 'js-extra-options'
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
newColorField newColorField
...@@ -57,6 +79,23 @@ class @LabelsSelect ...@@ -57,6 +79,23 @@ class @LabelsSelect
# This allows us to enable the button when ready # This allows us to enable the button when ready
enableLabelCreateButton = -> enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt '' if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$newLabelCreateButton.enable() $newLabelCreateButton.enable()
else else
$newLabelCreateButton.disable() $newLabelCreateButton.disable()
...@@ -90,41 +129,78 @@ class @LabelsSelect ...@@ -90,41 +129,78 @@ class @LabelsSelect
else else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click' $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
saveLabelData = ->
selected = $dropdown
.closest('.selectbox')
.find("input[name='#{$dropdown.data('field-name')}']")
.map(->
@value
).get()
data = {}
data[abilityName] = {}
data[abilityName].label_ids = selected
if not selected.length
data[abilityName].label_ids = ['']
$loading.fadeIn()
$.ajax(
type: 'PUT'
url: issueUpdateURL
dataType: 'JSON'
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
data.issueURLSplit = issueURLSplit
if not data.labels.length
template = labelNoneHTMLTemplate()
else
template = labelHTMLTemplate(data)
href = $value
.show()
.html(template)
$value
.find('a')
.each((i) ->
setTimeout(=>
glAnimate($(@), 'pulse')
,200 * i
)
)
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: labelUrl url: labelUrl
).done (data) -> ).done (data) ->
if showNo if $dropdown.hasClass 'js-extra-options'
data.unshift( if showNo
id: 0 data.unshift(
title: 'No Label' id: 0
) title: 'No Label'
)
if showAny
data.unshift( if showAny
isAny: true data.unshift(
title: 'Any Label' isAny: true
) title: 'Any Label'
)
if data.length > 2
data.splice 2, 0, 'divider' if data.length > 2
data.splice 2, 0, 'divider'
callback data callback data
renderRow: (label) -> renderRow: (label) ->
if $.isArray(selectedLabel) selectedClass = ''
selected = '' if $selectbox.find("input[type='hidden']\
$.each selectedLabel, (i, selectedLbl) -> [name='#{$dropdown.data('field-name')}']\
selectedLbl = selectedLbl.trim() [value='#{label.id}']").length
if selected is '' and label.title is selectedLbl selectedClass = 'is-active'
selected = 'is-active'
else
selected = if label.title is selectedLabel then 'is-active' else ''
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selected}'> <a href='#' class='#{selectedClass}'>
#{color} #{color}
#{label.title} #{label.title}
</a> </a>
...@@ -133,6 +209,7 @@ class @LabelsSelect ...@@ -133,6 +209,7 @@ class @LabelsSelect
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label' if selected and selected.title isnt 'Any Label'
selected.title selected.title
...@@ -142,8 +219,19 @@ class @LabelsSelect ...@@ -142,8 +219,19 @@ class @LabelsSelect
id: (label) -> id: (label) ->
if label.isAny? if label.isAny?
'' ''
else else if $dropdown.hasClass "js-filter-submit"
label.title label.title
else
label.id
hidden: ->
$selectbox.hide()
$value.show()
if $dropdown.hasClass 'js-multiselect'
saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: -> clicked: ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
...@@ -153,4 +241,9 @@ class @LabelsSelect ...@@ -153,4 +241,9 @@ class @LabelsSelect
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
if $dropdown.hasClass 'js-multiselect'
return
else
saveLabelData()
) )
((w) ->
w.glAnimate = ($el, animation, done) ->
$el
.removeClass()
.addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
$(this).removeClass()
return
return
return
) window
\ No newline at end of file
class @MilestoneSelect class @MilestoneSelect
constructor: -> constructor: (currentProject) ->
if currentProject?
_this = @
@currentProject = JSON.parse(currentProject)
$('.js-milestone-select').each (i, dropdown) -> $('.js-milestone-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
milestonesUrl = $dropdown.data('milestones') milestonesUrl = $dropdown.data('milestones')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedMilestone = $dropdown.data('selected') selectedMilestone = $dropdown.data('selected')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
useId = $dropdown.data('use-id') useId = $dropdown.data('use-id')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
issuableId = $dropdown.data('issuable-id')
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
if issueUpdateURL
milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: milestonesUrl url: milestonesUrl
).done (data) -> ).done (data) ->
if showNo if $dropdown.hasClass "js-extra-options"
data.unshift( if showNo
id: '0' data.unshift(
title: 'No Milestone' id: '0'
) title: 'No Milestone'
)
if showAny
data.unshift(
isAny: true
title: 'Any Milestone'
)
if data.length > 2 if showAny
data.splice 2, 0, 'divider' data.unshift(
isAny: true
title: 'Any Milestone'
)
if data.length > 2
data.splice 2, 0, 'divider'
callback(data) callback(data)
filterable: true filterable: true
search: search:
...@@ -53,13 +70,38 @@ class @MilestoneSelect ...@@ -53,13 +70,38 @@ class @MilestoneSelect
milestone.id milestone.id
isSelected: (milestone) -> isSelected: (milestone) ->
milestone.title is selectedMilestone milestone.title is selectedMilestone
clicked: -> hidden: ->
page = $('body').data 'page' $selectbox.hide()
isIssueIndex = page is 'projects:issues:index' $value.show()
isMRIndex = page is page is 'projects:merge_requests:index' clicked: (e) ->
if $dropdown.hasClass 'js-filter-bulk-update'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) return
Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.parents('form').submit()
) else
selected = $selectbox
.find('input[type="hidden"]')
.val()
data = {}
data[abilityName] = {}
data[abilityName].milestone_id = selected
$loading
.fadeIn()
$.ajax(
type: 'PUT'
url: issueUpdateURL
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
$milestoneLink = $value
.show()
.find('a')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
$value.html(milestoneLinkTemplate(data.milestone))
else
$value.html(milestoneLinkNoneTemplate)
)
\ No newline at end of file
class @UsersSelect class @UsersSelect
constructor: -> constructor: (currentUser) ->
@usersPath = "/autocomplete/users.json" @usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
if currentUser?
@currentUser = JSON.parse(currentUser)
$('.js-user-search').each (i, dropdown) => $('.js-user-search').each (i, dropdown) =>
$dropdown = $(dropdown) $dropdown = $(dropdown)
...@@ -12,6 +14,67 @@ class @UsersSelect ...@@ -12,6 +14,67 @@ class @UsersSelect
firstUser = $dropdown.data('first-user') firstUser = $dropdown.data('first-user')
selectedId = $dropdown.data('selected') selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
issueURL = $dropdown.data('issueUpdate')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
abilityName = $dropdown.data('ability-name')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
$block.on('click', '.js-assign-yourself', (e) =>
e.preventDefault()
assignTo(@currentUser.id)
)
assignTo = (selected) ->
data = {}
data[abilityName] = {}
data[abilityName].assignee_id = selected
$loading
.fadeIn()
$.ajax(
type: 'PUT'
dataType: 'json'
url: issueURL
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
if data.assignee
user =
name: data.assignee.name
username: data.assignee.username
avatar: data.assignee.avatar_url
else
user =
name: 'Unassigned'
username: ''
avatar: ''
$value.html(noAssigneeTemplate(user))
$value.find('a').attr('href')
noAssigneeTemplate = _.template(
'<% if (username) { %>
<a class="author_link " href="/u/<%= username %>">
<% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %>
<span class="author"><%= name %></span>
<span class="username">
@<%= username %>
</span>
</a>
<% } else { %>
<span class="assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
</a>
</span>
<% } %>'
)
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) => data: (term, callback) =>
...@@ -57,20 +120,36 @@ class @UsersSelect ...@@ -57,20 +120,36 @@ class @UsersSelect
fields: ['name', 'username'] fields: ['name', 'username']
selectable: true selectable: true
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected && 'id' of selected if selected && 'id' of selected
selected.name selected.name
else else
defaultLabel defaultLabel
inputId: 'issue_assignee_id'
hidden: (e) ->
$selectbox.hide()
$value.show()
clicked: -> clicked: ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index' isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-bulk-update')
return
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
selected = $dropdown
.closest('.selectbox')
.find("input[name='#{$dropdown.data('field-name')}']").val()
assignTo(selected)
renderRow: (user) -> renderRow: (user) ->
username = if user.username then "@#{user.username}" else "" username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false avatar = if user.avatar_url then user.avatar_url else false
...@@ -87,17 +166,25 @@ class @UsersSelect ...@@ -87,17 +166,25 @@ class @UsersSelect
if avatar if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li> # split into three parts so we can remove the username section if nessesary
<a href='#' class='dropdown-menu-user-link #{selected}'> listWithName = "<li>
#{img} <a href='#' class='dropdown-menu-user-link #{selected}'>
<strong class='dropdown-menu-user-full-name'> #{img}
#{user.name} <strong class='dropdown-menu-user-full-name'>
</strong> #{user.name}
<span class='dropdown-menu-user-username'> </strong>"
#{username}
</span> listWithUserName = "<span class='dropdown-menu-user-username'>
</a> #{username}
</li>" </span>"
listClosingTags = "</a>
</li>"
if username is ''
listWithUserName = ''
listWithName + listWithUserName + listClosingTags
) )
$('.ajax-users-select').each (i, select) => $('.ajax-users-select').each (i, select) =>
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css *= require cropper.css
*= require animate
*/ */
/* /*
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
color: $dropdown-link-color; color: $dropdown-link-color;
line-height: 34px; line-height: 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 2px; border-radius: 2px;
white-space: nowrap; white-space: nowrap;
...@@ -167,13 +167,13 @@ ...@@ -167,13 +167,13 @@
} }
.dropdown-menu-user-link { .dropdown-menu-user-link {
padding-top: 7px; padding-top: 10px;
padding-bottom: 7px; padding-bottom: 7px;
} }
.dropdown-menu-user-full-name { .dropdown-menu-user-full-name {
display: block; display: block;
font-weight: 600; font-weight: 500;
line-height: 16px; line-height: 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
......
...@@ -30,6 +30,10 @@ ...@@ -30,6 +30,10 @@
} }
.issuable-sidebar { .issuable-sidebar {
a {
color: inherit;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -89,7 +93,7 @@ ...@@ -89,7 +93,7 @@
} }
.cross-project-reference { .cross-project-reference {
color: $gl-link-color; color: inherit;
span { span {
white-space: nowrap; white-space: nowrap;
...@@ -133,6 +137,12 @@ ...@@ -133,6 +137,12 @@
.value { .value {
line-height: 1; line-height: 1;
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
} }
.bold { .bold {
...@@ -252,6 +262,15 @@ ...@@ -252,6 +262,15 @@
text-decoration: none; text-decoration: none;
} }
} }
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
}
.open .dropdown-menu {
width: 100%;
}
} }
.btn-default.gutter-toggle { .btn-default.gutter-toggle {
......
...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController
@merge_requests = @issue.referenced_merge_requests(current_user) @merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue) respond_to do |format|
format.html
format.json do
render json: @issue.to_json(include: [:milestone, :labels])
end
end
end end
def create def create
...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
format.json do format.json do
render json: { render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
saved: @issue.valid?,
assignee_avatar_url: @issue.assignee.try(:avatar_url)
}
end end
end end
end end
......
...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request]) @merge_request.target_project, @merge_request])
end end
format.json do format.json do
render json: { render json: @merge_request.to_json(include: [:milestone, :labels, :assignee])
saved: @merge_request.valid?,
assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
}
end end
end end
else else
......
...@@ -19,7 +19,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -19,7 +19,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
......
...@@ -60,7 +60,7 @@ module DropdownsHelper ...@@ -60,7 +60,7 @@ module DropdownsHelper
title_output << content_tag(:span, title) title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times') icon('times', class: 'dropdown-menu-close-icon')
end end
title_output.html_safe title_output.html_safe
......
...@@ -16,6 +16,16 @@ module IssuablesHelper ...@@ -16,6 +16,16 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end end
def issuable_json_path(issuable)
project = issuable.project
if issuable.kind_of?(MergeRequest)
namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
else
namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
end
end
def prev_issuable_for(issuable) def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end end
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
= render "projects/merge_requests/show/mr_box" = render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default .append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open? - if @merge_request.open?
......
...@@ -9,13 +9,13 @@ ...@@ -9,13 +9,13 @@
.filter-item.inline .filter-item.inline
- if params[:author_id] - if params[:author_id]
= hidden_field_tag(:author_id, params[:author_id]) = hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline .filter-item.inline
- if params[:assignee_id] - if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id]) = hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter .filter-item.inline.milestone-filter
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown" = render "shared/issuable/label_dropdown"
.pull-right .pull-right
= render 'shared/sort_dropdown' = render 'shared/sort_dropdown'
...@@ -38,11 +37,10 @@ ...@@ -38,11 +37,10 @@
%li %li
%a{href: "#", data: {id: "close"}} Closed %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline .filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
.filter-item.inline .filter-item.inline
......
- if params[:milestone_title] - if params[:milestone_title]
= hidden_field_tag(:milestone_title, params[:milestone_title]) = hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", = dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if @project - if @project
%ul.dropdown-footer-list %ul.dropdown-footer-list
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
= icon('user') = icon('user')
.title.hide-collapsed .title.hide-collapsed
Assignee Assignee
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed .value.bold.hide-collapsed
...@@ -39,10 +40,14 @@ ...@@ -39,10 +40,14 @@
%span.username %span.username
= issuable.assignee.to_reference = issuable.assignee.to_reference
- else - else
.light None %span.assign-yourself
No assignee -
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed .selectbox.hide-collapsed
= users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon
...@@ -54,6 +59,7 @@ ...@@ -54,6 +59,7 @@
No No
.title.hide-collapsed .title.hide-collapsed
Milestone Milestone
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed .value.bold.hide-collapsed
...@@ -62,10 +68,10 @@ ...@@ -62,10 +68,10 @@
= issuable.milestone.title = issuable.milestone.title
- else - else
.light None .light None
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= hidden_field_tag :issuable_context = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
= f.submit class: 'btn hide'
- if issuable.project.labels.any? - if issuable.project.labels.any?
.block.labels .block.labels
...@@ -75,6 +81,7 @@ ...@@ -75,6 +81,7 @@
= issuable.labels.count = issuable.labels.count
.title.hide-collapsed .title.hide-collapsed
Labels Labels
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
...@@ -84,8 +91,31 @@ ...@@ -84,8 +91,31 @@
- else - else
.light None .light None
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - issuable.labels.each do |label|
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%span.dropdown-toggle-text
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
= dropdown_title("Assign labels")
= dropdown_filter("Search labels")
= dropdown_content
- if @project
= dropdown_footer do
%ul.dropdown-footer-list
- if can? current_user, :admin_label, @project
%li
%a.dropdown-toggle-page{href: "#"}
Create new
%li
= link_to namespace_project_labels_path(@project.namespace, @project) do
- if can? current_user, :admin_label, @project
Manage labels
- else
View labels
= render "shared/issuable/participants", participants: issuable.participants(current_user) = render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user - if current_user
...@@ -116,5 +146,7 @@ ...@@ -116,5 +146,7 @@
= clipboard_button(clipboard_text: project_ref) = clipboard_button(clipboard_text: project_ref)
:javascript :javascript
new Subscription('.subscription'); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new IssuableContext(); new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription')
...@@ -31,7 +31,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -31,7 +31,7 @@ feature 'Issue filtering by Milestone', feature: true do
def filter_by_milestone(title) def filter_by_milestone(title)
find(".js-milestone-select").click find(".js-milestone-select").click
sleep 0.5 sleep 0.5
find(".milestone-filter a", text: title).click find(".milestone-filter .dropdown-content a", text: title).click
sleep 1 sleep 1
end end
end end
...@@ -34,20 +34,7 @@ describe 'Issues', feature: true do ...@@ -34,20 +34,7 @@ describe 'Issues', feature: true do
fill_in 'issue_title', with: 'bug 345' fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description' fill_in 'issue_description', with: 'bug description'
end end
it 'does not change issue count' do
expect { click_button 'Save changes' }.to_not change { Issue.count }
end
it 'should update issue fields' do
click_button 'Save changes'
expect(page).to have_content @user.name
expect(page).to have_content 'bug 345'
expect(page).to have_content project.name
end
end end
end end
describe 'Editing issue assignee' do describe 'Editing issue assignee' do
...@@ -70,7 +57,7 @@ describe 'Issues', feature: true do ...@@ -70,7 +57,7 @@ describe 'Issues', feature: true do
click_button 'Save changes' click_button 'Save changes'
page.within('.assignee') do page.within('.assignee') do
expect(page).to have_content 'None' expect(page).to have_content 'No assignee - assign yourself'
end end
expect(issue.reload.assignee).to be_nil expect(issue.reload.assignee).to be_nil
...@@ -198,20 +185,26 @@ describe 'Issues', feature: true do ...@@ -198,20 +185,26 @@ describe 'Issues', feature: true do
end end
describe 'update assignee from issue#show' do describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user) } let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
context 'by autorized user' do context 'by autorized user' do
it 'with dropdown menu' do it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue) visit namespace_project_issue_path(project.namespace, project, issue)
find('.issuable-sidebar #issue_assignee_id'). page.within('.assignee') do
set project.team.members.first.id expect(page).to have_content "#{@user.name}"
click_button 'Update Issue' end
find('.block.assignee .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-menu-user-link').click
sleep 2
page.within('.assignee') do
expect(page).to have_content 'No assignee'
end
expect(page).to have_content 'Assignee' expect(issue.reload.assignee).to be_nil
has_select?('issue_assignee_id',
selected: project.team.members.first.name)
end end
end end
...@@ -221,8 +214,6 @@ describe 'Issues', feature: true do ...@@ -221,8 +214,6 @@ describe 'Issues', feature: true do
before :each do before :each do
project.team << [[guest], :guest] project.team << [[guest], :guest]
issue.assignee = @user
issue.save
end end
it 'shows assignee text', js: true do it 'shows assignee text', js: true do
...@@ -241,20 +232,23 @@ describe 'Issues', feature: true do ...@@ -241,20 +232,23 @@ describe 'Issues', feature: true do
context 'by authorized user' do context 'by authorized user' do
it 'with dropdown menu' do
visit namespace_project_issue_path(project.namespace, project, issue)
find('.issuable-sidebar'). it 'allows user to select unassigned', js: true do
select(milestone.title, from: 'issue_milestone_id') visit namespace_project_issue_path(project.namespace, project, issue)
click_button 'Update Issue'
expect(page).to have_content "Milestone changed to #{milestone.title}" page.within('.milestone') do
expect(page).to have_content "None"
end
find('.block.milestone .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-content li').click
sleep 2
page.within('.milestone') do page.within('.milestone') do
expect(page).to have_content milestone.title expect(page).to have_content 'None'
end end
has_select?('issue_assignee_id', selected: milestone.title) expect(issue.reload.milestone).to be_nil
end end
end end
...@@ -283,25 +277,6 @@ describe 'Issues', feature: true do ...@@ -283,25 +277,6 @@ describe 'Issues', feature: true do
issue.assignee = user2 issue.assignee = user2
issue.save issue.save
end end
it 'allows user to remove assignee', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
page.within('.assignee') do
expect(page).to have_content user2.name
end
find('.assignee .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.user-result').click
page.within('.assignee') do
expect(page).to have_content 'None'
end
sleep 2 # wait for ajax stuff to complete
expect(issue.reload.assignee).to be_nil
end
end end
end end
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment