Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
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
Show 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 {
}
setupLabels
(
$input
)
{
const
fetchData
=
this
.
fetchData
.
bind
(
this
);
const
LABEL_COMMAND
=
{
LABEL
:
'
/label
'
,
UNLABEL
:
'
/unlabel
'
,
RELABEL
:
'
/relabel
'
};
let
command
=
''
;
$input
.
atwho
({
at
:
'
~
'
,
alias
:
'
labels
'
,
...
...
@@ -309,8 +313,45 @@ class GfmAutoComplete {
title
:
sanitize
(
m
.
title
),
color
:
m
.
color
,
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 {
return
resultantValue
;
},
matcher
(
flag
,
subtext
)
{
// 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
(
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
match
=
GfmAutoComplete
.
defaultMatcher
(
flag
,
subtext
,
this
.
app
.
controllers
);
if
(
match
)
{
return
match
[
1
];
...
...
@@ -420,8 +448,27 @@ class GfmAutoComplete {
return
dataToInspect
&&
(
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
.
atTypeMap
=
{
...
...
app/controllers/projects/autocomplete_sources_controller.rb
View file @
68f0092c
...
...
@@ -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
'
I
ssue'
def
target
case
params
[
:type
]
&
.
downcase
when
'
i
ssue'
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
])
when
'
C
ommit'
when
'
c
ommit'
@project
.
commit
(
params
[
:type_id
])
end
end
...
...
app/services/projects/autocomplete_service.rb
View file @
68f0092c
...
...
@@ -20,8 +20,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_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
def
commands
(
noteable
,
type
)
...
...
@@ -33,7 +48,7 @@ module Projects
@project
.
merge_requests
.
build
end
return
[]
unless
noteable
&&
noteable
.
is_a?
(
Issuable
)
return
[]
unless
noteable
&
.
is_a?
(
Issuable
)
opts
=
{
project:
project
,
...
...
app/views/layouts/_init_auto_complete.html.haml
View file @
68f0092c
...
...
@@ -10,7 +10,7 @@
members
:
"
#{
members_project_autocomplete_sources_path
(
project
,
type:
noteable_type
,
type_id:
params
[
:id
])
}
"
,
issues
:
"
#{
issues_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
)
}
"
,
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 @@ feature 'GFM autocomplete', :js do
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
)
expect
(
item
).
to
have_content
(
value
)
expect
(
item
).
not_to
have_content
(
"
\"
#{
value
}
\"
"
)
...
...
@@ -232,4 +315,27 @@ feature 'GFM autocomplete', :js do
expect
(
note
.
value
).
not_to
include
(
"
\"
#{
value
}
\"
"
)
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
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