diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index f3d60276a6be8e778ba7666916937659eb6cafa9..f74a32b6372976b702d53d67e9823939433a7a18 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -4,21 +4,24 @@ module QuickActions
   class InterpretService < BaseService
     include Gitlab::Utils::StrongMemoize
     include Gitlab::QuickActions::Dsl
+    include Gitlab::QuickActions::IssueActions
+    include Gitlab::QuickActions::IssueAndMergeRequestActions
+    include Gitlab::QuickActions::IssuableActions
+    include Gitlab::QuickActions::MergeRequestActions
+    include Gitlab::QuickActions::CommitActions
+    include Gitlab::QuickActions::CommonActions
 
-    attr_reader :issuable
+    attr_reader :quick_action_target
 
     # Counts how many commands have been executed.
     # Used to display relevant feedback on UI when a note
     # with only commands has been processed.
     attr_accessor :commands_executed_count
 
-    SHRUG = '炉\\锛�(銉�)锛�/炉'.freeze
-    TABLEFLIP = '(鈺扳枴掳)鈺傅 鈹烩攣鈹�'.freeze
-
-    # Takes an issuable and returns an array of all the available commands
+    # Takes an quick_action_target and returns an array of all the available commands
     # represented with .to_h
-    def available_commands(issuable)
-      @issuable = issuable
+    def available_commands(quick_action_target)
+      @quick_action_target = quick_action_target
 
       self.class.command_definitions.map do |definition|
         next unless definition.available?(self)
@@ -29,10 +32,10 @@ module QuickActions
 
     # 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.
-    def execute(content, issuable, only: nil)
+    def execute(content, quick_action_target, only: nil)
       return [content, {}] unless current_user.can?(:use_quick_actions)
 
-      @issuable = issuable
+      @quick_action_target = quick_action_target
       @updates = {}
 
       content, commands = extractor.extract_commands(content, only: only)
@@ -43,10 +46,10 @@ module QuickActions
 
     # 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)
+    def explain(content, quick_action_target)
       return [content, []] unless current_user.can?(:use_quick_actions)
 
-      @issuable = issuable
+      @quick_action_target = quick_action_target
 
       content, commands = extractor.extract_commands(content)
       commands = explain_commands(commands)
@@ -59,601 +62,6 @@ module QuickActions
       Gitlab::QuickActions::Extractor.new(self.class.command_definitions)
     end
 
-    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.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.open? &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
-    end
-    command :close do
-      @updates[:state_event] = 'close'
-    end
-
-    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.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.closed? &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
-    end
-    command :reopen do
-      @updates[:state_event] = 'reopen'
-    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) &&
-        issuable.persisted? &&
-        issuable.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
-    end
-    command :merge do
-      @updates[:merge] = params[:merge_request_diff_head_sha]
-    end
-
-    desc 'Change title'
-    explanation do |title_param|
-      "Changes the title to \"#{title_param}\"."
-    end
-    params '<New title>'
-    condition do
-      issuable.persisted? &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
-    end
-    command :title do |title_param|
-      @updates[:title] = title_param
-    end
-
-    desc 'Assign'
-    # rubocop: disable CodeReuse/ActiveRecord
-    explanation do |users|
-      users = issuable.allows_multiple_assignees? ? users : users.take(1)
-      "Assigns #{users.map(&:to_reference).to_sentence}."
-    end
-    # rubocop: enable CodeReuse/ActiveRecord
-    params do
-      issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
-    end
-    condition do
-      current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    parse_params do |assignee_param|
-      extract_users(assignee_param)
-    end
-    command :assign do |users|
-      next if users.empty?
-
-      if issuable.allows_multiple_assignees?
-        @updates[:assignee_ids] ||= issuable.assignees.map(&:id)
-        @updates[:assignee_ids] += users.map(&:id)
-      else
-        @updates[:assignee_ids] = [users.first.id]
-      end
-    end
-
-    desc do
-      if issuable.allows_multiple_assignees?
-        'Remove all or specific assignee(s)'
-      else
-        'Remove assignee'
-      end
-    end
-    explanation do |users = nil|
-      assignees = issuable.assignees
-      assignees &= users if users.present? && issuable.allows_multiple_assignees?
-      "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
-    end
-    params do
-      issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
-    end
-    condition do
-      issuable.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.assignees.any? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    parse_params do |unassign_param|
-      # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
-      extract_users(unassign_param) if issuable.allows_multiple_assignees?
-    end
-    command :unassign do |users = nil|
-      if issuable.allows_multiple_assignees? && users&.any?
-        @updates[:assignee_ids] ||= issuable.assignees.map(&:id)
-        @updates[:assignee_ids] -= users.map(&:id)
-      else
-        @updates[:assignee_ids] = []
-      end
-    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) &&
-        find_milestones(project, state: 'active').any?
-    end
-    parse_params do |milestone_param|
-      extract_references(milestone_param, :milestone).first ||
-        find_milestones(project, title: milestone_param.strip).first
-    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.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.milestone_id? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :remove_milestone do
-      @updates[:milestone_id] = nil
-    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
-      parent &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", parent) &&
-        find_labels.any?
-    end
-    command :label do |labels_param|
-      label_ids = find_label_ids(labels_param)
-
-      if label_ids.any?
-        @updates[:add_label_ids] ||= []
-        @updates[:add_label_ids] += label_ids
-
-        @updates[:add_label_ids].uniq!
-      end
-    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.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.labels.any? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", parent)
-    end
-    command :unlabel do |labels_param = nil|
-      if labels_param.present?
-        label_ids = find_label_ids(labels_param)
-
-        if label_ids.any?
-          @updates[:remove_label_ids] ||= []
-          @updates[:remove_label_ids] += label_ids
-
-          @updates[:remove_label_ids].uniq!
-        end
-      else
-        @updates[:label_ids] = []
-      end
-    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.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.labels.any? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :relabel do |labels_param|
-      label_ids = find_label_ids(labels_param)
-
-      if label_ids.any?
-        @updates[:label_ids] ||= []
-        @updates[:label_ids] += label_ids
-
-        @updates[:label_ids].uniq!
-      end
-    end
-
-    desc 'Copy labels and milestone from other issue or merge request'
-    explanation do |source_issuable|
-      "Copy labels and milestone from #{source_issuable.to_reference}."
-    end
-    params '#issue | !merge_request'
-    condition do
-      [MergeRequest, Issue].include?(issuable.class) &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
-    end
-    parse_params do |issuable_param|
-      extract_references(issuable_param, :issue).first ||
-        extract_references(issuable_param, :merge_request).first
-    end
-    command :copy_metadata do |source_issuable|
-      if source_issuable.present? && source_issuable.project.id == issuable.project.id
-        @updates[:add_label_ids] = source_issuable.labels.map(&:id)
-        @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
-      end
-    end
-
-    desc 'Add a todo'
-    explanation 'Adds a todo.'
-    condition do
-      issuable.is_a?(Issuable) &&
-        issuable.persisted? &&
-        !TodoService.new.todo_exist?(issuable, current_user)
-    end
-    command :todo do
-      @updates[:todo_event] = 'add'
-    end
-
-    desc 'Mark todo as done'
-    explanation 'Marks todo as done.'
-    condition do
-      issuable.persisted? &&
-        TodoService.new.todo_exist?(issuable, current_user)
-    end
-    command :done do
-      @updates[:todo_event] = 'done'
-    end
-
-    desc 'Subscribe'
-    explanation do
-      "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}."
-    end
-    condition do
-      issuable.is_a?(Issuable) &&
-        issuable.persisted? &&
-        !issuable.subscribed?(current_user, project)
-    end
-    command :subscribe do
-      @updates[:subscription_event] = 'subscribe'
-    end
-
-    desc 'Unsubscribe'
-    explanation do
-      "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}."
-    end
-    condition do
-      issuable.is_a?(Issuable) &&
-        issuable.persisted? &&
-        issuable.subscribed?(current_user, project)
-    end
-    command :unsubscribe do
-      @updates[:subscription_event] = 'unsubscribe'
-    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
-    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) &&
-        issuable.due_date? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :remove_due_date do
-      @updates[:due_date] = nil
-    end
-
-    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.respond_to?(:work_in_progress?) &&
-        # Allow it to mark as WIP on MR creation page _or_ through MR notes.
-        (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable))
-    end
-    command :wip do
-      @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
-    end
-
-    desc 'Toggle emoji award'
-    explanation do |name|
-      "Toggles :#{name}: emoji award." if name
-    end
-    params ':emoji:'
-    condition do
-      issuable.is_a?(Issuable) &&
-        issuable.persisted?
-    end
-    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)
-        @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
-    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 subtract spent time'
-    explanation do |time_spent, time_spent_date|
-      if time_spent
-        if time_spent > 0
-          verb = 'Adds'
-          value = time_spent
-        else
-          verb = 'Subtracts'
-          value = -time_spent
-        end
-
-        "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
-      end
-    end
-    params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
-    condition do
-      issuable.is_a?(TimeTrackable) &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-    end
-    parse_params do |raw_time_date|
-      Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
-    end
-    command :spend do |time_spent, time_spent_date|
-      if time_spent
-        @updates[:spend_time] = {
-          duration: time_spent,
-          user_id: current_user.id,
-          spent_at: time_spent_date
-        }
-      end
-    end
-
-    desc 'Remove time estimate'
-    explanation 'Removes time estimate.'
-    condition do
-      issuable.persisted? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :remove_estimate do
-      @updates[:time_estimate] = 0
-    end
-
-    desc 'Remove spent time'
-    explanation 'Removes spent time.'
-    condition do
-      issuable.persisted? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :remove_time_spent do
-      @updates[:spend_time] = { duration: :reset, user_id: current_user.id }
-    end
-
-    desc "Append the comment with #{SHRUG}"
-    params '<Comment>'
-    substitution :shrug do |comment|
-      "#{comment} #{SHRUG}"
-    end
-
-    desc "Append the comment with #{TABLEFLIP}"
-    params '<Comment>'
-    substitution :tableflip do |comment|
-      "#{comment} #{TABLEFLIP}"
-    end
-
-    desc "Lock the discussion"
-    explanation "Locks the discussion"
-    condition do
-      [MergeRequest, Issue].include?(issuable.class) &&
-        issuable.persisted? &&
-        !issuable.discussion_locked? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-    end
-    command :lock do
-      @updates[:discussion_locked] = true
-    end
-
-    desc "Unlock the discussion"
-    explanation "Unlocks the discussion"
-    condition do
-      [MergeRequest, Issue].include?(issuable.class) &&
-        issuable.persisted? &&
-        issuable.discussion_locked? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-    end
-    command :unlock do
-      @updates[:discussion_locked] = false
-    end
-
-    # This is a dummy command, so that it appears in the autocomplete commands
-    desc 'CC'
-    params '@user'
-    command :cc
-
-    desc 'Set target branch'
-    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
-    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_exists?(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) &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable) &&
-        issuable.project.boards.count == 1
-    end
-
-    command :board_move do |target_list_name|
-      label_ids = find_label_ids(target_list_name)
-
-      if label_ids.size == 1
-        label_id = label_ids.first
-
-        # Ensure this label corresponds to a list on the board
-        next unless Label.on_project_boards(issuable.project_id).id_in(label_id).exists?
-
-        @updates[:remove_label_ids] = issuable
-            .labels
-            .on_project_boards(issuable.project_id)
-            .id_not_in(label_id)
-            .pluck_primary_key
-
-        @updates[:add_label_ids] = [label_id]
-      end
-    end
-
-    desc 'Mark this issue as a duplicate of another issue'
-    explanation do |duplicate_reference|
-      "Marks this issue as a duplicate of #{duplicate_reference}."
-    end
-    params '#issue'
-    condition do
-      issuable.is_a?(Issue) &&
-        issuable.persisted? &&
-        current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
-    end
-    command :duplicate do |duplicate_param|
-      canonical_issue = extract_references(duplicate_param, :issue).first
-
-      if canonical_issue.present?
-        @updates[:canonical_issue_id] = canonical_issue.id
-      end
-    end
-
-    desc 'Move this issue to another project.'
-    explanation do |path_to_project|
-      "Moves this issue to #{path_to_project}."
-    end
-    params 'path/to/project'
-    condition do
-      issuable.is_a?(Issue) &&
-        issuable.persisted? &&
-        current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-    end
-    command :move do |target_project_path|
-      target_project = Project.find_by_full_path(target_project_path)
-
-      if target_project.present?
-        @updates[:target_project] = target_project
-      end
-    end
-
-    desc 'Make issue confidential.'
-    explanation do
-      'Makes this issue confidential'
-    end
-    condition do
-      issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-    end
-    command :confidential do
-      @updates[:confidential] = true
-    end
-
-    desc 'Tag this commit.'
-    explanation do |tag_name, message|
-      with_message = %{ with "#{message}"} if message.present?
-      "Tags this commit to #{tag_name}#{with_message}."
-    end
-    params 'v1.2.3 <message>'
-    parse_params do |tag_name_and_message|
-      tag_name_and_message.split(' ', 2)
-    end
-    condition do
-      issuable.is_a?(Commit) && current_user.can?(:push_code, project)
-    end
-    command :tag do |tag_name, message|
-      @updates[:tag_name] = tag_name
-      @updates[:tag_message] = message
-    end
-
-    desc 'Create a merge request.'
-    explanation do |branch_name = nil|
-      branch_text = branch_name ? "branch '#{branch_name}'" : 'a branch'
-      "Creates #{branch_text} and a merge request to resolve this issue"
-    end
-    params "<branch name>"
-    condition do
-      issuable.is_a?(Issue) && current_user.can?(:create_merge_request_in, project) && current_user.can?(:push_code, project)
-    end
-    command :create_merge_request do |branch_name = nil|
-      @updates[:create_merge_request] = {
-        branch_name: branch_name,
-        issue_iid: issuable.iid
-      }
-    end
-
     # rubocop: disable CodeReuse/ActiveRecord
     def extract_users(params)
       return [] if params.nil?
