diff options
author | blackst0ne <blackst0ne.ru@gmail.com> | 2017-06-10 10:03:40 +1100 |
---|---|---|
committer | blackst0ne <blackst0ne.ru@gmail.com> | 2017-06-18 11:36:47 +1100 |
commit | 4bfe9c4d67ebd43627641e02a3dcaad945fcaefe (patch) | |
tree | 6d97160bf44fe69a21853f44f30e8ef33b48657d | |
parent | ae9d191e566b036798816e5f8bf5ef1dc0ff3e90 (diff) | |
download | gitlab-ce-22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.tar.gz |
Limit autocomplete menu to applied labels22680-unlabel-slash-command-limit-autocomplete-to-applied-labels
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 |