Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2016-06-30 18:34:19 +0300
committerRémy Coutable <remy@rymai.me>2016-08-13 01:05:57 +0300
commit0eea8c885743575b0e93a98846b3663e9903aa66 (patch)
tree9b1903bcb03789d15ed255b76be5d683f3b1e547 /app
parent11eefba891f214eefc1efa334adbcc9e979c0ce3 (diff)
Support slash commands in noteable description and notes
Some important things to note: - commands are removed from noteable.description / note.note - commands are translated to params so that they are treated as normal params in noteable Creation services - the logic is not in the models but in the Creation services, which is the right place for advanced logic that has nothing to do with what models should be responsible of! - UI/JS needs to be updated to handle notes which consist of commands only - the `/merge` command is not handled yet Other improvements: - Don't process commands in commit notes and display a flash is note is only commands - Add autocomplete for slash commands - Add description and params to slash command DSL methods - Ensure replying by email with a commands-only note works - Use :subscription_event instead of calling noteable.subscribe - Support :todo_event in IssuableBaseService Signed-off-by: Rémy Coutable <remy@rymai.me>
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6 (renamed from app/assets/javascripts/gfm_auto_complete.js)38
-rw-r--r--app/assets/javascripts/notes.js7
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss5
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/services/issuable_base_service.rb79
-rw-r--r--app/services/issues/create_service.rb20
-rw-r--r--app/services/merge_requests/create_service.rb25
-rw-r--r--app/services/notes/create_service.rb50
-rw-r--r--app/services/projects/autocomplete_service.rb4
-rw-r--r--app/services/slash_commands/interpret_service.rb133
12 files changed, 318 insertions, 50 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js.es6
index 2e5b15f4b77..21639c7c084 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -223,7 +223,7 @@
}
}
});
- return this.input.atwho({
+ this.input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
@@ -249,6 +249,41 @@
}
}
});
+ return this.input.atwho({
+ at: '/',
+ alias: 'commands',
+ displayTpl: function(value) {
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ },
+ insertTpl: function(value) {
+ var tpl = "\n/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
+ }
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert
+ }
+ });
},
destroyAtWho: function() {
return this.input.atwho('destroy');
@@ -265,6 +300,7 @@
this.input.atwho('load', 'mergerequests', data.mergerequests);
this.input.atwho('load', ':', data.emojis);
this.input.atwho('load', '~', data.labels);
+ this.input.atwho('load', '/', data.commands);
return $(':focus').trigger('keyup');
}
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9ece474d994..99bc1a640a8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -231,7 +231,12 @@
var $notesList, votesBlock;
if (!note.valid) {
if (note.award) {
- new Flash('You have already awarded this emoji!', 'alert');
+ new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
+ }
+ else {
+ if (note.errors.commands_only) {
+ new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+ }
}
return;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 96565da1bc9..edea4ad00eb 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -147,3 +147,8 @@
color: $gl-link-color;
}
}
+
+.atwho-view small.description {
+ float: right;
+ padding: 3px 5px;
+}
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 766b7e9cf22..f2422729364 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -125,7 +125,7 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id,
name: note.name
}
- elsif note.valid?
+ elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
attrs = {
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 47efbd4a939..64d31e4a3a0 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -145,7 +145,8 @@ class ProjectsController < Projects::ApplicationController
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
- members: participants
+ members: participants,
+ commands: autocomplete.commands
}
respond_to do |format|
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index ff866c2faa5..fd859e134e5 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -17,7 +17,7 @@ class TodosFinder
attr_accessor :current_user, :params
- def initialize(current_user, params)
+ def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2d96efe1042..b365e19c4a8 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -69,14 +69,9 @@ class IssuableBaseService < BaseService
end
def filter_labels
- if params[:add_label_ids].present? || params[:remove_label_ids].present?
- params.delete(:label_ids)
-
- filter_labels_in_param(:add_label_ids)
- filter_labels_in_param(:remove_label_ids)
- else
- filter_labels_in_param(:label_ids)
- end
+ filter_labels_in_param(:add_label_ids)
+ filter_labels_in_param(:remove_label_ids)
+ filter_labels_in_param(:label_ids)
end
def filter_labels_in_param(key)
@@ -85,23 +80,65 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id)
end
- def update_issuable(issuable, attributes)
+ def process_label_ids(attributes, base_label_ids: [], merge_all: false)
+ label_ids = attributes.delete(:label_ids) { [] }
+ add_label_ids = attributes.delete(:add_label_ids) { [] }
+ remove_label_ids = attributes.delete(:remove_label_ids) { [] }
+
+ new_label_ids = base_label_ids
+ new_label_ids |= label_ids if merge_all || (add_label_ids.empty? && remove_label_ids.empty?)
+ new_label_ids |= add_label_ids
+ new_label_ids -= remove_label_ids
+
+ new_label_ids
+ end
+
+ def merge_slash_commands_into_params!
+ command_params = SlashCommands::InterpretService.new(project, current_user).
+ execute(params[:description])
+
+ params.merge!(command_params)
+ end
+
+ def create_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
- add_label_ids = attributes.delete(:add_label_ids)
- remove_label_ids = attributes.delete(:remove_label_ids)
+ attributes.delete(:state_event)
+ params[:author] ||= current_user
+ label_ids = process_label_ids(attributes, merge_all: true)
+
+ issuable.assign_attributes(attributes)
+
+ if issuable.save
+ issuable.update_attributes(label_ids: label_ids)
+ end
+ end
+ end
- issuable.label_ids |= add_label_ids if add_label_ids
- issuable.label_ids -= remove_label_ids if remove_label_ids
+ def create(issuable)
+ merge_slash_commands_into_params!
+ filter_params
- issuable.assign_attributes(attributes.merge(updated_by: current_user))
+ if params.present? && create_issuable(issuable, params)
+ handle_creation(issuable)
+ issuable.create_cross_references!(current_user)
+ execute_hooks(issuable)
+ end
+
+ issuable
+ end
- issuable.save
+ def update_issuable(issuable, attributes)
+ issuable.with_transaction_returning_status do
+ attributes[:label_ids] = process_label_ids(attributes, base_label_ids: issuable.label_ids)
+
+ issuable.update(attributes.merge(updated_by: current_user))
end
end
def update(issuable)
change_state(issuable)
change_subscription(issuable)
+ change_todo(issuable)
filter_params
old_labels = issuable.labels.to_a
@@ -134,6 +171,18 @@ class IssuableBaseService < BaseService
end
end
+ def change_todo(issuable)
+ case params.delete(:todo_event)
+ when 'mark'
+ todo_service.mark_todo(issuable, current_user)
+ when 'done'
+ todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
+ if todo
+ todo_service.mark_todos_as_done([todo], current_user)
+ end
+ end
+ end
+
def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 5e2de2ccf64..1b03d7f4c05 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,25 +1,19 @@
module Issues
class CreateService < Issues::BaseService
def execute
- filter_params
- label_params = params.delete(:label_ids)
+ issue = project.issues.new
request = params.delete(:request)
api = params.delete(:api)
- issue = project.issues.new(params)
- issue.author = params[:author] || current_user
issue.spam = spam_check_service.execute(request, api)
- if issue.save
- issue.update_attributes(label_ids: label_params)
- notification_service.new_issue(issue, current_user)
- todo_service.new_issue(issue, current_user)
- event_service.open_issue(issue, current_user)
- issue.create_cross_references!(current_user)
- execute_hooks(issue, 'open')
- end
+ create(issue)
+ end
- issue
+ def handle_creation(issuable)
+ event_service.open_issue(issuable, current_user)
+ notification_service.new_issue(issuable, current_user)
+ todo_service.new_issue(issuable, current_user)
end
private
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 96a25330af1..0b592cd5620 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -7,26 +7,19 @@ module MergeRequests
source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
- filter_params
- label_params = params.delete(:label_ids)
- force_remove_source_branch = params.delete(:force_remove_source_branch)
+ params[:target_project_id] ||= source_project.id
- merge_request = MergeRequest.new(params)
+ merge_request = MergeRequest.new
merge_request.source_project = source_project
- merge_request.target_project ||= source_project
- merge_request.author = current_user
- merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
- if merge_request.save
- merge_request.update_attributes(label_ids: label_params)
- event_service.open_mr(merge_request, current_user)
- notification_service.new_merge_request(merge_request, current_user)
- todo_service.new_merge_request(merge_request, current_user)
- merge_request.create_cross_references!(current_user)
- execute_hooks(merge_request)
- end
+ create(merge_request)
+ end
- merge_request
+ def handle_creation(issuable)
+ event_service.open_mr(issuable, current_user)
+ notification_service.new_merge_request(issuable, current_user)
+ todo_service.new_merge_request(issuable, current_user)
end
end
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 18971bd0be3..d7531a5d63b 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -11,13 +11,61 @@ module Notes
return noteable.create_award_emoji(note.award_emoji_name, current_user)
end
+ # We execute commands (extracted from `params[:note]`) on the noteable
+ # **before** we save the note because if the note consists of commands
+ # only, there is no need be create a note!
+ commands_executed = execute_slash_commands!(note)
+
if note.save
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
- TodoService.new.new_note(note, current_user)
+ todo_service.new_note(note, current_user)
+ end
+
+ if commands_executed && note.note.blank?
+ note.errors.add(:commands_only, 'Your commands are being executed.')
end
note
end
+
+ private
+
+ def execute_slash_commands!(note)
+ noteable_update_service = noteable_update_service(note.noteable_type)
+ return unless noteable_update_service
+
+ command_params = SlashCommands::InterpretService.new(project, current_user).
+ execute(note.note)
+
+ commands = execute_or_filter_commands(command_params, note)
+
+ if commands.any?
+ noteable_update_service.new(project, current_user, commands).execute(note.noteable)
+ end
+ end
+
+ def execute_or_filter_commands(commands, note)
+ final_commands = commands.reduce({}) do |memo, (command_key, command_value)|
+ if command_key != :due_date || note.noteable.respond_to?(:due_date)
+ memo[command_key] = command_value
+ end
+
+ memo
+ end
+
+ final_commands
+ end
+
+ def noteable_update_service(noteable_type)
+ case noteable_type
+ when 'Issue'
+ Issues::UpdateService
+ when 'MergeRequest'
+ MergeRequests::UpdateService
+ else
+ nil
+ end
+ end
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 23b6668e0d1..e943d2ffbcb 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -15,5 +15,9 @@ module Projects
def labels
@project.labels.select([:title, :color])
end
+
+ def commands
+ SlashCommands::InterpretService.command_definitions
+ end
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
new file mode 100644
index 00000000000..2c92a4f7de5
--- /dev/null
+++ b/app/services/slash_commands/interpret_service.rb
@@ -0,0 +1,133 @@
+module SlashCommands
+ class InterpretService < BaseService
+ include Gitlab::SlashCommands::Dsl
+
+ # Takes a text and interpret the commands that are extracted from it.
+ # Returns a hash of changes to be applied to a record.
+ def execute(content)
+ @updates = {}
+
+ commands = extractor.extract_commands!(content)
+ commands.each do |command|
+ __send__(*command)
+ end
+
+ @updates
+ end
+
+ private
+
+ def extractor
+ @extractor ||= Gitlab::SlashCommands::Extractor.new(self.class.command_names)
+ end
+
+ desc 'Close this issue or merge request'
+ command :close do
+ @updates[:state_event] = 'close'
+ end
+
+ desc 'Reopen this issue or merge request'
+ command :open, :reopen do
+ @updates[:state_event] = 'reopen'
+ end
+
+ desc 'Reassign'
+ params '@user'
+ command :assign, :reassign do |assignee_param|
+ user = extract_references(assignee_param, :user).first
+ return unless user
+
+ @updates[:assignee_id] = user.id
+ end
+
+ desc 'Remove assignee'
+ command :unassign, :remove_assignee do
+ @updates[:assignee_id] = nil
+ end
+
+ desc 'Change milestone'
+ params '%"milestone"'
+ command :milestone do |milestone_param|
+ milestone = extract_references(milestone_param, :milestone).first
+ return unless milestone
+
+ @updates[:milestone_id] = milestone.id
+ end
+
+ desc 'Remove milestone'
+ command :clear_milestone, :remove_milestone do
+ @updates[:milestone_id] = nil
+ end
+
+ desc 'Add label(s)'
+ params '~label1 ~"label 2"'
+ command :label, :labels do |labels_param|
+ label_ids = find_label_ids(labels_param)
+ return if label_ids.empty?
+
+ @updates[:add_label_ids] = label_ids
+ end
+
+ desc 'Remove label(s)'
+ params '~label1 ~"label 2"'
+ command :unlabel, :remove_label, :remove_labels do |labels_param|
+ label_ids = find_label_ids(labels_param)
+ return if label_ids.empty?
+
+ @updates[:remove_label_ids] = label_ids
+ end
+
+ desc 'Remove all labels'
+ command :clear_labels, :clear_label do
+ @updates[:label_ids] = []
+ end
+
+ desc 'Add a todo'
+ command :todo do
+ @updates[:todo_event] = 'mark'
+ end
+
+ desc 'Mark todo as done'
+ command :done do
+ @updates[:todo_event] = 'done'
+ end
+
+ desc 'Subscribe'
+ command :subscribe do
+ @updates[:subscription_event] = 'subscribe'
+ end
+
+ desc 'Unsubscribe'
+ command :unsubscribe do
+ @updates[:subscription_event] = 'unsubscribe'
+ end
+
+ desc 'Set a due date'
+ params '<YYYY-MM-DD> | <N days>'
+ command :due_date do |due_date_param|
+ due_date = begin
+ Time.now + ChronicDuration.parse(due_date_param)
+ rescue ChronicDuration::DurationParseError
+ Date.parse(due_date_param) rescue nil
+ end
+
+ @updates[:due_date] = due_date if due_date
+ end
+
+ desc 'Remove due date'
+ command :clear_due_date do
+ @updates[:due_date] = nil
+ end
+
+ def find_label_ids(labels_param)
+ extract_references(labels_param, :label).map(&:id)
+ end
+
+ def extract_references(cmd_arg, type)
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ ext.analyze(cmd_arg, author: current_user)
+
+ ext.references(type)
+ end
+ end
+end