@@ -683,7 +91,7 @@ module QuickActions
 
     def group
       strong_memoize(:group) do
-        issuable.group if issuable.respond_to?(:group)
+        quick_action_target.group if quick_action_target.respond_to?(:group)
       end
     end
 
diff --git a/ee/app/services/ee/quick_actions/interpret_service.rb b/ee/app/services/ee/quick_actions/interpret_service.rb
index 27b456993342901bbe628c885b90d8e86ee4318f..eb3df8027000395ab57d254e6cd31700d66911c0 100644
--- a/ee/app/services/ee/quick_actions/interpret_service.rb
+++ b/ee/app/services/ee/quick_actions/interpret_service.rb
@@ -4,108 +4,14 @@ module EE
   module QuickActions
     module InterpretService
       extend ActiveSupport::Concern
-
       # We use "prepended" here instead of including Gitlab::QuickActions::Dsl,
       # as doing so would clear any existing command definitions.
       prepended do
-        desc 'Change assignee(s)'
-        explanation do
-          'Change assignee(s).'
-        end
-        params '@user1 @user2'
-        condition do
-          issuable.is_a?(::Issuable) &&
-            issuable.allows_multiple_assignees? &&
-            issuable.persisted? &&
-            current_user.can?(:"admin_#{issuable.to_ability_name}", project)
-        end
-        command :reassign do |reassign_param|
-          @updates[:assignee_ids] = extract_users(reassign_param).map(&:id)
-        end
-
-        desc 'Set weight'
-        explanation do |weight|
-          "Sets weight to #{weight}." if weight
-        end
-        params "0, 1, 2, 鈥�"
-        condition do
-          issuable.is_a?(::Issuable) &&
-            issuable.supports_weight? &&
-            current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-        end
-        parse_params do |weight|
-          weight.to_i if weight.to_i > 0
-        end
-        command :weight do |weight|
-          @updates[:weight] = weight if weight
-        end
-
-        desc 'Clear weight'
-        explanation 'Clears weight.'
-        condition do
-          issuable.is_a?(::Issuable) &&
-            issuable.persisted? &&
-            issuable.supports_weight? &&
-            issuable.weight? &&
-            current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-        end
-        command :clear_weight do
-          @updates[:weight] = nil
-        end
-
-        desc 'Add to epic'
-        explanation 'Adds an issue to an epic.'
-        condition do
-          issuable.is_a?(::Issue) &&
-            issuable.project.group&.feature_available?(:epics) &&
-            current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-        end
-        params '<&epic | group&epic | Epic URL>'
-        command :epic do |epic_param|
-          @updates[:epic] = extract_epic(epic_param)
-        end
-
-        desc 'Remove from epic'
-        explanation 'Removes an issue from an epic.'
-        condition do
-          issuable.is_a?(::Issue) &&
-            issuable.persisted? &&
-            issuable.project.group&.feature_available?(:epics) &&
-            current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
-        end
-        command :remove_epic do
-          @updates[:epic] = nil
-        end
-
-        desc 'Approve a merge request'
-        explanation 'Approve the current merge request.'
-        condition do
-          issuable.is_a?(MergeRequest) && issuable.persisted? && issuable.can_approve?(current_user)
-        end
-        command :approve do
-          if issuable.can_approve?(current_user)
-            ::MergeRequests::ApprovalService.new(issuable.project, current_user).execute(issuable)
-          end
-        end
-
-        desc 'Promote issue to an epic'
-        explanation 'Promote issue to an epic.'
-        warning 'may expose confidential information'
-        condition do
-          issuable.is_a?(Issue) &&
-            issuable.persisted? &&
-            current_user.can?(:admin_issue, project) &&
-            current_user.can?(:create_epic, project.group)
-        end
-        command :promote do
-          Epics::IssuePromoteService.new(issuable.project, current_user).execute(issuable)
-        end
-      end
-
-      def extract_epic(params)
-        return if params.nil?
-
-        extract_references(params, :epic).first
+        # rubocop: disable Cop/InjectEnterpriseEditionModule
+        include EE::Gitlab::QuickActions::IssueActions
+        include EE::Gitlab::QuickActions::MergeRequestActions
+        include EE::Gitlab::QuickActions::IssueAndMergeRequestActions
+        # rubocop: enable Cop/InjectEnterpriseEditionModule
       end
     end
   end
diff --git a/ee/lib/ee/gitlab/quick_actions/issue_actions.rb b/ee/lib/ee/gitlab/quick_actions/issue_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bd8eeb5cdb2ea19587691047535b4b313a2bd5c4
--- /dev/null
+++ b/ee/lib/ee/gitlab/quick_actions/issue_actions.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module EE
+  module Gitlab
+    module QuickActions
+      module IssueActions
+        extend ActiveSupport::Concern
+        include ::Gitlab::QuickActions::Dsl
+
+        included do
+          desc 'Add to epic'
+          explanation 'Adds an issue to an epic.'
+          types Issue
+          condition do
+            quick_action_target.project.group&.feature_available?(:epics) &&
+              current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+          end
+          params '<&epic | group&epic | Epic URL>'
+          command :epic do |epic_param|
+            @updates[:epic] = extract_epic(epic_param)
+          end
+
+          desc 'Remove from epic'
+          explanation 'Removes an issue from an epic.'
+          types Issue
+          condition do
+            quick_action_target.persisted? &&
+              quick_action_target.project.group&.feature_available?(:epics) &&
+              current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+          end
+          command :remove_epic do
+            @updates[:epic] = nil
+          end
+
+          desc 'Promote issue to an epic'
+          explanation 'Promote issue to an epic.'
+          warning 'may expose confidential information'
+          types Issue
+          condition do
+            quick_action_target.persisted? &&
+              current_user.can?(:admin_issue, project) &&
+              current_user.can?(:create_epic, project.group)
+          end
+          command :promote do
+            Epics::IssuePromoteService.new(quick_action_target.project, current_user).execute(quick_action_target)
+          end
+
+          def extract_epic(params)
+            return if params.nil?
+
+            extract_references(params, :epic).first
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/ee/gitlab/quick_actions/issue_and_merge_request_actions.rb b/ee/lib/ee/gitlab/quick_actions/issue_and_merge_request_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7e02540be9473d346677024285ec42dd6f2b11d
--- /dev/null
+++ b/ee/lib/ee/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module EE
+  module Gitlab
+    module QuickActions
+      module IssueAndMergeRequestActions
+        extend ActiveSupport::Concern
+        include ::Gitlab::QuickActions::Dsl
+
+        included do
+          desc 'Change assignee(s)'
+          explanation do
+            'Change assignee(s).'
+          end
+          params '@user1 @user2'
+          types Issue, MergeRequest
+          condition do
+            quick_action_target.allows_multiple_assignees? &&
+              quick_action_target.persisted? &&
+              current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+          end
+          command :reassign do |reassign_param|
+            @updates[:assignee_ids] = extract_users(reassign_param).map(&:id)
+          end
+
+          desc 'Set weight'
+          explanation do |weight|
+            "Sets weight to #{weight}." if weight
+          end
+          params "0, 1, 2, 鈥�"
+          types Issue, MergeRequest
+          condition do
+            quick_action_target.supports_weight? &&
+              current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+          end
+          parse_params do |weight|
+            weight.to_i if weight.to_i > 0
+          end
+          command :weight do |weight|
+            @updates[:weight] = weight if weight
+          end
+
+          desc 'Clear weight'
+          explanation 'Clears weight.'
+          types Issue, MergeRequest
+          condition do
+            quick_action_target.persisted? &&
+              quick_action_target.supports_weight? &&
+              quick_action_target.weight? &&
+              current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+          end
+          command :clear_weight do
+            @updates[:weight] = nil
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/ee/gitlab/quick_actions/merge_request_actions.rb b/ee/lib/ee/gitlab/quick_actions/merge_request_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9f2c37d011426c72e661899b88460a6f78de7f6
--- /dev/null
+++ b/ee/lib/ee/gitlab/quick_actions/merge_request_actions.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module EE
+  module Gitlab
+    module QuickActions
+      module MergeRequestActions
+        extend ActiveSupport::Concern
+        include ::Gitlab::QuickActions::Dsl
+
+        included do
+          desc 'Approve a merge request'
+          explanation 'Approve the current merge request.'
+          types MergeRequest
+          condition do
+            quick_action_target.persisted? && quick_action_target.can_approve?(current_user)
+          end
+          command :approve do
+            if quick_action_target.can_approve?(current_user)
+              ::MergeRequests::ApprovalService.new(quick_action_target.project, current_user).execute(quick_action_target)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/command_definition.rb b/lib/gitlab/quick_actions/command_definition.rb
index e7bfcb16582efe2022b1c36361887b804ffb4b19..93030fd454e73c7b60f02f9c27891e15b8134142 100644
--- a/lib/gitlab/quick_actions/command_definition.rb
+++ b/lib/gitlab/quick_actions/command_definition.rb
@@ -4,7 +4,7 @@ module Gitlab
   module QuickActions
     class CommandDefinition
       attr_accessor :name, :aliases, :description, :explanation, :params,
-        :condition_block, :parse_params_block, :action_block, :warning
+        :condition_block, :parse_params_block, :action_block, :warning, :types
 
       def initialize(name, attributes = {})
         @name = name
@@ -17,6 +17,7 @@ module Gitlab
         @condition_block = attributes[:condition_block]
         @parse_params_block = attributes[:parse_params_block]
         @action_block = attributes[:action_block]
+        @types = attributes[:types] || []
       end
 
       def all_names
@@ -28,6 +29,7 @@ module Gitlab
       end
 
       def available?(context)
+        return false unless valid_type?(context)
         return true unless condition_block
 
         context.instance_exec(&condition_block)
@@ -96,6 +98,10 @@ module Gitlab
 
         context.instance_exec(arg, &parse_params_block)
       end
+
+      def valid_type?(context)
+        types.blank? || types.any? { |type| context.quick_action_target.is_a?(type) }
+      end
     end
   end
 end
