Commit 4d4a9b7c authored by Douwe Maan's avatar Douwe Maan

Merge branch 'notes-are-awardables' into 'master'

Notes are awardables

## What does this MR do?

Makes sure we can  comments/notes.

## What are the relevant issue numbers?
Follows up upon !2901, depends on !3785

Closes #3655 

## Screenshots (if relevant)
TODO



See merge request !4291
parents ac4e3e8c 8dc1fa0d
...@@ -23,7 +23,7 @@ class Dispatcher ...@@ -23,7 +23,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -54,7 +54,7 @@ class Dispatcher ...@@ -54,7 +54,7 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler() gl.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
window.emojiAliases = -> gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -162,13 +162,14 @@ class @Notes ...@@ -162,13 +162,14 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
unless note.valid unless note.valid
if note.award if note.award
flash = new Flash('You have already used this award emoji!', 'alert') flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content') flash.pinTo('.header-content')
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.name) votesBlock = $('.js-awards-block').eq 0
awardsHandler.scrollToAwards() gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding; padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color; border-color: $table-border-color;
color: $gl-gray; color: $gl-gray;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
...@@ -95,6 +95,7 @@ ...@@ -95,6 +95,7 @@
.award-control { .award-control {
margin-right: 5px; margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
line-height: 20px; line-height: 20px;
...@@ -108,7 +109,8 @@ ...@@ -108,7 +109,8 @@
} }
&.is-loading { &.is-loading {
.award-control-icon-normal { .award-control-icon-normal,
.emoji-icon {
display: none; display: none;
} }
......
...@@ -69,6 +69,10 @@ ul.notes { ...@@ -69,6 +69,10 @@ ul.notes {
.note-edit-form { .note-edit-form {
display: block; display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
} }
} }
...@@ -116,10 +120,38 @@ ul.notes { ...@@ -116,10 +120,38 @@ ul.notes {
} }
} }
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
.award-control {
font-size: 13px;
padding: 2px 5px;
}
}
.note-header { .note-header {
padding-bottom: 3px; padding-bottom: 3px;
} }
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
} }
} }
......
...@@ -9,13 +9,22 @@ module ToggleAwardEmoji ...@@ -9,13 +9,22 @@ module ToggleAwardEmoji
name = params.require(:name) name = params.require(:name)
awardable.toggle_award_emoji(name, current_user) awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(awardable, current_user) TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true } render json: { ok: true }
end end
private private
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
else
awardable
end
end
def awardable def awardable
raise NotImplementedError raise NotImplementedError
end end
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include ToggleAwardEmoji
# Authorize # Authorize
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
...@@ -61,6 +63,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -61,6 +63,7 @@ class Projects::NotesController < Projects::ApplicationController
def note def note
@note ||= @project.notes.find(params[:id]) @note ||= @project.notes.find(params[:id])
end end
alias_method :awardable, :note
def note_to_html(note) def note_to_html(note)
render_to_string( render_to_string(
......
...@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base ...@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Participable include Participable
include Mentionable include Mentionable
include Awardable
default_value_for :system, false default_value_for :system, false
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
gl.awardMenuUrl = "#{emojis_path}" gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder .award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } %button.btn.award-control.js-add-award{ type: "button" }
= icon('smile-o', class: "award-control-icon award-control-icon-normal") = icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading") = icon('spinner spin', class: "award-control-icon award-control-icon-loading")
%span.award-control-text %span.award-control-text
......
...@@ -22,6 +22,9 @@ ...@@ -22,6 +22,9 @@
%span.note-role %span.note-role
= access = access
- if note_editable - if note_editable
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o')
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
...@@ -30,9 +33,11 @@ ...@@ -30,9 +33,11 @@
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) .note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.attachment.url - if note.attachment.url
.note-attachment .note-attachment
......
...@@ -758,6 +758,7 @@ Rails.application.routes.draw do ...@@ -758,6 +758,7 @@ Rails.application.routes.draw do
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do member do
post :toggle_award_emoji
delete :delete_attachment delete :delete_attachment
end end
end end
......
require('spec_helper')
describe Projects::NotesController do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
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: note.id, name: "thumbsup")
end.to change { note.award_emoji.count }.by(1)
expect(response.status).to eq(200)
end
it "removes the already awarded emoji" do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: note.id, name: "thumbsup")
expect do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: note.id, name: "thumbsup")
end.to change { AwardEmoji.count }.by(-1)
expect(response.status).to eq(200)
end
end
end
#= require awards_handler
#= require jquery
#= require jquery.cookie
#= require ./fixtures/emoji_menu
awardsHandler = null
window.gl or= {}
gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
gl.awardMenuUrl = '/emojis'
lazyAssert = (done, assertFn) ->
setTimeout -> # Maybe jasmine.clock here?
assertFn()
done()
, 333
describe 'AwardsHandler', ->
fixture.preload 'awards_handler.html'
beforeEach ->
fixture.load 'awards_handler.html'
awardsHandler = new AwardsHandler
spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
spyOn(jQuery, 'get').and.callFake (req, cb) ->
expect(req).toBe '/emojis'
cb window.emojiMenu
describe '::showEmojiMenu', ->
it 'should show emoji menu when Add emoji button clicked', (done) ->
$('.js-add-award').eq(0).click()
lazyAssert done, ->
$emojiMenu = $ '.emoji-menu'
expect($emojiMenu.length).toBe 1
expect($emojiMenu.hasClass('is-visible')).toBe yes
expect($emojiMenu.find('#emoji_search').length).toBe 1
expect($('.js-awards-block.current').length).toBe 1
it 'should also show emoji menu for the smiley icon in notes', (done) ->
$('.note-action-button').click()
lazyAssert done, ->
$emojiMenu = $ '.emoji-menu'
expect($emojiMenu.length).toBe 1
it 'should remove emoji menu when body is clicked', (done) ->
$('.js-add-award').eq(0).click()
lazyAssert done, ->
$emojiMenu = $('.emoji-menu')
$('body').click()
expect($emojiMenu.length).toBe 1
expect($emojiMenu.hasClass('is-visible')).toBe no
expect($('.js-awards-block.current').length).toBe 0
describe '::addAwardToEmojiBar', ->
it 'should add emoji to votes block', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
expect($emojiButton.length).toBe 1
expect($emojiButton.next('.js-counter').text()).toBe '1'
expect($votesBlock.hasClass('hidden')).toBe no
it 'should remove the emoji when we click again', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
expect($emojiButton.length).toBe 0
it 'should decrement the emoji counter', ->
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
$emojiButton = $votesBlock.find '[data-emoji=heart]'
$emojiButton.next('.js-counter').text 5
awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
expect($emojiButton.length).toBe 1
expect($emojiButton.next('.js-counter').text()).toBe '4'
describe '::getAwardUrl', ->
it 'should return the url for request', ->
expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'
describe '::addAward and ::checkMutuality', ->
it 'should handle :+1: and :-1: mutuality', ->
awardUrl = awardsHandler.getAwardUrl()
$votesBlock = $('.js-awards-block').eq 0
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent()
$thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent()
awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no
expect($thumbsUpEmoji.hasClass('active')).toBe yes
expect($thumbsDownEmoji.hasClass('active')).toBe no
$thumbsUpEmoji.tooltip()
$thumbsDownEmoji.tooltip()
awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes
expect($thumbsUpEmoji.hasClass('active')).toBe no
expect($thumbsDownEmoji.hasClass('active')).toBe yes
describe '::removeEmoji', ->
it 'should remove emoji', ->
awardUrl = awardsHandler.getAwardUrl()
$votesBlock = $('.js-awards-block').eq 0
awardsHandler.addAward $votesBlock, awardUrl, 'fire', no
expect($votesBlock.find('[data-emoji=fire]').length).toBe 1
awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button')
expect($votesBlock.find('[data-emoji=fire]').length).toBe 0
describe 'search', ->
it 'should filter the emoji', ->
$('.js-add-award').eq(0).click()
expect($('[data-emoji=angel]').is(':visible')).toBe yes
expect($('[data-emoji=anger]').is(':visible')).toBe yes
$('#emoji_search').val('ali').trigger 'keyup'
expect($('[data-emoji=angel]').is(':visible')).toBe no
expect($('[data-emoji=anger]').is(':visible')).toBe no
expect($('[data-emoji=alien]').is(':visible')).toBe yes
expect($('h5.emoji-search').is(':visible')).toBe yes
describe 'emoji menu', ->
selector = '[data-emoji=sunglasses]'
openEmojiMenuAndAddEmoji = ->
$('.js-add-award').eq(0).click()
$menu = $ '.emoji-menu'
$block = $ '.js-awards-block'
$emoji = $menu.find ".emoji-menu-list-item #{selector}"
expect($emoji.length).toBe 1
expect($block.find(selector).length).toBe 0
$emoji.click()
expect($menu.hasClass('.is-visible')).toBe no
expect($block.find(selector).length).toBe 1
it 'should add selected emoji to awards block', ->
openEmojiMenuAndAddEmoji()
it 'should remove already selected emoji', ->
openEmojiMenuAndAddEmoji()
$('.js-add-award').eq(0).click()
$block = $ '.js-awards-block'
$emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}"
$emoji.click()
expect($block.find(selector).length).toBe 0
...@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', -> ...@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', ->
} }
it 'does not respond to other keyCodes', -> it 'does not respond to other keyCodes', ->
$('input').trigger(keydownEvent(keyCode: 32)) $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to Enter alone', -> it 'does not respond to Enter alone', ->
$('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to repeated events', -> it 'does not respond to repeated events', ->
$('input').trigger(keydownEvent(repeat: true)) $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
...@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', -> ...@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', ->
# only run the tests that apply to the current platform # only run the tests that apply to the current platform
if navigator.userAgent.match(/Macintosh/) if navigator.userAgent.match(/Macintosh/)
it 'responds to Meta+Enter', -> it 'responds to Meta+Enter', ->
$('input').trigger(keydownEvent()) $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered() expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', -> it 'excludes other modifier keys', ->
$('input').trigger(keydownEvent(altKey: true)) $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
$('input').trigger(keydownEvent(ctrlKey: true)) $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true))
$('input').trigger(keydownEvent(shiftKey: true)) $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
else else
it 'responds to Ctrl+Enter', -> it 'responds to Ctrl+Enter', ->
$('input').trigger(keydownEvent()) $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered() expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', -> it 'excludes other modifier keys', ->
$('input').trigger(keydownEvent(altKey: true)) $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
$('input').trigger(keydownEvent(metaKey: true)) $('input.quick-submit-input').trigger(keydownEvent(metaKey: true))
$('input').trigger(keydownEvent(shiftKey: true)) $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered() expect(@spies.submit).not.toHaveBeenTriggered()
......
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
.description.js-task-list-container.is-task-list-enabled
.wiki
%p Qui exercitationem magnam optio quae fuga earum odio.
%textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
%small.edited-text
.content-block.content-block-small
.awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
%span.award-control-text.js-counter 0
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
%span.award-control-text.js-counter 0
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%section.issuable-discussion
#notes
%ul#notes-list.notes.main-notes-list.timeline
%li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
.timeline-entry-inner
.timeline-icon
%a{:href => "/u/agustin"}
%img.avatar.s40{:alt => "", :src => "#"}/
.timeline-content
.note-header
%a.author_link{:href => "/u/agustin"}
%span.author Brenna Stokes
.inline.note-headline-light
@agustin commented
%a{:href => "#note_348"}
%time 11 days ago
.note-actions
%span.note-role Reporter
%a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
%i.fa.fa-spinner.fa-spin
%i.fa.fa-smile-o
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
.note-awards
.awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%form.js-quick-submit{ action: '/foo' } %form.js-quick-submit{ action: '/foo' }
%input{ type: 'text' } %input{ type: 'text', class: 'quick-submit-input'}
%textarea %textarea
%input{ type: 'submit'} Submit %input{ type: 'submit'} Submit
......
This diff is collapsed.
#= require jquery-ui #= require jquery-ui/autocomplete
#= require new_branch_form #= require new_branch_form
describe 'Branch', -> describe 'Branch', ->
......
...@@ -9,6 +9,16 @@ describe Note, models: true do ...@@ -9,6 +9,16 @@ describe Note, models: true do
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Mentionable) }
it { is_expected.to include_module(Awardable) }
it { is_expected.to include_module(Gitlab::CurrentSettings) }
end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:note) }
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
......
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