diff options
author | Rémy Coutable <remy@rymai.me> | 2017-05-05 09:07:56 +0000 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2017-05-05 09:07:56 +0000 |
commit | 8b336ae124408566116c36d198264c9175bcbe9b (patch) | |
tree | 1132d797275e108624f008609f1321a679552d62 | |
parent | 8d059643f93f0d1b99ba153cf7321e905e7d43bb (diff) | |
parent | 45e4c665653cd511b0d96119d3973652c43238bb (diff) | |
download | gitlab-ce-8b336ae124408566116c36d198264c9175bcbe9b.tar.gz |
Merge branch 'adam-separate-slash-commands' into 'master'
Display slash commands outcome when previewing Markdown
Closes #21531
See merge request !10054
28 files changed, 785 insertions, 133 deletions
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 07eea98e737..4a3df2fd465 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -2,8 +2,9 @@ // MarkdownPreview // -// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, -// and showing a warning when more than `x` users are referenced. +// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview +// (including the explanation of slash commands), and showing a warning when +// more than `x` users are referenced. // (function () { var lastTextareaPreviewed; @@ -17,32 +18,45 @@ // Minimum number of users referenced before triggering a warning MarkdownPreview.prototype.referenceThreshold = 10; + MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.showPreview = function ($form) { var mdText; var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); if (preview.hasClass('md-preview-loading')) { return; } mdText = $form.find('textarea.markdown-area').val(); if (mdText.trim().length === 0) { - preview.text('Nothing to preview.'); + preview.text(this.emptyMessage); this.hideReferencedUsers($form); } else { preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, (function (response) { - preview.removeClass('md-preview-loading').html(response.body); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; + } else { + body = this.emptyMessage; + } + + preview.removeClass('md-preview-loading').html(body); preview.renderGFM(); this.renderReferencedUsers(response.references.users, $form); + + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } }).bind(this)); } }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { - if (!window.preview_markdown_path) { + MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { return; } if (text === this.ajaxCache.text) { @@ -51,7 +65,7 @@ } $.ajax({ type: 'POST', - url: window.preview_markdown_path, + url: url, data: { text: text }, @@ -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; }()); @@ -137,6 +167,8 @@ $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); $form.find('.md-preview-holder').hide(); + + markdownPreview.hideReferencedCommands($form); }); $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { diff --git a/app/controllers/concerns/markdown_preview.rb b/app/controllers/concerns/markdown_preview.rb deleted file mode 100644 index 40eff267348..00000000000 --- a/app/controllers/concerns/markdown_preview.rb +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 96125684da0..887d18dbec3 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,6 +1,4 @@ class Projects::WikisController < Projects::ApplicationController - include MarkdownPreview - before_action :authorize_read_wiki! before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_admin_wiki!, only: :destroy @@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController end def preview_markdown - context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } - - render_markdown_preview(params[:text], context) + result = PreviewMarkdownService.new(@project, current_user, params).execute + + render json: { + body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), + references: { + users: result[:users] + } + } end private diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9f6ee4826e6..69310b26e76 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,6 @@ class ProjectsController < Projects::ApplicationController include IssuableCollections include ExtractsPath - include MarkdownPreview before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :project, except: [:index, :new, :create] @@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController end 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 private diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index da1ae9a34d9..afed27a41d1 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -3,7 +3,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions include SnippetsActions - include MarkdownPreview include RendersBlob before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] @@ -90,7 +89,14 @@ class SnippetsController < ApplicationController end 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 protected diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 1336c676134..88f9a691a17 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -122,6 +122,10 @@ module GitlabRoutingHelper namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) end + def preview_markdown_path(project, *args) + preview_markdown_namespace_project_path(project.namespace, project, *args) + end + def toggle_subscription_path(entity, *args) if entity.is_a?(Issue) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb new file mode 100644 index 00000000000..10d45bbf73c --- /dev/null +++ b/app/services/preview_markdown_service.rb @@ -0,0 +1,45 @@ +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 diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 6aeebc26685..f1bbc7032d5 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -2,7 +2,7 @@ module SlashCommands class InterpretService < BaseService include Gitlab::SlashCommands::Dsl - attr_reader :issuable, :options + attr_reader :issuable # 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. @@ -12,23 +12,21 @@ module SlashCommands @issuable = issuable @updates = {} - opts = { - issuable: issuable, - current_user: current_user, - project: project, - params: params - } - - content, commands = extractor.extract_commands(content, opts) + content, commands = extractor.extract_commands(content, context) + extract_updates(commands, context) + [content, @updates] + end - commands.each do |name, arg| - definition = self.class.command_definitions_by_name[name.to_sym] - next unless definition + # Takes a text and interprets the commands that are extracted from it. + # Returns the content without commands, and array of changes explained. + def explain(content, issuable) + return [content, []] unless current_user.can?(:use_slash_commands) - definition.execute(self, opts, arg) - end + @issuable = issuable - [content, @updates] + content, commands = extractor.extract_commands(content, context) + commands = explain_commands(commands, context) + [content, commands] end private @@ -40,6 +38,9 @@ module SlashCommands desc do "Close this #{issuable.to_ability_name.humanize(capitalize: false)}" end + explanation do + "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.open? && @@ -52,6 +53,9 @@ module SlashCommands desc do "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}" end + explanation do + "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.closed? && @@ -62,6 +66,7 @@ module SlashCommands end desc 'Merge (when the pipeline succeeds)' + explanation 'Merges this merge request when the pipeline succeeds.' condition do last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && @@ -73,6 +78,9 @@ module SlashCommands end desc 'Change title' + explanation do |title_param| + "Changes the title to \"#{title_param}\"." + end params '<New title>' condition do issuable.persisted? && @@ -83,18 +91,25 @@ module SlashCommands end desc 'Assign' + explanation do |user| + "Assigns #{user.to_reference}." if user + end params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :assign do |assignee_param| - user = extract_references(assignee_param, :user).first - user ||= User.find_by(username: assignee_param) - + parse_params do |assignee_param| + extract_references(assignee_param, :user).first || + User.find_by(username: assignee_param) + end + command :assign do |user| @updates[:assignee_id] = user.id if user end desc 'Remove assignee' + explanation do + "Removes assignee #{issuable.assignee.to_reference}." + end condition do issuable.persisted? && issuable.assignee_id? && @@ -105,19 +120,26 @@ module SlashCommands end desc 'Set milestone' + explanation do |milestone| + "Sets the milestone to #{milestone.to_reference}." if milestone + end params '%"milestone"' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) && project.milestones.active.any? end - command :milestone do |milestone_param| - milestone = extract_references(milestone_param, :milestone).first - milestone ||= project.milestones.find_by(title: milestone_param.strip) - + parse_params do |milestone_param| + extract_references(milestone_param, :milestone).first || + project.milestones.find_by(title: milestone_param.strip) + end + command :milestone do |milestone| @updates[:milestone_id] = milestone.id if milestone end desc 'Remove milestone' + explanation do + "Removes #{issuable.milestone.to_reference(format: :name)} milestone." + end condition do issuable.persisted? && issuable.milestone_id? && @@ -128,6 +150,11 @@ module SlashCommands end 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"' condition do available_labels = LabelsFinder.new(current_user, project_id: project.id).execute @@ -147,6 +174,14 @@ module SlashCommands end 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"' condition do issuable.persisted? && @@ -169,6 +204,10 @@ module SlashCommands end 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"' condition do issuable.persisted? && @@ -187,6 +226,7 @@ module SlashCommands end desc 'Add a todo' + explanation 'Adds a todo.' condition do issuable.persisted? && !TodoService.new.todo_exist?(issuable, current_user) @@ -196,6 +236,7 @@ module SlashCommands end desc 'Mark todo as done' + explanation 'Marks todo as done.' condition do issuable.persisted? && TodoService.new.todo_exist?(issuable, current_user) @@ -205,6 +246,9 @@ module SlashCommands end desc 'Subscribe' + explanation do + "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && !issuable.subscribed?(current_user, project) @@ -214,6 +258,9 @@ module SlashCommands end desc 'Unsubscribe' + explanation do + "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." + end condition do issuable.persisted? && issuable.subscribed?(current_user, project) @@ -223,18 +270,23 @@ module SlashCommands end 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>' condition do issuable.respond_to?(:due_date) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :due do |due_date_param| - due_date = Chronic.parse(due_date_param).try(:to_date) - + parse_params do |due_date_param| + Chronic.parse(due_date_param).try(:to_date) + end + command :due do |due_date| @updates[:due_date] = due_date if due_date end desc 'Remove due date' + explanation 'Removes the due date.' condition do issuable.persisted? && issuable.respond_to?(:due_date) && @@ -245,8 +297,11 @@ module SlashCommands @updates[:due_date] = nil end - desc do - "Toggle the Work In Progress status" + desc '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 condition do issuable.persisted? && @@ -257,45 +312,72 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end - desc 'Toggle emoji reward' + desc 'Toggle emoji award' + explanation do |name| + "Toggles :#{name}: emoji award." if name + end params ':emoji:' condition do issuable.persisted? end - command :award do |emoji| - name = award_emoji_name(emoji) + parse_params do |emoji_param| + 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) @updates[:emoji_award] = name end end 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>' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :estimate do |raw_duration| - time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) - + parse_params do |raw_duration| + Gitlab::TimeTrackingFormatter.parse(raw_duration) + end + command :estimate do |time_estimate| if time_estimate @updates[:time_estimate] = time_estimate end end 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>' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) end - command :spend do |raw_duration| - time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) - + parse_params do |raw_duration| + Gitlab::TimeTrackingFormatter.parse(raw_duration) + end + command :spend do |time_spent| if time_spent @updates[:spend_time] = { duration: time_spent, user: current_user } end end desc 'Remove time estimate' + explanation 'Removes time estimate.' condition do issuable.persisted? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) @@ -305,6 +387,7 @@ module SlashCommands end desc 'Remove spent time' + explanation 'Removes spent time.' condition do issuable.persisted? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) @@ -318,19 +401,28 @@ module SlashCommands params '@user' 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>' condition do issuable.respond_to?(:target_branch) && (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || issuable.new_record?) end - command :target_branch do |target_branch_param| - branch_name = target_branch_param.strip + parse_params do |target_branch_param| + target_branch_param.strip + end + command :target_branch do |branch_name| @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) end 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"' condition do issuable.is_a?(Issue) && @@ -352,11 +444,35 @@ module SlashCommands 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) - label_ids_by_reference = extract_references(labels_param, :label).map(&:id) - labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) + find_labels(labels_param).map(&:id) + end - label_ids_by_reference | labels_ids_by_name + def explain_commands(commands, opts) + commands.map do |name, arg| + definition = self.class.definition_by_name(name) + next unless definition + + 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 def extract_references(arg, type) @@ -366,9 +482,13 @@ module SlashCommands ext.references(type) end - def award_emoji_name(emoji) - match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern) - match[1] if match + def context + { + issuable: issuable, + current_user: current_user, + project: project, + params: params + } end end end diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 8d3aa4d1a74..7c7573862d0 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -26,7 +26,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .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...' .clearfix .error-alert diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index e9e06e5c8e3..3f5b0c54e50 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -5,14 +5,9 @@ - content_for :project_javascripts do - 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 :javascript window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.preview_markdown_path = "#{preview_markdown_path}"; - content_for :header_content do .js-dropdown-menu-projects diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 23e27c1105c..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,3 +1,5 @@ +- referenced_users = local_assigns.fetch(:referenced_users, nil) + .md-area .md-header %ul.nav-links.clearfix @@ -28,9 +30,10 @@ .md-write-holder = 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 %span = icon("exclamation-triangle") diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 0f4a8508751..2e978fda624 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -9,7 +9,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .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/notes/hints' .clearfix diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a1efc0b051a..00230b0bdf8 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = 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_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/notes/hints' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 0d835a9e949..46f785fefca 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,8 @@ - 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| = hidden_field_tag :view, diff_view @@ -18,7 +22,7 @@ -# DiffNote = 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, attr: :note, classes: 'note-textarea js-note-text', diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 555228623cc..2a66addb08a 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,7 +1,7 @@ %ul#notes-list.notes.main-notes-list.timeline = render "shared/notes/notes" -= render 'projects/notes/edit_form' += render 'projects/notes/edit_form', project: @project %ul.notes.notes-form.timeline %li.timeline-entry diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 79d8d721aa9..faa24a3c88e 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -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| - = 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/notes/hints' .error-alert diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 160d4c7a223..a6894b9adc0 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -28,7 +28,7 @@ .form-group = label_tag :release_description, 'Release notes', class: 'control-label' .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/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. diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0d2cd4a7476..00869aff27b 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -12,7 +12,7 @@ .form-group = f.label :content, class: 'control-label' .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/notes/hints' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 17107f55a2d..7748351b333 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,7 +17,7 @@ = 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/description', issuable: issuable, form: form += render 'shared/issuable/form/description', issuable: issuable, form: form, project: project - if issuable.respond_to?(:confidential) .form-group diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml index dbace9ce401..cbc7125c0d5 100644 --- a/app/views/shared/issuable/form/_description.html.haml +++ b/app/views/shared/issuable/form/_description.html.haml @@ -1,15 +1,22 @@ +- project = local_assigns.fetch(:project) - issuable = local_assigns.fetch(:issuable) - 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.label :description, 'Description', class: 'control-label' .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, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + supports_slash_commands: supports_slash_commands + = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands .clearfix .error-alert diff --git a/changelogs/unreleased/preview-separate-slash-commands.yml b/changelogs/unreleased/preview-separate-slash-commands.yml new file mode 100644 index 00000000000..6240ccc957c --- /dev/null +++ b/changelogs/unreleased/preview-separate-slash-commands.yml @@ -0,0 +1,4 @@ +--- +title: Display slash commands outcome when previewing Markdown +merge_request: 10054 +author: Rares Sfirlogea diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb index 60d35be2599..12a385f90fd 100644 --- a/lib/gitlab/slash_commands/command_definition.rb +++ b/lib/gitlab/slash_commands/command_definition.rb @@ -1,16 +1,19 @@ module Gitlab module SlashCommands 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 = {}) @name = name - @aliases = attributes[:aliases] || [] - @description = attributes[:description] || '' - @params = attributes[:params] || [] + @aliases = attributes[:aliases] || [] + @description = attributes[:description] || '' + @explanation = attributes[:explanation] || '' + @params = attributes[:params] || [] @condition_block = attributes[:condition_block] - @action_block = attributes[:action_block] + @parse_params_block = attributes[:parse_params_block] + @action_block = attributes[:action_block] end def all_names @@ -28,14 +31,20 @@ module Gitlab context.instance_exec(&condition_block) 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) return if noop? || !available?(opts) - if arg.present? - context.instance_exec(arg, &action_block) - elsif action_block.arity == 0 - context.instance_exec(&action_block) - end + execute_block(action_block, context, arg) end def to_h(opts) @@ -52,6 +61,23 @@ module Gitlab params: params } 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 diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb index 50b0937d267..614bafbe1b2 100644 --- a/lib/gitlab/slash_commands/dsl.rb +++ b/lib/gitlab/slash_commands/dsl.rb @@ -44,6 +44,22 @@ module Gitlab @params = params 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 # to be returned by `.command_names` & `.command_definitions`. # It accepts a block that will be evaluated with the context given to @@ -61,6 +77,24 @@ module Gitlab @condition_block = block 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 # comment. # It accepts aliases and takes a block. @@ -75,11 +109,13 @@ module Gitlab definition = CommandDefinition.new( name, - aliases: aliases, - description: @description, - params: @params, - condition_block: @condition_block, - action_block: block + aliases: aliases, + description: @description, + explanation: @explanation, + params: @params, + condition_block: @condition_block, + parse_params_block: @parse_params_block, + action_block: block ) self.command_definitions << definition @@ -89,8 +125,14 @@ module Gitlab end @description = nil + @explanation = nil @params = nil @condition_block = nil + @parse_params_block = nil + end + + def definition_by_name(name) + command_definitions_by_name[name.to_sym] end end end diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb index c9c2f314e57..5b9173d3d3f 100644 --- a/spec/lib/gitlab/slash_commands/command_definition_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb @@ -167,6 +167,58 @@ describe Gitlab::SlashCommands::CommandDefinition do 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 diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb index 2763d950716..33b49a5ddf9 100644 --- a/spec/lib/gitlab/slash_commands/dsl_spec.rb +++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb @@ -11,67 +11,99 @@ describe Gitlab::SlashCommands::Dsl do end params 'The first argument' - command :one_arg, :once, :first do |arg1| - arg1 + explanation 'Static explanation' + command :explanation_with_aliases, :once, :first do |arg| + arg end desc do "A dynamic description for #{noteable.upcase}" end params 'The first argument', 'The second argument' - command :two_args do |arg1, arg2| - [arg1, arg2] + command :dynamic_description do |args| + args.split end command :cc + explanation do |arg| + "Action does something with #{arg}" + end condition do project == 'foo' end command :cond_action do |arg| arg end + + parse_params do |raw_arg| + raw_arg.strip + end + command :with_params_parsing do |parsed| + parsed + end end end describe '.command_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.aliases).to eq([:none]) 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.condition_block).to be_nil 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(one_arg_def.aliases).to eq([:once, :first]) - expect(one_arg_def.description).to eq('') - expect(one_arg_def.params).to eq(['The first argument']) - expect(one_arg_def.condition_block).to be_nil - expect(one_arg_def.action_block).to be_a_kind_of(Proc) + expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases) + expect(explanation_with_aliases_def.aliases).to eq([:once, :first]) + expect(explanation_with_aliases_def.description).to eq('') + expect(explanation_with_aliases_def.explanation).to eq('Static explanation') + expect(explanation_with_aliases_def.params).to eq(['The first argument']) + 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(two_args_def.aliases).to eq([]) - expect(two_args_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(two_args_def.condition_block).to be_nil - expect(two_args_def.action_block).to be_a_kind_of(Proc) + expect(dynamic_description_def.name).to eq(:dynamic_description) + expect(dynamic_description_def.aliases).to eq([]) + expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE') + expect(dynamic_description_def.explanation).to eq('') + expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument']) + 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.aliases).to eq([]) expect(cc_def.description).to eq('') + expect(cc_def.explanation).to eq('') expect(cc_def.params).to eq([]) expect(cc_def.condition_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.aliases).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.condition_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 diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb new file mode 100644 index 00000000000..b2fb5c91313 --- /dev/null +++ b/spec/services/preview_markdown_service_spec.rb @@ -0,0 +1,67 @@ +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 diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 29e65fe7ce6..46cfac4a128 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -798,4 +798,211 @@ describe SlashCommands::InterpretService, services: true do 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 diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 5bbe36d9b7f..6efcdd7a1de 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -257,4 +257,19 @@ shared_examples 'issuable record that supports slash commands in its description 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 |