diff --git a/lib/gitlab/quick_actions/commit_actions.rb b/lib/gitlab/quick_actions/commit_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..62c0fbb5afd9a034c1422b98279254fc2cbc0a43
--- /dev/null
+++ b/lib/gitlab/quick_actions/commit_actions.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module CommitActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      included do
+        # Commit only quick actions definitions
+        desc 'Tag this commit.'
+        explanation do |tag_name, message|
+          with_message = %{ with "#{message}"} if message.present?
+          "Tags this commit to #{tag_name}#{with_message}."
+        end
+        params 'v1.2.3 <message>'
+        parse_params do |tag_name_and_message|
+          tag_name_and_message.split(' ', 2)
+        end
+        types Commit
+        condition do
+          current_user.can?(:push_code, project)
+        end
+        command :tag do |tag_name, message|
+          @updates[:tag_name] = tag_name
+          @updates[:tag_message] = message
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/common_actions.rb b/lib/gitlab/quick_actions/common_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d2732d4826ed7ab055946bfba8c1f283098e136
--- /dev/null
+++ b/lib/gitlab/quick_actions/common_actions.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module CommonActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      included do
+        # This is a dummy command, so that it appears in the autocomplete commands
+        desc 'CC'
+        params '@user'
+        command :cc
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/dsl.rb b/lib/gitlab/quick_actions/dsl.rb
index a3aab92061bdc3c4c1d36dc2ad3ab5ddd053ad86..ecb2169151e847ccd05275b7b0e2919d5ea1dfce 100644
--- a/lib/gitlab/quick_actions/dsl.rb
+++ b/lib/gitlab/quick_actions/dsl.rb
@@ -24,7 +24,7 @@ module Gitlab
         # Example:
         #
         #   desc do
-        #     "This is a dynamic description for #{noteable.to_ability_name}"
+        #     "This is a dynamic description for #{quick_action_target.to_ability_name}"
         #   end
         #   command :command_key do |arguments|
         #     # Awesome code block
@@ -66,6 +66,23 @@ module Gitlab
           @explanation = block_given? ? block : text
         end
 
+        # Allows to define type(s) that must be met in order for the command
+        # to be returned by `.command_names` & `.command_definitions`.
+        #
+        # It is being evaluated before the conditions block is being evaluated
+        #
+        # If no types are passed then any type is allowed as the check is simply skipped.
+        #
+        # Example:
+        #
+        #   types Commit, Issue, MergeRequest
+        #   command :command_key do |arguments|
+        #     # Awesome code block
+        #   end
+        def types(*types_list)
+          @types = types_list
+        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
@@ -144,7 +161,8 @@ module Gitlab
             params: @params,
             condition_block: @condition_block,
             parse_params_block: @parse_params_block,
-            action_block: block
+            action_block: block,
+            types: @types
           )
 
           self.command_definitions << definition
@@ -159,6 +177,7 @@ module Gitlab
           @condition_block = nil
           @warning = nil
           @parse_params_block = nil
+          @types = nil
         end
       end
     end
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ad2e15d19fa729c3260720f07de4cca8924268ef
--- /dev/null
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module IssuableActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      SHRUG = '炉\\锛�(銉�)锛�/炉'.freeze
+      TABLEFLIP = '(鈺扳枴掳)鈺傅 鈹烩攣鈹�'.freeze
+
+      included do
+        # Issue, MergeRequest, Epic: quick actions definitions
+        desc do
+          "Close this #{quick_action_target.to_ability_name.humanize(capitalize: false)}"
+        end
+        explanation do
+          "Closes this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
+        end
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.open? &&
+            current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :close do
+          @updates[:state_event] = 'close'
+        end
+
+        desc do
+          "Reopen this #{quick_action_target.to_ability_name.humanize(capitalize: false)}"
+        end
+        explanation do
+          "Reopens this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
+        end
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.closed? &&
+            current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :reopen do
+          @updates[:state_event] = 'reopen'
+        end
+
+        desc 'Change title'
+        explanation do |title_param|
+          "Changes the title to \"#{title_param}\"."
+        end
+        params '<New title>'
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :title do |title_param|
+          @updates[:title] = title_param
+        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"'
+        types Issuable
+        condition do
+          parent &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent) &&
+            find_labels.any?
+        end
+        command :label do |labels_param|
+          label_ids = find_label_ids(labels_param)
+
+          if label_ids.any?
+            @updates[:add_label_ids] ||= []
+            @updates[:add_label_ids] += label_ids
+
+            @updates[:add_label_ids].uniq!
+          end
+        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"'
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.labels.any? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent)
+        end
+        command :unlabel do |labels_param = nil|
+          if labels_param.present?
+            label_ids = find_label_ids(labels_param)
+
+            if label_ids.any?
+              @updates[:remove_label_ids] ||= []
+              @updates[:remove_label_ids] += label_ids
+
+              @updates[:remove_label_ids].uniq!
+            end
+          else
+            @updates[:label_ids] = []
+          end
+        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"'
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.labels.any? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent)
+        end
+        command :relabel do |labels_param|
+          label_ids = find_label_ids(labels_param)
+
+          if label_ids.any?
+            @updates[:label_ids] ||= []
+            @updates[:label_ids] += label_ids
+
+            @updates[:label_ids].uniq!
+          end
+        end
+
+        desc 'Add a todo'
+        explanation 'Adds a todo.'
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            !TodoService.new.todo_exist?(quick_action_target, current_user)
+        end
+        command :todo do
+          @updates[:todo_event] = 'add'
+        end
+
+        desc 'Mark todo as done'
+        explanation 'Marks todo as done.'
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            TodoService.new.todo_exist?(quick_action_target, current_user)
+        end
+        command :done do
+          @updates[:todo_event] = 'done'
+        end
+
+        desc 'Subscribe'
+        explanation do
+          "Subscribes to this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
+        end
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            !quick_action_target.subscribed?(current_user, project)
+        end
+        command :subscribe do
+          @updates[:subscription_event] = 'subscribe'
+        end
+
+        desc 'Unsubscribe'
+        explanation do
+          "Unsubscribes from this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
+        end
+        types Issuable
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.subscribed?(current_user, project)
+        end
+        command :unsubscribe do
+          @updates[:subscription_event] = 'unsubscribe'
+        end
+
+        desc 'Toggle emoji award'
+        explanation do |name|
+          "Toggles :#{name}: emoji award." if name
+        end
+        params ':emoji:'
+        types Issuable
+        condition do
+          quick_action_target.persisted?
+        end
+        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 && quick_action_target.user_can_award?(current_user)
+            @updates[:emoji_award] = name
+          end
+        end
+
+        desc "Append the comment with #{SHRUG}"
+        params '<Comment>'
+        types Issuable
+        substitution :shrug do |comment|
+          "#{comment} #{SHRUG}"
+        end
+
+        desc "Append the comment with #{TABLEFLIP}"
+        params '<Comment>'
+        types Issuable
+        substitution :tableflip do |comment|
+          "#{comment} #{TABLEFLIP}"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f08e8740a21fc1e03493cf622955cf5dd314a53
--- /dev/null
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module IssueActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      included do
+        # Issue only quick actions definition
+        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>'
+        types Issue
+        condition do
+          quick_action_target.respond_to?(:due_date) &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        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.'
+        types Issue
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.respond_to?(:due_date) &&
+            quick_action_target.due_date? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        command :remove_due_date do
+          @updates[:due_date] = nil
+        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"'
+        types Issue
+        condition do
+          current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) &&
+            quick_action_target.project.boards.count == 1
+        end
+        # rubocop: disable CodeReuse/ActiveRecord
+        command :board_move do |target_list_name|
+          label_ids = find_label_ids(target_list_name)
+
+          if label_ids.size == 1
+            label_id = label_ids.first
+
+            # Ensure this label corresponds to a list on the board
+            next unless Label.on_project_boards(quick_action_target.project_id).where(id: label_id).exists?
+
+            @updates[:remove_label_ids] =
+              quick_action_target.labels.on_project_boards(quick_action_target.project_id).where.not(id: label_id).pluck(:id)
+            @updates[:add_label_ids] = [label_id]
+          end
+        end
+        # rubocop: enable CodeReuse/ActiveRecord
+
+        desc 'Mark this issue as a duplicate of another issue'
+        explanation do |duplicate_reference|
+          "Marks this issue as a duplicate of #{duplicate_reference}."
+        end
+        params '#issue'
+        types Issue
+        condition do
+          quick_action_target.persisted? &&
+            current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :duplicate do |duplicate_param|
+          canonical_issue = extract_references(duplicate_param, :issue).first
+
+          if canonical_issue.present?
+            @updates[:canonical_issue_id] = canonical_issue.id
+          end
+        end
+
+        desc 'Move this issue to another project.'
+        explanation do |path_to_project|
+          "Moves this issue to #{path_to_project}."
+        end
+        params 'path/to/project'
+        types Issue
+        condition do
+          quick_action_target.persisted? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        command :move do |target_project_path|
+          target_project = Project.find_by_full_path(target_project_path)
+
+          if target_project.present?
+            @updates[:target_project] = target_project
+          end
+        end
+
+        desc 'Make issue confidential.'
+        explanation do
+          'Makes this issue confidential'
+        end
+        types Issue
+        condition do
+          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :confidential do
+          @updates[:confidential] = true
+        end
+
+        desc 'Create a merge request.'
+        explanation do |branch_name = nil|
+          branch_text = branch_name ? "branch '#{branch_name}'" : 'a branch'
+          "Creates #{branch_text} and a merge request to resolve this issue"
+        end
+        params "<branch name>"
+        types Issue
+        condition do
+          current_user.can?(:create_merge_request_in, project) && current_user.can?(:push_code, project)
+        end
+        command :create_merge_request do |branch_name = nil|
+          @updates[:create_merge_request] = {
+            branch_name: branch_name,
+            issue_iid: quick_action_target.iid
+          }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..08872eda4107683616ec380625878c3bfb4797f2
--- /dev/null
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module IssueAndMergeRequestActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      included do
+        # Issue, MergeRequest: quick actions definitions
+        desc 'Assign'
+        # rubocop: disable CodeReuse/ActiveRecord
+        explanation do |users|
+          users = quick_action_target.allows_multiple_assignees? ? users : users.take(1)
+          "Assigns #{users.map(&:to_reference).to_sentence}."
+        end
+        # rubocop: enable CodeReuse/ActiveRecord
+        params do
+          quick_action_target.allows_multiple_assignees? ? '@user1 @user2' : '@user'
+        end
+        types Issue, MergeRequest
+        condition do
+          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        parse_params do |assignee_param|
+          extract_users(assignee_param)
+        end
+        command :assign do |users|
+          next if users.empty?
+
+          if quick_action_target.allows_multiple_assignees?
+            @updates[:assignee_ids] ||= quick_action_target.assignees.map(&:id)
+            @updates[:assignee_ids] += users.map(&:id)
+          else
+            @updates[:assignee_ids] = [users.first.id]
+          end
+        end
+
+        desc do
+          if quick_action_target.allows_multiple_assignees?
+            'Remove all or specific assignee(s)'
+          else
+            'Remove assignee'
+          end
+        end
+        explanation do |users = nil|
+          assignees = quick_action_target.assignees
+          assignees &= users if users.present? && quick_action_target.allows_multiple_assignees?
+          "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
+        end
+        params do
+          quick_action_target.allows_multiple_assignees? ? '@user1 @user2' : ''
+        end
+        types Issue, MergeRequest
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.assignees.any? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        parse_params do |unassign_param|
+          # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
+          extract_users(unassign_param) if quick_action_target.allows_multiple_assignees?
+        end
+        command :unassign do |users = nil|
+          if quick_action_target.allows_multiple_assignees? && users&.any?
+            @updates[:assignee_ids] ||= quick_action_target.assignees.map(&:id)
+            @updates[:assignee_ids] -= users.map(&:id)
+          else
+            @updates[:assignee_ids] = []
+          end
+        end
+
+        desc 'Set milestone'
+        explanation do |milestone|
+          "Sets the milestone to #{milestone.to_reference}." if milestone
+        end
+        params '%"milestone"'
+        types Issue, MergeRequest
+        condition do
+          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
+            find_milestones(project, state: 'active').any?
+        end
+        parse_params do |milestone_param|
+          extract_references(milestone_param, :milestone).first ||
+            find_milestones(project, title: milestone_param.strip).first
+        end
+        command :milestone do |milestone|
+          @updates[:milestone_id] = milestone.id if milestone
+        end
+
+        desc 'Remove milestone'
+        explanation do
+          "Removes #{quick_action_target.milestone.to_reference(format: :name)} milestone."
+        end
+        types Issue, MergeRequest
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.milestone_id? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        command :remove_milestone do
+          @updates[:milestone_id] = nil
+        end
+
+        desc 'Copy labels and milestone from other issue or merge request'
+        explanation do |source_issuable|
+          "Copy labels and milestone from #{source_issuable.to_reference}."
+        end
+        params '#issue | !merge_request'
+        types Issue, MergeRequest
+        condition do
+          current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        parse_params do |issuable_param|
+          extract_references(issuable_param, :issue).first ||
+            extract_references(issuable_param, :merge_request).first
+        end
+        command :copy_metadata do |source_issuable|
+          if source_issuable.present? && source_issuable.project.id == quick_action_target.project.id
+            @updates[:add_label_ids] = source_issuable.labels.map(&:id)
+            @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
+          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>'
+        types Issue, MergeRequest
+        condition do
+          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        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 subtract spent time'
+        explanation do |time_spent, time_spent_date|
+          if time_spent
+            if time_spent > 0
+              verb = 'Adds'
+              value = time_spent
+            else
+              verb = 'Subtracts'
+              value = -time_spent
+            end
+
+            "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
+          end
+        end
+        params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
+        types Issue, MergeRequest
+        condition do
+          current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        parse_params do |raw_time_date|
+          Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
+        end
+        command :spend do |time_spent, time_spent_date|
+          if time_spent
+            @updates[:spend_time] = {
+              duration: time_spent,
+              user_id: current_user.id,
+              spent_at: time_spent_date
+            }
+          end
+        end
+
+        desc 'Remove time estimate'
+        explanation 'Removes time estimate.'
+        types Issue, MergeRequest
+        condition do
+          quick_action_target.persisted? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        command :remove_estimate do
+          @updates[:time_estimate] = 0
+        end
+
+        desc 'Remove spent time'
+        explanation 'Removes spent time.'
+        condition do
+          quick_action_target.persisted? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
+        end
+        types Issue, MergeRequest
+        command :remove_time_spent do
+          @updates[:spend_time] = { duration: :reset, user_id: current_user.id }
+        end
+
+        desc "Lock the discussion"
+        explanation "Locks the discussion"
+        types Issue, MergeRequest
+        condition do
+          quick_action_target.persisted? &&
+            !quick_action_target.discussion_locked? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :lock do
+          @updates[:discussion_locked] = true
+        end
+
+        desc "Unlock the discussion"
+        explanation "Unlocks the discussion"
+        types Issue, MergeRequest
+        condition do
+          quick_action_target.persisted? &&
+            quick_action_target.discussion_locked? &&
+            current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
+        end
+        command :unlock do
+          @updates[:discussion_locked] = false
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bade59182a1c03b4e1db3b6c9f58ae6fd3ebea18
--- /dev/null
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module QuickActions
+    module MergeRequestActions
+      extend ActiveSupport::Concern
+      include Gitlab::QuickActions::Dsl
+
+      included do
+        # MergeRequest only quick actions definitions
+        desc 'Merge (when the pipeline succeeds)'
+        explanation 'Merges this merge request when the pipeline succeeds.'
+        types MergeRequest
+        condition do
+          last_diff_sha = params && params[:merge_request_diff_head_sha]
+          quick_action_target.persisted? &&
+            quick_action_target.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
+        end
+        command :merge do
+          @updates[:merge] = params[:merge_request_diff_head_sha]
+        end
+
+        desc 'Toggle the Work In Progress status'
+        explanation do
+          verb = quick_action_target.work_in_progress? ? 'Unmarks' : 'Marks'
+          noun = quick_action_target.to_ability_name.humanize(capitalize: false)
+          "#{verb} this #{noun} as Work In Progress."
+        end
+        types MergeRequest
+        condition do
+          quick_action_target.respond_to?(:work_in_progress?) &&
+            # Allow it to mark as WIP on MR creation page _or_ through MR notes.
+            (quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
+        end
+        command :wip do
+          @updates[:wip_event] = quick_action_target.work_in_progress? ? 'unwip' : 'wip'
+        end
+
+        desc 'Set target branch'
+        explanation do |branch_name|
+          "Sets target branch to #{branch_name}."
+        end
+        params '<Local branch name>'
+        types MergeRequest
+        condition do
+          quick_action_target.respond_to?(:target_branch) &&
+            (current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) ||
+              quick_action_target.new_record?)
+        end
+        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_exists?(branch_name)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index 896cf16a7159a7c54dfc578b05c0298671d05646..76761e148e4ca876ebdf957902889f3be1f4ffca 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -3,8 +3,41 @@ require 'rails_helper'
 describe 'Issues > User uses quick actions', :js do
   include Spec::Support::Helpers::Features::NotesHelpers
 
