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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
de784ac1
Commit
de784ac1
authored
Mar 09, 2019
by
Hiroyuki Sato
Committed by
Nick Thomas
Mar 09, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Filter merge requests by target branch
parent
6908c5f7
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
204 additions
and
16 deletions
+204
-16
app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
...ts/filtered_search/add_extra_tokens_for_merge_requests.js
+12
-0
app/assets/javascripts/filtered_search/available_dropdown_mappings.js
...avascripts/filtered_search/available_dropdown_mappings.js
+31
-0
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+3
-10
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+11
-1
app/assets/stylesheets/framework/filters.scss
app/assets/stylesheets/framework/filters.scss
+10
-1
app/controllers/autocomplete_controller.rb
app/controllers/autocomplete_controller.rb
+8
-1
app/finders/merge_requests_finder.rb
app/finders/merge_requests_finder.rb
+1
-1
app/models/merge_request.rb
app/models/merge_request.rb
+16
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+5
-0
changelogs/unreleased/filter-merge-requests-by-target-branch.yml
...ogs/unreleased/filter-merge-requests-by-target-branch.yml
+5
-0
config/routes.rb
config/routes.rb
+1
-0
spec/controllers/autocomplete_controller_spec.rb
spec/controllers/autocomplete_controller_spec.rb
+31
-0
spec/features/merge_requests/user_filters_by_target_branch_spec.rb
...ures/merge_requests/user_filters_by_target_branch_spec.rb
+45
-0
spec/finders/merge_requests_finder_spec.rb
spec/finders/merge_requests_finder_spec.rb
+1
-1
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
...pts/filtered_search/filtered_search_visual_tokens_spec.js
+4
-0
spec/javascripts/helpers/filtered_search_spec_helper.js
spec/javascripts/helpers/filtered_search_spec_helper.js
+1
-1
spec/models/merge_request_spec.rb
spec/models/merge_request_spec.rb
+19
-0
No files found.
app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
View file @
de784ac1
...
...
@@ -13,4 +13,16 @@ export default IssuableTokenKeys => {
IssuableTokenKeys
.
tokenKeys
.
push
(
wipToken
);
IssuableTokenKeys
.
tokenKeysWithAlternative
.
push
(
wipToken
);
const
targetBranchToken
=
{
key
:
'
target-branch
'
,
type
:
'
string
'
,
param
:
''
,
symbol
:
''
,
icon
:
'
arrow-right
'
,
tag
:
'
branch
'
,
};
IssuableTokenKeys
.
tokenKeys
.
push
(
targetBranchToken
);
IssuableTokenKeys
.
tokenKeysWithAlternative
.
push
(
targetBranchToken
);
};
app/assets/javascripts/filtered_search/available_dropdown_mappings.js
View file @
de784ac1
...
...
@@ -5,6 +5,7 @@ import DropdownEmoji from './dropdown_emoji';
import
NullDropdown
from
'
./null_dropdown
'
;
import
DropdownAjaxFilter
from
'
./dropdown_ajax_filter
'
;
import
DropdownUtils
from
'
./dropdown_utils
'
;
import
{
mergeUrlParams
}
from
'
../lib/utils/url_utility
'
;
export
default
class
AvailableDropdownMappings
{
constructor
(
container
,
baseEndpoint
,
groupsOnly
,
includeAncestorGroups
,
includeDescendantGroups
)
{
...
...
@@ -13,6 +14,7 @@ export default class AvailableDropdownMappings {
this
.
groupsOnly
=
groupsOnly
;
this
.
includeAncestorGroups
=
includeAncestorGroups
;
this
.
includeDescendantGroups
=
includeDescendantGroups
;
this
.
filteredSearchInput
=
this
.
container
.
querySelector
(
'
.filtered-search
'
);
}
getAllowedMappings
(
supportedTokens
)
{
...
...
@@ -102,6 +104,15 @@ export default class AvailableDropdownMappings {
},
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-runner-tag
'
),
},
'
target-branch
'
:
{
reference
:
null
,
gl
:
DropdownNonUser
,
extraArguments
:
{
endpoint
:
this
.
getMergeRequestTargetBranchesEndpoint
(),
symbol
:
''
,
},
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-target-branch
'
),
},
};
}
...
...
@@ -130,4 +141,24 @@ export default class AvailableDropdownMappings {
getRunnerTagsEndpoint
()
{
return
`
${
this
.
baseEndpoint
}
/admin/runners/tag_list.json`
;
}
getMergeRequestTargetBranchesEndpoint
()
{
const
endpoint
=
`
${
gon
.
relative_url_root
||
''
}
/autocomplete/merge_request_target_branches.json`
;
const
params
=
{
group_id
:
this
.
getGroupId
(),
project_id
:
this
.
getProjectId
(),
};
return
mergeUrlParams
(
params
,
endpoint
);
}
getGroupId
()
{
return
this
.
filteredSearchInput
.
getAttribute
(
'
data-group-id
'
)
||
''
;
}
getProjectId
()
{
return
this
.
filteredSearchInput
.
getAttribute
(
'
data-project-id
'
)
||
''
;
}
}
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
de784ac1
...
...
@@ -504,14 +504,7 @@ export default class FilteredSearchManager {
const
match
=
this
.
filteredSearchTokenKeys
.
searchByKeyParam
(
keyParam
);
if
(
match
)
{
// Use lastIndexOf because the token key is allowed to contain underscore
// e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
const
lastIndexOf
=
keyParam
.
lastIndexOf
(
'
_
'
);
let
sanitizedKey
=
lastIndexOf
!==
-
1
?
keyParam
.
slice
(
0
,
lastIndexOf
)
:
keyParam
;
// Replace underscore with hyphen in the sanitizedkey.
// e.g. 'my_reaction' => 'my-reaction'
sanitizedKey
=
sanitizedKey
.
replace
(
'
_
'
,
'
-
'
);
const
{
symbol
}
=
match
;
const
{
key
,
symbol
}
=
match
;
let
quotationsToUse
=
''
;
if
(
sanitizedValue
.
indexOf
(
'
'
)
!==
-
1
)
{
...
...
@@ -520,10 +513,10 @@ export default class FilteredSearchManager {
}
hasFilteredSearch
=
true
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
sanitizedK
ey
,
sanitizedValue
);
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
k
ey
,
sanitizedValue
);
const
{
uppercaseTokenName
,
capitalizeTokenValue
}
=
match
;
FilteredSearchVisualTokens
.
addFilterVisualToken
(
sanitizedK
ey
,
k
ey
,
`
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
,
{
canEdit
,
...
...
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
View file @
de784ac1
...
...
@@ -69,11 +69,21 @@ export default class FilteredSearchVisualTokens {
}
static
addVisualTokenElement
(
name
,
value
,
options
=
{})
{
const
{
isSearchTerm
=
false
,
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
}
=
options
;
const
{
isSearchTerm
=
false
,
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
tokenClass
=
`search-token-
${
name
.
toLowerCase
()}
`
,
}
=
options
;
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
if
(
!
isSearchTerm
)
{
li
.
classList
.
add
(
tokenClass
);
}
if
(
value
)
{
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
({
canEdit
,
...
...
app/assets/stylesheets/framework/filters.scss
View file @
de784ac1
...
...
@@ -108,6 +108,8 @@
}
.value-container
{
display
:
flex
;
align-items
:
center
;
background-color
:
$white-normal
;
color
:
$filter-value-text-color
;
border-radius
:
0
2px
2px
0
;
...
...
@@ -121,7 +123,7 @@
.remove-token
{
display
:
inline-block
;
padding-left
:
4
px
;
padding-left
:
8
px
;
padding-right
:
0
;
.fa-close
{
...
...
@@ -412,3 +414,10 @@
padding
:
8px
16px
;
text-align
:
center
;
}
.search-token-target-branch
{
.value
{
font-family
:
$monospace-font
;
font-size
:
13px
;
}
}
app/controllers/autocomplete_controller.rb
View file @
de784ac1
# frozen_string_literal: true
class
AutocompleteController
<
ApplicationController
skip_before_action
:authenticate_user!
,
only:
[
:users
,
:award_emojis
]
skip_before_action
:authenticate_user!
,
only:
[
:users
,
:award_emojis
,
:merge_request_target_branches
]
def
users
project
=
Autocomplete
::
ProjectFinder
...
...
@@ -38,4 +38,11 @@ class AutocompleteController < ApplicationController
def
award_emojis
render
json:
AwardedEmojiFinder
.
new
(
current_user
).
execute
end
def
merge_request_target_branches
merge_requests
=
MergeRequestsFinder
.
new
(
current_user
,
params
).
execute
target_branches
=
merge_requests
.
recent_target_branches
render
json:
target_branches
.
map
{
|
target_branch
|
{
title:
target_branch
}
}
end
end
app/finders/merge_requests_finder.rb
View file @
de784ac1
...
...
@@ -29,7 +29,7 @@
#
class
MergeRequestsFinder
<
IssuableFinder
def
self
.
scalar_params
@scalar_params
||=
super
+
[
:wip
]
@scalar_params
||=
super
+
[
:wip
,
:target_branch
]
end
def
klass
...
...
app/models/merge_request.rb
View file @
de784ac1
...
...
@@ -203,6 +203,22 @@ class MergeRequest < ActiveRecord::Base
'!'
end
# Returns the top 100 target branches
#
# The returned value is a Array containing branch names
# sort by updated_at of merge request:
#
# ['master', 'develop', 'production']
#
# limit - The maximum number of target branch to return.
def
self
.
recent_target_branches
(
limit:
100
)
group
(
:target_branch
)
.
select
(
:target_branch
)
.
reorder
(
'MAX(merge_requests.updated_at) DESC'
)
.
limit
(
limit
)
.
pluck
(
:target_branch
)
end
def
rebase_in_progress?
strong_memoize
(
:rebase_in_progress
)
do
# The source project can be deleted
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
de784ac1
...
...
@@ -137,6 +137,11 @@
%li
.filter-dropdown-item
{
data:
{
value:
'no'
,
capitalize:
true
}
}
%button
.btn.btn-link
{
type:
'button'
}
=
_
(
'No'
)
#js-dropdown-target-branch
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link.js-data-value.monospace
{{title}}
=
render_if_exists
'shared/issuable/filter_weight'
,
type:
type
...
...
changelogs/unreleased/filter-merge-requests-by-target-branch.yml
0 → 100644
View file @
de784ac1
---
title
:
Add target branch filter to merge requests search bar
merge_request
:
24380
author
:
Hiroyuki Sato
type
:
added
config/routes.rb
View file @
de784ac1
...
...
@@ -43,6 +43,7 @@ Rails.application.routes.draw do
get
'/autocomplete/users/:id'
=>
'autocomplete#user'
get
'/autocomplete/projects'
=>
'autocomplete#projects'
get
'/autocomplete/award_emojis'
=>
'autocomplete#award_emojis'
get
'/autocomplete/merge_request_target_branches'
=>
'autocomplete#merge_request_target_branches'
# Search
get
'search'
=>
'search#show'
...
...
spec/controllers/autocomplete_controller_spec.rb
View file @
de784ac1
...
...
@@ -371,5 +371,36 @@ describe AutocompleteController do
expect
(
json_response
[
3
]).
to
match
(
'name'
=>
'thumbsdown'
)
end
end
context
'Get merge_request_target_branches'
do
let
(
:user2
)
{
create
(
:user
)
}
let!
(
:merge_request1
)
{
create
(
:merge_request
,
source_project:
project
,
target_branch:
'feature'
)
}
context
'unauthorized user'
do
it
'returns empty json'
do
get
:merge_request_target_branches
expect
(
json_response
).
to
be_empty
end
end
context
'sign in as user without any accesible merge requests'
do
it
'returns empty json'
do
sign_in
(
user2
)
get
:merge_request_target_branches
expect
(
json_response
).
to
be_empty
end
end
context
'sign in as user with a accesible merge request'
do
it
'returns json'
do
sign_in
(
user
)
get
:merge_request_target_branches
expect
(
json_response
).
to
contain_exactly
({
'title'
=>
'feature'
})
end
end
end
end
end
spec/features/merge_requests/user_filters_by_target_branch_spec.rb
0 → 100644
View file @
de784ac1
require
'rails_helper'
describe
'Merge Requests > User filters by target branch'
,
:js
do
include
FilteredSearchHelpers
let!
(
:project
)
{
create
(
:project
,
:public
,
:repository
)
}
let!
(
:user
)
{
project
.
creator
}
let!
(
:mr1
)
{
create
(
:merge_request
,
source_project:
project
,
target_project:
project
,
source_branch:
'feature'
,
target_branch:
'master'
)
}
let!
(
:mr2
)
{
create
(
:merge_request
,
source_project:
project
,
target_project:
project
,
source_branch:
'feature'
,
target_branch:
'merged-target'
)
}
before
do
sign_in
(
user
)
visit
project_merge_requests_path
(
project
)
end
context
'filtering by target-branch:master'
do
it
'applies the filter'
do
input_filtered_search
(
'target-branch:master'
)
expect
(
page
).
to
have_issuable_counts
(
open:
1
,
closed:
0
,
all:
1
)
expect
(
page
).
to
have_content
mr1
.
title
expect
(
page
).
not_to
have_content
mr2
.
title
end
end
context
'filtering by target-branch:merged-target'
do
it
'applies the filter'
do
input_filtered_search
(
'target-branch:merged-target'
)
expect
(
page
).
to
have_issuable_counts
(
open:
1
,
closed:
0
,
all:
1
)
expect
(
page
).
not_to
have_content
mr1
.
title
expect
(
page
).
to
have_content
mr2
.
title
end
end
context
'filtering by target-branch:feature'
do
it
'applies the filter'
do
input_filtered_search
(
'target-branch:feature'
)
expect
(
page
).
to
have_issuable_counts
(
open:
0
,
closed:
0
,
all:
0
)
expect
(
page
).
not_to
have_content
mr1
.
title
expect
(
page
).
not_to
have_content
mr2
.
title
end
end
end
spec/finders/merge_requests_finder_spec.rb
View file @
de784ac1
...
...
@@ -36,7 +36,7 @@ describe MergeRequestsFinder do
let
(
:project5
)
{
create_project_without_n_plus_1
(
group:
subgroup
)
}
let
(
:project6
)
{
create_project_without_n_plus_1
(
group:
subgroup
)
}
let!
(
:merge_request1
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project2
,
target_project:
project1
)
}
let!
(
:merge_request1
)
{
create
(
:merge_request
,
author:
user
,
source_project:
project2
,
target_project:
project1
,
target_branch:
'merged-target'
)
}
let!
(
:merge_request2
)
{
create
(
:merge_request
,
:conflict
,
author:
user
,
source_project:
project2
,
target_project:
project1
,
state:
'closed'
)
}
let!
(
:merge_request3
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project2
,
target_project:
project2
,
state:
'locked'
,
title:
'thing WIP thing'
)
}
let!
(
:merge_request4
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project3
,
target_project:
project3
,
title:
'WIP thing'
)
}
...
...
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
View file @
de784ac1
...
...
@@ -293,6 +293,7 @@ describe('Filtered Search Visual Tokens', () => {
subject
.
addVisualTokenElement
(
'
milestone
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
search-token-milestone
'
)).
toEqual
(
true
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
milestone
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
...
...
@@ -302,6 +303,7 @@ describe('Filtered Search Visual Tokens', () => {
subject
.
addVisualTokenElement
(
'
label
'
,
'
Frontend
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
search-token-label
'
)).
toEqual
(
true
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
Frontend
'
);
...
...
@@ -317,10 +319,12 @@ describe('Filtered Search Visual Tokens', () => {
const
labelToken
=
tokens
[
0
];
const
assigneeToken
=
tokens
[
1
];
expect
(
labelToken
.
classList
.
contains
(
'
search-token-label
'
)).
toEqual
(
true
);
expect
(
labelToken
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
labelToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
label
'
);
expect
(
labelToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
Frontend
'
);
expect
(
assigneeToken
.
classList
.
contains
(
'
search-token-assignee
'
)).
toEqual
(
true
);
expect
(
assigneeToken
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
assigneeToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
assignee
'
);
expect
(
assigneeToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
@root
'
);
...
...
spec/javascripts/helpers/filtered_search_spec_helper.js
View file @
de784ac1
...
...
@@ -5,7 +5,7 @@ export default class FilteredSearchSpecHelper {
static
createFilterVisualToken
(
name
,
value
,
isSelected
=
false
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
,
'
filtered-search-token
'
);
li
.
classList
.
add
(
'
js-visual-token
'
,
'
filtered-search-token
'
,
`search-token-
${
name
}
`
);
li
.
innerHTML
=
`
<div class="selectable
${
isSelected
?
'
selected
'
:
''
}
" role="button">
...
...
spec/models/merge_request_spec.rb
View file @
de784ac1
...
...
@@ -270,6 +270,25 @@ describe MergeRequest do
end
end
describe
'.recent_target_branches'
do
let
(
:project
)
{
create
(
:project
)
}
let!
(
:merge_request1
)
{
create
(
:merge_request
,
:opened
,
source_project:
project
,
target_branch:
'feature'
)
}
let!
(
:merge_request2
)
{
create
(
:merge_request
,
:closed
,
source_project:
project
,
target_branch:
'merge-test'
)
}
let!
(
:merge_request3
)
{
create
(
:merge_request
,
:opened
,
source_project:
project
,
target_branch:
'fix'
)
}
let!
(
:merge_request4
)
{
create
(
:merge_request
,
:closed
,
source_project:
project
,
target_branch:
'feature'
)
}
before
do
merge_request1
.
update_columns
(
updated_at:
1
.
day
.
since
)
merge_request2
.
update_columns
(
updated_at:
2
.
days
.
since
)
merge_request3
.
update_columns
(
updated_at:
3
.
days
.
since
)
merge_request4
.
update_columns
(
updated_at:
4
.
days
.
since
)
end
it
'returns target branches sort by updated at desc'
do
expect
(
described_class
.
recent_target_branches
).
to
match_array
([
'feature'
,
'merge-test'
,
'fix'
])
end
end
describe
'#target_branch_sha'
do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
...
...
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