summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorblackst0ne <blackst0ne.ru@gmail.com>2017-06-10 10:03:40 +1100
committerblackst0ne <blackst0ne.ru@gmail.com>2017-06-18 11:36:47 +1100
commit4bfe9c4d67ebd43627641e02a3dcaad945fcaefe (patch)
tree6d97160bf44fe69a21853f44f30e8ef33b48657d
parentae9d191e566b036798816e5f8bf5ef1dc0ff3e90 (diff)
downloadgitlab-ce-22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.tar.gz
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js50
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb16
-rw-r--r--app/services/projects/autocomplete_service.rb21
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml2
-rw-r--r--changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml4
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb60
6 files changed, 141 insertions, 12 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 401dec1a370..791166cc332 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -288,6 +288,10 @@ class GfmAutoComplete {
}
setupLabels($input) {
+ const fetchData = this.fetchData.bind(this);
+ const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
+ let command = '';
+
$input.atwho({
at: '~',
alias: 'labels',
@@ -310,8 +314,54 @@ class GfmAutoComplete {
title: sanitize(m.title),
color: m.color,
search: m.title,
+ set: m.set,
}));
},
+ matcher(flag, subtext) {
+ // Copy & paste defaut matcher getDefaultCallbacks() -> matcher(flag, subtext)
+ // Regex parses entered text by the rule like.
+ // at-symbol (~ in this case) has to be between numbers,
+ // letters (including unicode letters).
+ const atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ const atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ const targetSubtext = subtext.split(/\s+/g).pop();
+ const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
+ const accentAChar = decodeURI('%C3%80');
+ const accentYChar = decodeURI('%C3%BF');
+ const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+ const match = regexp.exec(targetSubtext);
+ const subtextNodes = subtext.split(/\n+/g).pop().split(/\s+/g);
+
+ // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
+ command = subtextNodes.find((node) => {
+ if (node === LABEL_COMMAND.LABEL ||
+ node === LABEL_COMMAND.RELABEL ||
+ node === LABEL_COMMAND.UNLABEL) { return node; }
+ return null;
+ });
+
+ return match && match.length ? match[1] : null;
+ },
+ filter(query, data, searchKey) {
+ if (GfmAutoComplete.isLoading(data)) {
+ fetchData(this.$inputor, this.at);
+ return data;
+ }
+
+ if (data === GfmAutoComplete.defaultLoadingData) {
+ return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
+ }
+
+ if (command === LABEL_COMMAND.LABEL || command === LABEL_COMMAND.RELABEL) {
+ // Return labels with set: undefined.
+ return data.filter(label => !label.set);
+ } else if (command === LABEL_COMMAND.UNLABEL) {
+ // Return labels with set: true.
+ return data.filter(label => label.set);
+ }
+
+ return data;
+ },
},
});
}
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index ffb54390965..45c66b63ea5 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:members]
def members
- render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
def issues
@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def labels
- render json: @autocomplete_service.labels
+ render json: @autocomplete_service.labels(target)
end
def milestones
@@ -22,7 +22,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def commands
- render json: @autocomplete_service.commands(noteable, params[:type])
+ render json: @autocomplete_service.commands(target, params[:type])
end
private
@@ -31,13 +31,13 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
end
- def noteable
- case params[:type]
- when 'Issue'
+ def target
+ case params[:type]&.downcase
+ when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
- when 'MergeRequest'
+ when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
- when 'Commit'
+ when 'commit'
@project.commit(params[:type_id])
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 015f2828921..9f73fa795be 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -12,8 +12,23 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels
- LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
+ def labels(target = nil)
+ labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
+
+ return labels unless target&.respond_to?(:labels)
+
+ issuable_labels = target.labels.pluck(:title)
+
+ if issuable_labels
+ labels = labels.as_json(only: [:title, :color])
+
+ issuable_labels.each do |issuable_label|
+ found_label = labels.find { |label| label['title'] == issuable_label }
+ found_label[:set] = true if found_label
+ end
+ end
+
+ labels
end
def commands(noteable, type)
@@ -25,7 +40,7 @@ module Projects
@project.merge_requests.build
end
- return [] unless noteable && noteable.is_a?(Issuable)
+ return [] unless noteable&.is_a?(Issuable)
opts = {
project: project,
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 6caaba240bb..baaa53ac658 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -8,7 +8,7 @@
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
- labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
};
diff --git a/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
new file mode 100644
index 00000000000..fc1cc7d3194
--- /dev/null
+++ b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Limit autocomplete menu to applied labels
+merge_request: 11110
+author: @blackst0ne
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 350473437a8..e6732b938d3 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -235,4 +235,64 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
+
+ context 'labels' do
+ let(:backend) { create(:label, project: project, title: 'backend') }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:feature_proposal) { create(:label, project: project, title: 'feature proposal') }
+
+ it 'shows proper labels on "~", "/label ~", and "/relabel ~"' do
+ issue.labels << [backend, bug, feature_proposal]
+
+ note = find('#note_note')
+
+ # ~
+ page.within('.timeline-content-form') do
+ note.native.send_keys('~')
+ end
+
+ expect(page).to have_selector('.atwho-view li', count: 3)
+ expect(page).to have_content(backend.title)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(feature_proposal.title)
+
+ # /label ~
+ page.within('.timeline-content-form') do
+ note.set('')
+ note.native.send_keys('/label ~')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+ expect(page).to have_selector('.atwho-view li', count: 3)
+ expect(page).to have_content(backend.title)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(feature_proposal.title)
+
+ # /relabel ~
+ page.within('.timeline-content-form') do
+ note.set('')
+ note.native.send_keys('/relabel ~')
+ end
+
+ expect(page).to have_selector('.atwho-view li', count: 3)
+ expect(page).to have_content(backend.title)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(feature_proposal.title)
+ end
+
+ it 'shows proper labels on "/unlabel ~"' do
+ issue.labels << [backend]
+
+ note = find('#note_note')
+
+ page.within('.timeline-content-form') do
+ note.native.send_keys('/unlabel ~')
+ end
+
+ expect(page).to have_selector('.atwho-view li', count: 1)
+ expect(page).to have_content(backend.title)
+ expect(page).not_to have_content(bug.title)
+ expect(page).not_to have_content(feature_proposal.title)
+ end
+ end
end