-  it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
+  context "issuable common quick actions" do
+    let(:new_url_opts) { {} }
+    let(:maintainer) { create(:user) }
+    let(:project) { create(:project, :public) }
+    let!(:label_bug) { create(:label, project: project, title: 'bug') }
+    let!(:label_feature) { create(:label, project: project, title: 'feature') }
+    let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
     let(:issuable) { create(:issue, project: project) }
+    let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])}
+
+    it_behaves_like 'assign quick action', :issue
+    it_behaves_like 'unassign quick action', :issue
+    it_behaves_like 'close quick action', :issue
+    it_behaves_like 'reopen quick action', :issue
+    it_behaves_like 'title quick action', :issue
+    it_behaves_like 'todo quick action', :issue
+    it_behaves_like 'done quick action', :issue
+    it_behaves_like 'subscribe quick action', :issue
+    it_behaves_like 'unsubscribe quick action', :issue
+    it_behaves_like 'lock quick action', :issue
+    it_behaves_like 'unlock quick action', :issue
+    it_behaves_like 'milestone quick action', :issue
+    it_behaves_like 'remove_milestone quick action', :issue
+    it_behaves_like 'label quick action', :issue
+    it_behaves_like 'unlabel quick action', :issue
+    it_behaves_like 'relabel quick action', :issue
+    it_behaves_like 'award quick action', :issue
+    it_behaves_like 'estimate quick action', :issue
+    it_behaves_like 'remove_estimate quick action', :issue
+    it_behaves_like 'spend quick action', :issue
+    it_behaves_like 'remove_time_spent quick action', :issue
+    it_behaves_like 'shrug quick action', :issue
+    it_behaves_like 'tableflip quick action', :issue
+    it_behaves_like 'copy_metadata quick action', :issue
+    it_behaves_like 'issuable time tracker', :issue
   end
 
   describe 'issue-only commands' do
@@ -15,37 +48,17 @@ describe 'Issues > User uses quick actions', :js do
       project.add_maintainer(user)
       sign_in(user)
       visit project_issue_path(project, issue)
+      wait_for_all_requests
     end
 
     after do
       wait_for_requests
     end
 
-    describe 'time tracking' do
-      let(:issue) { create(:issue, project: project) }
-
-      before do
-        visit project_issue_path(project, issue)
-      end
-
-      it_behaves_like 'issuable time tracker'
-    end
-
     describe 'adding a due date from note' do
       let(:issue) { create(:issue, project: project) }
 
-      context 'when the current user can update the due date' do
-        it 'does not create a note, and sets the due date accordingly' do
-          add_note("/due 2016-08-28")
-
-          expect(page).not_to have_content '/due 2016-08-28'
-          expect(page).to have_content 'Commands applied'
-
-          issue.reload
-
-          expect(issue.due_date).to eq Date.new(2016, 8, 28)
-        end
-      end
+      it_behaves_like 'due quick action available and date can be added'
 
       context 'when the current user cannot update the due date' do
         let(:guest) { create(:user) }
@@ -56,35 +69,14 @@ describe 'Issues > User uses quick actions', :js do
           visit project_issue_path(project, issue)
         end
 
-        it 'does not create a note, and sets the due date accordingly' do
-          add_note("/due 2016-08-28")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          issue.reload
-
-          expect(issue.due_date).to be_nil
-        end
+        it_behaves_like 'due quick action not available'
       end
     end
 
     describe 'removing a due date from note' do
       let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
 
-      context 'when the current user can update the due date' do
-        it 'does not create a note, and removes the due date accordingly' do
-          expect(issue.due_date).to eq Date.new(2016, 8, 28)
-
-          add_note("/remove_due_date")
-
-          expect(page).not_to have_content '/remove_due_date'
-          expect(page).to have_content 'Commands applied'
-
-          issue.reload
-
-          expect(issue.due_date).to be_nil
-        end
-      end
+      it_behaves_like 'remove_due_date action available and due date can be removed'
 
       context 'when the current user cannot update the due date' do
         let(:guest) { create(:user) }
@@ -95,15 +87,7 @@ describe 'Issues > User uses quick actions', :js do
           visit project_issue_path(project, issue)
         end
 
-        it 'does not create a note, and sets the due date accordingly' do
-          add_note("/remove_due_date")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          issue.reload
-
-          expect(issue.due_date).to eq Date.new(2016, 8, 28)
-        end
+        it_behaves_like 'remove_due_date action not available'
       end
     end
 
@@ -274,6 +258,7 @@ describe 'Issues > User uses quick actions', :js do
           gitlab_sign_out
           sign_in(user)
           visit project_issue_path(project, issue)
+          wait_for_requests
         end
 
         it 'moves the issue' do
@@ -295,6 +280,7 @@ describe 'Issues > User uses quick actions', :js do
           gitlab_sign_out
           sign_in(user)
           visit project_issue_path(project, issue)
+          wait_for_requests
         end
 
         it 'does not move the issue' do
@@ -312,6 +298,7 @@ describe 'Issues > User uses quick actions', :js do
           gitlab_sign_out
           sign_in(user)
           visit project_issue_path(project, issue)
+          wait_for_requests
         end
 
         it 'does not move the issue' do
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index b81478a481f85504ae5ed28d8aae750c735f7456..a2b5859bd1e632b25a4f9dd9bdc53b1675cbbf48 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -9,9 +9,41 @@ describe 'Merge request > User uses quick actions', :js do
   let(:merge_request) { create(:merge_request, source_project: project) }
   let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
 
-  it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
+  context "issuable common quick actions" do
+    let!(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
+    let(:maintainer) { create(:user) }
+    let(:project) { create(:project, :public, :repository) }
+    let!(:label_bug) { create(:label, project: project, title: 'bug') }
+    let!(:label_feature) { create(:label, project: project, title: 'feature') }
+    let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
     let(:issuable) { create(:merge_request, source_project: project) }
-    let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
+    let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])}
+
+    it_behaves_like 'assign quick action', :merge_request
+    it_behaves_like 'unassign quick action', :merge_request
+    it_behaves_like 'close quick action', :merge_request
+    it_behaves_like 'reopen quick action', :merge_request
+    it_behaves_like 'title quick action', :merge_request
+    it_behaves_like 'todo quick action', :merge_request
+    it_behaves_like 'done quick action', :merge_request
+    it_behaves_like 'subscribe quick action', :merge_request
+    it_behaves_like 'unsubscribe quick action', :merge_request
+    it_behaves_like 'lock quick action', :merge_request
+    it_behaves_like 'unlock quick action', :merge_request
+    it_behaves_like 'milestone quick action', :merge_request
+    it_behaves_like 'remove_milestone quick action', :merge_request
+    it_behaves_like 'label quick action', :merge_request
+    it_behaves_like 'unlabel quick action', :merge_request
+    it_behaves_like 'relabel quick action', :merge_request
+    it_behaves_like 'award quick action', :merge_request
+    it_behaves_like 'estimate quick action', :merge_request
+    it_behaves_like 'remove_estimate quick action', :merge_request
+    it_behaves_like 'spend quick action', :merge_request
+    it_behaves_like 'remove_time_spent quick action', :merge_request
+    it_behaves_like 'shrug quick action', :merge_request
+    it_behaves_like 'tableflip quick action', :merge_request
+    it_behaves_like 'copy_metadata quick action', :merge_request
+    it_behaves_like 'issuable time tracker', :merge_request
   end
 
   describe 'merge-request-only commands' do
@@ -24,20 +56,12 @@ describe 'Merge request > User uses quick actions', :js do
       project.add_maintainer(user)
     end
 
-    describe 'time tracking' do
-      before do
-        sign_in(user)
-        visit project_merge_request_path(project, merge_request)
-      end
-
-      it_behaves_like 'issuable time tracker'
-    end
-
     describe 'toggling the WIP prefix in the title from note' do
       context 'when the current user can toggle the WIP prefix' do
         before do
           sign_in(user)
           visit project_merge_request_path(project, merge_request)
