Merge branch 'awardables' into 'master'
Awardables TODO: - [x] Fix specs - [x] Write new specs - [X] Check if the migrations can stay offline migrations -- (seems not to make 8.8) - [X] API expose Awards -> Next merge window See merge request !3785
This commit is contained in:
commit
9440ac669f
|
@ -1,201 +1,300 @@
|
|||
class @AwardsHandler
|
||||
constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
|
||||
$('.js-add-award').on 'click', (event) =>
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
@showEmojiMenu()
|
||||
constructor: ->
|
||||
|
||||
@aliases = emojiAliases()
|
||||
|
||||
$(document)
|
||||
.off 'click', '.js-add-award'
|
||||
.on 'click', '.js-add-award', (event) =>
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
@showEmojiMenu $(event.currentTarget)
|
||||
|
||||
$('html').on 'click', (event) ->
|
||||
if !$(event.target).closest('.emoji-menu').length
|
||||
unless $(event.target).closest('.emoji-menu').length
|
||||
if $('.emoji-menu').is(':visible')
|
||||
$('.js-add-award.is-active').removeClass 'is-active'
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
|
||||
$('.awards')
|
||||
.off 'click'
|
||||
.on 'click', '.js-emoji-btn', @handleClick
|
||||
$(document)
|
||||
.off 'click', '.js-emoji-btn'
|
||||
.on 'click', '.js-emoji-btn', @handleClick
|
||||
|
||||
@renderFrequentlyUsedBlock()
|
||||
|
||||
handleClick: (e) ->
|
||||
handleClick: (e) =>
|
||||
|
||||
e.preventDefault()
|
||||
emoji = $(this)
|
||||
.find('.icon')
|
||||
.data 'emoji'
|
||||
|
||||
if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
|
||||
awardsHandler.addAward 'thumbsdown'
|
||||
emoji = $(e.currentTarget).find('.icon').data 'emoji'
|
||||
@getVotesBlock().addClass 'js-awards-block'
|
||||
@addAward @getAwardUrl(), emoji
|
||||
|
||||
else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
|
||||
awardsHandler.addAward 'thumbsup'
|
||||
|
||||
awardsHandler.addAward emoji
|
||||
showEmojiMenu: ($addBtn) ->
|
||||
|
||||
$(this).trigger 'blur'
|
||||
$menu = $('.emoji-menu')
|
||||
|
||||
didUserClickEmoji: (that, emoji) ->
|
||||
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
|
||||
$(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
|
||||
if $menu.length
|
||||
$holder = $addBtn.closest('.js-award-holder')
|
||||
|
||||
showEmojiMenu: ->
|
||||
if $('.emoji-menu').length
|
||||
if $('.emoji-menu').is '.is-visible'
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
if $menu.is '.is-visible'
|
||||
$addBtn.removeClass 'is-active'
|
||||
$menu.removeClass 'is-visible'
|
||||
$('#emoji_search').blur()
|
||||
else
|
||||
$('.emoji-menu').addClass 'is-visible'
|
||||
$addBtn.addClass 'is-active'
|
||||
@positionMenu($menu, $addBtn)
|
||||
|
||||
$menu.addClass 'is-visible'
|
||||
$('#emoji_search').focus()
|
||||
else
|
||||
$('.js-add-award').addClass 'is-loading'
|
||||
$.get @getEmojisUrl, (response) =>
|
||||
$('.js-add-award').removeClass 'is-loading'
|
||||
$('.js-award-holder').append response
|
||||
$addBtn.addClass 'is-loading is-active'
|
||||
url = $addBtn.data 'award-menu-url'
|
||||
|
||||
@createEmojiMenu url, =>
|
||||
$addBtn.removeClass 'is-loading'
|
||||
$menu = $('.emoji-menu')
|
||||
@positionMenu($menu, $addBtn)
|
||||
@renderFrequentlyUsedBlock()
|
||||
|
||||
setTimeout =>
|
||||
$('.emoji-menu').addClass 'is-visible'
|
||||
$menu.addClass 'is-visible'
|
||||
$('#emoji_search').focus()
|
||||
@setupSearch()
|
||||
, 200
|
||||
|
||||
addAward: (emoji) ->
|
||||
@postEmoji emoji, =>
|
||||
@addAwardToEmojiBar(emoji)
|
||||
|
||||
createEmojiMenu: (awardMenuUrl, callback) ->
|
||||
|
||||
$.get awardMenuUrl, (response) =>
|
||||
$('body').append response
|
||||
callback()
|
||||
|
||||
|
||||
positionMenu: ($menu, $addBtn) ->
|
||||
position = $addBtn.data('position')
|
||||
|
||||
# The menu could potentially be off-screen or in a hidden overflow element
|
||||
# So we position the element absolute in the body
|
||||
css =
|
||||
top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
|
||||
|
||||
if position? and position is 'right'
|
||||
css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
|
||||
$menu.addClass 'is-aligned-right'
|
||||
else
|
||||
css.left = "#{$addBtn.offset().left}px"
|
||||
$menu.removeClass 'is-aligned-right'
|
||||
|
||||
$menu.css(css)
|
||||
|
||||
|
||||
addAward: (awardUrl, emoji, checkMutuality = yes) ->
|
||||
|
||||
emoji = @normilizeEmojiName(emoji)
|
||||
@postEmoji awardUrl, emoji, =>
|
||||
@addAwardToEmojiBar(emoji, checkMutuality)
|
||||
|
||||
$('.js-awards-block-current').removeClass 'js-awards-block-current'
|
||||
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
|
||||
addAwardToEmojiBar: (emoji) ->
|
||||
|
||||
addAwardToEmojiBar: (emoji, checkForMutuality = yes) ->
|
||||
|
||||
@checkMutuality emoji if checkForMutuality
|
||||
@addEmojiToFrequentlyUsedList(emoji)
|
||||
|
||||
if @exist(emoji)
|
||||
if @isActive(emoji)
|
||||
@decrementCounter(emoji)
|
||||
emoji = @normilizeEmojiName(emoji)
|
||||
$emojiBtn = @findEmojiIcon(emoji).parent()
|
||||
|
||||
if $emojiBtn.length > 0
|
||||
if @isActive($emojiBtn)
|
||||
@decrementCounter($emojiBtn, emoji)
|
||||
else
|
||||
counter = @findEmojiIcon(emoji).siblings('.js-counter')
|
||||
counter = $emojiBtn.find('.js-counter')
|
||||
counter.text(parseInt(counter.text()) + 1)
|
||||
counter.parent().addClass('active')
|
||||
@addMeToAuthorList(emoji)
|
||||
$emojiBtn.addClass('active')
|
||||
@addMeToUserList(emoji)
|
||||
else
|
||||
@createEmoji(emoji)
|
||||
|
||||
exist: (emoji) ->
|
||||
@findEmojiIcon(emoji).length > 0
|
||||
|
||||
isActive: (emoji) ->
|
||||
@findEmojiIcon(emoji).parent().hasClass('active')
|
||||
getVotesBlock: -> return $ '.awards.js-awards-block'
|
||||
|
||||
decrementCounter: (emoji) ->
|
||||
counter = @findEmojiIcon(emoji).siblings('.js-counter')
|
||||
emojiIcon = counter.parent()
|
||||
if parseInt(counter.text()) > 1
|
||||
counter.text(parseInt(counter.text()) - 1)
|
||||
emojiIcon.removeClass('active')
|
||||
@removeMeFromAuthorList(emoji)
|
||||
else if emoji == 'thumbsup' || emoji == 'thumbsdown'
|
||||
emojiIcon.tooltip('destroy')
|
||||
counter.text(0)
|
||||
emojiIcon.removeClass('active')
|
||||
@removeMeFromAuthorList(emoji)
|
||||
|
||||
getAwardUrl: -> @getVotesBlock().data 'award-url'
|
||||
|
||||
|
||||
checkMutuality: (emoji) ->
|
||||
|
||||
awardUrl = @getAwardUrl()
|
||||
|
||||
if emoji in [ 'thumbsup', 'thumbsdown' ]
|
||||
mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
|
||||
|
||||
isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active'
|
||||
@addAward awardUrl, mutualVote, no if isAlreadyVoted
|
||||
|
||||
|
||||
isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active'
|
||||
|
||||
|
||||
decrementCounter: ($emojiBtn, emoji) ->
|
||||
isntNoteBody = $emojiBtn.closest('.note-body').length is 0
|
||||
counter = $('.js-counter', $emojiBtn)
|
||||
counterNumber = parseInt(counter.text())
|
||||
|
||||
if !isntNoteBody
|
||||
# If this is a note body, we just hide the award emoji row like the initial state
|
||||
$emojiBtn.closest('.js-awards-block').addClass 'hidden'
|
||||
|
||||
if counterNumber > 1
|
||||
counter.text(counterNumber - 1)
|
||||
@removeMeFromUserList($emojiBtn, emoji)
|
||||
else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody
|
||||
$emojiBtn.tooltip('destroy')
|
||||
counter.text('0')
|
||||
@removeMeFromUserList($emojiBtn, emoji)
|
||||
else
|
||||
emojiIcon.tooltip('destroy')
|
||||
emojiIcon.remove()
|
||||
$emojiBtn.tooltip('destroy')
|
||||
$emojiBtn.remove()
|
||||
|
||||
$emojiBtn.removeClass('active')
|
||||
|
||||
|
||||
getAwardTooltip: ($awardBlock) ->
|
||||
|
||||
return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title')
|
||||
|
||||
|
||||
removeMeFromUserList: ($emojiBtn, emoji) ->
|
||||
|
||||
awardBlock = $emojiBtn
|
||||
originalTitle = @getAwardTooltip awardBlock
|
||||
|
||||
authors = originalTitle.split ', '
|
||||
authors.splice authors.indexOf('me'), 1
|
||||
|
||||
newAuthors = authors.join ', '
|
||||
|
||||
removeMeFromAuthorList: (emoji) ->
|
||||
awardBlock = @findEmojiIcon(emoji).parent()
|
||||
authors = awardBlock
|
||||
.attr('data-original-title')
|
||||
.split(', ')
|
||||
authors.splice(authors.indexOf('me'),1)
|
||||
awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.attr('data-original-title', authors.join(', '))
|
||||
.closest '.js-emoji-btn'
|
||||
.removeData 'original-title'
|
||||
.removeData 'title'
|
||||
.attr 'data-original-title', newAuthors
|
||||
.attr 'data-title', newAuthors
|
||||
|
||||
@resetTooltip(awardBlock)
|
||||
|
||||
addMeToAuthorList: (emoji) ->
|
||||
|
||||
addMeToUserList: (emoji) ->
|
||||
|
||||
awardBlock = @findEmojiIcon(emoji).parent()
|
||||
origTitle = awardBlock.attr('data-original-title').trim()
|
||||
authors = []
|
||||
origTitle = @getAwardTooltip awardBlock
|
||||
users = []
|
||||
|
||||
if origTitle
|
||||
authors = origTitle.split(', ')
|
||||
authors.push('me')
|
||||
awardBlock.attr('data-original-title', authors.join(', '))
|
||||
users = origTitle.trim().split(', ')
|
||||
|
||||
users.push('me')
|
||||
awardBlock.attr('title', users.join(', '))
|
||||
|
||||
@resetTooltip(awardBlock)
|
||||
|
||||
|
||||
resetTooltip: (award) ->
|
||||
award.tooltip('destroy')
|
||||
|
||||
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
|
||||
# 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
|
||||
setTimeout (->
|
||||
award.tooltip()
|
||||
), 200
|
||||
|
||||
|
||||
createEmoji: (emoji) ->
|
||||
emojiCssClass = @resolveNameToCssClass(emoji)
|
||||
createEmoji_: (emoji) ->
|
||||
|
||||
nodes = []
|
||||
nodes.push(
|
||||
"<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
|
||||
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
|
||||
"<span class='award-control-text js-counter'>1</span>",
|
||||
"</button>"
|
||||
)
|
||||
emojiCssClass = @resolveNameToCssClass emoji
|
||||
|
||||
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
|
||||
<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
|
||||
<span class='award-control-text js-counter'>1</span>
|
||||
</button>"
|
||||
|
||||
emoji_node = $(buttonHtml)
|
||||
.insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)'
|
||||
.find '.emoji-icon'
|
||||
.data 'emoji', emoji
|
||||
|
||||
$(nodes.join("\n"))
|
||||
.insertBefore('.js-award-holder')
|
||||
.find('.emoji-icon')
|
||||
.data('emoji', emoji)
|
||||
$('.award-control').tooltip()
|
||||
|
||||
resolveNameToCssClass: (emoji) ->
|
||||
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
|
||||
$currentBlock = $ '.js-awards-block'
|
||||
|
||||
if emojiIcon.length > 0
|
||||
unicodeName = emojiIcon.data('unicode-name')
|
||||
if $currentBlock.is '.hidden'
|
||||
$currentBlock.removeClass 'hidden'
|
||||
|
||||
|
||||
createEmoji: (emoji) ->
|
||||
|
||||
return @createEmoji_ emoji if $('.emoji-menu').length
|
||||
|
||||
awardMenuUrl = gl.awardMenuUrl or '/emojis'
|
||||
@createEmojiMenu awardMenuUrl, => @createEmoji emoji
|
||||
|
||||
|
||||
resolveNameToCssClass: (emoji) ->
|
||||
|
||||
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
|
||||
|
||||
if emoji_icon.length > 0
|
||||
unicodeName = emoji_icon.data('unicode-name')
|
||||
else
|
||||
# Find by alias
|
||||
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
|
||||
|
||||
"emoji-#{unicodeName}"
|
||||
return "emoji-#{unicodeName}"
|
||||
|
||||
postEmoji: (emoji, callback) ->
|
||||
$.post @postEmojiUrl, { note: {
|
||||
note: ":#{emoji}:"
|
||||
noteable_type: @noteableType
|
||||
noteable_id: @noteableId
|
||||
}},(data) ->
|
||||
|
||||
postEmoji: (awardUrl, emoji, callback) ->
|
||||
$.post awardUrl, { name: emoji }, (data) ->
|
||||
if data.ok
|
||||
callback.call()
|
||||
|
||||
findEmojiIcon: (emoji) ->
|
||||
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
|
||||
$(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']")
|
||||
|
||||
scrollToAwards: ->
|
||||
$('body, html').animate({
|
||||
scrollTop: $('.awards').offset().top - 80
|
||||
}, 200)
|
||||
|
||||
normilizeEmojiName: (emoji) ->
|
||||
@aliases[emoji] || emoji
|
||||
|
||||
addEmojiToFrequentlyUsedList: (emoji) ->
|
||||
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
|
||||
frequentlyUsedEmojis.push(emoji)
|
||||
$.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 })
|
||||
frequently_used_emojis = @getFrequentlyUsedEmojis()
|
||||
frequently_used_emojis.push(emoji)
|
||||
$.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 })
|
||||
|
||||
getFrequentlyUsedEmojis: ->
|
||||
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',')
|
||||
_.compact(_.uniq(frequentlyUsedEmojis))
|
||||
frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',')
|
||||
_.compact(_.uniq(frequently_used_emojis))
|
||||
|
||||
renderFrequentlyUsedBlock: ->
|
||||
if $.cookie('frequently_used_emojis')
|
||||
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
|
||||
frequently_used_emojis = @getFrequentlyUsedEmojis()
|
||||
|
||||
ul = $('<ul>')
|
||||
ul = $("<ul class='clearfix emoji-menu-list'>")
|
||||
|
||||
for emoji in frequentlyUsedEmojis
|
||||
do (emoji) ->
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
|
||||
for emoji in frequently_used_emojis
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
|
||||
|
||||
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
|
||||
|
||||
setupSearch: ->
|
||||
$('input.emoji-search').keyup (ev) =>
|
||||
$('input.emoji-search').on 'keyup', (ev) =>
|
||||
term = $(ev.target).val()
|
||||
|
||||
# Clean previous search results
|
||||
|
@ -204,12 +303,12 @@ class @AwardsHandler
|
|||
if term
|
||||
# Generate a search result block
|
||||
h5 = $('<h5>').text('Search results').addClass('emoji-search')
|
||||
foundEmojis = @searchEmojis(term).show()
|
||||
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis)
|
||||
found_emojis = @searchEmojis(term).show()
|
||||
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide()
|
||||
$('.emoji-menu-content').append(h5).append(ul)
|
||||
else
|
||||
$('.emoji-menu-content').children().show()
|
||||
|
||||
searchEmojis: (term)->
|
||||
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
|
||||
$(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
|
||||
|
|
|
@ -22,6 +22,7 @@ class Dispatcher
|
|||
new Issue()
|
||||
shortcut_handler = new ShortcutsIssuable()
|
||||
new ZenMode()
|
||||
window.awardsHandler = new AwardsHandler()
|
||||
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
|
||||
new Milestone()
|
||||
when 'dashboard:todos:index'
|
||||
|
@ -52,6 +53,7 @@ class Dispatcher
|
|||
new Diff()
|
||||
shortcut_handler = new ShortcutsIssuable(true)
|
||||
new ZenMode()
|
||||
window.awardsHandler = new AwardsHandler()
|
||||
when "projects:merge_requests:diffs"
|
||||
new Diff()
|
||||
new ZenMode()
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
window.emojiAliases = ->
|
||||
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
|
|
@ -167,7 +167,7 @@ class @Notes
|
|||
return
|
||||
|
||||
if note.award
|
||||
awardsHandler.addAwardToEmojiBar(note.note)
|
||||
awardsHandler.addAwardToEmojiBar(note.name)
|
||||
awardsHandler.scrollToAwards()
|
||||
|
||||
# render note if it not present in loaded list
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
.awards {
|
||||
line-height: 34px;
|
||||
|
||||
.emoji-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
@ -9,8 +7,6 @@
|
|||
|
||||
.emoji-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 3px;
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
|
@ -23,7 +19,12 @@
|
|||
opacity: 0;
|
||||
transform: scale(.2);
|
||||
transform-origin: 0 -45px;
|
||||
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
|
||||
transition: .3s cubic-bezier(.87,-.41,.19,1.44);
|
||||
transition-property: transform, opacity;
|
||||
|
||||
&.is-aligned-right {
|
||||
transform-origin: 100% -45px;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
pointer-events: all;
|
||||
|
@ -107,7 +108,7 @@
|
|||
}
|
||||
|
||||
&.is-loading {
|
||||
.award-control-icon {
|
||||
.award-control-icon-normal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
module ToggleAwardEmoji
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authenticate_user!, only: [:toggle_award_emoji]
|
||||
end
|
||||
|
||||
def toggle_award_emoji
|
||||
name = params.require(:name)
|
||||
|
||||
awardable.toggle_award_emoji(name, current_user)
|
||||
TodoService.new.new_award_emoji(awardable, current_user)
|
||||
|
||||
render json: { ok: true }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def awardable
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -1,6 +1,7 @@
|
|||
class Projects::IssuesController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include IssuableActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
|
||||
|
@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
|
||||
def show
|
||||
@note = @project.notes.new(noteable: @issue)
|
||||
@notes = @issue.notes.nonawards.with_associations.fresh
|
||||
@notes = @issue.notes.with_associations.fresh
|
||||
@noteable = @issue
|
||||
|
||||
respond_to do |format|
|
||||
|
@ -169,6 +170,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
alias_method :subscribable_resource, :issue
|
||||
alias_method :issuable, :issue
|
||||
alias_method :awardable, :issue
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_404 unless can?(current_user, :read_issue, @issue)
|
||||
|
|
|
@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
include ToggleSubscriptionAction
|
||||
include DiffHelper
|
||||
include IssuableActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :merge_request, only: [
|
||||
|
@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
|
||||
.execute(@merge_request)
|
||||
.execute(@merge_request)
|
||||
@status = :merge_when_build_succeeds
|
||||
else
|
||||
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
|
||||
|
@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
alias_method :subscribable_resource, :merge_request
|
||||
alias_method :issuable, :merge_request
|
||||
alias_method :awardable, :merge_request
|
||||
|
||||
def closes_issues
|
||||
@closes_issues ||= @merge_request.closes_issues
|
||||
|
@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
def define_show_vars
|
||||
# Build a note object for comment form
|
||||
@note = @project.notes.new(noteable: @merge_request)
|
||||
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
|
||||
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh
|
||||
@discussions = @notes.discussions
|
||||
@noteable = @merge_request
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
before_action :authorize_read_note!
|
||||
before_action :authorize_create_note!, only: [:create]
|
||||
before_action :authorize_admin_note!, only: [:update, :destroy]
|
||||
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
|
||||
before_action :find_current_user_notes, only: [:index]
|
||||
|
||||
def index
|
||||
current_fetched_at = Time.now.to_i
|
||||
|
@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def award_toggle
|
||||
noteable = if note_params[:noteable_type] == "issue"
|
||||
project.issues.find(note_params[:noteable_id])
|
||||
else
|
||||
project.merge_requests.find(note_params[:noteable_id])
|
||||
end
|
||||
|
||||
data = {
|
||||
author: current_user,
|
||||
is_award: true,
|
||||
note: note_params[:note].delete(":")
|
||||
}
|
||||
|
||||
note = noteable.notes.find_by(data)
|
||||
|
||||
if note
|
||||
note.destroy
|
||||
else
|
||||
Notes::CreateService.new(project, current_user, note_params).execute
|
||||
end
|
||||
|
||||
render json: { ok: true }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note
|
||||
|
@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def note_json(note)
|
||||
if note.valid?
|
||||
if note.is_a?(AwardEmoji)
|
||||
{
|
||||
valid: note.valid?,
|
||||
award: true,
|
||||
id: note.id,
|
||||
name: note.name
|
||||
}
|
||||
elsif note.valid?
|
||||
{
|
||||
valid: true,
|
||||
id: note.id,
|
||||
discussion_id: note.discussion_id,
|
||||
html: note_to_html(note),
|
||||
award: note.is_award,
|
||||
award: false,
|
||||
note: note.note,
|
||||
discussion_html: note_to_discussion_html(note),
|
||||
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
|
||||
|
@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
else
|
||||
{
|
||||
valid: false,
|
||||
award: note.is_award,
|
||||
award: false,
|
||||
errors: note.errors
|
||||
}
|
||||
end
|
||||
|
|
|
@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
|
||||
|
||||
@suggestions = {
|
||||
emojis: AwardEmoji.urls,
|
||||
emojis: Gitlab::AwardEmoji.urls,
|
||||
issues: autocomplete.issues,
|
||||
milestones: autocomplete.milestones,
|
||||
mergerequests: autocomplete.merge_requests,
|
||||
|
|
|
@ -12,9 +12,9 @@ class NotesFinder
|
|||
when "commit"
|
||||
project.notes.for_commit_id(target_id).non_diff_notes
|
||||
when "issue"
|
||||
project.issues.find(target_id).notes.nonawards.inc_author
|
||||
project.issues.find(target_id).notes.inc_author
|
||||
when "merge_request"
|
||||
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
|
||||
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
|
||||
when "snippet", "project_snippet"
|
||||
project.snippets.find(target_id).notes
|
||||
else
|
||||
|
|
|
@ -145,16 +145,14 @@ module IssuesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def emoji_author_list(notes, current_user)
|
||||
list = notes.map do |note|
|
||||
note.author == current_user ? "me" : note.author.name
|
||||
end
|
||||
|
||||
list.join(", ")
|
||||
def award_user_list(awards, current_user)
|
||||
awards.map do |award|
|
||||
award.user == current_user ? 'me' : award.user.name
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
def note_active_class(notes, current_user)
|
||||
if current_user && notes.pluck(:author_id).include?(current_user.id)
|
||||
def award_active_class(awards, current_user)
|
||||
if current_user && awards.find { |a| a.user_id == current_user.id }
|
||||
"active"
|
||||
else
|
||||
""
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
class AwardEmoji < ActiveRecord::Base
|
||||
DOWNVOTE_NAME = "thumbsdown".freeze
|
||||
UPVOTE_NAME = "thumbsup".freeze
|
||||
|
||||
include Participable
|
||||
|
||||
belongs_to :awardable, polymorphic: true
|
||||
belongs_to :user
|
||||
|
||||
validates :awardable, :user, presence: true
|
||||
validates :name, presence: true, inclusion: { in: Emoji.emojis_names }
|
||||
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
|
||||
|
||||
participant :user
|
||||
|
||||
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
|
||||
scope :upvotes, -> { where(name: UPVOTE_NAME) }
|
||||
|
||||
def downvote?
|
||||
self.name == DOWNVOTE_NAME
|
||||
end
|
||||
|
||||
def upvote?
|
||||
self.name == UPVOTE_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,81 @@
|
|||
module Awardable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :award_emoji, as: :awardable, dependent: :destroy
|
||||
|
||||
if self < Participable
|
||||
participant :award_emoji
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def order_upvotes_desc
|
||||
order_votes_desc(AwardEmoji::UPVOTE_NAME)
|
||||
end
|
||||
|
||||
def order_downvotes_desc
|
||||
order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
|
||||
end
|
||||
|
||||
def order_votes_desc(emoji_name)
|
||||
awardable_table = self.arel_table
|
||||
awards_table = AwardEmoji.arel_table
|
||||
|
||||
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
|
||||
awards_table[:awardable_id].eq(awardable_table[:id]).and(
|
||||
awards_table[:awardable_type].eq(self.name).and(
|
||||
awards_table[:name].eq(emoji_name)
|
||||
)
|
||||
)
|
||||
).join_sources
|
||||
|
||||
joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
|
||||
end
|
||||
end
|
||||
|
||||
def grouped_awards(with_thumbs: true)
|
||||
awards = award_emoji.group_by(&:name)
|
||||
|
||||
if with_thumbs
|
||||
awards[AwardEmoji::UPVOTE_NAME] ||= []
|
||||
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
|
||||
end
|
||||
|
||||
awards
|
||||
end
|
||||
|
||||
def downvotes
|
||||
award_emoji.downvotes.count
|
||||
end
|
||||
|
||||
def upvotes
|
||||
award_emoji.upvotes.count
|
||||
end
|
||||
|
||||
def emoji_awardable?
|
||||
true
|
||||
end
|
||||
|
||||
def awarded_emoji?(emoji_name, current_user)
|
||||
award_emoji.where(name: emoji_name, user: current_user).exists?
|
||||
end
|
||||
|
||||
def create_award_emoji(name, current_user)
|
||||
return unless emoji_awardable?
|
||||
|
||||
award_emoji.create(name: name, user: current_user)
|
||||
end
|
||||
|
||||
def remove_award_emoji(name, current_user)
|
||||
award_emoji.where(name: name, user: current_user).destroy_all
|
||||
end
|
||||
|
||||
def toggle_award_emoji(emoji_name, current_user)
|
||||
if awarded_emoji?(emoji_name, current_user)
|
||||
remove_award_emoji(emoji_name, current_user)
|
||||
else
|
||||
create_award_emoji(emoji_name, current_user)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,7 @@ module Issuable
|
|||
include Mentionable
|
||||
include Subscribable
|
||||
include StripAttribute
|
||||
include Awardable
|
||||
|
||||
included do
|
||||
belongs_to :author, class_name: "User"
|
||||
|
@ -115,29 +116,6 @@ module Issuable
|
|||
end
|
||||
end
|
||||
|
||||
def order_downvotes_desc
|
||||
order_votes_desc('thumbsdown')
|
||||
end
|
||||
|
||||
def order_upvotes_desc
|
||||
order_votes_desc('thumbsup')
|
||||
end
|
||||
|
||||
def order_votes_desc(award_emoji_name)
|
||||
issuable_table = self.arel_table
|
||||
note_table = Note.arel_table
|
||||
|
||||
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
|
||||
note_table[:noteable_id].eq(issuable_table[:id]).and(
|
||||
note_table[:noteable_type].eq(self.name).and(
|
||||
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
|
||||
)
|
||||
)
|
||||
).join_sources
|
||||
|
||||
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
|
||||
end
|
||||
|
||||
def with_label(title, sort = nil)
|
||||
if title.is_a?(Array) && title.size > 1
|
||||
joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
|
||||
|
@ -179,14 +157,6 @@ module Issuable
|
|||
opened? || reopened?
|
||||
end
|
||||
|
||||
def downvotes
|
||||
notes.awards.where(note: "thumbsdown").count
|
||||
end
|
||||
|
||||
def upvotes
|
||||
notes.awards.where(note: "thumbsup").count
|
||||
end
|
||||
|
||||
def user_notes_count
|
||||
notes.user.count
|
||||
end
|
||||
|
|
|
@ -110,6 +110,10 @@ class LegacyDiffNote < Note
|
|||
@active
|
||||
end
|
||||
|
||||
def award_emoji_supported?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_diff
|
||||
|
|
|
@ -21,11 +21,8 @@ class Note < ActiveRecord::Base
|
|||
delegate :name, :email, to: :author, prefix: true
|
||||
delegate :title, to: :noteable, allow_nil: true
|
||||
|
||||
before_validation :set_award!
|
||||
|
||||
validates :note, :project, presence: true
|
||||
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
|
||||
validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
|
||||
|
||||
# Attachments are deprecated and are handled by Markdown uploader
|
||||
validates :attachment, file_size: { maximum: :max_attachment_size }
|
||||
|
||||
|
@ -43,8 +40,6 @@ class Note < ActiveRecord::Base
|
|||
mount_uploader :attachment, AttachmentUploader
|
||||
|
||||
# Scopes
|
||||
scope :awards, ->{ where(is_award: true) }
|
||||
scope :nonawards, ->{ where(is_award: false) }
|
||||
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
|
||||
scope :system, ->{ where(system: true) }
|
||||
scope :user, ->{ where(system: false) }
|
||||
|
@ -109,19 +104,6 @@ class Note < ActiveRecord::Base
|
|||
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
|
||||
end
|
||||
end
|
||||
|
||||
def grouped_awards
|
||||
notes = {}
|
||||
|
||||
awards.select(:note).distinct.map do |note|
|
||||
notes[note.note] = where(note: note.note)
|
||||
end
|
||||
|
||||
notes["thumbsup"] ||= Note.none
|
||||
notes["thumbsdown"] ||= Note.none
|
||||
|
||||
notes
|
||||
end
|
||||
end
|
||||
|
||||
def cross_reference?
|
||||
|
@ -205,44 +187,24 @@ class Note < ActiveRecord::Base
|
|||
Event.reset_event_cache_for(self)
|
||||
end
|
||||
|
||||
def downvote?
|
||||
is_award && note == "thumbsdown"
|
||||
end
|
||||
|
||||
def upvote?
|
||||
is_award && note == "thumbsup"
|
||||
end
|
||||
|
||||
def editable?
|
||||
!system? && !is_award
|
||||
!system?
|
||||
end
|
||||
|
||||
def cross_reference_not_visible_for?(user)
|
||||
cross_reference? && referenced_mentionables(user).empty?
|
||||
end
|
||||
|
||||
# Checks if note is an award added as a comment
|
||||
#
|
||||
# If note is an award, this method sets is_award to true
|
||||
# and changes content of the note to award name.
|
||||
#
|
||||
# Method is executed as a before_validation callback.
|
||||
#
|
||||
def set_award!
|
||||
return unless awards_supported? && contains_emoji_only?
|
||||
|
||||
self.is_award = true
|
||||
self.note = award_emoji_name
|
||||
def award_emoji?
|
||||
award_emoji_supported? && contains_emoji_only?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_blank_line_code!
|
||||
self.line_code = nil if self.line_code.blank?
|
||||
end
|
||||
|
||||
def awards_supported?
|
||||
(for_issue? || for_merge_request?) && !diff_note?
|
||||
def award_emoji_supported?
|
||||
noteable.is_a?(Awardable)
|
||||
end
|
||||
|
||||
def contains_emoji_only?
|
||||
|
@ -251,6 +213,6 @@ class Note < ActiveRecord::Base
|
|||
|
||||
def award_emoji_name
|
||||
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
|
||||
AwardEmoji.normilize_emoji_name(original_name)
|
||||
Gitlab::AwardEmoji.normalize_emoji_name(original_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -84,6 +84,7 @@ class User < ActiveRecord::Base
|
|||
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
has_many :award_emoji, as: :awardable, dependent: :destroy
|
||||
|
||||
#
|
||||
# Validations
|
||||
|
|
|
@ -24,6 +24,7 @@ module Issues
|
|||
@new_issue = create_new_issue
|
||||
|
||||
rewrite_notes
|
||||
rewrite_award_emoji
|
||||
add_note_moved_from
|
||||
|
||||
# Old issue tasks
|
||||
|
@ -72,6 +73,14 @@ module Issues
|
|||
end
|
||||
end
|
||||
|
||||
def rewrite_award_emoji
|
||||
@old_issue.award_emoji.each do |award|
|
||||
new_award = award.dup
|
||||
new_award.awardable = @new_issue
|
||||
new_award.save
|
||||
end
|
||||
end
|
||||
|
||||
def rewrite_content(content)
|
||||
return unless content
|
||||
|
||||
|
|
|
@ -5,6 +5,13 @@ module Notes
|
|||
note.author = current_user
|
||||
note.system = false
|
||||
|
||||
if note.award_emoji?
|
||||
noteable = note.noteable
|
||||
todo_service.new_award_emoji(noteable, current_user)
|
||||
|
||||
return noteable.create_award_emoji(note.award_emoji_name, current_user)
|
||||
end
|
||||
|
||||
if note.save
|
||||
# Finish the harder work in the background
|
||||
NewNoteWorker.perform_in(2.seconds, note.id, params)
|
||||
|
|
|
@ -8,7 +8,7 @@ module Notes
|
|||
|
||||
def execute
|
||||
# Skip system notes, like status changes and cross-references and awards
|
||||
unless @note.system || @note.is_award
|
||||
unless @note.system?
|
||||
EventCreateService.new.leave_note(@note, @note.author)
|
||||
@note.create_cross_references!
|
||||
execute_note_hooks
|
||||
|
|
|
@ -130,8 +130,7 @@ class NotificationService
|
|||
|
||||
# ignore gitlab service messages
|
||||
return true if note.note.start_with?('Status changed to closed')
|
||||
return true if note.cross_reference? && note.system == true
|
||||
return true if note.is_award
|
||||
return true if note.cross_reference? && note.system?
|
||||
|
||||
target = note.noteable
|
||||
|
||||
|
|
|
@ -122,6 +122,14 @@ class TodoService
|
|||
handle_note(note, current_user)
|
||||
end
|
||||
|
||||
# When an emoji is awarded we should:
|
||||
#
|
||||
# * mark all pending todos related to the awardable for the current user as done
|
||||
#
|
||||
def new_award_emoji(awardable, current_user)
|
||||
mark_pending_todos_as_done(awardable, current_user)
|
||||
end
|
||||
|
||||
# When marking pending todos as done we should:
|
||||
#
|
||||
# * mark all pending todos related to the target for the current user as done
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
|
||||
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
|
||||
- awards_sort(grouped_emojis).each do |emoji, awards|
|
||||
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
|
||||
= emoji_icon(emoji)
|
||||
%span.award-control-text.js-counter
|
||||
= awards.count
|
||||
|
||||
- if current_user
|
||||
:javascript
|
||||
gl.awardMenuUrl = "#{emojis_path}"
|
||||
|
||||
.award-menu-holder.js-award-holder
|
||||
%button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } }
|
||||
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
|
||||
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
|
||||
%span.award-control-text
|
||||
Add
|
|
@ -1,9 +1,9 @@
|
|||
.emoji-menu
|
||||
.emoji-menu-content
|
||||
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
|
||||
- AwardEmoji.emoji_by_category.each do |category, emojis|
|
||||
- Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
|
||||
%h5.emoji-menu-title
|
||||
= AwardEmoji::CATEGORIES[category]
|
||||
= Gitlab::AwardEmoji::CATEGORIES[category]
|
||||
%ul.clearfix.emoji-menu-list
|
||||
- emojis.each do |emoji|
|
||||
%li.pull-left.text-center.emoji-menu-list-item
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
= icon('thumbs-down')
|
||||
= downvotes
|
||||
|
||||
- note_count = issue.notes.user.nonawards.count
|
||||
- note_count = issue.notes.user.count
|
||||
%li
|
||||
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
|
||||
= icon('comments')
|
||||
|
|
|
@ -68,9 +68,9 @@
|
|||
#related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
|
||||
// This element is filled in using JavaScript.
|
||||
|
||||
.content-block.content-block-small
|
||||
= render 'new_branch'
|
||||
= render 'votes/votes_block', votable: @issue
|
||||
.content-block.content-block-small
|
||||
= render 'new_branch'
|
||||
= render 'award_emoji/awards_block', awardable: @issue, inline: true
|
||||
|
||||
%section.issuable-discussion
|
||||
= render 'projects/issues/discussion'
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
= icon('thumbs-down')
|
||||
= downvotes
|
||||
|
||||
- note_count = merge_request.mr_and_commit_notes.user.nonawards.count
|
||||
- note_count = merge_request.mr_and_commit_notes.user.count
|
||||
%li
|
||||
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
|
||||
= icon('comments')
|
||||
|
|
|
@ -6,4 +6,3 @@
|
|||
|
||||
- if @merge_requests.present?
|
||||
= paginate @merge_requests, theme: "gitlab"
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
%li.notes-tab
|
||||
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
|
||||
Discussion
|
||||
%span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count
|
||||
%span.badge= @merge_request.mr_and_commit_notes.user.count
|
||||
%li.commits-tab
|
||||
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
|
||||
Commits
|
||||
|
@ -67,7 +67,7 @@
|
|||
.tab-content
|
||||
#notes.notes.tab-pane.voting_notes
|
||||
.content-block.content-block-small.oneline-block
|
||||
= render 'votes/votes_block', votable: @merge_request
|
||||
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
|
||||
|
||||
.row
|
||||
%section.col-md-12
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
.awards.votes-block
|
||||
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
|
||||
%button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
|
||||
= emoji_icon(emoji, sprite: false)
|
||||
%span.award-control-text.js-counter
|
||||
= notes.count
|
||||
|
||||
- if current_user
|
||||
%div.award-menu-holder.js-award-holder
|
||||
%a.btn.award-control.js-add-award{"href" => "#"}
|
||||
= icon('smile-o', {class: "award-control-icon"})
|
||||
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
|
||||
%span.award-control-text
|
||||
Add
|
||||
|
||||
- if current_user
|
||||
:javascript
|
||||
var getEmojisUrl = "#{emojis_path}";
|
||||
var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
|
||||
var noteableType = "#{votable.class.name.underscore}";
|
||||
var noteableId = "#{votable.id}";
|
||||
var unicodes = #{AwardEmoji.unicode.to_json};
|
||||
|
||||
window.awardsHandler = new AwardsHandler(
|
||||
getEmojisUrl,
|
||||
postEmojiUrl,
|
||||
noteableType,
|
||||
noteableId,
|
||||
unicodes
|
||||
);
|
|
@ -8,3 +8,7 @@
|
|||
# inflect.irregular 'person', 'people'
|
||||
# inflect.uncountable %w( fish sheep )
|
||||
# end
|
||||
#
|
||||
ActiveSupport::Inflector.inflections do |inflect|
|
||||
inflect.uncountable %w(award_emoji)
|
||||
end
|
||||
|
|
|
@ -652,6 +652,7 @@ Rails.application.routes.draw do
|
|||
post :cancel_merge_when_build_succeeds
|
||||
get :ci_status
|
||||
post :toggle_subscription
|
||||
post :toggle_award_emoji
|
||||
post :remove_wip
|
||||
end
|
||||
|
||||
|
@ -727,6 +728,7 @@ Rails.application.routes.draw do
|
|||
resources :issues, constraints: { id: /\d+/ } do
|
||||
member do
|
||||
post :toggle_subscription
|
||||
post :toggle_award_emoji
|
||||
get :referenced_merge_requests
|
||||
get :related_branches
|
||||
get :can_create_branch
|
||||
|
@ -757,10 +759,6 @@ Rails.application.routes.draw do
|
|||
member do
|
||||
delete :delete_attachment
|
||||
end
|
||||
|
||||
collection do
|
||||
post :award_toggle
|
||||
end
|
||||
end
|
||||
|
||||
resources :uploads, only: [:create] do
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
class AddAwardEmoji < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :award_emoji do |t|
|
||||
t.string :name
|
||||
t.references :user
|
||||
t.references :awardable, polymorphic: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :award_emoji, :user_id
|
||||
add_index :award_emoji, [:awardable_type, :awardable_id]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration
|
||||
def change
|
||||
def up
|
||||
execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
|
||||
|
||||
execute "DELETE FROM notes WHERE is_award = true"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class RemoveNoteIsAward < ActiveRecord::Migration
|
||||
def change
|
||||
remove_column :notes, :is_award, :boolean
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -100,6 +100,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do
|
|||
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
|
||||
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
|
||||
|
||||
create_table "award_emoji", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "user_id"
|
||||
t.integer "awardable_id"
|
||||
t.string "awardable_type"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
|
||||
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
|
||||
|
||||
create_table "broadcast_messages", force: :cascade do |t|
|
||||
t.text "message", null: false
|
||||
t.datetime "starts_at"
|
||||
|
@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
|
|||
t.boolean "system", default: false, null: false
|
||||
t.text "st_diff"
|
||||
t.integer "updated_by_id"
|
||||
t.boolean "is_award", default: false, null: false
|
||||
t.string "type"
|
||||
end
|
||||
|
||||
|
@ -646,7 +657,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
|
|||
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
|
||||
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
|
||||
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
|
||||
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
|
||||
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
|
||||
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
|
||||
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
|
||||
|
|
|
@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
|
||||
issue = Issue.find_by(title: 'Release 0.4')
|
||||
create_list(:upvote_note, 2, project: project, noteable: issue)
|
||||
create(:downvote_note, project: project, noteable: issue)
|
||||
awardable = Issue.find_by(title: 'Release 0.4')
|
||||
create_list(:award_emoji, 2, awardable: awardable)
|
||||
create(:award_emoji, :downvote, awardable: awardable)
|
||||
end
|
||||
|
||||
step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
|
||||
issue = Issue.find_by(title: 'Tweet control')
|
||||
create(:upvote_note, project: project, noteable: issue)
|
||||
create_list(:downvote_note, 2, project: project, noteable: issue)
|
||||
awardable = Issue.find_by(title: 'Tweet control')
|
||||
create(:award_emoji, :upvote, awardable: awardable)
|
||||
create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
|
||||
end
|
||||
|
||||
step 'The list should be sorted by "Least popular"' do
|
||||
|
|
|
@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
|
|||
|
||||
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
|
||||
merge_request = MergeRequest.find_by(title: 'Bug NS-04')
|
||||
create_list(:upvote_note, 2, project: project, noteable: merge_request)
|
||||
create(:downvote_note, project: project, noteable: merge_request)
|
||||
create_list(:award_emoji, 2, awardable: merge_request)
|
||||
create(:award_emoji, :downvote, awardable: merge_request)
|
||||
end
|
||||
|
||||
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
|
||||
merge_request = MergeRequest.find_by(title: 'Bug NS-06')
|
||||
create(:upvote_note, project: project, noteable: merge_request)
|
||||
create_list(:downvote_note, 2, project: project, noteable: merge_request)
|
||||
awardable = MergeRequest.find_by(title: 'Bug NS-06')
|
||||
create(:award_emoji, awardable: awardable)
|
||||
create_list(:award_emoji, 2, :downvote, awardable: awardable)
|
||||
end
|
||||
|
||||
step 'The list should be sorted by "Least popular"' do
|
||||
|
|
|
@ -171,15 +171,17 @@ module API
|
|||
expose :label_names, as: :labels
|
||||
expose :milestone, using: Entities::Milestone
|
||||
expose :assignee, :author, using: Entities::UserBasic
|
||||
|
||||
expose :subscribed do |issue, options|
|
||||
issue.subscribed?(options[:current_user])
|
||||
end
|
||||
expose :user_notes_count
|
||||
expose :upvotes, :downvotes
|
||||
end
|
||||
|
||||
class MergeRequest < ProjectEntity
|
||||
expose :target_branch, :source_branch
|
||||
expose :upvotes, :downvotes
|
||||
expose :upvotes, :downvotes
|
||||
expose :author, :assignee, using: Entities::UserBasic
|
||||
expose :source_project_id, :target_project_id
|
||||
expose :label_names, as: :labels
|
||||
|
@ -217,8 +219,8 @@ module API
|
|||
expose :system?, as: :system
|
||||
expose :noteable_id, :noteable_type
|
||||
# upvote? and downvote? are deprecated, always return false
|
||||
expose :upvote?, as: :upvote
|
||||
expose :downvote?, as: :downvote
|
||||
expose(:upvote?) { |note| false }
|
||||
expose(:downvote?) { |note| false }
|
||||
end
|
||||
|
||||
class MRNote < Grape::Entity
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
class AwardEmoji
|
||||
CATEGORIES = {
|
||||
other: "Other",
|
||||
objects: "Objects",
|
||||
places: "Places",
|
||||
travel_places: "Travel",
|
||||
emoticons: "Emoticons",
|
||||
objects_symbols: "Symbols",
|
||||
nature: "Nature",
|
||||
celebration: "Celebration",
|
||||
people: "People",
|
||||
activity: "Activity",
|
||||
flags: "Flags",
|
||||
food_drink: "Food"
|
||||
}.with_indifferent_access
|
||||
|
||||
CATEGORY_ALIASES = {
|
||||
symbols: "objects_symbols",
|
||||
foods: "food_drink",
|
||||
travel: "travel_places"
|
||||
}.with_indifferent_access
|
||||
|
||||
def self.normilize_emoji_name(name)
|
||||
aliases[name] || name
|
||||
end
|
||||
|
||||
def self.emoji_by_category
|
||||
unless @emoji_by_category
|
||||
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
|
||||
|
||||
emojis.each do |emoji_name, data|
|
||||
data["name"] = emoji_name
|
||||
|
||||
# Skip Fitzpatrick(tone) modifiers
|
||||
next if data["category"] == "modifier"
|
||||
|
||||
category = CATEGORY_ALIASES[data["category"]] || data["category"]
|
||||
|
||||
@emoji_by_category[category] << data
|
||||
end
|
||||
|
||||
@emoji_by_category = @emoji_by_category.sort.to_h
|
||||
end
|
||||
|
||||
@emoji_by_category
|
||||
end
|
||||
|
||||
def self.emojis
|
||||
@emojis ||= begin
|
||||
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
|
||||
JSON.parse(File.read(json_path))
|
||||
end
|
||||
end
|
||||
|
||||
def self.unicode
|
||||
@unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!)
|
||||
end
|
||||
|
||||
def self.aliases
|
||||
@aliases ||= begin
|
||||
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
|
||||
JSON.parse(File.read(json_path))
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array of Emoji names and their asset URLs.
|
||||
def self.urls
|
||||
@urls ||= begin
|
||||
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
|
||||
prefix = Gitlab::Application.config.assets.prefix
|
||||
digest = Gitlab::Application.config.assets.digest
|
||||
|
||||
JSON.parse(File.read(path)).map do |hash|
|
||||
if digest
|
||||
fname = "#{hash['unicode']}-#{hash['digest']}"
|
||||
else
|
||||
fname = hash['unicode']
|
||||
end
|
||||
|
||||
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,84 @@
|
|||
module Gitlab
|
||||
class AwardEmoji
|
||||
CATEGORIES = {
|
||||
other: "Other",
|
||||
objects: "Objects",
|
||||
places: "Places",
|
||||
travel_places: "Travel",
|
||||
emoticons: "Emoticons",
|
||||
objects_symbols: "Symbols",
|
||||
nature: "Nature",
|
||||
celebration: "Celebration",
|
||||
people: "People",
|
||||
activity: "Activity",
|
||||
flags: "Flags",
|
||||
food_drink: "Food"
|
||||
}.with_indifferent_access
|
||||
|
||||
CATEGORY_ALIASES = {
|
||||
symbols: "objects_symbols",
|
||||
foods: "food_drink",
|
||||
travel: "travel_places"
|
||||
}.with_indifferent_access
|
||||
|
||||
def self.normalize_emoji_name(name)
|
||||
aliases[name] || name
|
||||
end
|
||||
|
||||
def self.emoji_by_category
|
||||
unless @emoji_by_category
|
||||
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
|
||||
|
||||
emojis.each do |emoji_name, data|
|
||||
data["name"] = emoji_name
|
||||
|
||||
# Skip Fitzpatrick(tone) modifiers
|
||||
next if data["category"] == "modifier"
|
||||
|
||||
category = CATEGORY_ALIASES[data["category"]] || data["category"]
|
||||
|
||||
@emoji_by_category[category] << data
|
||||
end
|
||||
|
||||
@emoji_by_category = @emoji_by_category.sort.to_h
|
||||
end
|
||||
|
||||
@emoji_by_category
|
||||
end
|
||||
|
||||
def self.emojis
|
||||
@emojis ||=
|
||||
begin
|
||||
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
|
||||
JSON.parse(File.read(json_path))
|
||||
end
|
||||
end
|
||||
|
||||
def self.aliases
|
||||
@aliases ||=
|
||||
begin
|
||||
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
|
||||
JSON.parse(File.read(json_path))
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array of Emoji names and their asset URLs.
|
||||
def self.urls
|
||||
@urls ||= begin
|
||||
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
|
||||
prefix = Gitlab::Application.config.assets.prefix
|
||||
digest = Gitlab::Application.config.assets.digest
|
||||
|
||||
JSON.parse(File.read(path)).map do |hash|
|
||||
if digest
|
||||
fname = "#{hash['unicode']}-#{hash['digest']}"
|
||||
else
|
||||
fname = hash['unicode']
|
||||
end
|
||||
|
||||
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31,9 +31,9 @@ describe GroupsController do
|
|||
let(:issue_2) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
create_list(:upvote_note, 3, project: project, noteable: issue_2)
|
||||
create_list(:upvote_note, 2, project: project, noteable: issue_1)
|
||||
create_list(:downvote_note, 2, project: project, noteable: issue_2)
|
||||
create_list(:award_emoji, 3, awardable: issue_2)
|
||||
create_list(:award_emoji, 2, awardable: issue_1)
|
||||
create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
@ -56,9 +56,9 @@ describe GroupsController do
|
|||
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
|
||||
|
||||
before do
|
||||
create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
|
||||
create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
|
||||
create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
|
||||
create_list(:award_emoji, 3, awardable: merge_request_2)
|
||||
create_list(:award_emoji, 2, awardable: merge_request_1)
|
||||
create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
|
|
@ -250,4 +250,20 @@ describe Projects::IssuesController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #toggle_award_emoji' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
it "toggles the award emoji" do
|
||||
expect do
|
||||
post(:toggle_award_emoji, namespace_id: project.namespace.path,
|
||||
project_id: project.path, id: issue.iid, name: "thumbsup")
|
||||
end.to change { issue.award_emoji.count }.by(1)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
FactoryGirl.define do
|
||||
factory :award_emoji do
|
||||
name "thumbsup"
|
||||
user
|
||||
awardable factory: :issue
|
||||
|
||||
trait :upvote
|
||||
trait :downvote do
|
||||
name "thumbsdown"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,8 +16,6 @@ FactoryGirl.define do
|
|||
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote
|
||||
factory :note_on_project_snippet, traits: [:on_project_snippet]
|
||||
factory :system_note, traits: [:system]
|
||||
factory :downvote_note, traits: [:award, :downvote]
|
||||
factory :upvote_note, traits: [:award, :upvote]
|
||||
|
||||
trait :on_commit do
|
||||
noteable nil
|
||||
|
@ -46,10 +44,6 @@ FactoryGirl.define do
|
|||
system true
|
||||
end
|
||||
|
||||
trait :award do
|
||||
is_award true
|
||||
end
|
||||
|
||||
trait :downvote do
|
||||
note "thumbsdown"
|
||||
end
|
||||
|
|
|
@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do
|
|||
end
|
||||
|
||||
context 'click the thumbsup emoji' do
|
||||
|
||||
it 'should increment the thumbsup emoji', js: true do
|
||||
find('[data-emoji="thumbsup"]').click
|
||||
sleep 2
|
||||
|
@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do
|
|||
end
|
||||
|
||||
context 'click the thumbsdown emoji' do
|
||||
|
||||
it 'should increment the thumbsdown emoji', js: true do
|
||||
find('[data-emoji="thumbsdown"]').click
|
||||
sleep 2
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
require 'rails_helper'
|
||||
|
||||
feature 'Issue awards', js: true, feature: true do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
describe 'logged in' do
|
||||
before do
|
||||
login_as(user)
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
end
|
||||
|
||||
it 'should add award to issue' do
|
||||
first('.js-emoji-btn').click
|
||||
expect(page).to have_selector('.js-emoji-btn.active')
|
||||
expect(first('.js-emoji-btn')).to have_content '1'
|
||||
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
expect(first('.js-emoji-btn')).to have_content '1'
|
||||
end
|
||||
|
||||
it 'should remove award from issue' do
|
||||
first('.js-emoji-btn').click
|
||||
find('.js-emoji-btn.active').click
|
||||
expect(first('.js-emoji-btn')).to have_content '0'
|
||||
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
expect(first('.js-emoji-btn')).to have_content '0'
|
||||
end
|
||||
|
||||
it 'should only have one menu on the page' do
|
||||
first('.js-add-award').click
|
||||
expect(page).to have_selector('.emoji-menu')
|
||||
|
||||
expect(page).to have_selector('.emoji-menu', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'logged out' do
|
||||
before do
|
||||
visit namespace_project_issue_path(project.namespace, project, issue)
|
||||
end
|
||||
|
||||
it 'should not see award menu button' do
|
||||
expect(page).not_to have_selector('.js-award-holder')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -125,7 +125,7 @@ describe 'Issues', feature: true do
|
|||
describe 'Issue info' do
|
||||
it 'excludes award_emoji from comment count' do
|
||||
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
|
||||
create(:upvote_note, noteable: issue, project: project)
|
||||
create(:award_emoji, awardable: issue)
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
require 'rails_helper'
|
||||
|
||||
feature 'Merge request awards', js: true, feature: true do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
||||
describe 'logged in' do
|
||||
before do
|
||||
login_as(user)
|
||||
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
end
|
||||
|
||||
it 'should add award to merge request' do
|
||||
first('.js-emoji-btn').click
|
||||
expect(page).to have_selector('.js-emoji-btn.active')
|
||||
expect(first('.js-emoji-btn')).to have_content '1'
|
||||
|
||||
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
expect(first('.js-emoji-btn')).to have_content '1'
|
||||
end
|
||||
|
||||
it 'should remove award from merge request' do
|
||||
first('.js-emoji-btn').click
|
||||
find('.js-emoji-btn.active').click
|
||||
expect(first('.js-emoji-btn')).to have_content '0'
|
||||
|
||||
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
expect(first('.js-emoji-btn')).to have_content '0'
|
||||
end
|
||||
|
||||
it 'should only have one menu on the page' do
|
||||
first('.js-add-award').click
|
||||
expect(page).to have_selector('.emoji-menu')
|
||||
|
||||
expect(page).to have_selector('.emoji-menu', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'logged out' do
|
||||
before do
|
||||
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
end
|
||||
|
||||
it 'should not see award menu button' do
|
||||
expect(page).not_to have_selector('.js-award-holder')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,20 +4,6 @@ describe 'Comments', feature: true do
|
|||
include RepoHelpers
|
||||
include WaitForAjax
|
||||
|
||||
describe 'On merge requests page', feature: true do
|
||||
it 'excludes award_emoji from comment count' do
|
||||
merge_request = create(:merge_request)
|
||||
project = merge_request.source_project
|
||||
create(:upvote_note, noteable: merge_request, project: project)
|
||||
|
||||
login_as :admin
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
|
||||
expect(merge_request.mr_and_commit_notes.count).to eq 1
|
||||
expect(page.all('.merge-request-no-comments').first.text).to eq "0"
|
||||
end
|
||||
end
|
||||
|
||||
describe 'On a merge request', js: true, feature: true do
|
||||
let!(:project) { create(:project) }
|
||||
let!(:merge_request) do
|
||||
|
@ -147,17 +133,6 @@ describe 'Comments', feature: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'comment info' do
|
||||
it 'excludes award_emoji from comment count' do
|
||||
create(:upvote_note, noteable: merge_request, project: project)
|
||||
|
||||
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
|
||||
|
||||
expect(merge_request.mr_and_commit_notes.count).to eq 2
|
||||
expect(find('.notes-tab span.badge').text).to eq "1"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'On a merge request diff', js: true, feature: true do
|
||||
|
|
|
@ -163,18 +163,15 @@ describe IssuesHelper do
|
|||
it { is_expected.to eq("!1, !2, or !3") }
|
||||
end
|
||||
|
||||
describe "note_active_class" do
|
||||
before do
|
||||
@note = create :note
|
||||
@note1 = create :note
|
||||
end
|
||||
describe '#award_active_class' do
|
||||
let!(:upvote) { create(:award_emoji) }
|
||||
|
||||
it "returns empty string for unauthenticated user" do
|
||||
expect(note_active_class(Note.all, nil)).to eq("")
|
||||
expect(award_active_class(AwardEmoji.all, nil)).to eq("")
|
||||
end
|
||||
|
||||
it "returns active string for author" do
|
||||
expect(note_active_class(Note.all, @note.author)).to eq("active")
|
||||
expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AwardEmoji do
|
||||
describe Gitlab::AwardEmoji do
|
||||
describe '.urls' do
|
||||
subject { AwardEmoji.urls }
|
||||
subject { Gitlab::AwardEmoji.urls }
|
||||
|
||||
it { is_expected.to be_an_instance_of(Array) }
|
||||
it { is_expected.not_to be_empty }
|
||||
|
@ -19,7 +19,7 @@ describe AwardEmoji do
|
|||
|
||||
describe '.emoji_by_category' do
|
||||
it "only contains known categories" do
|
||||
undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys
|
||||
undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
|
||||
expect(undefined_categories).to be_empty
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AwardEmoji, models: true do
|
||||
describe 'Associations' do
|
||||
it { is_expected.to belong_to(:awardable) }
|
||||
it { is_expected.to belong_to(:user) }
|
||||
end
|
||||
|
||||
describe 'modules' do
|
||||
it { is_expected.to include_module(Participable) }
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_presence_of(:awardable) }
|
||||
it { is_expected.to validate_presence_of(:user) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
|
||||
# To circumvent a bug in the shoulda matchers
|
||||
describe "scoped uniqueness validation" do
|
||||
it "rejects duplicate award emoji" do
|
||||
user = create(:user)
|
||||
issue = create(:issue)
|
||||
create(:award_emoji, user: user, awardable: issue)
|
||||
new_award = build(:award_emoji, user: user, awardable: issue)
|
||||
|
||||
expect(new_award).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Issue, "Awardable" do
|
||||
let!(:issue) { create(:issue) }
|
||||
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
|
||||
|
||||
describe "Associations" do
|
||||
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
|
||||
end
|
||||
|
||||
describe "ClassMethods" do
|
||||
let!(:issue2) { create(:issue) }
|
||||
|
||||
before do
|
||||
create(:award_emoji, awardable: issue2)
|
||||
end
|
||||
|
||||
it "orders on upvotes" do
|
||||
expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
|
||||
end
|
||||
|
||||
it "orders on downvotes" do
|
||||
expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
|
||||
end
|
||||
end
|
||||
|
||||
describe "#upvotes" do
|
||||
it "counts the number of upvotes" do
|
||||
expect(issue.upvotes).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "#downvotes" do
|
||||
it "counts the number of downvotes" do
|
||||
expect(issue.downvotes).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "#toggle_award_emoji" do
|
||||
it "adds an emoji if it isn't awarded yet" do
|
||||
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by 1
|
||||
end
|
||||
|
||||
it "toggles already awarded emoji" do
|
||||
|
||||
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by -1
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,10 @@ describe Issue, "Issuable" do
|
|||
it { is_expected.to have_many(:todos).dependent(:destroy) }
|
||||
end
|
||||
|
||||
describe 'Included modules' do
|
||||
it { is_expected.to include_module(Awardable) }
|
||||
end
|
||||
|
||||
describe "Validation" do
|
||||
before do
|
||||
allow(subject).to receive(:set_iid).and_return(false)
|
||||
|
@ -245,8 +249,8 @@ describe Issue, "Issuable" do
|
|||
let(:project) { issue.project }
|
||||
|
||||
before do
|
||||
issue.notes.awards.create!(note: "thumbsup", author: user, project: project)
|
||||
issue.notes.awards.create!(note: "thumbsdown", author: user, project: project)
|
||||
create(:award_emoji, :upvote, awardable: issue)
|
||||
create(:award_emoji, :downvote, awardable: issue)
|
||||
end
|
||||
|
||||
it "returns correct values" do
|
||||
|
|
|
@ -171,23 +171,6 @@ describe Note, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.grouped_awards' do
|
||||
before do
|
||||
create :note, note: "smile", is_award: true
|
||||
create :note, note: "smile", is_award: true
|
||||
end
|
||||
|
||||
it "returns grouped hash of notes" do
|
||||
expect(Note.grouped_awards.keys.size).to eq(3)
|
||||
expect(Note.grouped_awards["smile"]).to match_array(Note.all)
|
||||
end
|
||||
|
||||
it "returns thumbsup and thumbsdown always" do
|
||||
expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
|
||||
expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
|
||||
end
|
||||
end
|
||||
|
||||
describe "editable?" do
|
||||
it "returns true" do
|
||||
note = build(:note)
|
||||
|
@ -198,11 +181,6 @@ describe Note, models: true do
|
|||
note = build(:note, system: true)
|
||||
expect(note.editable?).to be_falsy
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
note = build(:note, is_award: true, note: "smiley")
|
||||
expect(note.editable?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe "cross_reference_not_visible_for?" do
|
||||
|
@ -229,29 +207,6 @@ describe Note, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "set_award!" do
|
||||
let(:merge_request) { create :merge_request }
|
||||
|
||||
it "converts aliases to actual name" do
|
||||
note = create(:note, note: ":+1:",
|
||||
noteable: merge_request,
|
||||
project: merge_request.project)
|
||||
|
||||
expect(note.reload.note).to eq("thumbsup")
|
||||
end
|
||||
|
||||
it "is not an award emoji when comment is on a diff" do
|
||||
note = create(:note_on_merge_request_diff, note: ":blowfish:",
|
||||
noteable: merge_request,
|
||||
project: merge_request.project,
|
||||
line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2")
|
||||
note = note.reload
|
||||
|
||||
expect(note.note).to eq(":blowfish:")
|
||||
expect(note.is_award?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe 'clear_blank_line_code!' do
|
||||
it 'clears a blank line code before validation' do
|
||||
note = build(:note, line_code: ' ')
|
||||
|
|
|
@ -30,6 +30,7 @@ describe User, models: true do
|
|||
it { is_expected.to have_one(:abuse_report) }
|
||||
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:todos).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
|
|
|
@ -249,7 +249,6 @@ describe API::API, api: true do
|
|||
expect(json_response['milestone']).to be_a Hash
|
||||
expect(json_response['assignee']).to be_a Hash
|
||||
expect(json_response['author']).to be_a Hash
|
||||
expect(json_response['user_notes_count']).to be(1)
|
||||
end
|
||||
|
||||
it "should return a project issue by id" do
|
||||
|
|
|
@ -138,7 +138,6 @@ describe API::API, api: true do
|
|||
expect(json_response['work_in_progress']).to be_falsy
|
||||
expect(json_response['merge_when_build_succeeds']).to be_falsy
|
||||
expect(json_response['merge_status']).to eq('can_be_merged')
|
||||
expect(json_response['user_notes_count']).to be(2)
|
||||
end
|
||||
|
||||
it "should return merge_request" do
|
||||
|
|
|
@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do
|
|||
let!(:milestone2) do
|
||||
create(:milestone, project_id: new_project.id, title: 'v9.0')
|
||||
end
|
||||
let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
|
||||
|
||||
let!(:new_issue) { move_service.execute(old_issue, new_project) }
|
||||
end
|
||||
|
@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do
|
|||
it 'preserves create time' do
|
||||
expect(old_issue.created_at).to eq new_issue.created_at
|
||||
end
|
||||
|
||||
it 'moves the award emoji' do
|
||||
expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
|
||||
end
|
||||
end
|
||||
|
||||
context 'issue with notes' do
|
||||
|
|
|
@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do
|
|||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
|
||||
|
||||
@note = Notes::CreateService.new(project, user, opts).execute
|
||||
end
|
||||
|
||||
|
@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
|
|||
project.team << [user, :master]
|
||||
end
|
||||
|
||||
it "creates emoji note" do
|
||||
it "creates an award emoji" do
|
||||
opts = {
|
||||
note: ':smile: ',
|
||||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
note = Notes::CreateService.new(project, user, opts).execute
|
||||
|
||||
@note = Notes::CreateService.new(project, user, opts).execute
|
||||
|
||||
expect(@note).to be_valid
|
||||
expect(@note.note).to eq('smile')
|
||||
expect(@note.is_award).to be_truthy
|
||||
expect(note).to be_valid
|
||||
expect(note.name).to eq('smile')
|
||||
end
|
||||
|
||||
it "creates regular note if emoji name is invalid" do
|
||||
|
@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do
|
|||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
note = Notes::CreateService.new(project, user, opts).execute
|
||||
|
||||
@note = Notes::CreateService.new(project, user, opts).execute
|
||||
expect(note).to be_valid
|
||||
expect(note.note).to eq(opts[:note])
|
||||
end
|
||||
|
||||
expect(@note).to be_valid
|
||||
expect(@note.note).to eq(opts[:note])
|
||||
expect(@note.is_award).to be_falsy
|
||||
it "normalizes the emoji name" do
|
||||
opts = {
|
||||
note: ':+1:',
|
||||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
|
||||
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
|
||||
|
||||
Notes::CreateService.new(project, user, opts).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -156,7 +156,6 @@ describe TodoService, services: true do
|
|||
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
|
||||
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
|
||||
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
|
||||
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
|
||||
let(:system_note) { create(:system_note, project: project, noteable: issue) }
|
||||
|
||||
it 'mark related pending todos to the noteable for the note author as done' do
|
||||
|
@ -169,13 +168,6 @@ describe TodoService, services: true do
|
|||
expect(second_todo.reload).to be_done
|
||||
end
|
||||
|
||||
it 'mark related pending todos to the noteable for the award note author as done' do
|
||||
service.new_note(award_note, john_doe)
|
||||
|
||||
expect(first_todo.reload).to be_done
|
||||
expect(second_todo.reload).to be_done
|
||||
end
|
||||
|
||||
it 'does not mark related pending todos it is a system note' do
|
||||
service.new_note(system_note, john_doe)
|
||||
|
||||
|
@ -306,6 +298,15 @@ describe TodoService, services: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#new_award_emoji' do
|
||||
it 'marks related pending todos to the target for the user as done' do
|
||||
todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author)
|
||||
service.new_award_emoji(mr_assigned, john_doe)
|
||||
|
||||
expect(todo.reload).to be_done
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_request_build_failed' do
|
||||
it 'creates a pending todo for the merge request author' do
|
||||
service.merge_request_build_failed(mr_unassigned)
|
||||
|
|
Loading…
Reference in New Issue