Commit 8b336ae1 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'adam-separate-slash-commands' into 'master'

Display slash commands outcome when previewing Markdown

Closes #21531

See merge request !10054
parents 8d059643 45e4c665
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview
// and showing a warning when more than `x` users are referenced. // (including the explanation of slash commands), and showing a warning when
// more than `x` users are referenced.
// //
(function () { (function () {
var lastTextareaPreviewed; var lastTextareaPreviewed;
...@@ -17,32 +18,45 @@ ...@@ -17,32 +18,45 @@
// Minimum number of users referenced before triggering a warning // Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10; MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText; var mdText;
var preview = $form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) { if (preview.hasClass('md-preview-loading')) {
return; return;
} }
mdText = $form.find('textarea.markdown-area').val(); mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text('Nothing to preview.'); preview.text(this.emptyMessage);
this.hideReferencedUsers($form); this.hideReferencedUsers($form);
} else { } else {
preview.addClass('md-preview-loading').text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
this.fetchMarkdownPreview(mdText, (function (response) { this.fetchMarkdownPreview(mdText, url, (function (response) {
preview.removeClass('md-preview-loading').html(response.body); var body;
if (response.body.length > 0) {
body = response.body;
} else {
body = this.emptyMessage;
}
preview.removeClass('md-preview-loading').html(body);
preview.renderGFM(); preview.renderGFM();
this.renderReferencedUsers(response.references.users, $form); this.renderReferencedUsers(response.references.users, $form);
if (response.references.commands) {
this.renderReferencedCommands(response.references.commands, $form);
}
}).bind(this)); }).bind(this));
} }
}; };
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!window.preview_markdown_path) { if (!url) {
return; return;
} }
if (text === this.ajaxCache.text) { if (text === this.ajaxCache.text) {
...@@ -51,7 +65,7 @@ ...@@ -51,7 +65,7 @@
} }
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: window.preview_markdown_path, url: url,
data: { data: {
text: text text: text
}, },
...@@ -83,6 +97,22 @@ ...@@ -83,6 +97,22 @@
} }
}; };
MarkdownPreview.prototype.hideReferencedCommands = function ($form) {
$form.find('.referenced-commands').hide();
};
MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) {
var referencedCommands;
referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) {
referencedCommands.html(commands);
referencedCommands.show();
} else {
referencedCommands.html('');
referencedCommands.hide();
}
};
return MarkdownPreview; return MarkdownPreview;
}()); }());
...@@ -137,6 +167,8 @@ ...@@ -137,6 +167,8 @@
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
markdownPreview.hideReferencedCommands($form);
}); });
$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
......
module MarkdownPreview
private
def render_markdown_preview(text, markdown_context = {})
render json: {
body: view_context.markdown(text, markdown_context),
references: {
users: preview_referenced_users(text)
}
}
end
def preview_referenced_users(text)
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
end
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } result = PreviewMarkdownService.new(@project, current_user, params).execute
render_markdown_preview(params[:text], context) render json: {
body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: result[:users]
}
}
end end
private private
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController ...@@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text]) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text]),
references: {
users: result[:users],
commands: view_context.markdown(result[:commands])
}
}
end end
private private
......
...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController ...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
include RendersBlob include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController ...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text], skip_project_check: true),
references: {
users: result[:users]
}
}
end end
protected protected
......
...@@ -122,6 +122,10 @@ module GitlabRoutingHelper ...@@ -122,6 +122,10 @@ module GitlabRoutingHelper
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end end
def preview_markdown_path(project, *args)
preview_markdown_namespace_project_path(project.namespace, project, *args)
end
def toggle_subscription_path(entity, *args) def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue) if entity.is_a?(Issue)
toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
......
class PreviewMarkdownService < BaseService
def execute
text, commands = explain_slash_commands(params[:text])
users = find_user_references(text)
success(
text: text,
users: users,
commands: commands.join(' ')
)
end
private
def explain_slash_commands(text)
return text, [] unless %w(Issue MergeRequest).include?(commands_target_type)
slash_commands_service = SlashCommands::InterpretService.new(project, current_user)
slash_commands_service.explain(text, find_commands_target)
end
def find_user_references(text)
extractor = Gitlab::ReferenceExtractor.new(project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
def find_commands_target
if commands_target_id.present?
finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder
finder.new(current_user, project_id: project.id).find(commands_target_id)
else
collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests
collection.build
end
end
def commands_target_type
params[:slash_commands_target_type]
end
def commands_target_id
params[:slash_commands_target_id]
end
end
...@@ -2,7 +2,7 @@ module SlashCommands ...@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl include Gitlab::SlashCommands::Dsl
attr_reader :issuable, :options attr_reader :issuable
# Takes a text and interprets the commands that are extracted from it. # Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record. # Returns the content without commands, and hash of changes to be applied to a record.
...@@ -12,23 +12,21 @@ module SlashCommands ...@@ -12,23 +12,21 @@ module SlashCommands
@issuable = issuable @issuable = issuable
@updates = {} @updates = {}
opts = { content, commands = extractor.extract_commands(content, context)
issuable: issuable, extract_updates(commands, context)
current_user: current_user, [content, @updates]
project: project, end
params: params
}
content, commands = extractor.extract_commands(content, opts)
commands.each do |name, arg| # Takes a text and interprets the commands that are extracted from it.
definition = self.class.command_definitions_by_name[name.to_sym] # Returns the content without commands, and array of changes explained.
next unless definition def explain(content, issuable)
return [content, []] unless current_user.can?(:use_slash_commands)
definition.execute(self, opts, arg) @issuable = issuable
end
[content, @updates] content, commands = extractor.extract_commands(content, context)
commands = explain_commands(commands, context)
[content, commands]
end end
private private
...@@ -40,6 +38,9 @@ module SlashCommands ...@@ -40,6 +38,9 @@ module SlashCommands
desc do desc do
"Close this #{issuable.to_ability_name.humanize(capitalize: false)}" "Close this #{issuable.to_ability_name.humanize(capitalize: false)}"
end end
explanation do
"Closes this #{issuable.to_ability_name.humanize(capitalize: false)}."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.open? && issuable.open? &&
...@@ -52,6 +53,9 @@ module SlashCommands ...@@ -52,6 +53,9 @@ module SlashCommands
desc do desc do
"Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}" "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}"
end end
explanation do
"Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.closed? && issuable.closed? &&
...@@ -62,6 +66,7 @@ module SlashCommands ...@@ -62,6 +66,7 @@ module SlashCommands
end end
desc 'Merge (when the pipeline succeeds)' desc 'Merge (when the pipeline succeeds)'
explanation 'Merges this merge request when the pipeline succeeds.'
condition do condition do
last_diff_sha = params && params[:merge_request_diff_head_sha] last_diff_sha = params && params[:merge_request_diff_head_sha]
issuable.is_a?(MergeRequest) && issuable.is_a?(MergeRequest) &&
...@@ -73,6 +78,9 @@ module SlashCommands ...@@ -73,6 +78,9 @@ module SlashCommands
end end
desc 'Change title' desc 'Change title'
explanation do |title_param|
"Changes the title to \"#{title_param}\"."
end
params '<New title>' params '<New title>'
condition do condition do
issuable.persisted? && issuable.persisted? &&
...@@ -83,18 +91,25 @@ module SlashCommands ...@@ -83,18 +91,25 @@ module SlashCommands
end end
desc 'Assign' desc 'Assign'
explanation do |user|
"Assigns #{user.to_reference}." if user
end
params '@user' params '@user'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :assign do |assignee_param| parse_params do |assignee_param|
user = extract_references(assignee_param, :user).first extract_references(assignee_param, :user).first ||
user ||= User.find_by(username: assignee_param) User.find_by(username: assignee_param)
end
command :assign do |user|
@updates[:assignee_id] = user.id if user @updates[:assignee_id] = user.id if user
end end
desc 'Remove assignee' desc 'Remove assignee'
explanation do
"Removes assignee #{issuable.assignee.to_reference}."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.assignee_id? && issuable.assignee_id? &&
...@@ -105,19 +120,26 @@ module SlashCommands ...@@ -105,19 +120,26 @@ module SlashCommands
end end
desc 'Set milestone' desc 'Set milestone'
explanation do |milestone|
"Sets the milestone to #{milestone.to_reference}." if milestone
end
params '%"milestone"' params '%"milestone"'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.milestones.active.any? project.milestones.active.any?
end end
command :milestone do |milestone_param| parse_params do |milestone_param|
milestone = extract_references(milestone_param, :milestone).first extract_references(milestone_param, :milestone).first ||
milestone ||= project.milestones.find_by(title: milestone_param.strip) project.milestones.find_by(title: milestone_param.strip)
end
command :milestone do |milestone|
@updates[:milestone_id] = milestone.id if milestone @updates[:milestone_id] = milestone.id if milestone
end end
desc 'Remove milestone' desc 'Remove milestone'
explanation do
"Removes #{issuable.milestone.to_reference(format: :name)} milestone."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.milestone_id? && issuable.milestone_id? &&
...@@ -128,6 +150,11 @@ module SlashCommands ...@@ -128,6 +150,11 @@ module SlashCommands
end end
desc 'Add label(s)' desc 'Add label(s)'
explanation do |labels_param|
labels = find_label_references(labels_param)
"Adds #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
...@@ -147,6 +174,14 @@ module SlashCommands ...@@ -147,6 +174,14 @@ module SlashCommands
end end
desc 'Remove all or specific label(s)' desc 'Remove all or specific label(s)'
explanation do |labels_param = nil|
if labels_param.present?
labels = find_label_references(labels_param)
"Removes #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
else
'Removes all labels.'
end
end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
issuable.persisted? && issuable.persisted? &&
...@@ -169,6 +204,10 @@ module SlashCommands ...@@ -169,6 +204,10 @@ module SlashCommands
end end
desc 'Replace all label(s)' desc 'Replace all label(s)'
explanation do |labels_param|
labels = find_label_references(labels_param)
"Replaces all labels with #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
issuable.persisted? && issuable.persisted? &&
...@@ -187,6 +226,7 @@ module SlashCommands ...@@ -187,6 +226,7 @@ module SlashCommands
end end
desc 'Add a todo' desc 'Add a todo'
explanation 'Adds a todo.'
condition do condition do
issuable.persisted? && issuable.persisted? &&
!TodoService.new.todo_exist?(issuable, current_user) !TodoService.new.todo_exist?(issuable, current_user)
...@@ -196,6 +236,7 @@ module SlashCommands ...@@ -196,6 +236,7 @@ module SlashCommands
end end
desc 'Mark todo as done' desc 'Mark todo as done'
explanation 'Marks todo as done.'
condition do condition do
issuable.persisted? && issuable.persisted? &&
TodoService.new.todo_exist?(issuable, current_user) TodoService.new.todo_exist?(issuable, current_user)
...@@ -205,6 +246,9 @@ module SlashCommands ...@@ -205,6 +246,9 @@ module SlashCommands
end end
desc 'Subscribe' desc 'Subscribe'
explanation do
"Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
!issuable.subscribed?(current_user, project) !issuable.subscribed?(current_user, project)
...@@ -214,6 +258,9 @@ module SlashCommands ...@@ -214,6 +258,9 @@ module SlashCommands
end end
desc 'Unsubscribe' desc 'Unsubscribe'
explanation do
"Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}."
end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.subscribed?(current_user, project) issuable.subscribed?(current_user, project)
...@@ -223,18 +270,23 @@ module SlashCommands ...@@ -223,18 +270,23 @@ module SlashCommands
end end
desc 'Set due date' desc 'Set due date'
explanation do |due_date|
"Sets the due date to #{due_date.to_s(:medium)}." if due_date
end
params '<in 2 days | this Friday | December 31st>' params '<in 2 days | this Friday | December 31st>'
condition do condition do
issuable.respond_to?(:due_date) && issuable.respond_to?(:due_date) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :due do |due_date_param| parse_params do |due_date_param|
due_date = Chronic.parse(due_date_param).try(:to_date) Chronic.parse(due_date_param).try(:to_date)
end
command :due do |due_date|
@updates[:due_date] = due_date if due_date @updates[:due_date] = due_date if due_date
end end
desc 'Remove due date' desc 'Remove due date'
explanation 'Removes the due date.'
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.respond_to?(:due_date) && issuable.respond_to?(:due_date) &&
...@@ -245,8 +297,11 @@ module SlashCommands ...@@ -245,8 +297,11 @@ module SlashCommands
@updates[:due_date] = nil @updates[:due_date] = nil
end end
desc do desc 'Toggle the Work In Progress status'
"Toggle the Work In Progress status" explanation do
verb = issuable.work_in_progress? ? 'Unmarks' : 'Marks'
noun = issuable.to_ability_name.humanize(capitalize: false)
"#{verb} this #{noun} as Work In Progress."
end end
condition do condition do
issuable.persisted? && issuable.persisted? &&
...@@ -257,45 +312,72 @@ module SlashCommands ...@@ -257,45 +312,72 @@ module SlashCommands
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end end
desc 'Toggle emoji reward' desc 'Toggle emoji award'
explanation do |name|
"Toggles :#{name}: emoji award." if name
end
params ':emoji:' params ':emoji:'
condition do condition do
issuable.persisted? issuable.persisted?
end end
command :award do |emoji| parse_params do |emoji_param|
name = award_emoji_name(emoji) match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern)
match[1] if match
end
command :award do |name|
if name && issuable.user_can_award?(current_user, name) if name && issuable.user_can_award?(current_user, name)
@updates[:emoji_award] = name @updates[:emoji_award] = name
end end
end end
desc 'Set time estimate' desc 'Set time estimate'
explanation do |time_estimate|
time_estimate = Gitlab::TimeTrackingFormatter.output(time_estimate)
"Sets time estimate to #{time_estimate}." if time_estimate
end
params '<1w 3d 2h 14m>' params '<1w 3d 2h 14m>'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :estimate do |raw_duration| parse_params do |raw_duration|
time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) Gitlab::TimeTrackingFormatter.parse(raw_duration)
end
command :estimate do |time_estimate|
if time_estimate if time_estimate
@updates[:time_estimate] = time_estimate @updates[:time_estimate] = time_estimate
end end
end end
desc 'Add or substract spent time' desc 'Add or substract spent time'
explanation do |time_spent|
if time_spent
if time_spent > 0
verb = 'Adds'
value = time_spent
else
verb = 'Substracts'
value = -time_spent
end
"#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
end
end
params '<1h 30m | -1h 30m>' params '<1h 30m | -1h 30m>'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end end
command :spend do |raw_duration| parse_params do |raw_duration|
time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) Gitlab::TimeTrackingFormatter.parse(raw_duration)
end
command :spend do |time_spent|
if time_spent if time_spent
@updates[:spend_time] = { duration: time_spent, user: current_user } @updates[:spend_time] = { duration: time_spent, user: current_user }
end end
end end
desc 'Remove time estimate' desc 'Remove time estimate'
explanation 'Removes time estimate.'
condition do condition do
issuable.persisted? && issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
...@@ -305,6 +387,7 @@ module SlashCommands ...@@ -305,6 +387,7 @@ module SlashCommands
end end
desc 'Remove spent time' desc 'Remove spent time'
explanation 'Removes spent time.'
condition do condition do
issuable.persisted? && issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
...@@ -318,19 +401,28 @@ module SlashCommands ...@@ -318,19 +401,28 @@ module SlashCommands
params '@user' params '@user'
command :cc command :cc
desc 'Defines target branch for MR' desc 'Define target branch for MR'
explanation do |branch_name|
"Sets target branch to #{branch_name}."
end
params '<Local branch name>' params '<Local branch name>'
condition do condition do
issuable.respond_to?(:target_branch) && issuable.respond_to?(:target_branch) &&
(current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
issuable.new_record?) issuable.new_record?)
end end
command :target_branch do |target_branch_param| parse_params do |target_branch_param|
branch_name = target_branch_param.strip target_branch_param.strip
end
command :target_branch do |branch_name|
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end end
desc 'Move issue from one column of the board to another' desc 'Move issue from one column of the board to another'
explanation do |target_list_name|
label = find_label_references(target_list_name).first
"Moves issue to #{label} column in the board." if label
end
params '~"Target column"' params '~"Target column"'
condition do condition do
issuable.is_a?(Issue) && issuable.is_a?(Issue) &&
...@@ -352,11 +444,35 @@ module SlashCommands ...@@ -352,11 +444,35 @@ module SlashCommands
end end
end end
def find_labels(labels_param)
extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
end
def find_label_references(labels_param)
find_labels(labels_param).map(&:to_reference)
end
def find_label_ids(labels_param) def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id) find_labels(labels_param).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) end
def explain_commands(commands, opts)
commands.map do |name, arg|
definition = self.class.definition_by_name(name)
next unless definition
label_ids_by_reference | labels_ids_by_name definition.explain(self, opts, arg)
end.compact
end
def extract_updates(commands, opts)
commands.each do |name, arg|
definition = self.class.definition_by_name(name)
next unless definition
definition.execute(self, opts, arg)
end
end end
def extract_references(arg, type) def extract_references(arg, type)
...@@ -366,9 +482,13 @@ module SlashCommands ...@@ -366,9 +482,13 @@ module SlashCommands
ext.references(type) ext.references(type)
end end
def award_emoji_name(emoji) def context
match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern) {
match[1] if match issuable: issuable,
current_user: current_user,
project: project,
params: params
}
end end
end end
end end
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: '' } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -5,14 +5,9 @@ ...@@ -5,14 +5,9 @@
- content_for :project_javascripts do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
- else
- preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
- if current_user - if current_user
:javascript :javascript
window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
- content_for :header_content do - content_for :header_content do
.js-dropdown-menu-projects .js-dropdown-menu-projects
......
- referenced_users = local_assigns.fetch(:referenced_users, nil)
.md-area .md-area
.md-header .md-header
%ul.nav-links.clearfix %ul.nav-links.clearfix
...@@ -28,9 +30,10 @@ ...@@ -28,9 +30,10 @@
.md-write-holder .md-write-holder
= yield = yield
.md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) } .md.md-preview-holder.js-md-preview.hide.md-preview{ data: { url: url } }
.referenced-commands.hide
- if defined?(referenced_users) && referenced_users - if referenced_users
.referenced-users.hide .referenced-users.hide
%span %span
= icon("exclamation-triangle") = icon("exclamation-triangle")
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type' = hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
= render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
......
- supports_slash_commands = note_supports_slash_commands?(@note) - supports_slash_commands = note_supports_slash_commands?(@note)
- if supports_slash_commands
- preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id)
- else
- preview_url = preview_markdown_path(@project)
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
...@@ -18,7 +22,7 @@ ...@@ -18,7 +22,7 @@
-# DiffNote -# DiffNote
= f.hidden_field :position = f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: f, = render 'projects/zen', f: f,
attr: :note, attr: :note,
classes: 'note-textarea js-note-text', classes: 'note-textarea js-note-text',
......
%ul#notes-list.notes.main-notes-list.timeline %ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes" = render "shared/notes/notes"
= render 'projects/notes/edit_form' = render 'projects/notes/edit_form', project: @project
%ul.notes.notes-form.timeline %ul.notes.notes-form.timeline
%li.timeline-entry %li.timeline-entry
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.form-group .form-group
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.form-group .form-group
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do
= render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
= render 'shared/issuable/form/description', issuable: issuable, form: form = render 'shared/issuable/form/description', issuable: issuable, form: form, project: project
- if issuable.respond_to?(:confidential) - if issuable.respond_to?(:confidential)
.form-group .form-group
......
- project = local_assigns.fetch(:project)
- issuable = local_assigns.fetch(:issuable) - issuable = local_assigns.fetch(:issuable)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- supports_slash_commands = issuable.new_record?
- if supports_slash_commands
- preview_url = preview_markdown_path(project, slash_commands_target_type: issuable.class.name)
- else
- preview_url = preview_markdown_path(project)
.form-group.detail-page-description .form-group.detail-page-description
= form.label :description, 'Description', class: 'control-label' = form.label :description, 'Description', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea', classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: !issuable.persisted? supports_slash_commands: supports_slash_commands
= render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
.clearfix .clearfix
.error-alert .error-alert
---
title: Display slash commands outcome when previewing Markdown
merge_request: 10054
author: Rares Sfirlogea
module Gitlab module Gitlab
module SlashCommands module SlashCommands
class CommandDefinition class CommandDefinition
attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block attr_accessor :name, :aliases, :description, :explanation, :params,
:condition_block, :parse_params_block, :action_block
def initialize(name, attributes = {}) def initialize(name, attributes = {})
@name = name @name = name
@aliases = attributes[:aliases] || [] @aliases = attributes[:aliases] || []
@description = attributes[:description] || '' @description = attributes[:description] || ''
@explanation = attributes[:explanation] || ''
@params = attributes[:params] || [] @params = attributes[:params] || []
@condition_block = attributes[:condition_block] @condition_block = attributes[:condition_block]
@parse_params_block = attributes[:parse_params_block]
@action_block = attributes[:action_block] @action_block = attributes[:action_block]
end end
...@@ -28,14 +31,20 @@ module Gitlab ...@@ -28,14 +31,20 @@ module Gitlab
context.instance_exec(&condition_block) context.instance_exec(&condition_block)
end end
def explain(context, opts, arg)
return unless available?(opts)
if explanation.respond_to?(:call)
execute_block(explanation, context, arg)
else
explanation
end
end
def execute(context, opts, arg) def execute(context, opts, arg)
return if noop? || !available?(opts) return if noop? || !available?(opts)
if arg.present? execute_block(action_block, context, arg)
context.instance_exec(arg, &action_block)
elsif action_block.arity == 0
context.instance_exec(&action_block)
end
end end
def to_h(opts) def to_h(opts)
...@@ -52,6 +61,23 @@ module Gitlab ...@@ -52,6 +61,23 @@ module Gitlab
params: params params: params
} }
end end
private
def execute_block(block, context, arg)
if arg.present?
parsed = parse_params(arg, context)
context.instance_exec(parsed, &block)
elsif block.arity == 0
context.instance_exec(&block)
end
end
def parse_params(arg, context)
return arg unless parse_params_block
context.instance_exec(arg, &parse_params_block)
end
end end
end end
end end
...@@ -44,6 +44,22 @@ module Gitlab ...@@ -44,6 +44,22 @@ module Gitlab
@params = params @params = params
end end
# Allows to give an explanation of what the command will do when
# executed. This explanation is shown when rendering the Markdown
# preview.
#
# Example:
#
# explanation do |arguments|
# "Adds label(s) #{arguments.join(' ')}"
# end
# command :command_key do |arguments|
# # Awesome code block
# end
def explanation(text = '', &block)
@explanation = block_given? ? block : text
end
# Allows to define conditions that must be met in order for the command # Allows to define conditions that must be met in order for the command
# to be returned by `.command_names` & `.command_definitions`. # to be returned by `.command_names` & `.command_definitions`.
# It accepts a block that will be evaluated with the context given to # It accepts a block that will be evaluated with the context given to
...@@ -61,6 +77,24 @@ module Gitlab ...@@ -61,6 +77,24 @@ module Gitlab
@condition_block = block @condition_block = block
end end
# Allows to perform initial parsing of parameters. The result is passed
# both to `command` and `explanation` blocks, instead of the raw
# parameters.
# It accepts a block that will be evaluated with the context given to
# `CommandDefintion#to_h`.
#
# Example:
#
# parse_params do |raw|
# raw.strip
# end
# command :command_key do |parsed|
# # Awesome code block
# end
def parse_params(&block)
@parse_params_block = block
end
# Registers a new command which is recognizeable from body of email or # Registers a new command which is recognizeable from body of email or
# comment. # comment.
# It accepts aliases and takes a block. # It accepts aliases and takes a block.
...@@ -77,8 +111,10 @@ module Gitlab ...@@ -77,8 +111,10 @@ module Gitlab
name, name,
aliases: aliases, aliases: aliases,
description: @description, description: @description,
explanation: @explanation,
params: @params, params: @params,
condition_block: @condition_block, condition_block: @condition_block,
parse_params_block: @parse_params_block,
action_block: block action_block: block
) )
...@@ -89,8 +125,14 @@ module Gitlab ...@@ -89,8 +125,14 @@ module Gitlab
end end
@description = nil @description = nil
@explanation = nil
@params = nil @params = nil
@condition_block = nil @condition_block = nil
@parse_params_block = nil
end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end end
end end
end end
......
...@@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do ...@@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do
end end
end end
end end
context 'when the command defines parse_params block' do
before do
subject.parse_params_block = ->(raw) { raw.strip }
subject.action_block = ->(parsed) { self.received_arg = parsed }
end
it 'executes the command passing the parsed param' do
subject.execute(context, {}, 'something ')
expect(context.received_arg).to eq('something')
end
end
end
end
end
describe '#explain' do
context 'when the command is not available' do
before do
subject.condition_block = proc { false }
subject.explanation = 'Explanation'
end
it 'returns nil' do
result = subject.explain({}, {}, nil)
expect(result).to be_nil
end
end
context 'when the explanation is a static string' do
before do
subject.explanation = 'Explanation'
end
it 'returns this static string' do
result = subject.explain({}, {}, nil)
expect(result).to eq 'Explanation'
end
end
context 'when the explanation is dynamic' do
before do
subject.explanation = proc { |arg| "Dynamic #{arg}" }
end
it 'invokes the proc' do
result = subject.explain({}, {}, 'explanation')
expect(result).to eq 'Dynamic explanation'
end end
end end
end end
......
...@@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do ...@@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do
end end
params 'The first argument' params 'The first argument'
command :one_arg, :once, :first do |arg1| explanation 'Static explanation'
arg1 command :explanation_with_aliases, :once, :first do |arg|
arg
end end
desc do desc do
"A dynamic description for #{noteable.upcase}" "A dynamic description for #{noteable.upcase}"
end end
params 'The first argument', 'The second argument' params 'The first argument', 'The second argument'
command :two_args do |arg1, arg2| command :dynamic_description do |args|
[arg1, arg2] args.split
end end
command :cc command :cc
explanation do |arg|
"Action does something with #{arg}"
end
condition do condition do
project == 'foo' project == 'foo'
end end
command :cond_action do |arg| command :cond_action do |arg|
arg arg
end end
parse_params do |raw_arg|
raw_arg.strip
end
command :with_params_parsing do |parsed|
parsed
end
end end
end end
describe '.command_definitions' do describe '.command_definitions' do
it 'returns an array with commands definitions' do it 'returns an array with commands definitions' do
no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions no_args_def, explanation_with_aliases_def, dynamic_description_def,
cc_def, cond_action_def, with_params_parsing_def =
DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args) expect(no_args_def.name).to eq(:no_args)
expect(no_args_def.aliases).to eq([:none]) expect(no_args_def.aliases).to eq([:none])
expect(no_args_def.description).to eq('A command with no args') expect(no_args_def.description).to eq('A command with no args')
expect(no_args_def.explanation).to eq('')
expect(no_args_def.params).to eq([]) expect(no_args_def.params).to eq([])
expect(no_args_def.condition_block).to be_nil expect(no_args_def.condition_block).to be_nil
expect(no_args_def.action_block).to be_a_kind_of(Proc) expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(no_args_def.parse_params_block).to be_nil
expect(one_arg_def.name).to eq(:one_arg) expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases)
expect(one_arg_def.aliases).to eq([:once, :first]) expect(explanation_with_aliases_def.aliases).to eq([:once, :first])
expect(one_arg_def.description).to eq('') expect(explanation_with_aliases_def.description).to eq('')
expect(one_arg_def.params).to eq(['The first argument']) expect(explanation_with_aliases_def.explanation).to eq('Static explanation')
expect(one_arg_def.condition_block).to be_nil expect(explanation_with_aliases_def.params).to eq(['The first argument'])
expect(one_arg_def.action_block).to be_a_kind_of(Proc) expect(explanation_with_aliases_def.condition_block).to be_nil
expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
expect(explanation_with_aliases_def.parse_params_block).to be_nil
expect(two_args_def.name).to eq(:two_args) expect(dynamic_description_def.name).to eq(:dynamic_description)
expect(two_args_def.aliases).to eq([]) expect(dynamic_description_def.aliases).to eq([])
expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE') expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE')
expect(two_args_def.params).to eq(['The first argument', 'The second argument']) expect(dynamic_description_def.explanation).to eq('')
expect(two_args_def.condition_block).to be_nil expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument'])
expect(two_args_def.action_block).to be_a_kind_of(Proc) expect(dynamic_description_def.condition_block).to be_nil
expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
expect(dynamic_description_def.parse_params_block).to be_nil
expect(cc_def.name).to eq(:cc) expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([]) expect(cc_def.aliases).to eq([])
expect(cc_def.description).to eq('') expect(cc_def.description).to eq('')
expect(cc_def.explanation).to eq('')
expect(cc_def.params).to eq([]) expect(cc_def.params).to eq([])
expect(cc_def.condition_block).to be_nil expect(cc_def.condition_block).to be_nil
expect(cc_def.action_block).to be_nil expect(cc_def.action_block).to be_nil
expect(cc_def.parse_params_block).to be_nil
expect(cond_action_def.name).to eq(:cond_action) expect(cond_action_def.name).to eq(:cond_action)
expect(cond_action_def.aliases).to eq([]) expect(cond_action_def.aliases).to eq([])
expect(cond_action_def.description).to eq('') expect(cond_action_def.description).to eq('')
expect(cond_action_def.explanation).to be_a_kind_of(Proc)
expect(cond_action_def.params).to eq([]) expect(cond_action_def.params).to eq([])
expect(cond_action_def.condition_block).to be_a_kind_of(Proc) expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
expect(cond_action_def.action_block).to be_a_kind_of(Proc) expect(cond_action_def.action_block).to be_a_kind_of(Proc)
expect(cond_action_def.parse_params_block).to be_nil
expect(with_params_parsing_def.name).to eq(:with_params_parsing)
expect(with_params_parsing_def.aliases).to eq([])
expect(with_params_parsing_def.description).to eq('')
expect(with_params_parsing_def.explanation).to eq('')
expect(with_params_parsing_def.params).to eq([])
expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
end end
end end
end end
require 'spec_helper'
describe PreviewMarkdownService do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
before do
project.add_developer(user)
end
describe 'user references' do
let(:params) { { text: "Take a look #{user.to_reference}" } }
let(:service) { described_class.new(project, user, params) }
it 'returns users referenced in text' do
result = service.execute
expect(result[:users]).to eq [user.username]
end
end
context 'new note with slash commands' do
let(:issue) { create(:issue, project: project) }
let(:params) do
{
text: "Please do it\n/assign #{user.to_reference}",
slash_commands_target_type: 'Issue',
slash_commands_target_id: issue.id
}
end
let(:service) { described_class.new(project, user, params) }
it 'removes slash commands from text' do
result = service.execute
expect(result[:text]).to eq 'Please do it'
end
it 'explains slash commands effect' do
result = service.execute
expect(result[:commands]).to eq "Assigns #{user.to_reference}."
end
end
context 'merge request description' do
let(:params) do
{
text: "My work\n/estimate 2y",
slash_commands_target_type: 'MergeRequest'
}
end
let(:service) { described_class.new(project, user, params) }
it 'removes slash commands from text' do
result = service.execute
expect(result[:text]).to eq 'My work'
end
it 'explains slash commands effect' do
result = service.execute
expect(result[:commands]).to eq 'Sets time estimate to 2y.'
end
end
end
...@@ -798,4 +798,211 @@ describe SlashCommands::InterpretService, services: true do ...@@ -798,4 +798,211 @@ describe SlashCommands::InterpretService, services: true do
end end
end end
end end
describe '#explain' do
let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe 'close command' do
let(:content) { '/close' }
it 'includes issuable name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Closes this issue.'])
end
end
describe 'reopen command' do
let(:content) { '/reopen' }
let(:merge_request) { create(:merge_request, :closed, source_project: project) }
it 'includes issuable name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Reopens this merge request.'])
end
end
describe 'title command' do
let(:content) { '/title This is new title' }
it 'includes new title' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Changes the title to "This is new title".'])
end
end
describe 'assign command' do
let(:content) { "/assign @#{developer.username} do it!" }
it 'includes only the user reference' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(["Assigns @#{developer.username}."])
end
end
describe 'unassign command' do
let(:content) { '/unassign' }
let(:issue) { create(:issue, project: project, assignee: developer) }
it 'includes current assignee reference' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Removes assignee @#{developer.username}."])
end
end
describe 'milestone command' do
let(:content) { '/milestone %wrong-milestone' }
let!(:milestone) { create(:milestone, project: project, title: '9.10') }
it 'is empty when milestone reference is wrong' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq([])
end
end
describe 'remove milestone command' do
let(:content) { '/remove_milestone' }
let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
it 'includes current milestone name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Removes %"9.10" milestone.'])
end
end
describe 'label command' do
let(:content) { '/label ~missing' }
let!(:label) { create(:label, project: project) }
it 'is empty when there are no correct labels' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq([])
end
end
describe 'unlabel command' do
let(:content) { '/unlabel' }
it 'says all labels if no parameter provided' do
merge_request.update!(label_ids: [bug.id])
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Removes all labels.'])
end
end
describe 'relabel command' do
let(:content) { '/relabel Bug' }
let!(:bug) { create(:label, project: project, title: 'Bug') }
let(:feature) { create(:label, project: project, title: 'Feature') }
it 'includes label name' do
issue.update!(label_ids: [feature.id])
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."])
end
end
describe 'subscribe command' do
let(:content) { '/subscribe' }
it 'includes issuable name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Subscribes to this issue.'])
end
end
describe 'unsubscribe command' do
let(:content) { '/unsubscribe' }
it 'includes issuable name' do
merge_request.subscribe(developer, project)
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Unsubscribes from this merge request.'])
end
end
describe 'due command' do
let(:content) { '/due April 1st 2016' }
it 'includes the date' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Sets the due date to Apr 1, 2016.'])
end
end
describe 'wip command' do
let(:content) { '/wip' }
it 'includes the new status' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Marks this merge request as Work In Progress.'])
end
end
describe 'award command' do
let(:content) { '/award :confetti_ball: ' }
it 'includes the emoji' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
end
end
describe 'estimate command' do
let(:content) { '/estimate 79d' }
it 'includes the formatted duration' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.'])
end
end
describe 'spend command' do
let(:content) { '/spend -120m' }
it 'includes the formatted duration and proper verb' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(['Substracts 2h spent time.'])
end
end
describe 'target branch command' do
let(:content) { '/target_branch my-feature ' }
it 'includes the branch name' do
_, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Sets target branch to my-feature.'])
end
end
describe 'board move command' do
let(:content) { '/board_move ~bug' }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:board) { create(:board, project: project) }
it 'includes the label name' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
end
end
end
end end
...@@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description ...@@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description
end end
end end
end end
describe "preview of note on #{issuable_type}" do
it 'removes slash commands from note and explains them' do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "Awesome!\n/assign @bob "
click_on 'Preview'
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
expect(page).to have_content 'Assigns @bob.'
end
end
end
end end
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