+          wait_for_requests
         end
 
         it 'adds the WIP: prefix to the title' do
@@ -135,11 +159,16 @@ describe 'Merge request > User uses quick actions', :js do
         visit project_merge_request_path(project, merge_request)
       end
 
-      it 'does not recognize the command nor create a note' do
-        add_note('/due 2016-08-28')
+      it_behaves_like 'due quick action not available'
+    end
 
-        expect(page).not_to have_content '/due 2016-08-28'
+    describe 'removing a due date from note' do
+      before do
+        sign_in(user)
+        visit project_merge_request_path(project, merge_request)
       end
+
+      it_behaves_like 'remove_due_date action not available'
     end
 
     describe '/target_branch command in merge request' do
diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
index 136cfb5bcc587df799ba08234ae77eaa8363711f..b6e0adbc1c2b9b60c314bdac834858fd9f847ac7 100644
--- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb
+++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
@@ -69,6 +69,36 @@ describe Gitlab::QuickActions::CommandDefinition do
         expect(subject.available?(opts)).to be true
       end
     end
+
+    context "when the command has types" do
+      before do
+        subject.types = [Issue, Commit]
+      end
+
+      context "when the command target type is allowed" do
+        it "returns true" do
+          opts[:quick_action_target] = Issue.new
+          expect(subject.available?(opts)).to be true
+        end
+      end
+
+      context "when the command target type is not allowed" do
+        it "returns true" do
+          opts[:quick_action_target] = MergeRequest.new
+          expect(subject.available?(opts)).to be false
+        end
+      end
+    end
+
+    context "when the command has no types" do
+      it "any target type is allowed" do
+        opts[:quick_action_target] = Issue.new
+        expect(subject.available?(opts)).to be true
+
+        opts[:quick_action_target] = MergeRequest.new
+        expect(subject.available?(opts)).to be true
+      end
+    end
   end
 
   describe "#execute" do
diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb
index fd4df8694ba966434e68a273958b3134cad346c6..185adab1ff6c27b19ed7f5566b2dd04466465c92 100644
--- a/spec/lib/gitlab/quick_actions/dsl_spec.rb
+++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb
@@ -48,13 +48,19 @@ describe Gitlab::QuickActions::Dsl do
       substitution :something do |text|
         "#{text} Some complicated thing you want in here"
       end
+
+      desc 'A command with types'
+      types Issue, Commit
+      command :has_types do
+        "Has Issue and Commit types"
+      end
     end
   end
 
   describe '.command_definitions' do
     it 'returns an array with commands definitions' do
       no_args_def, explanation_with_aliases_def, dynamic_description_def,
-      cc_def, cond_action_def, with_params_parsing_def, substitution_def =
+      cc_def, cond_action_def, with_params_parsing_def, substitution_def, has_types =
         DummyClass.command_definitions
 
       expect(no_args_def.name).to eq(:no_args)
@@ -63,6 +69,7 @@ describe Gitlab::QuickActions::Dsl do
       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.types).to eq([])
       expect(no_args_def.action_block).to be_a_kind_of(Proc)
       expect(no_args_def.parse_params_block).to be_nil
       expect(no_args_def.warning).to eq('')
@@ -73,6 +80,7 @@ describe Gitlab::QuickActions::Dsl do
       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.types).to eq([])
       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(explanation_with_aliases_def.warning).to eq('Possible problem!')
@@ -83,6 +91,7 @@ describe Gitlab::QuickActions::Dsl do
       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.types).to eq([])
       expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
       expect(dynamic_description_def.parse_params_block).to be_nil
       expect(dynamic_description_def.warning).to eq('')
@@ -93,6 +102,7 @@ describe Gitlab::QuickActions::Dsl do
       expect(cc_def.explanation).to eq('')
       expect(cc_def.params).to eq([])
       expect(cc_def.condition_block).to be_nil
+      expect(cc_def.types).to eq([])
       expect(cc_def.action_block).to be_nil
       expect(cc_def.parse_params_block).to be_nil
       expect(cc_def.warning).to eq('')
@@ -103,6 +113,7 @@ describe Gitlab::QuickActions::Dsl do
       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.types).to eq([])
       expect(cond_action_def.action_block).to be_a_kind_of(Proc)
       expect(cond_action_def.parse_params_block).to be_nil
       expect(cond_action_def.warning).to eq('')
@@ -113,6 +124,7 @@ describe Gitlab::QuickActions::Dsl do
       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.types).to eq([])
       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)
       expect(with_params_parsing_def.warning).to eq('')
@@ -123,9 +135,21 @@ describe Gitlab::QuickActions::Dsl do
       expect(substitution_def.explanation).to eq('')
       expect(substitution_def.params).to eq(['<Comment>'])
       expect(substitution_def.condition_block).to be_nil
+      expect(substitution_def.types).to eq([])
       expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
       expect(substitution_def.parse_params_block).to be_nil
       expect(substitution_def.warning).to eq('')
+
+      expect(has_types.name).to eq(:has_types)
+      expect(has_types.aliases).to eq([])
+      expect(has_types.description).to eq('A command with types')
+      expect(has_types.explanation).to eq('')
+      expect(has_types.params).to eq([])
+      expect(has_types.condition_block).to be_nil
+      expect(has_types.types).to eq([Issue, Commit])
+      expect(has_types.action_block).to be_a_kind_of(Proc)
+      expect(has_types.parse_params_block).to be_nil
+      expect(has_types.warning).to eq('')
     end
   end
 end
