Merge branch '25437-just-emoji' into 'master'
#25437 Allow posting of just an emoji in comment; add /award slash command Closes #25437 See merge request !9382
This commit is contained in:
commit
523fb730c2
16 changed files with 138 additions and 86 deletions
|
@ -39,8 +39,9 @@ require('../../subbable_resource');
|
|||
listenForSlashCommands() {
|
||||
$(document).on('ajax:success', '.gfm-form', (e, data) => {
|
||||
const subscribedCommands = ['spend_time', 'time_estimate'];
|
||||
const changedCommands = data.commands_changes;
|
||||
|
||||
const changedCommands = data.commands_changes
|
||||
? Object.keys(data.commands_changes)
|
||||
: [];
|
||||
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
|
||||
this.fetchIssuable();
|
||||
}
|
||||
|
|
|
@ -246,12 +246,21 @@ require('./task_list');
|
|||
};
|
||||
|
||||
Notes.prototype.handleCreateChanges = function(note) {
|
||||
var votesBlock;
|
||||
if (typeof note === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
|
||||
$.get(mrRefreshWidgetUrl);
|
||||
if (note.commands_changes) {
|
||||
if ('merge' in note.commands_changes) {
|
||||
$.get(mrRefreshWidgetUrl);
|
||||
}
|
||||
|
||||
if ('emoji_award' in note.commands_changes) {
|
||||
votesBlock = $('.js-awards-block').eq(0);
|
||||
gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
|
||||
return gl.awardsHandler.scrollToAwards();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -262,26 +271,16 @@ require('./task_list');
|
|||
*/
|
||||
|
||||
Notes.prototype.renderNote = function(note) {
|
||||
var $notesList, votesBlock;
|
||||
var $notesList;
|
||||
if (!note.valid) {
|
||||
if (note.award) {
|
||||
new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
|
||||
}
|
||||
else {
|
||||
if (note.errors.commands_only) {
|
||||
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
|
||||
this.refresh();
|
||||
}
|
||||
if (note.errors.commands_only) {
|
||||
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
|
||||
this.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (note.award) {
|
||||
votesBlock = $('.js-awards-block').eq(0);
|
||||
gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
|
||||
return gl.awardsHandler.scrollToAwards();
|
||||
// render note if it not present in loaded list
|
||||
// or skip if rendered
|
||||
} else if (this.isNewNote(note)) {
|
||||
|
||||
if (this.isNewNote(note)) {
|
||||
this.note_ids.push(note.id);
|
||||
$notesList = $('ul.main-notes-list');
|
||||
$notesList.append(note.html).syntaxHighlight();
|
||||
|
|
|
@ -148,17 +148,10 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
|
||||
def note_json(note)
|
||||
attrs = {
|
||||
award: false,
|
||||
id: note.id
|
||||
}
|
||||
|
||||
if note.is_a?(AwardEmoji)
|
||||
attrs.merge!(
|
||||
valid: note.valid?,
|
||||
award: true,
|
||||
name: note.name
|
||||
)
|
||||
elsif note.persisted?
|
||||
if note.persisted?
|
||||
Banzai::NoteRenderer.render([note], @project, current_user)
|
||||
|
||||
attrs.merge!(
|
||||
|
@ -198,7 +191,7 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
attrs[:commands_changes] = note.commands_changes unless attrs[:award]
|
||||
attrs[:commands_changes] = note.commands_changes
|
||||
attrs
|
||||
end
|
||||
|
||||
|
|
|
@ -231,10 +231,6 @@ class Note < ActiveRecord::Base
|
|||
note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
|
||||
end
|
||||
|
||||
def award_emoji_name
|
||||
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
|
||||
end
|
||||
|
||||
def to_ability_name
|
||||
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
|
||||
end
|
||||
|
|
|
@ -203,6 +203,7 @@ class IssuableBaseService < BaseService
|
|||
change_state(issuable)
|
||||
change_subscription(issuable)
|
||||
change_todo(issuable)
|
||||
toggle_award(issuable)
|
||||
filter_params(issuable)
|
||||
old_labels = issuable.labels.to_a
|
||||
old_mentioned_users = issuable.mentioned_users.to_a
|
||||
|
@ -263,6 +264,14 @@ class IssuableBaseService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def toggle_award(issuable)
|
||||
award = params.delete(:emoji_award)
|
||||
if award
|
||||
todo_service.new_award_emoji(issuable, current_user)
|
||||
issuable.toggle_award_emoji(award, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def has_changes?(issuable, old_labels: [])
|
||||
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
|
||||
|
||||
|
|
|
@ -8,14 +8,6 @@ module Notes
|
|||
note.author = current_user
|
||||
note.system = false
|
||||
|
||||
if note.award_emoji?
|
||||
noteable = note.noteable
|
||||
if noteable.user_can_award?(current_user, note.award_emoji_name)
|
||||
todo_service.new_award_emoji(noteable, current_user)
|
||||
return noteable.create_award_emoji(note.award_emoji_name, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
# We execute commands (extracted from `params[:note]`) on the noteable
|
||||
# **before** we save the note because if the note consists of commands
|
||||
# only, there is no need be create a note!
|
||||
|
@ -48,7 +40,7 @@ module Notes
|
|||
note.errors.add(:commands_only, 'Commands applied')
|
||||
end
|
||||
|
||||
note.commands_changes = command_params.keys
|
||||
note.commands_changes = command_params
|
||||
end
|
||||
|
||||
note
|
||||
|
|
|
@ -255,6 +255,18 @@ module SlashCommands
|
|||
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
|
||||
end
|
||||
|
||||
desc 'Toggle emoji reward'
|
||||
params ':emoji:'
|
||||
condition do
|
||||
issuable.persisted?
|
||||
end
|
||||
command :award do |emoji|
|
||||
name = award_emoji_name(emoji)
|
||||
if name && issuable.user_can_award?(current_user, name)
|
||||
@updates[:emoji_award] = name
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Set time estimate'
|
||||
params '<1w 3d 2h 14m>'
|
||||
condition do
|
||||
|
@ -329,5 +341,10 @@ module SlashCommands
|
|||
|
||||
ext.references(type)
|
||||
end
|
||||
|
||||
def award_emoji_name(emoji)
|
||||
match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)
|
||||
match[1] if match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
4
changelogs/unreleased/25437-just-emoji.yml
Normal file
4
changelogs/unreleased/25437-just-emoji.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Introduce /award slash command; Allow posting of just an emoji in comment
|
||||
merge_request: 9382
|
||||
author: mhasbini
|
|
@ -35,3 +35,4 @@ do.
|
|||
| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time |
|
||||
| `/remove_time_spent` | Remove time spent |
|
||||
| `/target_branch <Branch Name>` | Set target branch for current merge request |
|
||||
| `/award :emoji:` | Toggle award for :emoji: |
|
||||
|
|
|
@ -42,4 +42,4 @@ Feature: Award Emoji
|
|||
@javascript
|
||||
Scenario: I add award emoji using regular comment
|
||||
Given I leave comment with a single emoji
|
||||
Then I have award added
|
||||
Then I have new comment with emoji added
|
||||
|
|
|
@ -44,6 +44,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
|
|||
end
|
||||
end
|
||||
|
||||
step 'I have new comment with emoji added' do
|
||||
expect(page).to have_selector ".emoji[title=':smile:']"
|
||||
end
|
||||
|
||||
step 'I have award added' do
|
||||
page.within '.awards' do
|
||||
expect(page).to have_selector '.js-emoji-btn'
|
||||
|
|
|
@ -67,6 +67,18 @@ describe 'Awards Emoji', feature: true do
|
|||
expect(page).not_to have_selector(emoji_counter)
|
||||
end
|
||||
end
|
||||
|
||||
context 'execute /award slash command' do
|
||||
it 'toggles the emoji award on noteable', js: true do
|
||||
execute_slash_command('/award :100:')
|
||||
|
||||
expect(find(noteable_award_counter)).to have_text("1")
|
||||
|
||||
execute_slash_command('/award :100:')
|
||||
|
||||
expect(page).not_to have_selector(noteable_award_counter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -80,6 +92,15 @@ describe 'Awards Emoji', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
def execute_slash_command(cmd)
|
||||
within('.js-main-target-form') do
|
||||
fill_in 'note[note]', with: cmd
|
||||
click_button 'Comment'
|
||||
end
|
||||
|
||||
wait_for_ajax
|
||||
end
|
||||
|
||||
def thumbsup_emoji
|
||||
page.all(emoji_counter).first
|
||||
end
|
||||
|
@ -92,6 +113,10 @@ describe 'Awards Emoji', feature: true do
|
|||
'span.js-counter'
|
||||
end
|
||||
|
||||
def noteable_award_counter
|
||||
".awards .active"
|
||||
end
|
||||
|
||||
def toggle_smiley_emoji(status)
|
||||
within('.note') do
|
||||
find('.note-emoji-button').click
|
||||
|
|
|
@ -225,11 +225,11 @@ describe API::Notes, api: true do
|
|||
context 'when the user is posting an award emoji on an issue created by someone else' do
|
||||
let(:issue2) { create(:issue, project: project) }
|
||||
|
||||
it 'returns an award emoji' do
|
||||
it 'creates a new issue note' do
|
||||
post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
|
||||
|
||||
expect(response).to have_http_status(201)
|
||||
expect(json_response['awardable_id']).to eq issue2.id
|
||||
expect(json_response['body']).to eq(':+1:')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -228,11 +228,11 @@ describe API::V3::Notes, api: true do
|
|||
context 'when the user is posting an award emoji on an issue created by someone else' do
|
||||
let(:issue2) { create(:issue, project: project) }
|
||||
|
||||
it 'returns an award emoji' do
|
||||
it 'creates a new issue note' do
|
||||
post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
|
||||
|
||||
expect(response).to have_http_status(201)
|
||||
expect(json_response['awardable_id']).to eq issue2.id
|
||||
expect(json_response['body']).to eq(':+1:')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -102,47 +102,19 @@ describe Notes::CreateService, services: true do
|
|||
expect(subject.note).to eq(params[:note])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "award emoji" do
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
end
|
||||
describe 'note with emoji only' do
|
||||
it 'creates regular note' do
|
||||
opts = {
|
||||
note: ':smile: ',
|
||||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
note = described_class.new(project, user, opts).execute
|
||||
|
||||
it "creates an award emoji" do
|
||||
opts = {
|
||||
note: ':smile: ',
|
||||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
note = described_class.new(project, user, opts).execute
|
||||
|
||||
expect(note).to be_valid
|
||||
expect(note.name).to eq('smile')
|
||||
end
|
||||
|
||||
it "creates regular note if emoji name is invalid" do
|
||||
opts = {
|
||||
note: ':smile: moretext:',
|
||||
noteable_type: 'Issue',
|
||||
noteable_id: issue.id
|
||||
}
|
||||
note = described_class.new(project, user, opts).execute
|
||||
|
||||
expect(note).to be_valid
|
||||
expect(note.note).to eq(opts[:note])
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
described_class.new(project, user, opts).execute
|
||||
expect(note).to be_valid
|
||||
expect(note.note).to eq(':smile:')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -267,6 +267,14 @@ describe SlashCommands::InterpretService, services: true do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'award command' do
|
||||
it 'toggle award 100 emoji if content containts /award :100:' do
|
||||
_, updates = service.execute(content, issuable)
|
||||
|
||||
expect(updates).to eq(emoji_award: "100")
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'reopen command' do
|
||||
let(:content) { '/reopen' }
|
||||
let(:issuable) { issue }
|
||||
|
@ -654,6 +662,37 @@ describe SlashCommands::InterpretService, services: true do
|
|||
end
|
||||
end
|
||||
|
||||
context '/award command' do
|
||||
it_behaves_like 'award command' do
|
||||
let(:content) { '/award :100:' }
|
||||
let(:issuable) { issue }
|
||||
end
|
||||
|
||||
it_behaves_like 'award command' do
|
||||
let(:content) { '/award :100:' }
|
||||
let(:issuable) { merge_request }
|
||||
end
|
||||
|
||||
context 'ignores command with no argument' do
|
||||
it_behaves_like 'empty command' do
|
||||
let(:content) { '/award' }
|
||||
let(:issuable) { issue }
|
||||
end
|
||||
end
|
||||
|
||||
context 'ignores non-existing / invalid emojis' do
|
||||
it_behaves_like 'empty command' do
|
||||
let(:content) { '/award noop' }
|
||||
let(:issuable) { issue }
|
||||
end
|
||||
|
||||
it_behaves_like 'empty command' do
|
||||
let(:content) { '/award :lorem_ipsum:' }
|
||||
let(:issuable) { issue }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '/target_branch command' do
|
||||
let(:non_empty_project) { create(:project) }
|
||||
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
|
||||
|
|
Loading…
Reference in a new issue