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
e0800285
Commit
e0800285
authored
Mar 10, 2016
by
Jacob Schatz
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dropdown-js' into 'master'
Dropdown JS See merge request !3112
parents
3d4063ef
ee9a6d7f
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
842 additions
and
39 deletions
+842
-39
app/assets/javascripts/api.js.coffee
app/assets/javascripts/api.js.coffee
+14
-0
app/assets/javascripts/gl_dropdown.js.coffee
app/assets/javascripts/gl_dropdown.js.coffee
+270
-0
app/assets/javascripts/issue_status_select.js.coffee
app/assets/javascripts/issue_status_select.js.coffee
+11
-0
app/assets/javascripts/labels_select.js.coffee
app/assets/javascripts/labels_select.js.coffee
+92
-0
app/assets/javascripts/milestone_select.js.coffee
app/assets/javascripts/milestone_select.js.coffee
+60
-0
app/assets/javascripts/users_select.js.coffee
app/assets/javascripts/users_select.js.coffee
+75
-0
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+30
-6
app/assets/stylesheets/framework/filters.scss
app/assets/stylesheets/framework/filters.scss
+1
-0
app/assets/stylesheets/pages/labels.scss
app/assets/stylesheets/pages/labels.scss
+22
-0
app/helpers/dropdowns_helper.rb
app/helpers/dropdowns_helper.rb
+100
-0
app/views/help/ui.html.haml
app/views/help/ui.html.haml
+45
-0
app/views/shared/issuable/_filter.html.haml
app/views/shared/issuable/_filter.html.haml
+78
-13
features/steps/dashboard/issues.rb
features/steps/dashboard/issues.rb
+13
-4
features/steps/dashboard/merge_requests.rb
features/steps/dashboard/merge_requests.rb
+13
-4
features/steps/project/issues/filter_labels.rb
features/steps/project/issues/filter_labels.rb
+4
-1
features/steps/project/issues/issues.rb
features/steps/project/issues/issues.rb
+6
-5
spec/features/issues/filter_by_milestone_spec.rb
spec/features/issues/filter_by_milestone_spec.rb
+4
-3
spec/features/merge_requests/filter_by_milestone_spec.rb
spec/features/merge_requests/filter_by_milestone_spec.rb
+4
-3
No files found.
app/assets/javascripts/api.js.coffee
View file @
e0800285
...
...
@@ -4,6 +4,7 @@
namespaces_path
:
"/api/:version/namespaces.json"
group_projects_path
:
"/api/:version/groups/:id/projects.json"
projects_path
:
"/api/:version/projects.json"
labels_path
:
"/api/:version/projects/:id/labels"
group
:
(
group_id
,
callback
)
->
url
=
Api
.
buildUrl
(
Api
.
group_path
)
...
...
@@ -61,6 +62,19 @@
).
done
(
projects
)
->
callback
(
projects
)
newLabel
:
(
project_id
,
data
,
callback
)
->
url
=
Api
.
buildUrl
(
Api
.
labels_path
)
url
=
url
.
replace
(
':id'
,
project_id
)
data
.
private_token
=
gon
.
api_token
$
.
ajax
(
url
:
url
type
:
"POST"
data
:
data
dataType
:
"json"
).
done
(
label
)
->
callback
(
label
)
# Return group projects list. Filtered by query
groupProjects
:
(
group_id
,
query
,
callback
)
->
url
=
Api
.
buildUrl
(
Api
.
group_projects_path
)
...
...
app/assets/javascripts/gl_dropdown.js.coffee
0 → 100644
View file @
e0800285
class
GitLabDropdownFilter
BLUR_KEYCODES
=
[
27
,
40
]
constructor
:
(
@
dropdown
,
@
options
)
->
@
input
=
@
dropdown
.
find
(
".dropdown-input .dropdown-input-field"
)
# Key events
timeout
=
""
@
input
.
on
"keyup"
,
(
e
)
=>
if
e
.
keyCode
is
13
&&
@
input
.
val
()
isnt
""
if
@
options
.
enterCallback
@
options
.
enterCallback
()
return
clearTimeout
timeout
timeout
=
setTimeout
=>
blur_field
=
@
shouldBlur
e
.
keyCode
search_text
=
@
input
.
val
()
if
blur_field
@
input
.
blur
()
if
@
options
.
remote
@
options
.
query
search_text
,
(
data
)
=>
@
options
.
callback
(
data
)
else
@
filter
search_text
,
250
shouldBlur
:
(
keyCode
)
->
return
BLUR_KEYCODES
.
indexOf
(
keyCode
)
>=
0
filter
:
(
search_text
)
->
data
=
@
options
.
data
()
results
=
data
if
search_text
isnt
""
results
=
fuzzaldrinPlus
.
filter
(
data
,
search_text
,
key
:
@
options
.
keys
)
@
options
.
callback
results
class
GitLabDropdownRemote
constructor
:
(
@
dataEndpoint
,
@
options
)
->
execute
:
->
if
typeof
@
dataEndpoint
is
"string"
@
fetchData
()
else
if
typeof
@
dataEndpoint
is
"function"
if
@
options
.
beforeSend
@
options
.
beforeSend
()
# Fetch the data by calling the data funcfion
@
dataEndpoint
""
,
(
data
)
=>
if
@
options
.
success
@
options
.
success
(
data
)
if
@
options
.
beforeSend
@
options
.
beforeSend
()
# Fetch the data through ajax if the data is a string
fetchData
:
->
$
.
ajax
(
url
:
@
dataEndpoint
,
dataType
:
@
options
.
dataType
,
beforeSend
:
=>
if
@
options
.
beforeSend
@
options
.
beforeSend
()
success
:
(
data
)
=>
if
@
options
.
success
@
options
.
success
(
data
)
)
class
GitLabDropdown
LOADING_CLASS
=
"is-loading"
PAGE_TWO_CLASS
=
"is-page-two"
ACTIVE_CLASS
=
"is-active"
constructor
:
(
@
el
,
@
options
)
->
self
=
@
@
dropdown
=
$
(
@
el
).
parent
()
search_fields
=
if
@
options
.
search
then
@
options
.
search
.
fields
else
[];
if
@
options
.
data
# Remote data
@
remote
=
new
GitLabDropdownRemote
@
options
.
data
,
{
dataType
:
@
options
.
dataType
,
beforeSend
:
@
toggleLoading
.
bind
(
@
)
success
:
(
data
)
=>
@
fullData
=
data
@
parseData
@
fullData
}
# Init filiterable
if
@
options
.
filterable
@
filter
=
new
GitLabDropdownFilter
@
dropdown
,
remote
:
@
options
.
filterRemote
query
:
@
options
.
data
keys
:
@
options
.
search
.
fields
data
:
=>
return
@
fullData
callback
:
(
data
)
=>
@
parseData
data
enterCallback
:
=>
@
selectFirstRow
()
# Event listeners
@
dropdown
.
on
"shown.bs.dropdown"
,
@
opened
@
dropdown
.
on
"hidden.bs.dropdown"
,
@
hidden
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
@
dropdown
.
find
(
".dropdown-toggle-page, .dropdown-menu-back"
).
on
"click"
,
(
e
)
=>
e
.
preventDefault
()
e
.
stopPropagation
()
@
togglePage
()
if
@
options
.
selectable
selector
=
".dropdown-content a"
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one .dropdown-content a"
@
dropdown
.
on
"click"
,
selector
,
(
e
)
->
self
.
rowClicked
$
(
@
)
if
self
.
options
.
clicked
self
.
options
.
clicked
()
toggleLoading
:
->
$
(
'.dropdown-menu'
,
@
dropdown
).
toggleClass
LOADING_CLASS
togglePage
:
->
menu
=
$
(
'.dropdown-menu'
,
@
dropdown
)
if
menu
.
hasClass
(
PAGE_TWO_CLASS
)
if
@
remote
@
remote
.
execute
()
menu
.
toggleClass
PAGE_TWO_CLASS
parseData
:
(
data
)
->
@
renderedData
=
data
# Render each row
html
=
$
.
map
data
,
(
obj
)
=>
return
@
renderItem
(
obj
)
if
@
options
.
filterable
and
data
.
length
is
0
# render no matching results
html
=
[
@
noResults
()]
# Render the full menu
full_html
=
@
renderMenu
(
html
.
join
(
""
))
@
appendMenu
(
full_html
)
opened
:
=>
contentHtml
=
$
(
'.dropdown-content'
,
@
dropdown
).
html
()
if
@
remote
&&
contentHtml
is
""
@
remote
.
execute
()
if
@
options
.
filterable
@
dropdown
.
find
(
".dropdown-input-field"
).
focus
()
hidden
:
=>
if
@
options
.
filterable
@
dropdown
.
find
(
".dropdown-input-field"
).
blur
().
val
(
""
)
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
$
(
'.dropdown-menu'
,
@
dropdown
).
removeClass
PAGE_TWO_CLASS
# Render the full menu
renderMenu
:
(
html
)
->
menu_html
=
""
if
@
options
.
renderMenu
menu_html
=
@
options
.
renderMenu
(
html
)
else
menu_html
=
"<ul>
#{
html
}
</ul>"
return
menu_html
# Append the menu into the dropdown
appendMenu
:
(
html
)
->
selector
=
'.dropdown-content'
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one .dropdown-content"
$
(
selector
,
@
dropdown
).
html
html
# Render the row
renderItem
:
(
data
)
->
html
=
""
return
"<li class='divider'></li>"
if
data
is
"divider"
if
@
options
.
renderRow
# Call the render function
html
=
@
options
.
renderRow
(
data
)
else
selected
=
if
@
options
.
isSelected
then
@
options
.
isSelected
(
data
)
else
false
url
=
if
@
options
.
url
then
@
options
.
url
(
data
)
else
"#"
text
=
if
@
options
.
text
then
@
options
.
text
(
data
)
else
""
cssClass
=
""
;
if
selected
cssClass
=
"is-active"
html
=
"<li>"
html
+=
"<a href='
#{
url
}
' class='
#{
cssClass
}
'>"
html
+=
text
html
+=
"</a>"
html
+=
"</li>"
return
html
noResults
:
->
html
=
"<li>"
html
+=
"<a href='#' class='is-focused'>"
html
+=
"No matching results."
html
+=
"</a>"
html
+=
"</li>"
rowClicked
:
(
el
)
->
fieldName
=
@
options
.
fieldName
field
=
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
)
if
el
.
hasClass
(
ACTIVE_CLASS
)
field
.
remove
()
else
fieldName
=
@
options
.
fieldName
selectedIndex
=
el
.
parent
().
index
()
if
@
renderedData
selectedObject
=
@
renderedData
[
selectedIndex
]
value
=
if
@
options
.
id
then
@
options
.
id
(
selectedObject
,
el
)
else
selectedObject
.
id
if
@
options
.
multiSelect
oldValue
=
field
.
val
()
if
oldValue
value
=
"
#{
oldValue
}
,
#{
value
}
"
else
@
dropdown
.
find
(
ACTIVE_CLASS
).
removeClass
ACTIVE_CLASS
field
.
remove
()
# Toggle active class for the tick mark
el
.
toggleClass
"is-active"
if
value
if
!
field
.
length
# Create hidden input for form
input
=
"<input type='hidden' name='
#{
fieldName
}
' />"
@
dropdown
.
before
input
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
).
val
value
selectFirstRow
:
->
selector
=
'.dropdown-content li:first-child a'
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
$
(
selector
).
trigger
"click"
$
.
fn
.
glDropdown
=
(
opts
)
->
return
@
.
each
->
new
GitLabDropdown
@
,
opts
app/assets/javascripts/issue_status_select.js.coffee
0 → 100644
View file @
e0800285
class
@
IssueStatusSelect
constructor
:
->
$
(
'.js-issue-status'
).
each
(
i
,
el
)
->
fieldName
=
$
(
el
).
data
(
"field-name"
)
$
(
el
).
glDropdown
(
selectable
:
true
fieldName
:
fieldName
id
:
(
obj
,
el
)
->
$
(
el
).
data
(
"id"
)
)
app/assets/javascripts/labels_select.js.coffee
0 → 100644
View file @
e0800285
class
@
LabelsSelect
constructor
:
->
$
(
'.js-label-select'
).
each
(
i
,
dropdown
)
->
projectId
=
$
(
dropdown
).
data
(
'project-id'
)
labelUrl
=
$
(
dropdown
).
data
(
"labels"
)
selectedLabel
=
$
(
dropdown
).
data
(
'selected'
)
if
selectedLabel
selectedLabel
=
selectedLabel
.
split
(
","
)
newLabelField
=
$
(
'#new_label_name'
)
newColorField
=
$
(
'#new_label_color'
)
showNo
=
$
(
dropdown
).
data
(
'show-no'
)
showAny
=
$
(
dropdown
).
data
(
'show-any'
)
if
newLabelField
.
length
$
(
'.suggest-colors-dropdown a'
).
on
"click"
,
(
e
)
->
e
.
preventDefault
()
e
.
stopPropagation
()
newColorField
.
val
$
(
this
).
data
(
"color"
)
$
(
'.js-dropdown-label-color-preview'
)
.
css
'background-color'
,
$
(
this
).
data
(
"color"
)
.
addClass
'is-active'
$
(
'.js-new-label-btn'
).
on
"click"
,
(
e
)
->
e
.
preventDefault
()
e
.
stopPropagation
()
if
newLabelField
.
val
()
isnt
""
&&
newColorField
.
val
()
isnt
""
$
(
'.js-new-label-btn'
).
disable
()
# Create new label with API
Api
.
newLabel
projectId
,
{
name
:
newLabelField
.
val
()
color
:
newColorField
.
val
()
},
(
label
)
->
$
(
'.js-new-label-btn'
).
enable
()
$
(
'.dropdown-menu-back'
,
$
(
dropdown
).
parent
()).
trigger
"click"
$
(
dropdown
).
glDropdown
(
data
:
(
term
,
callback
)
->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$
.
ajax
(
url
:
labelUrl
).
done
(
data
)
->
html
=
$
(
data
)
data
=
[]
html
.
find
(
'.label-row a'
).
each
->
data
.
push
(
title
:
$
(
@
).
text
().
trim
()
)
if
showNo
data
.
unshift
(
id
:
"0"
title
:
'No label'
)
if
showAny
data
.
unshift
(
title
:
'Any label'
)
if
data
.
length
>
2
data
.
splice
2
,
0
,
"divider"
callback
data
renderRow
:
(
label
)
->
if
$
.
isArray
(
selectedLabel
)
selected
=
""
$
.
each
selectedLabel
,
(
i
,
selectedLbl
)
->
selectedLbl
=
selectedLbl
.
trim
()
if
selected
is
""
&&
label
.
title
is
selectedLbl
selected
=
"is-active"
else
selected
=
if
label
.
title
is
selectedLabel
then
"is-active"
else
""
"<li>
<a href='#' class='
#{
selected
}
'>
#{
label
.
title
}
</a>
</li>"
filterable
:
true
search
:
fields
:
[
'title'
]
selectable
:
true
fieldName
:
$
(
dropdown
).
data
(
'field-name'
)
id
:
(
label
)
->
label
.
title
clicked
:
->
if
$
(
dropdown
).
hasClass
"js-filter-submit"
$
(
dropdown
).
parents
(
'form'
).
submit
()
)
app/assets/javascripts/milestone_select.js.coffee
0 → 100644
View file @
e0800285
class
@
MilestoneSelect
constructor
:
->
$
(
'.js-milestone-select'
).
each
(
i
,
dropdown
)
->
projectId
=
$
(
dropdown
).
data
(
'project-id'
)
milestonesUrl
=
$
(
dropdown
).
data
(
'milestones'
)
selectedMilestone
=
$
(
dropdown
).
data
(
'selected'
)
showNo
=
$
(
dropdown
).
data
(
'show-no'
)
showAny
=
$
(
dropdown
).
data
(
'show-any'
)
useId
=
$
(
dropdown
).
data
(
'use-id'
)
$
(
dropdown
).
glDropdown
(
data
:
(
term
,
callback
)
->
$
.
ajax
(
url
:
milestonesUrl
).
done
(
data
)
->
html
=
$
(
data
)
data
=
[]
html
.
find
(
'.milestone strong a'
).
each
->
link
=
$
(
@
).
attr
(
"href"
).
split
(
"/"
)
data
.
push
(
id
:
link
[
link
.
length
-
1
]
title
:
$
(
@
).
text
().
trim
()
)
if
showNo
data
.
unshift
(
id
:
"0"
title
:
'No Milestone'
)
if
showAny
data
.
unshift
(
title
:
'Any Milestone'
)
if
data
.
length
>
2
data
.
splice
2
,
0
,
"divider"
callback
(
data
)
filterable
:
true
search
:
fields
:
[
'title'
]
selectable
:
true
fieldName
:
$
(
dropdown
).
data
(
'field-name'
)
text
:
(
milestone
)
->
milestone
.
title
id
:
(
milestone
)
->
if
!
useId
if
milestone
.
title
isnt
"Any milestone"
milestone
.
title
else
""
else
milestone
.
id
isSelected
:
(
milestone
)
->
milestone
.
title
is
selectedMilestone
clicked
:
->
if
$
(
dropdown
).
hasClass
"js-filter-submit"
$
(
dropdown
).
parents
(
'form'
).
submit
()
)
app/assets/javascripts/users_select.js.coffee
View file @
e0800285
...
...
@@ -3,6 +3,81 @@ class @UsersSelect
@
usersPath
=
"/autocomplete/users.json"
@
userPath
=
"/autocomplete/users/:id.json"
$
(
'.js-user-search'
).
each
(
i
,
dropdown
)
=>
@
projectId
=
$
(
dropdown
).
data
(
'project-id'
)
@
showCurrentUser
=
$
(
dropdown
).
data
(
'current-user'
)
showNullUser
=
$
(
dropdown
).
data
(
'null-user'
)
showAnyUser
=
$
(
dropdown
).
data
(
'any-user'
)
firstUser
=
$
(
dropdown
).
data
(
'first-user'
)
selectedId
=
$
(
dropdown
).
data
(
'selected'
)
$
(
dropdown
).
glDropdown
(
data
:
(
term
,
callback
)
=>
@
users
term
,
(
users
)
=>
if
term
.
length
is
0
showDivider
=
0
if
firstUser
# Move current user to the front of the list
for
obj
,
index
in
users
if
obj
.
username
==
firstUser
users
.
splice
(
index
,
1
)
users
.
unshift
(
obj
)
break
if
showNullUser
showDivider
+=
1
users
.
unshift
(
name
:
'Unassigned'
,
id
:
0
)
if
showAnyUser
showDivider
+=
1
name
=
showAnyUser
name
=
'Any User'
if
name
==
true
anyUser
=
{
name
:
name
,
id
:
null
}
users
.
unshift
(
anyUser
)
if
showDivider
users
.
splice
(
showDivider
,
0
,
"divider"
)
# Send the data back
callback
users
filterable
:
true
filterRemote
:
true
search
:
fields
:
[
'name'
,
'username'
]
selectable
:
true
fieldName
:
$
(
dropdown
).
data
(
'field-name'
)
clicked
:
->
if
$
(
dropdown
).
hasClass
"js-filter-submit"
$
(
dropdown
).
parents
(
'form'
).
submit
()
renderRow
:
(
user
)
->
username
=
if
user
.
username
then
"@
#{
user
.
username
}
"
else
""
avatar
=
if
user
.
avatar_url
then
user
.
avatar_url
else
false
selected
=
if
user
.
id
is
selectedId
then
"is-active"
else
""
img
=
""
if
avatar
img
=
"<img src='
#{
avatar
}
' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link
#{
selected
}
'>
#{
img
}
<strong class='dropdown-menu-user-full-name'>
#{
user
.
name
}
</strong>
<span class='dropdown-menu-user-username'>
#{
username
}
</span>
</a>
</li>"
)
$
(
'.ajax-users-select'
).
each
(
i
,
select
)
=>
@
projectId
=
$
(
select
).
data
(
'project-id'
)
@
groupId
=
$
(
select
).
data
(
'group-id'
)
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
e0800285
...
...
@@ -29,8 +29,8 @@
.dropdown-menu-toggle
{
position
:
relative
;
min-
width
:
160px
;
padding
:
5px
20px
5
px
10px
;
width
:
160px
;
padding
:
6px
20px
6
px
10px
;
background-color
:
$dropdown-toggle-bg
;
color
:
$dropdown-toggle-color
;
font-size
:
15px
;
...
...
@@ -65,7 +65,7 @@
position
:
absolute
;
top
:
100%
;
left
:
0
;
z-index
:
9
999
;
z-index
:
9
;
width
:
240px
;
margin-top
:
2px
;
margin-bottom
:
0
;
...
...
@@ -117,15 +117,19 @@
white-space
:
nowrap
;
overflow
:
hidden
;
&
:hover
{
&
:hover
,
&
:focus
,
&
.is-focused
{
background-color
:
$dropdown-link-hover-bg
;
text-decoration
:
none
;
outline
:
0
;
}
}
}
.dropdown-menu-paging
{
.dropdown-page-two
{
.dropdown-page-two
,
.dropdown-menu-back
{
display
:
none
;
}
...
...
@@ -134,7 +138,8 @@
display
:
none
;
}
.dropdown-page-two
{
.dropdown-page-two
,
.dropdown-menu-back
{
display
:
block
;
}
}
...
...
@@ -157,6 +162,7 @@
.dropdown-menu-user-full-name
{
display
:
block
;
margin-bottom
:
2px
;
font-weight
:
600
;
line-height
:
1
;
text-overflow
:
ellipsis
;
overflow
:
hidden
;
...
...
@@ -232,6 +238,7 @@
font-size
:
14px
;
border
:
0
;
background
:
none
;
outline
:
0
;
&
:hover
{
color
:
darken
(
$dropdown-title-btn-color
,
15%
);
...
...
@@ -298,6 +305,14 @@
border-top
:
1px
solid
$dropdown-divider-color
;
}
.dropdown-footer-list
{
font-size
:
14px
;
a
{