diff --git a/spec/support/features/issuable_quick_actions_shared_examples.rb b/spec/support/features/issuable_quick_actions_shared_examples.rb
deleted file mode 100644
index 2a883ce1074f2a2a8cf020cbb6a9c708e2246a96..0000000000000000000000000000000000000000
--- a/spec/support/features/issuable_quick_actions_shared_examples.rb
+++ /dev/null
@@ -1,389 +0,0 @@
-# Specifications for behavior common to all objects with executable attributes.
-# It takes a `issuable_type`, and expect an `issuable`.
-
-shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type|
-  include Spec::Support::Helpers::Features::NotesHelpers
-
-  let(:maintainer) { create(:user) }
-  let(:project) do
-    case issuable_type
-    when :merge_request
-      create(:project, :public, :repository)
-    when :issue
-      create(:project, :public)
-    end
-  end
-  let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
-  let!(:label_bug) { create(:label, project: project, title: 'bug') }
-  let!(:label_feature) { create(:label, project: project, title: 'feature') }
-  let(:new_url_opts) { {} }
-
-  before do
-    project.add_maintainer(maintainer)
-
-    gitlab_sign_in(maintainer)
-  end
-
-  after do
-    # Ensure all outstanding Ajax requests are complete to avoid database deadlocks
-    wait_for_requests
-  end
-
-  describe "new #{issuable_type}", :js do
-    context 'with commands in the description' do
-      it "creates the #{issuable_type} and interpret commands accordingly" do
-        case issuable_type
-        when :merge_request
-          visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts)
-        when :issue
-          visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts)
-        end
-        fill_in "#{issuable_type}_title", with: 'bug 345'
-        fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\""
-        click_button "Submit #{issuable_type}".humanize
-
-        issuable = project.public_send(issuable_type.to_s.pluralize).first
-
-        expect(issuable.description).to eq "bug description"
-        expect(issuable.labels).to eq [label_bug]
-        expect(issuable.milestone).to eq milestone
-        expect(page).to have_content 'bug 345'
-        expect(page).to have_content 'bug description'
-      end
-    end
-  end
-
-  describe "note on #{issuable_type}", :js do
-    before do
-      visit public_send("project_#{issuable_type}_path", project, issuable)
-    end
-
-    context 'with a note containing commands' do
-      it 'creates a note without the commands and interpret the commands accordingly' do
-        assignee = create(:user, username: 'bob')
-        add_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
-
-        expect(page).to have_content 'Awesome!'
-        expect(page).not_to have_content '/assign @bob'
-        expect(page).not_to have_content '/label ~bug'
-        expect(page).not_to have_content '/milestone %"ASAP"'
-
-        wait_for_requests
-        issuable.reload
-        note = issuable.notes.user.first
-
-        expect(note.note).to eq "Awesome!"
-        expect(issuable.assignees).to eq [assignee]
-        expect(issuable.labels).to eq [label_bug]
-        expect(issuable.milestone).to eq milestone
-      end
-
-      it 'removes the quick action from note and explains it in the preview' do
-        preview_note("Awesome!\n\n/close")
-
-        expect(page).to have_content 'Awesome!'
-        expect(page).not_to have_content '/close'
-        issuable_name = issuable.is_a?(Issue) ? 'issue' : 'merge request'
-        expect(page).to have_content "Closes this #{issuable_name}."
-      end
-    end
-
-    context 'with a note containing only commands' do
-      it 'does not create a note but interpret the commands accordingly' do
-        assignee = create(:user, username: 'bob')
-        add_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
-
-        expect(page).not_to have_content '/assign @bob'
-        expect(page).not_to have_content '/label ~bug'
-        expect(page).not_to have_content '/milestone %"ASAP"'
-        expect(page).to have_content 'Commands applied'
-
-        issuable.reload
-
-        expect(issuable.notes.user).to be_empty
-        expect(issuable.assignees).to eq [assignee]
-        expect(issuable.labels).to eq [label_bug]
-        expect(issuable.milestone).to eq milestone
-      end
-    end
-
-    context "with a note closing the #{issuable_type}" do
-      before do
-        expect(issuable).to be_open
-      end
-
-      context "when current user can close #{issuable_type}" do
-        it "closes the #{issuable_type}" do
-          add_note("/close")
-
-          expect(page).not_to have_content '/close'
-          expect(page).to have_content 'Commands applied'
-
-          expect(issuable.reload).to be_closed
-        end
-      end
-
-      context "when current user cannot close #{issuable_type}" do
-        before do
-          guest = create(:user)
-          project.add_guest(guest)
-
-          gitlab_sign_out
-          gitlab_sign_in(guest)
-          visit public_send("project_#{issuable_type}_path", project, issuable)
-        end
-
-        it "does not close the #{issuable_type}" do
-          add_note("/close")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          expect(issuable).to be_open
-        end
-      end
-    end
-
-    context "with a note reopening the #{issuable_type}" do
-      before do
-        issuable.close
-        expect(issuable).to be_closed
-      end
-
-      context "when current user can reopen #{issuable_type}" do
-        it "reopens the #{issuable_type}" do
-          add_note("/reopen")
-
-          expect(page).not_to have_content '/reopen'
-          expect(page).to have_content 'Commands applied'
-
-          expect(issuable.reload).to be_open
-        end
-      end
-
-      context "when current user cannot reopen #{issuable_type}" do
-        before do
-          guest = create(:user)
-          project.add_guest(guest)
-
-          gitlab_sign_out
-          gitlab_sign_in(guest)
-          visit public_send("project_#{issuable_type}_path", project, issuable)
-        end
-
-        it "does not reopen the #{issuable_type}" do
-          add_note("/reopen")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          expect(issuable).to be_closed
-        end
-      end
-    end
-
-    context "with a note changing the #{issuable_type}'s title" do
-      context "when current user can change title of #{issuable_type}" do
-        it "reopens the #{issuable_type}" do
-          add_note("/title Awesome new title")
-
-          expect(page).not_to have_content '/title'
-          expect(page).to have_content 'Commands applied'
-
-          expect(issuable.reload.title).to eq 'Awesome new title'
-        end
-      end
-
-      context "when current user cannot change title of #{issuable_type}" do
-        before do
-          guest = create(:user)
-          project.add_guest(guest)
-
-          gitlab_sign_out
-          gitlab_sign_in(guest)
-          visit public_send("project_#{issuable_type}_path", project, issuable)
-        end
-
-        it "does not change the #{issuable_type} title" do
-          add_note("/title Awesome new title")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          expect(issuable.reload.title).not_to eq 'Awesome new title'
-        end
-      end
-    end
-
-    context "with a note marking the #{issuable_type} as todo" do
-      it "creates a new todo for the #{issuable_type}" do
-        add_note("/todo")
-
-        expect(page).not_to have_content '/todo'
-        expect(page).to have_content 'Commands applied'
-
-        todos = TodosFinder.new(maintainer).execute
-        todo = todos.first
-
-        expect(todos.size).to eq 1
-        expect(todo).to be_pending
-        expect(todo.target).to eq issuable
-        expect(todo.author).to eq maintainer
-        expect(todo.user).to eq maintainer
-      end
-    end
-
-    context "with a note marking the #{issuable_type} as done" do
-      before do
-        TodoService.new.mark_todo(issuable, maintainer)
-      end
-
-      it "creates a new todo for the #{issuable_type}" do
-        todos = TodosFinder.new(maintainer).execute
-        todo = todos.first
-
-        expect(todos.size).to eq 1
-        expect(todos.first).to be_pending
-        expect(todo.target).to eq issuable
-        expect(todo.author).to eq maintainer
-        expect(todo.user).to eq maintainer
-
-        add_note("/done")
-
-        expect(page).not_to have_content '/done'
-        expect(page).to have_content 'Commands applied'
-
-        expect(todo.reload).to be_done
-      end
-    end
-
-    context "with a note subscribing to the #{issuable_type}" do
-      it "creates a new todo for the #{issuable_type}" do
-        expect(issuable.subscribed?(maintainer, project)).to be_falsy
-
-        add_note("/subscribe")
-
-        expect(page).not_to have_content '/subscribe'
-        expect(page).to have_content 'Commands applied'
-
-        expect(issuable.subscribed?(maintainer, project)).to be_truthy
-      end
-    end
-
-    context "with a note unsubscribing to the #{issuable_type} as done" do
-      before do
-        issuable.subscribe(maintainer, project)
-      end
-
-      it "creates a new todo for the #{issuable_type}" do
-        expect(issuable.subscribed?(maintainer, project)).to be_truthy
-
-        add_note("/unsubscribe")
-
-        expect(page).not_to have_content '/unsubscribe'
-        expect(page).to have_content 'Commands applied'
-
-        expect(issuable.subscribed?(maintainer, project)).to be_falsy
-      end
-    end
-
-    context "with a note assigning the #{issuable_type} to the current user" do
-      it "assigns the #{issuable_type} to the current user" do
-        add_note("/assign me")
-
-        expect(page).not_to have_content '/assign me'
-        expect(page).to have_content 'Commands applied'
-
-        expect(issuable.reload.assignees).to eq [maintainer]
-      end
-    end
-
-    context "with a note locking the #{issuable_type} discussion" do
-      before do
-        issuable.update(discussion_locked: false)
-        expect(issuable).not_to be_discussion_locked
-      end
-
-      context "when current user can lock #{issuable_type} discussion" do
-        it "locks the #{issuable_type} discussion" do
-          add_note("/lock")
-
-          expect(page).not_to have_content '/lock'
-          expect(page).to have_content 'Commands applied'
-
-          expect(issuable.reload).to be_discussion_locked
-        end
-      end
-
-      context "when current user cannot lock #{issuable_type}" do
-        before do
-          guest = create(:user)
-          project.add_guest(guest)
-
-          gitlab_sign_out
-          sign_in(guest)
-          visit public_send("project_#{issuable_type}_path", project, issuable)
-        end
-
-        it "does not lock the #{issuable_type} discussion" do
-          add_note("/lock")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          expect(issuable).not_to be_discussion_locked
-        end
-      end
-    end
-
-    context "with a note unlocking the #{issuable_type} discussion" do
-      before do
-        issuable.update(discussion_locked: true)
-        expect(issuable).to be_discussion_locked
-      end
-
-      context "when current user can unlock #{issuable_type} discussion" do
-        it "unlocks the #{issuable_type} discussion" do
-          add_note("/unlock")
-
-          expect(page).not_to have_content '/unlock'
-          expect(page).to have_content 'Commands applied'
-
-          expect(issuable.reload).not_to be_discussion_locked
-        end
-      end
-
-      context "when current user cannot unlock #{issuable_type}" do
-        before do
-          guest = create(:user)
-          project.add_guest(guest)
-
-          gitlab_sign_out
-          sign_in(guest)
-          visit public_send("project_#{issuable_type}_path", project, issuable)
-        end
-
-        it "does not unlock the #{issuable_type} discussion" do
-          add_note("/unlock")
-
-          expect(page).not_to have_content 'Commands applied'
-
-          expect(issuable).to be_discussion_locked
-        end
-      end
-    end
-  end
-
-  describe "preview of note on #{issuable_type}", :js do
-    it 'removes quick actions from note and explains them' do
-      create(:user, username: 'bob')
-
-      visit public_send("project_#{issuable_type}_path", 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
diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4604d8675075d98c434ffc499446a2aa08fbfa58
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'tag quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d97da6be192d77906d186604359070ca32b3a291
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+shared_examples 'assign quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets assign quick action accordingly" do
+      assignee = create(:user, username: 'bob')
+
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/assign @bob"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable.assignees).to eq [assignee]
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+
+    it "creates the #{issuable_type} and interprets assign quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/assign me"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable.assignees).to eq [maintainer]
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the assign quick action accordingly' do
+      assignee = create(:user, username: 'bob')
+      add_note("Awesome!\n\n/assign @bob")
+
+      expect(page).to have_content 'Awesome!'
+      expect(page).not_to have_content '/assign @bob'
+
+      wait_for_requests
+      issuable.reload
+      note = issuable.notes.user.first
+
+      expect(note.note).to eq 'Awesome!'
+      expect(issuable.assignees).to eq [assignee]
+    end
+
+    it "assigns the #{issuable_type} to the current user" do
+      add_note("/assign me")
+
+      expect(page).not_to have_content '/assign me'
+      expect(page).to have_content 'Commands applied'
+
+      expect(issuable.reload.assignees).to eq [maintainer]
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains assign quick action to bob' do
+      create(:user, username: 'bob')
+
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "Awesome!\n/assign @bob "
+        click_on 'Preview'
+
+        expect(page).not_to have_content '/assign @bob'
+        expect(page).to have_content 'Awesome!'
+        expect(page).to have_content 'Assigns @bob.'
+      end
+    end
+
+    it 'explains assign quick action to me' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "Awesome!\n/assign me"
+        click_on 'Preview'
+
+        expect(page).not_to have_content '/assign me'
+        expect(page).to have_content 'Awesome!'
+        expect(page).to have_content "Assigns @#{maintainer.username}."
+      end
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74cbfa3f4b4a38eed328cf7f4fe56fedecacda20
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+shared_examples 'award quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets award quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/award :100:"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.award_emoji).to eq []
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      expect(issuable.award_emoji).to eq []
+    end
+
+    it 'creates the note and interprets the award quick action accordingly' do
+      add_note("/award :100:")
+
+      wait_for_requests
+      expect(page).not_to have_content '/award'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.award_emoji.last.name).to eq('100')
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains label quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/award :100:')
+
+      expect(page).not_to have_content '/award'
+      expect(page).to have_selector "gl-emoji[data-name='100']"
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0d0b790a0e52a2277181abfe61fe8d3c3a2d6d6
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+shared_examples 'close quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets close quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/close"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      expect(issuable).to be_opened
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the close quick action accordingly' do
+      add_note("this is done, close\n\n/close")
+
+      wait_for_requests
+      expect(page).not_to have_content '/close'
+      expect(page).to have_content 'this is done, close'
+
+      issuable.reload
+      note = issuable.notes.user.first
+
+      expect(note.note).to eq 'this is done, close'
+      expect(issuable).to be_closed
+    end
+
+    context "when current user cannot close #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+      end
+
+      it "does not close the #{issuable_type}" do
+        add_note('/close')
+
+        expect(page).not_to have_content 'Commands applied'
+        expect(issuable).to be_open
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains close quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "this is done, close\n/close"
+        click_on 'Preview'
+
+        expect(page).not_to have_content '/close'
+        expect(page).to have_content 'this is done, close'
+        expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}."
+      end
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e1e3c7bc95bffed2c7892ace9cd23b7cf0bc391
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+shared_examples 'copy_metadata quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets copy_metadata quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/copy_metadata #{source_issuable.to_reference(project)}"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).last
+
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      issuable.reload
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable.milestone).to eq milestone
+      expect(issuable.labels).to match_array([label_bug, label_feature])
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets copy_metadata quick action accordingly' do
+      add_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+      wait_for_requests
+      expect(page).not_to have_content '/copy_metadata'
+      expect(page).to have_content 'Commands applied'
+      issuable.reload
+      expect(issuable.milestone).to eq milestone
+      expect(issuable.labels).to match_array([label_bug, label_feature])
+    end
+
+    context "when current user cannot copy_metadata" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not copy_metadata' do
+        add_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+        wait_for_requests
+        expect(page).not_to have_content '/copy_metadata'
+        expect(page).not_to have_content 'Commands applied'
+        issuable.reload
+        expect(issuable.milestone).not_to eq milestone
+        expect(issuable.labels).to eq []
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains copy_metadata quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+      expect(page).not_to have_content '/copy_metadata'
+      expect(page).to have_content "Copy labels and milestone from #{source_issuable.to_reference(project)}."
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8a72bbc13bfb2d91bd0c880886156f610b23046a
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+shared_examples 'done quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets done quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/done"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+
+      todos = TodosFinder.new(maintainer).execute
+      expect(todos.size).to eq 0
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      TodoService.new.mark_todo(issuable, maintainer)
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the done quick action accordingly' do
+      todos = TodosFinder.new(maintainer).execute
+      todo = todos.first
+      expect(todo.reload).to be_pending
+
+      expect(todos.size).to eq 1
+      expect(todo.target).to eq issuable
+      expect(todo.author).to eq maintainer
+      expect(todo.user).to eq maintainer
+
+      add_note('/done')
+
+      wait_for_requests
+      expect(page).not_to have_content '/done'
+      expect(page).to have_content 'Commands applied'
+      expect(todo.reload).to be_done
+    end
+
+    context "when current user cannot mark #{issuable_type} todo as done" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not set the #{issuable_type} todo as done" do
+        todos = TodosFinder.new(maintainer).execute
+        todo = todos.first
+        expect(todo.reload).to be_pending
+
+        expect(todos.size).to eq 1
+        expect(todo.target).to eq issuable
+        expect(todo.author).to eq maintainer
+        expect(todo.user).to eq maintainer
+
+        add_note('/done')
+
+        expect(page).not_to have_content 'Commands applied'
+        expect(todo.reload).to be_pending
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains done quick action' do
+      TodoService.new.mark_todo(issuable, maintainer)
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/done')
+
+      expect(page).not_to have_content '/done'
+      expect(page).to have_content "Marks todo as done."
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..648755d7e55619c86b0ca706694df2f99c948697
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+shared_examples 'estimate quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets estimate quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/estimate 1d 2h 3m"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.time_estimate).to eq 36180
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the estimate quick action accordingly' do
+      add_note("/estimate 1d 2h 3m")
+
+      wait_for_requests
+      expect(page).not_to have_content '/estimate'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.time_estimate).to eq 36180
+    end
+
+    context "when current user cannot set estimate to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not set estimate' do
+        add_note("/estimate ~bug ~feature")
+
+        wait_for_requests
+        expect(page).not_to have_content '/estimate'
+        expect(issuable.reload.time_estimate).to eq 0
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains estimate quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/estimate 1d 2h 3m')
+
+      expect(page).not_to have_content '/estimate'
+      expect(page).to have_content 'Sets time estimate to 1d 2h 3m.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9066e382b703fc30f4f96115c092c13ff9645d60
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+shared_examples 'label quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets label quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug ~feature"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.labels).to match_array([label_bug, label_feature])
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      expect(issuable.labels).to eq []
+    end
+
+    it 'creates the note and interprets the label quick action accordingly' do
+      add_note("/label ~bug ~feature")
+
+      wait_for_requests
+      expect(page).not_to have_content '/label'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.labels).to match_array([label_bug, label_feature])
+    end
+
+    context "when current user cannot set label to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not set label' do
+        add_note("/label ~bug ~feature")
+
+        wait_for_requests
+        expect(page).not_to have_content '/label'
+        expect(issuable.labels).to eq []
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains label quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/label ~bug ~feature')
+
+      expect(page).not_to have_content '/label'
+      expect(page).to have_content 'Adds bug feature labels.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d3197f2a4593463048ef0242c85882fa1d961c95
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+shared_examples 'lock quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets lock quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/lock"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable).not_to be_discussion_locked
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      issuable.update(discussion_locked: false)
+      expect(issuable).not_to be_discussion_locked
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the lock quick action accordingly' do
+      add_note('/lock')
+
+      wait_for_requests
+      expect(page).not_to have_content '/lock'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload).to be_discussion_locked
+    end
+
+    context "when current user cannot lock to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not lock the #{issuable_type}" do
+        add_note('/lock')
+
+        wait_for_requests
+        expect(page).not_to have_content '/lock'
+        expect(issuable).not_to be_discussion_locked
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains lock quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/lock')
+
+      expect(page).not_to have_content '/lock'
+      expect(page).to have_content "Locks the discussion"
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f16ce93b6a57ee2aadad925720637f1f59b8551
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+shared_examples 'milestone quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets milestone quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/milestone %\"ASAP\""
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.milestone).to eq milestone
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      expect(issuable.milestone).to be_nil
+    end
+
+    it 'creates the note and interprets the milestone quick action accordingly' do
+      add_note("/milestone %\"ASAP\"")
+
+      wait_for_requests
+      expect(page).not_to have_content '/milestone'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.milestone).to eq milestone
+    end
+
+    context "when current user cannot set milestone to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not set milestone' do
+        add_note('/milestone')
+
+        wait_for_requests
+        expect(page).not_to have_content '/milestone'
+        expect(issuable.reload.milestone).to be_nil
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains milestone quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note("/milestone %\"ASAP\"")
+
+      expect(page).not_to have_content '/milestone'
+      expect(page).to have_content 'Sets the milestone to %ASAP'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..643ae77516ad0d453de77719f35a3f8303d85ebf
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+shared_examples 'relabel quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets relabel quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /relabel ~feature"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.labels).to eq [label_bug, label_feature]
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      issuable.update(labels: [label_bug])
+    end
+
+    it 'creates the note and interprets the relabel quick action accordingly' do
+      add_note('/relabel ~feature')
+
+      wait_for_requests
+      expect(page).not_to have_content '/relabel'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.labels).to match_array([label_feature])
+    end
+
+    it 'creates the note and interprets the relabel quick action with empty param' do
+      add_note('/relabel')
+
+      wait_for_requests
+      expect(page).not_to have_content '/relabel'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.labels).to match_array([label_bug])
+    end
+
+    context "when current user cannot relabel to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not relabel' do
+        add_note('/relabel ~feature')
+
+        wait_for_requests
+        expect(page).not_to have_content '/relabel'
+        expect(issuable.labels).to match_array([label_bug])
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    before do
+      issuable.update(labels: [label_bug])
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+    end
+
+    it 'explains relabel all quick action' do
+      preview_note('/relabel ~feature')
+
+      expect(page).not_to have_content '/relabel'
+      expect(page).to have_content 'Replaces all labels with feature label.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..24f6f8d5bf4b240dde099ac63367e6afd11f4911
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_estimate quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets estimate quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/remove_estimate"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.time_estimate).to eq 0
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      issuable.update_attribute(:time_estimate, 36180)
+    end
+
+    it 'creates the note and interprets the remove_estimate quick action accordingly' do
+      add_note("/remove_estimate")
+
+      wait_for_requests
+      expect(page).not_to have_content '/remove_estimate'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.time_estimate).to eq 0
+    end
+
+    context "when current user cannot remove_estimate" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not remove_estimate' do
+        add_note('/remove_estimate')
+
+        wait_for_requests
+        expect(page).not_to have_content '/remove_estimate'
+        expect(issuable.reload.time_estimate).to eq 36180
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains remove_estimate quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/remove_estimate')
+
+      expect(page).not_to have_content '/remove_estimate'
+      expect(page).to have_content 'Removes time estimate.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..edd92d5cdbcbef42a21757e203feebd73285ef2c
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_milestone quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets remove_milestone quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/remove_milestone"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.milestone).to be_nil
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      issuable.update(milestone: milestone)
+      expect(issuable.milestone).to eq(milestone)
+    end
+
+    it 'creates the note and interprets the remove_milestone quick action accordingly' do
+      add_note("/remove_milestone")
+
+      wait_for_requests
+      expect(page).not_to have_content '/remove_milestone'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.milestone).to be_nil
+    end
+
+    context "when current user cannot remove milestone to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not remove milestone' do
+        add_note('/remove_milestone')
+
+        wait_for_requests
+        expect(page).not_to have_content '/remove_milestone'
+        expect(issuable.reload.milestone).to eq(milestone)
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains remove_milestone quick action' do
+      issuable.update(milestone: milestone)
+      expect(issuable.milestone).to eq(milestone)
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note("/remove_milestone")
+
+      expect(page).not_to have_content '/remove_milestone'
+      expect(page).to have_content 'Removes %ASAP milestone.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d5894b23180b7ea90b6751a914a0cc5ec105375
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_time_spent quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets remove_time_spent quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/remove_time_spent"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.total_time_spent).to eq 0
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      issuable.update!(spend_time: { duration: 36180, user_id: maintainer.id })
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the remove_time_spent quick action accordingly' do
+      add_note("/remove_time_spent")
+
+      wait_for_requests
+      expect(page).not_to have_content '/remove_time_spent'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.total_time_spent).to eq 0
+    end
+
+    context "when current user cannot set remove_time_spent time" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not set remove_time_spent time' do
+        add_note("/remove_time_spent")
+
+        wait_for_requests
+        expect(page).not_to have_content '/remove_time_spent'
+        expect(issuable.reload.total_time_spent).to eq 36180
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains remove_time_spent quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/remove_time_spent')
+
+      expect(page).not_to have_content '/remove_time_spent'
+      expect(page).to have_content 'Removes spent time.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af173e93bb569a1189673ed055e119fbb6fed23a
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'reopen quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets reopen quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/reopen"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      issuable.close
+      expect(issuable).to be_closed
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the reopen quick action accordingly' do
+      add_note('/reopen')
+
+      wait_for_requests
+      expect(page).not_to have_content '/reopen'
+      expect(page).to have_content 'Commands applied'
+
+      issuable.reload
+      expect(issuable).to be_opened
+    end
+
+    context "when current user cannot reopen #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not reopen the #{issuable_type}" do
+        add_note('/reopen')
+
+        expect(page).not_to have_content 'Commands applied'
+        expect(issuable).to be_closed
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains reopen quick action' do
+      issuable.close
+      expect(issuable).to be_closed
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/reopen')
+
+      expect(page).not_to have_content '/reopen'
+      expect(page).to have_content "Reopens this #{issuable_type.to_s.humanize.downcase}."
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a5268085853f6e2c37aaf3536f6a88b13d44baa
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+shared_examples 'shrug quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets shrug quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/shrug oops"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq "bug description\noops 炉\\锛�(銉�)锛�/炉"
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content "bug description\noops 炉\\锛�(銉�)锛�/炉"
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets shrug quick action accordingly' do
+      add_note("/shrug oops")
+
+      wait_for_requests
+      expect(page).not_to have_content '/shrug oops'
+      expect(page).to have_content "oops 炉\\锛�(銉�)锛�/炉"
+      expect(issuable.notes.last.note).to eq "oops 炉\\锛�(銉�)锛�/炉"
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains shrug quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/shrug oops')
+
+      expect(page).not_to have_content '/shrug'
+      expect(page).to have_content "oops 炉\\锛�(銉�)锛�/炉"
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..97b4885eba07b1187ff334551af993a9bec769bd
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+shared_examples 'spend quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets spend quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/spend 1d 2h 3m"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.total_time_spent).to eq 36180
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the spend quick action accordingly' do
+      add_note("/spend 1d 2h 3m")
+
+      wait_for_requests
+      expect(page).not_to have_content '/spend'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.total_time_spent).to eq 36180
+    end
+
+    context "when current user cannot set spend time" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not set spend time' do
+        add_note("/spend 1s 2h 3m")
+
+        wait_for_requests
+        expect(page).not_to have_content '/spend'
+        expect(issuable.reload.total_time_spent).to eq 0
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains spend quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/spend 1d 2h 3m')
+
+      expect(page).not_to have_content '/spend'
+      expect(page).to have_content 'Adds 1d 2h 3m spent time.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..15aefd511a5834cd32fd8704fd62ac0b99b5bb23
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+shared_examples 'subscribe quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets subscribe quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/subscribe"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.subscribed?(maintainer, project)).to be_truthy
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      expect(issuable.subscribed?(maintainer, project)).to be_falsy
+    end
+
+    it 'creates the note and interprets the subscribe quick action accordingly' do
+      add_note('/subscribe')
+
+      wait_for_requests
+      expect(page).not_to have_content '/subscribe'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.subscribed?(maintainer, project)).to be_truthy
+    end
+
+    context "when current user cannot subscribe to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not subscribe to the #{issuable_type}" do
+        add_note('/subscribe')
+
+        wait_for_requests
+        expect(page).not_to have_content '/subscribe'
+        expect(page).to have_content 'Commands applied'
+        expect(issuable.subscribed?(maintainer, project)).to be_falsy
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains subscribe quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/subscribe')
+
+      expect(page).not_to have_content '/subscribe'
+      expect(page).to have_content "Subscribes to this #{issuable_type.to_s.humanize.downcase}"
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef831e39872e8e96a46f187b8d99303b084ce9b2
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+shared_examples 'tableflip quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets tableflip quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/tableflip oops"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq "bug description\noops (鈺扳枴掳)鈺傅 鈹烩攣鈹�"
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content "bug description\noops (鈺扳枴掳)鈺傅 鈹烩攣鈹�"
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets tableflip quick action accordingly' do
+      add_note("/tableflip oops")
+
+      wait_for_requests
+      expect(page).not_to have_content '/tableflip oops'
+      expect(page).to have_content "oops (鈺扳枴掳)鈺傅 鈹烩攣鈹�"
+      expect(issuable.notes.last.note).to eq "oops (鈺扳枴掳)鈺傅 鈹烩攣鈹�"
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains tableflip quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/tableflip oops')
+
+      expect(page).not_to have_content '/tableflip'
+      expect(page).to have_content "oops (鈺扳枴掳)鈺傅 鈹烩攣鈹�"
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed904c8d539b0cb0aa5508f0c1031dbe9562d11e
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+shared_examples 'issuable time tracker' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+    visit public_send("project_#{issuable_type}_path", project, issuable)
+    wait_for_all_requests
+  end
+
+  after do
+    wait_for_requests
+  end
+
+  it 'renders the sidebar component empty state' do
+    page.within '.time-tracking-no-tracking-pane' do
+      expect(page).to have_content 'No estimate or time spent'
+    end
+  end
+
+  it 'updates the sidebar component when estimate is added' do
+    submit_time('/estimate 3w 1d 1h')
+
+    wait_for_requests
+    page.within '.time-tracking-estimate-only-pane' do
+      expect(page).to have_content '3w 1d 1h'
+    end
+  end
+
+  it 'updates the sidebar component when spent is added' do
+    submit_time('/spend 3w 1d 1h')
+
+    wait_for_requests
+    page.within '.time-tracking-spend-only-pane' do
+      expect(page).to have_content '3w 1d 1h'
+    end
+  end
+
+  it 'shows the comparison when estimate and spent are added' do
+    submit_time('/estimate 3w 1d 1h')
+    submit_time('/spend 3w 1d 1h')
+
+    wait_for_requests
+    page.within '.time-tracking-comparison-pane' do
+      expect(page).to have_content '3w 1d 1h'
+    end
+  end
+
+  it 'updates the sidebar component when estimate is removed' do
+    submit_time('/estimate 3w 1d 1h')
+    submit_time('/remove_estimate')
+
+    page.within '.time-tracking-component-wrap' do
+      expect(page).to have_content 'No estimate or time spent'
+    end
+  end
+
+  it 'updates the sidebar component when spent is removed' do
+    submit_time('/spend 3w 1d 1h')
+    submit_time('/remove_time_spent')
+
+    page.within '.time-tracking-component-wrap' do
+      expect(page).to have_content 'No estimate or time spent'
+    end
+  end
+
+  it 'shows the help state when icon is clicked' do
+    page.within '.time-tracking-component-wrap' do
+      find('.help-button').click
+      expect(page).to have_content 'Track time with quick actions'
+      expect(page).to have_content 'Learn more'
+    end
+  end
+
+  it 'hides the help state when close icon is clicked' do
+    page.within '.time-tracking-component-wrap' do
+      find('.help-button').click
+      find('.close-help-button').click
+
+      expect(page).not_to have_content 'Track time with quick actions'
+      expect(page).not_to have_content 'Learn more'
+    end
+  end
+
+  it 'displays the correct help url' do
+    page.within '.time-tracking-component-wrap' do
+      find('.help-button').click
+
+      expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
+    end
+  end
+end
+
+def submit_time(quick_action)
+  fill_in 'note[note]', with: quick_action
+  find('.js-comment-submit-button').click
+  wait_for_requests
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93a69093dde02cf66481b78f64aaa7531f1b998e
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+shared_examples 'title quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets title quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/title new title"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(issuable.title).to eq 'bug 345'
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the title quick action accordingly' do
+      add_note('/title New title')
+
+      wait_for_requests
+      expect(page).not_to have_content '/title new title'
+      expect(page).to have_content 'Commands applied'
+      expect(page).to have_content 'New title'
+
+      issuable.reload
+      expect(issuable.title).to eq 'New title'
+    end
+
+    context "when current user cannot set title #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not set title to the #{issuable_type}" do
+        add_note('/title New title')
+
+        expect(page).not_to have_content 'Commands applied'
+        expect(issuable.title).not_to eq 'New title'
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains title quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/title New title')
+      wait_for_requests
+
+      expect(page).not_to have_content '/title New title'
+      expect(page).to have_content 'Changes the title to "New title".'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cccc28127ce138cab9a2fd25d7247d34bd47944d
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+shared_examples 'todo quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets todo quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/todo"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+
+      todos = TodosFinder.new(maintainer).execute
+      expect(todos.size).to eq 0
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the todo quick action accordingly' do
+      add_note('/todo')
+
+      wait_for_requests
+      expect(page).not_to have_content '/todo'
+      expect(page).to have_content 'Commands applied'
+
+      todos = TodosFinder.new(maintainer).execute
+      todo = todos.first
+
+      expect(todos.size).to eq 1
+      expect(todo).to be_pending
+      expect(todo.target).to eq issuable
+      expect(todo.author).to eq maintainer
+      expect(todo.user).to eq maintainer
+    end
+
+    context "when current user cannot add todo #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not add todo the #{issuable_type}" do
+        add_note('/todo')
+
+        expect(page).not_to have_content 'Commands applied'
+        todos = TodosFinder.new(maintainer).execute
+        expect(todos.size).to eq 0
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains todo quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/todo')
+
+      expect(page).not_to have_content '/todo'
+      expect(page).to have_content "Adds a todo."
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0b1a52bc8602aa9bc4a91be93b96d92150fa7e99
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+shared_examples 'unassign quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets unassign quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/unassign @bob"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable.assignees).to eq []
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+
+    it "creates the #{issuable_type} and interprets unassign quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/unassign me"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable.assignees).to eq []
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the unassign quick action accordingly' do
+      assignee = create(:user, username: 'bob')
+      issuable.update(assignee_ids: [assignee.id])
+      expect(issuable.assignees).to eq [assignee]
+
+      add_note("Awesome!\n\n/unassign @bob")
+
+      expect(page).to have_content 'Awesome!'
+      expect(page).not_to have_content '/unassign @bob'
+
+      wait_for_requests
+      issuable.reload
+      note = issuable.notes.user.first
+
+      expect(note.note).to eq 'Awesome!'
+      expect(issuable.assignees).to eq []
+    end
+
+    it "unassigns the #{issuable_type} from current user" do
+      issuable.update(assignee_ids: [maintainer.id])
+      expect(issuable.reload.assignees).to eq [maintainer]
+      expect(issuable.assignees).to eq [maintainer]
+
+      add_note("/unassign me")
+
+      expect(page).not_to have_content '/unassign me'
+      expect(page).to have_content 'Commands applied'
+
+      expect(issuable.reload.assignees).to eq []
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains unassign quick action: from bob' do
+      assignee = create(:user, username: 'bob')
+      issuable.update(assignee_ids: [assignee.id])
+      expect(issuable.assignees).to eq [assignee]
+
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "Awesome!\n/unassign @bob "
+        click_on 'Preview'
+
+        expect(page).not_to have_content '/unassign @bob'
+        expect(page).to have_content 'Awesome!'
+        expect(page).to have_content 'Removes assignee @bob.'
+      end
+    end
+
+    it 'explains unassign quick action: from me' do
+      issuable.update(assignee_ids: [maintainer.id])
+      expect(issuable.assignees).to eq [maintainer]
+
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      page.within('.js-main-target-form') do
+        fill_in 'note[note]', with: "Awesome!\n/unassign me"
+        click_on 'Preview'
+
+        expect(page).not_to have_content '/unassign me'
+        expect(page).to have_content 'Awesome!'
+        expect(page).to have_content "Removes assignee @#{maintainer.username}."
+      end
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a1ee05841facef37bbce00e4f62522083903e94
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+shared_examples 'unlabel quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets unlabel quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /unlabel"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.labels).to eq [label_bug]
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      issuable.update(labels: [label_bug, label_feature])
+    end
+
+    it 'creates the note and interprets the unlabel all quick action accordingly' do
+      add_note("/unlabel")
+
+      wait_for_requests
+      expect(page).not_to have_content '/unlabel'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.labels).to eq []
+    end
+
+    it 'creates the note and interprets the unlabel some quick action accordingly' do
+      add_note("/unlabel ~bug")
+
+      wait_for_requests
+      expect(page).not_to have_content '/unlabel'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload.labels).to match_array([label_feature])
+    end
+
+    context "when current user cannot unlabel to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it 'does not unlabel' do
+        add_note("/unlabel")
+
+        wait_for_requests
+        expect(page).not_to have_content '/unlabel'
+        expect(issuable.labels).to match_array([label_bug, label_feature])
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    before do
+      issuable.update(labels: [label_bug, label_feature])
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+    end
+
+    it 'explains unlabel all quick action' do
+      preview_note('/unlabel')
+
+      expect(page).not_to have_content '/unlabel'
+      expect(page).to have_content 'Removes all labels.'
+    end
+
+    it 'explains unlabel some quick action' do
+      preview_note('/unlabel ~bug')
+
+      expect(page).not_to have_content '/unlabel'
+      expect(page).to have_content 'Removes bug label.'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..998ff99b32e68cf350e2131d11ab2058642a811a
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+shared_examples 'unlock quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets unlock quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/unlock"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable).not_to be_discussion_locked
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      issuable.update(discussion_locked: true)
+      expect(issuable).to be_discussion_locked
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+    end
+
+    it 'creates the note and interprets the unlock quick action accordingly' do
+      add_note('/unlock')
+
+      wait_for_requests
+      expect(page).not_to have_content '/unlock'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.reload).not_to be_discussion_locked
+    end
+
+    context "when current user cannot unlock to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not lock the #{issuable_type}" do
+        add_note('/unlock')
+
+        wait_for_requests
+        expect(page).not_to have_content '/unlock'
+        expect(issuable).to be_discussion_locked
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains unlock quick action' do
+      issuable.update(discussion_locked: true)
+      expect(issuable).to be_discussion_locked
+
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+
+      preview_note('/unlock')
+
+      expect(page).not_to have_content '/unlock'
+      expect(page).to have_content 'Unlocks the discussion'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bd92f13388995cade6756fa75cdfb86eccacfb69
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'unsubscribe quick action' do |issuable_type|
+  before do
+    project.add_maintainer(maintainer)
+    gitlab_sign_in(maintainer)
+  end
+
+  context "new #{issuable_type}", :js do
+    before do
+      case issuable_type
+      when :merge_request
+        visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      when :issue
+        visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+        wait_for_all_requests
+      end
+    end
+
+    it "creates the #{issuable_type} and interprets unsubscribe quick action accordingly" do
+      fill_in "#{issuable_type}_title", with: 'bug 345'
+      fill_in "#{issuable_type}_description", with: "bug description\n/unsubscribe"
+      click_button "Submit #{issuable_type}".humanize
+
+      issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+      expect(issuable.description).to eq 'bug description'
+      expect(issuable).to be_opened
+      expect(page).to have_content 'bug 345'
+      expect(page).to have_content 'bug description'
+      expect(issuable.subscribed?(maintainer, project)).to be_truthy
+    end
+  end
+
+  context "post note to existing #{issuable_type}" do
+    before do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      wait_for_all_requests
+      issuable.subscribe(maintainer, project)
+      expect(issuable.subscribed?(maintainer, project)).to be_truthy
+    end
+
+    it 'creates the note and interprets the unsubscribe quick action accordingly' do
+      add_note('/unsubscribe')
+
+      wait_for_requests
+      expect(page).not_to have_content '/unsubscribe'
+      expect(page).to have_content 'Commands applied'
+      expect(issuable.subscribed?(maintainer, project)).to be_falsey
+    end
+
+    context "when current user cannot unsubscribe to #{issuable_type}" do
+      before do
+        guest = create(:user)
+        project.add_guest(guest)
+
+        gitlab_sign_out
+        gitlab_sign_in(guest)
+        visit public_send("project_#{issuable_type}_path", project, issuable)
+        wait_for_all_requests
+      end
+
+      it "does not unsubscribe to the #{issuable_type}" do
+        add_note('/unsubscribe')
+
+        wait_for_requests
+        expect(page).not_to have_content '/unsubscribe'
+        expect(issuable.subscribed?(maintainer, project)).to be_truthy
+      end
+    end
+  end
+
+  context "preview of note on #{issuable_type}", :js do
+    it 'explains unsubscribe quick action' do
+      visit public_send("project_#{issuable_type}_path", project, issuable)
+      issuable.subscribe(maintainer, project)
+      expect(issuable.subscribed?(maintainer, project)).to be_truthy
+
+      preview_note('/unsubscribe')
+
+      expect(page).not_to have_content '/unsubscribe'
+      expect(page).to have_content "Unsubscribes from this #{issuable_type.to_s.humanize.downcase}."
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6edd20bb024e6ab88130f80bec823986b6b3259c
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'board_move quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c68e5aee842321883c5080821c1a134682d8b49d
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'confidential quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5bfc3bb222fbe253f4e2bb5ed73a22ec4f62f1e0
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'create_merge_request quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..db3ecccc339bde54515e3ff175953efa571139b8
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+shared_examples 'due quick action not available' do
+  it 'does not set the due date' do
+    add_note('/due 2016-08-28')
+
+    expect(page).not_to have_content 'Commands applied'
+    expect(page).not_to have_content '/due 2016-08-28'
+  end
+end
+
+shared_examples 'due quick action available and date can be added' do
+  it 'sets the due date accordingly' do
+    add_note('/due 2016-08-28')
+
+    expect(page).not_to have_content '/due 2016-08-28'
+    expect(page).to have_content 'Commands applied'
+
+    visit project_issue_path(project, issue)
+
+    page.within '.due_date' do
+      expect(page).to have_content 'Aug 28, 2016'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..24576fe00219339d98f85438b3d590d588a54003
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'duplicate quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..953e67b04232ab966afc46a1589bf5816c6428aa
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'move quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5904164fcfc6707fba806ea20b29533a9e3d3c75
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_due_date action not available' do
+  it 'does not remove the due date' do
+    add_note("/remove_due_date")
+
+    expect(page).not_to have_content 'Commands applied'
+    expect(page).not_to have_content '/remove_due_date'
+  end
+end
+
+shared_examples 'remove_due_date action available and due date can be removed' do
+  it 'removes the due date accordingly' do
+    add_note('/remove_due_date')
+
+    expect(page).not_to have_content '/remove_due_date'
+    expect(page).to have_content 'Commands applied'
+
+    visit project_issue_path(project, issue)
+
+    page.within '.due_date' do
+      expect(page).to have_content 'No due date'
+    end
+  end
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..31d88183f0d678b6fda5e2802f25558174eb9098
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'merge quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ccb4a85325b37ad840e04a86ece6f14cd47e61d5
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'target_branch quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6abb12b41b27b8a6990d2e0398a1c364cb976a35
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'wip quick action' do
+end