Commit de7abc06 authored by Martin Hanzel's avatar Martin Hanzel Committed by Kushal Pandya

Allow autocompleting scoped labels

The `:` key will no longer exit out of the autocomplete dialog,
allowing auto-completion of labels with colons in them.
parent e3fa9d12
...@@ -12,6 +12,7 @@ import 'core-js/es/promise/finally'; ...@@ -12,6 +12,7 @@ import 'core-js/es/promise/finally';
import 'core-js/es/string/code-point-at'; import 'core-js/es/string/code-point-at';
import 'core-js/es/string/from-code-point'; import 'core-js/es/string/from-code-point';
import 'core-js/es/string/includes'; import 'core-js/es/string/includes';
import 'core-js/es/string/starts-with';
import 'core-js/es/symbol'; import 'core-js/es/symbol';
import 'core-js/es/map'; import 'core-js/es/map';
import 'core-js/es/weak-map'; import 'core-js/es/weak-map';
......
...@@ -318,6 +318,7 @@ class GfmAutoComplete { ...@@ -318,6 +318,7 @@ class GfmAutoComplete {
} }
setupLabels($input) { setupLabels($input) {
const instance = this;
const fetchData = this.fetchData.bind(this); const fetchData = this.fetchData.bind(this);
const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' }; const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
let command = ''; let command = '';
...@@ -348,7 +349,6 @@ class GfmAutoComplete { ...@@ -348,7 +349,6 @@ class GfmAutoComplete {
})); }));
}, },
matcher(flag, subtext) { matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
const subtextNodes = subtext const subtextNodes = subtext
.split(/\n+/g) .split(/\n+/g)
.pop() .pop()
...@@ -366,6 +366,27 @@ class GfmAutoComplete { ...@@ -366,6 +366,27 @@ class GfmAutoComplete {
return null; return null;
}); });
// If any label matches the inserted text after the last `~`, suggest those labels,
// even if any spaces or funky characters were typed.
// This allows matching labels like "Accepting merge requests".
const labels = instance.cachedData[flag];
if (labels) {
if (!subtext.includes(flag)) {
// Do not match if there is no `~` before the cursor
return null;
}
const lastCandidate = subtext.split(flag).pop();
if (labels.find(label => label.title.startsWith(lastCandidate))) {
return lastCandidate;
}
} else {
// Load all labels into the autocompleter.
// This needs to happen if e.g. editing a label in an existing comment, because normally
// label data would only be loaded only once you type `~`.
fetchData(this.$inputor, this.at);
}
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null; return match && match.length ? match[1] : null;
}, },
filter(query, data, searchKey) { filter(query, data, searchKey) {
...@@ -563,8 +584,9 @@ class GfmAutoComplete { ...@@ -563,8 +584,9 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80'); const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF'); const accentYChar = decodeURI('%C3%BF');
// Holy regex, batman!
const regexp = new RegExp( const regexp = new RegExp(
`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-:]|[^\\x00-\\x7a])*)$`,
'gi', 'gi',
); );
......
---
title: Allow auto-completing scoped labels
merge_request: 29749
author:
type: added
...@@ -3,14 +3,14 @@ require 'rails_helper' ...@@ -3,14 +3,14 @@ require 'rails_helper'
describe 'GFM autocomplete', :js do describe 'GFM autocomplete', :js do
let(:issue_xss_title) { 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' } let(:issue_xss_title) { 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
let(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' } let(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
let(:label_xss_title) { 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'} let(:label_xss_title) { 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a' }
let(:milestone_xss_title) { 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a' } let(:milestone_xss_title) { 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a' }
let(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') } let(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') } let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -293,6 +293,70 @@ describe 'GFM autocomplete', :js do ...@@ -293,6 +293,70 @@ describe 'GFM autocomplete', :js do
expect(find('.atwho-view-ul').text).to have_content('alert label') expect(find('.atwho-view-ul').text).to have_content('alert label')
end end
end end
it 'allows colons when autocompleting scoped labels' do
create(:label, project: project, title: 'scoped:label')
note = find('#note-body')
type(note, '~scoped:')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('scoped:label')
end
end
it 'allows colons when autocompleting scoped labels with double colons' do
create(:label, project: project, title: 'scoped::label')
note = find('#note-body')
type(note, '~scoped::')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('scoped::label')
end
end
it 'allows spaces when autocompleting multi-word labels' do
create(:label, project: project, title: 'Accepting merge requests')
note = find('#note-body')
type(note, '~Accepting merge')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
end
end
it 'only autocompletes the latest label' do
create(:label, project: project, title: 'Accepting merge requests')
create(:label, project: project, title: 'Accepting job applicants')
note = find('#note-body')
type(note, '~Accepting merge requests foo bar ~Accepting job')
wait_for_requests
page.within '.atwho-container #at-view-labels' do
expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
end
end
it 'does not autocomplete labels if no tilde is typed' do
create(:label, project: project, title: 'Accepting merge requests')
note = find('#note-body')
type(note, 'Accepting merge')
wait_for_requests
expect(page).not_to have_css('.atwho-container #at-view-labels')
end
end end
shared_examples 'autocomplete suggestions' do shared_examples 'autocomplete suggestions' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment