Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
GitLab
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
projects.thm.de
GitLab
Commits
68f0092c
Commit
68f0092c
authored
Nov 28, 2017
by
Vitaliy @blackst0ne Klachkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Limit autocomplete menu to applied labels
parent
d199ecd4
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
199 additions
and
26 deletions
+199
-26
app/assets/javascripts/gfm_auto_complete.js
app/assets/javascripts/gfm_auto_complete.js
+61
-14
app/controllers/projects/autocomplete_sources_controller.rb
app/controllers/projects/autocomplete_sources_controller.rb
+8
-8
app/services/projects/autocomplete_service.rb
app/services/projects/autocomplete_service.rb
+18
-3
app/views/layouts/_init_auto_complete.html.haml
app/views/layouts/_init_auto_complete.html.haml
+1
-1
changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
...el-slash-command-limit-autocomplete-to-applied-labels.yml
+5
-0
spec/features/issues/gfm_autocomplete_spec.rb
spec/features/issues/gfm_autocomplete_spec.rb
+106
-0
No files found.
app/assets/javascripts/gfm_auto_complete.js
View file @
68f0092c
...
@@ -287,6 +287,10 @@ class GfmAutoComplete {
...
@@ -287,6 +287,10 @@ class GfmAutoComplete {
}
}
setupLabels
(
$input
)
{
setupLabels
(
$input
)
{
const
fetchData
=
this
.
fetchData
.
bind
(
this
);
const
LABEL_COMMAND
=
{
LABEL
:
'
/label
'
,
UNLABEL
:
'
/unlabel
'
,
RELABEL
:
'
/relabel
'
};
let
command
=
''
;
$input
.
atwho
({
$input
.
atwho
({
at
:
'
~
'
,
at
:
'
~
'
,
alias
:
'
labels
'
,
alias
:
'
labels
'
,
...
@@ -309,8 +313,45 @@ class GfmAutoComplete {
...
@@ -309,8 +313,45 @@ class GfmAutoComplete {
title
:
sanitize
(
m
.
title
),
title
:
sanitize
(
m
.
title
),
color
:
m
.
color
,
color
:
m
.
color
,
search
:
m
.
title
,
search
:
m
.
title
,
set
:
m
.
set
,
}));
}));
},
},
matcher
(
flag
,
subtext
)
{
const
match
=
GfmAutoComplete
.
defaultMatcher
(
flag
,
subtext
,
this
.
app
.
controllers
);
const
subtextNodes
=
subtext
.
split
(
/
\n
+/g
).
pop
().
split
(
GfmAutoComplete
.
regexSubtext
);
// 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
);
}
// The `LABEL_COMMAND.RELABEL` is intentionally skipped
// because we want to return all the labels (unfiltered) for that command.
if
(
command
===
LABEL_COMMAND
.
LABEL
)
{
// 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
;
},
},
},
});
});
}
}
...
@@ -346,20 +387,7 @@ class GfmAutoComplete {
...
@@ -346,20 +387,7 @@ class GfmAutoComplete {
return
resultantValue
;
return
resultantValue
;
},
},
matcher
(
flag
,
subtext
)
{
matcher
(
flag
,
subtext
)
{
// The below is taken from At.js source
const
match
=
GfmAutoComplete
.
defaultMatcher
(
flag
,
subtext
,
this
.
app
.
controllers
);
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
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
);
if
(
match
)
{
if
(
match
)
{
return
match
[
1
];
return
match
[
1
];
...
@@ -420,8 +448,27 @@ class GfmAutoComplete {
...
@@ -420,8 +448,27 @@ class GfmAutoComplete {
return
dataToInspect
&&
return
dataToInspect
&&
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
}
}
static
defaultMatcher
(
flag
,
subtext
,
controllers
)
{
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
const
atSymbolsWithBar
=
Object
.
keys
(
controllers
).
join
(
'
|
'
);
const
atSymbolsWithoutBar
=
Object
.
keys
(
controllers
).
join
(
''
);
const
targetSubtext
=
subtext
.
split
(
GfmAutoComplete
.
regexSubtext
).
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
'
);
return
regexp
.
exec
(
targetSubtext
);
}
}
}
GfmAutoComplete
.
regexSubtext
=
new
RegExp
(
/
\s
+/g
);
GfmAutoComplete
.
defaultLoadingData
=
[
'
loading
'
];
GfmAutoComplete
.
defaultLoadingData
=
[
'
loading
'
];
GfmAutoComplete
.
atTypeMap
=
{
GfmAutoComplete
.
atTypeMap
=
{
...
...
app/controllers/projects/autocomplete_sources_controller.rb
View file @
68f0092c
...
@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
...
@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action
:load_autocomplete_service
,
except:
[
:members
]
before_action
:load_autocomplete_service
,
except:
[
:members
]
def
members
def
members
render
json:
::
Projects
::
ParticipantsService
.
new
(
@project
,
current_user
).
execute
(
noteable
)
render
json:
::
Projects
::
ParticipantsService
.
new
(
@project
,
current_user
).
execute
(
target
)
end
end
def
issues
def
issues
...
@@ -14,7 +14,7 @@ def merge_requests
...
@@ -14,7 +14,7 @@ def merge_requests
end
end
def
labels
def
labels
render
json:
@autocomplete_service
.
labels
render
json:
@autocomplete_service
.
labels
(
target
)
end
end
def
milestones
def
milestones
...
@@ -22,7 +22,7 @@ def milestones
...
@@ -22,7 +22,7 @@ def milestones
end
end
def
commands
def
commands
render
json:
@autocomplete_service
.
commands
(
noteable
,
params
[
:type
])
render
json:
@autocomplete_service
.
commands
(
target
,
params
[
:type
])
end
end
private
private
...
@@ -31,13 +31,13 @@ def load_autocomplete_service
...
@@ -31,13 +31,13 @@ def load_autocomplete_service
@autocomplete_service
=
::
Projects
::
AutocompleteService
.
new
(
@project
,
current_user
)
@autocomplete_service
=
::
Projects
::
AutocompleteService
.
new
(
@project
,
current_user
)
end
end
def
noteable
def
target
case
params
[
:type
]
case
params
[
:type
]
&
.
downcase
when
'
I
ssue'
when
'
i
ssue'
IssuesFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
execute
.
find_by
(
iid:
params
[
:type_id
])
IssuesFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
execute
.
find_by
(
iid:
params
[
:type_id
])
when
'
MergeR
equest'
when
'
merger
equest'
MergeRequestsFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
execute
.
find_by
(
iid:
params
[
:type_id
])
MergeRequestsFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
execute
.
find_by
(
iid:
params
[
:type_id
])
when
'
C
ommit'
when
'
c
ommit'
@project
.
commit
(
params
[
:type_id
])
@project
.
commit
(
params
[
:type_id
])
end
end
end
end
...
...
app/services/projects/autocomplete_service.rb
View file @
68f0092c
...
@@ -20,8 +20,23 @@ def merge_requests
...
@@ -20,8 +20,23 @@ def merge_requests
MergeRequestsFinder
.
new
(
current_user
,
project_id:
project
.
id
,
state:
'opened'
).
execute
.
select
([
:iid
,
:title
])
MergeRequestsFinder
.
new
(
current_user
,
project_id:
project
.
id
,
state:
'opened'
).
execute
.
select
([
:iid
,
:title
])
end
end
def
labels
def
labels
(
target
=
nil
)
LabelsFinder
.
new
(
current_user
,
project_id:
project
.
id
).
execute
.
select
([
:title
,
:color
])
labels
=
LabelsFinder
.
new
(
current_user
,
project_id:
project
.
id
).
execute
.
select
([
:color
,
:title
])
return
labels
unless
target
&
.
respond_to?
(
:labels
)
issuable_label_titles
=
target
.
labels
.
pluck
(
:title
)
if
issuable_label_titles
labels
=
labels
.
as_json
(
only:
[
:title
,
:color
])
issuable_label_titles
.
each
do
|
issuable_label_title
|
found_label
=
labels
.
find
{
|
label
|
label
[
'title'
]
==
issuable_label_title
}
found_label
[
:set
]
=
true
if
found_label
end
end
labels
end
end
def
commands
(
noteable
,
type
)
def
commands
(
noteable
,
type
)
...
@@ -33,7 +48,7 @@ def commands(noteable, type)
...
@@ -33,7 +48,7 @@ def commands(noteable, type)
@project
.
merge_requests
.
build
@project
.
merge_requests
.
build
end
end
return
[]
unless
noteable
&&
noteable
.
is_a?
(
Issuable
)
return
[]
unless
noteable
&
.
is_a?
(
Issuable
)
opts
=
{
opts
=
{
project:
project
,
project:
project
,
...
...
app/views/layouts/_init_auto_complete.html.haml
View file @
68f0092c
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
members
:
"
#{
members_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
,
members
:
"
#{
members_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
,
issues
:
"
#{
issues_project_autocomplete_sources_path
(
project
)
}
"
,
issues
:
"
#{
issues_project_autocomplete_sources_path
(
project
)
}
"
,
mergeRequests
:
"
#{
merge_requests_project_autocomplete_sources_path
(
project
)
}
"
,
mergeRequests
:
"
#{
merge_requests_project_autocomplete_sources_path
(
project
)
}
"
,
labels
:
"
#{
labels_project_autocomplete_sources_path
(
project
)
}
"
,
labels
:
"
#{
labels_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
]
)
}
"
,
milestones
:
"
#{
milestones_project_autocomplete_sources_path
(
project
)
}
"
,
milestones
:
"
#{
milestones_project_autocomplete_sources_path
(
project
)
}
"
,
commands
:
"
#{
commands_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
commands
:
"
#{
commands_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
};
};
changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
0 → 100644
View file @
68f0092c
---
title
:
Limit autocomplete menu to applied labels
merge_request
:
11110
author
:
Vitaliy @blackst0ne Klachkov
type
:
added
spec/features/issues/gfm_autocomplete_spec.rb
View file @
68f0092c
...
@@ -220,6 +220,89 @@
...
@@ -220,6 +220,89 @@
end
end
end
end
# This context has jsut one example in each contexts in order to improve spec performance.
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'
)
}
context
'when no labels are assigned'
do
it
'shows labels'
do
note
=
find
(
'#note-body'
)
# It should show all the labels on "~".
type
(
note
,
'~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show all the labels on "/label ~".
type
(
note
,
'/label ~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show all the labels on "/relabel ~".
type
(
note
,
'/relabel ~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show no labels on "/unlabel ~".
type
(
note
,
'/unlabel ~'
)
expect_labels
(
not_shown:
[
backend
,
bug
,
feature_proposal
])
end
end
context
'when some labels are assigned'
do
before
do
issue
.
labels
<<
[
backend
]
end
it
'shows labels'
do
note
=
find
(
'#note-body'
)
# It should show all the labels on "~".
type
(
note
,
'~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show only unset labels on "/label ~".
type
(
note
,
'/label ~'
)
expect_labels
(
shown:
[
bug
,
feature_proposal
],
not_shown:
[
backend
])
# It should show all the labels on "/relabel ~".
type
(
note
,
'/relabel ~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show only set labels on "/unlabel ~".
type
(
note
,
'/unlabel ~'
)
expect_labels
(
shown:
[
backend
],
not_shown:
[
bug
,
feature_proposal
])
end
end
context
'when all labels are assigned'
do
before
do
issue
.
labels
<<
[
backend
,
bug
,
feature_proposal
]
end
it
'shows labels'
do
note
=
find
(
'#note-body'
)
# It should show all the labels on "~".
type
(
note
,
'~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show no labels on "/label ~".
type
(
note
,
'/label ~'
)
expect_labels
(
not_shown:
[
backend
,
bug
,
feature_proposal
])
# It should show all the labels on "/relabel ~".
type
(
note
,
'/relabel ~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
# It should show all the labels on "/unlabel ~".
type
(
note
,
'/unlabel ~'
)
expect_labels
(
shown:
[
backend
,
bug
,
feature_proposal
])
end
end
end
private
def
expect_to_wrap
(
should_wrap
,
item
,
note
,
value
)
def
expect_to_wrap
(
should_wrap
,
item
,
note
,
value
)
expect
(
item
).
to
have_content
(
value
)
expect
(
item
).
to
have_content
(
value
)
expect
(
item
).
not_to
have_content
(
"
\"
#{
value
}
\"
"
)
expect
(
item
).
not_to
have_content
(
"
\"
#{
value
}
\"
"
)
...
@@ -232,4 +315,27 @@ def expect_to_wrap(should_wrap, item, note, value)
...
@@ -232,4 +315,27 @@ def expect_to_wrap(should_wrap, item, note, value)
expect
(
note
.
value
).
not_to
include
(
"
\"
#{
value
}
\"
"
)
expect
(
note
.
value
).
not_to
include
(
"
\"
#{
value
}
\"
"
)
end
end
end
end
def
expect_labels
(
shown:
nil
,
not_shown:
nil
)
page
.
within
(
'.atwho-container'
)
do
if
shown
expect
(
page
).
to
have_selector
(
'.atwho-view li'
,
count:
shown
.
size
)
shown
.
each
{
|
label
|
expect
(
page
).
to
have_content
(
label
.
title
)
}
end
if
not_shown
expect
(
page
).
not_to
have_selector
(
'.atwho-view li'
)
unless
shown
not_shown
.
each
{
|
label
|
expect
(
page
).
not_to
have_content
(
label
.
title
)
}
end
end
end
# `note` is a textarea where the given text should be typed.
# We don't want to find it each time this function gets called.
def
type
(
note
,
text
)
page
.
within
(
'.timeline-content-form'
)
do
note
.
set
(
''
)
note
.
native
.
send_keys
(
text
)
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment