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
b9f35f4e
Commit
b9f35f4e
authored
Oct 04, 2018
by
Rémy Coutable
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'ce-to-ee-2018-10-03' into 'master'
CE upstream - 2018-10-03 09:21 UTC See merge request gitlab-org/gitlab-ee!7781
parents
b74defff
cd5f6a8f
Changes
33
Hide whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
408 additions
and
138 deletions
+408
-138
app/assets/javascripts/filtered_search/dropdown_hint.js
app/assets/javascripts/filtered_search/dropdown_hint.js
+5
-1
app/assets/javascripts/filtered_search/dropdown_utils.js
app/assets/javascripts/filtered_search/dropdown_utils.js
+3
-1
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+14
-3
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+30
-10
app/assets/javascripts/filtered_search/filtered_search_token_keys.js
...javascripts/filtered_search/filtered_search_token_keys.js
+27
-0
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+43
-11
app/assets/javascripts/pages/groups/merge_requests/index.js
app/assets/javascripts/pages/groups/merge_requests/index.js
+2
-0
app/assets/javascripts/pages/projects/merge_requests/index/index.js
.../javascripts/pages/projects/merge_requests/index/index.js
+3
-0
app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
...idebar/components/time_tracking/sidebar_time_tracking.vue
+4
-4
app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
...scripts/sidebar/components/time_tracking/time_tracker.vue
+16
-30
app/assets/javascripts/sidebar/mount_milestone_sidebar.js
app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+6
-4
app/assets/stylesheets/pages/commits.scss
app/assets/stylesheets/pages/commits.scss
+1
-0
app/finders/merge_requests_finder.rb
app/finders/merge_requests_finder.rb
+21
-2
app/models/merge_request.rb
app/models/merge_request.rb
+1
-1
app/views/admin/applications/show.html.haml
app/views/admin/applications/show.html.haml
+14
-7
app/views/doorkeeper/applications/show.html.haml
app/views/doorkeeper/applications/show.html.haml
+13
-6
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+18
-10
changelogs/unreleased/add-clipboard-button-to-application-id-and-secret.yml
...sed/add-clipboard-button-to-application-id-and-secret.yml
+5
-0
changelogs/unreleased/ccr-wip_filter.yml
changelogs/unreleased/ccr-wip_filter.yml
+5
-0
doc/api/merge_requests.md
doc/api/merge_requests.md
+1
-0
doc/user/project/merge_requests/img/filter_wip_merge_requests.png
.../project/merge_requests/img/filter_wip_merge_requests.png
+0
-0
doc/user/project/merge_requests/work_in_progress_merge_requests.md
...project/merge_requests/work_in_progress_merge_requests.md
+9
-2
lib/api/merge_requests.rb
lib/api/merge_requests.rb
+1
-1
locale/gitlab.pot
locale/gitlab.pot
+8
-5
spec/features/admin/admin_manage_applications_spec.rb
spec/features/admin/admin_manage_applications_spec.rb
+2
-2
spec/features/issues/filtered_search/dropdown_hint_spec.rb
spec/features/issues/filtered_search/dropdown_hint_spec.rb
+18
-0
spec/features/profiles/user_manages_applications_spec.rb
spec/features/profiles/user_manages_applications_spec.rb
+2
-2
spec/finders/merge_requests_finder_spec.rb
spec/finders/merge_requests_finder_spec.rb
+70
-16
spec/javascripts/filtered_search/dropdown_utils_spec.js
spec/javascripts/filtered_search/dropdown_utils_spec.js
+3
-2
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
...pts/filtered_search/filtered_search_visual_tokens_spec.js
+6
-2
spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
...pts/sidebar/components/time_tracking/time_tracker_spec.js
+18
-15
spec/models/merge_request_spec.rb
spec/models/merge_request_spec.rb
+1
-1
spec/requests/api/merge_requests_spec.rb
spec/requests/api/merge_requests_spec.rb
+38
-0
No files found.
app/assets/javascripts/filtered_search/dropdown_hint.js
View file @
b9f35f4e
...
@@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown {
...
@@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown {
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
.
join
(
'
'
));
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
.
join
(
'
'
));
}
}
FilteredSearchDropdownManager
.
addWordToInput
(
token
.
replace
(
'
:
'
,
''
),
''
,
false
,
this
.
container
);
const
key
=
token
.
replace
(
'
:
'
,
''
);
const
{
uppercaseTokenName
}
=
this
.
tokenKeys
.
searchByKey
(
key
);
FilteredSearchDropdownManager
.
addWordToInput
(
key
,
''
,
false
,
{
uppercaseTokenName
,
});
}
}
this
.
dismissDropdown
();
this
.
dismissDropdown
();
this
.
dispatchInputEvent
();
this
.
dispatchInputEvent
();
...
...
app/assets/javascripts/filtered_search/dropdown_utils.js
View file @
b9f35f4e
...
@@ -143,7 +143,9 @@ export default class DropdownUtils {
...
@@ -143,7 +143,9 @@ export default class DropdownUtils {
const
dataValue
=
selected
.
getAttribute
(
'
data-value
'
);
const
dataValue
=
selected
.
getAttribute
(
'
data-value
'
);
if
(
dataValue
)
{
if
(
dataValue
)
{
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
,
true
);
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
,
true
,
{
capitalizeTokenValue
:
selected
.
hasAttribute
(
'
data-capitalize
'
),
});
}
}
// Return boolean based on whether it was set
// Return boolean based on whether it was set
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
b9f35f4e
...
@@ -92,6 +92,11 @@ export default class FilteredSearchDropdownManager {
...
@@ -92,6 +92,11 @@ export default class FilteredSearchDropdownManager {
gl
:
DropdownEmoji
,
gl
:
DropdownEmoji
,
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-my-reaction
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-my-reaction
'
),
},
},
wip
:
{
reference
:
null
,
gl
:
DropdownNonUser
,
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-wip
'
),
},
status
:
{
status
:
{
reference
:
null
,
reference
:
null
,
gl
:
NullDropdown
,
gl
:
NullDropdown
,
...
@@ -150,10 +155,16 @@ export default class FilteredSearchDropdownManager {
...
@@ -150,10 +155,16 @@ export default class FilteredSearchDropdownManager {
return
endpoint
;
return
endpoint
;
}
}
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
)
{
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
,
options
=
{})
{
const
{
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
,
}
=
options
;
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
,
{
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
);
uppercaseTokenName
,
capitalizeTokenValue
,
});
input
.
value
=
''
;
input
.
value
=
''
;
if
(
clicked
)
{
if
(
clicked
)
{
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
b9f35f4e
...
@@ -428,7 +428,10 @@ export default class FilteredSearchManager {
...
@@ -428,7 +428,10 @@ export default class FilteredSearchManager {
if
(
isLastVisualTokenValid
)
{
if
(
isLastVisualTokenValid
)
{
tokens
.
forEach
((
t
)
=>
{
tokens
.
forEach
((
t
)
=>
{
input
.
value
=
input
.
value
.
replace
(
`
${
t
.
key
}
:
${
t
.
symbol
}${
t
.
value
}
`
,
''
);
input
.
value
=
input
.
value
.
replace
(
`
${
t
.
key
}
:
${
t
.
symbol
}${
t
.
value
}
`
,
''
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
t
.
key
,
`
${
t
.
symbol
}${
t
.
value
}
`
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
t
.
key
,
`
${
t
.
symbol
}${
t
.
value
}
`
,
{
uppercaseTokenName
:
this
.
filteredSearchTokenKeys
.
shouldUppercaseTokenName
(
t
.
key
),
capitalizeTokenValue
:
this
.
filteredSearchTokenKeys
.
shouldCapitalizeTokenValue
(
t
.
key
),
});
});
});
const
fragments
=
searchToken
.
split
(
'
:
'
);
const
fragments
=
searchToken
.
split
(
'
:
'
);
...
@@ -444,7 +447,10 @@ export default class FilteredSearchManager {
...
@@ -444,7 +447,10 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
);
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
);
}
}
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenKey
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenKey
,
null
,
{
uppercaseTokenName
:
this
.
filteredSearchTokenKeys
.
shouldUppercaseTokenName
(
tokenKey
),
capitalizeTokenValue
:
this
.
filteredSearchTokenKeys
.
shouldCapitalizeTokenValue
(
tokenKey
),
});
input
.
value
=
input
.
value
.
replace
(
`
${
tokenKey
}
:`
,
''
);
input
.
value
=
input
.
value
.
replace
(
`
${
tokenKey
}
:`
,
''
);
}
}
}
else
{
}
else
{
...
@@ -452,7 +458,10 @@ export default class FilteredSearchManager {
...
@@ -452,7 +458,10 @@ export default class FilteredSearchManager {
const
valueCompletedRegex
=
/
([
~%@
]{0,1}
".+"
)
|
([
~%@
]{0,1}
'.+'
)
|^
((?![
~%@
]
'
)(?![
~%@
]
"
)(?!
'
)(?!
"
))
.*/g
;
const
valueCompletedRegex
=
/
([
~%@
]{0,1}
".+"
)
|
([
~%@
]{0,1}
'.+'
)
|^
((?![
~%@
]
'
)(?![
~%@
]
"
)(?!
'
)(?!
"
))
.*/g
;
if
(
searchToken
.
match
(
valueCompletedRegex
)
&&
input
.
value
[
input
.
value
.
length
-
1
]
===
'
'
)
{
if
(
searchToken
.
match
(
valueCompletedRegex
)
&&
input
.
value
[
input
.
value
.
length
-
1
]
===
'
'
)
{
FilteredSearchVisualTokens
.
addFilterVisualToken
(
searchToken
);
const
tokenKey
=
FilteredSearchVisualTokens
.
getLastTokenPartial
();
FilteredSearchVisualTokens
.
addFilterVisualToken
(
searchToken
,
null
,
{
capitalizeTokenValue
:
this
.
filteredSearchTokenKeys
.
shouldCapitalizeTokenValue
(
tokenKey
),
});
// Trim the last space as seen in the if statement above
// Trim the last space as seen in the if statement above
input
.
value
=
input
.
value
.
replace
(
searchToken
,
''
).
trim
();
input
.
value
=
input
.
value
.
replace
(
searchToken
,
''
).
trim
();
...
@@ -503,7 +512,7 @@ export default class FilteredSearchManager {
...
@@ -503,7 +512,7 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens
.
addFilterVisualToken
(
FilteredSearchVisualTokens
.
addFilterVisualToken
(
condition
.
tokenKey
,
condition
.
tokenKey
,
condition
.
value
,
condition
.
value
,
canEdit
,
{
canEdit
}
,
);
);
}
else
{
}
else
{
// Sanitize value since URL converts spaces into +
// Sanitize value since URL converts spaces into +
...
@@ -529,10 +538,15 @@ export default class FilteredSearchManager {
...
@@ -529,10 +538,15 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
hasFilteredSearch
=
true
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
sanitizedKey
,
sanitizedValue
);
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
sanitizedKey
,
sanitizedValue
);
const
{
uppercaseTokenName
,
capitalizeTokenValue
}
=
match
;
FilteredSearchVisualTokens
.
addFilterVisualToken
(
FilteredSearchVisualTokens
.
addFilterVisualToken
(
sanitizedKey
,
sanitizedKey
,
`
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
,
`
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
,
canEdit
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
},
);
);
}
else
if
(
!
match
&&
keyParam
===
'
assignee_id
'
)
{
}
else
if
(
!
match
&&
keyParam
===
'
assignee_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
const
id
=
parseInt
(
value
,
10
);
...
@@ -540,7 +554,7 @@ export default class FilteredSearchManager {
...
@@ -540,7 +554,7 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
hasFilteredSearch
=
true
;
const
tokenName
=
'
assignee
'
;
const
tokenName
=
'
assignee
'
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
canEdit
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
}
);
}
}
}
else
if
(
!
match
&&
keyParam
===
'
author_id
'
)
{
}
else
if
(
!
match
&&
keyParam
===
'
author_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
const
id
=
parseInt
(
value
,
10
);
...
@@ -548,7 +562,7 @@ export default class FilteredSearchManager {
...
@@ -548,7 +562,7 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
hasFilteredSearch
=
true
;
const
tokenName
=
'
author
'
;
const
tokenName
=
'
author
'
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
canEdit
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
}
);
}
}
}
else
if
(
!
match
&&
keyParam
===
'
search
'
)
{
}
else
if
(
!
match
&&
keyParam
===
'
search
'
)
{
hasFilteredSearch
=
true
;
hasFilteredSearch
=
true
;
...
@@ -584,15 +598,17 @@ export default class FilteredSearchManager {
...
@@ -584,15 +598,17 @@ export default class FilteredSearchManager {
this
.
saveCurrentSearchQuery
();
this
.
saveCurrentSearchQuery
();
const
{
tokens
,
searchToken
}
const
tokenKeys
=
this
.
filteredSearchTokenKeys
.
getKeys
();
=
this
.
tokenizer
.
processTokens
(
searchQuery
,
this
.
filteredSearchTokenKeys
.
getKeys
()
);
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
searchQuery
,
tokenKeys
);
const
currentState
=
state
||
getParameterByName
(
'
state
'
)
||
'
opened
'
;
const
currentState
=
state
||
getParameterByName
(
'
state
'
)
||
'
opened
'
;
paths
.
push
(
`state=
${
currentState
}
`
);
paths
.
push
(
`state=
${
currentState
}
`
);
tokens
.
forEach
((
token
)
=>
{
tokens
.
forEach
((
token
)
=>
{
const
condition
=
this
.
filteredSearchTokenKeys
const
condition
=
this
.
filteredSearchTokenKeys
.
searchByConditionKeyValue
(
token
.
key
,
token
.
value
.
toLowerCase
());
.
searchByConditionKeyValue
(
token
.
key
,
token
.
value
.
toLowerCase
());
const
{
param
}
=
this
.
filteredSearchTokenKeys
.
searchByKey
(
token
.
key
)
||
{};
const
tokenConfig
=
this
.
filteredSearchTokenKeys
.
searchByKey
(
token
.
key
)
||
{};
const
{
param
}
=
tokenConfig
;
// Replace hyphen with underscore to use as request parameter
// Replace hyphen with underscore to use as request parameter
// e.g. 'my-reaction' => 'my_reaction'
// e.g. 'my-reaction' => 'my_reaction'
const
underscoredKey
=
token
.
key
.
replace
(
'
-
'
,
'
_
'
);
const
underscoredKey
=
token
.
key
.
replace
(
'
-
'
,
'
_
'
);
...
@@ -604,6 +620,10 @@ export default class FilteredSearchManager {
...
@@ -604,6 +620,10 @@ export default class FilteredSearchManager {
}
else
{
}
else
{
let
tokenValue
=
token
.
value
;
let
tokenValue
=
token
.
value
;
if
(
tokenConfig
.
lowercaseValueOnSubmit
)
{
tokenValue
=
tokenValue
.
toLowerCase
();
}
if
((
tokenValue
[
0
]
===
'
\'
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
\'
'
)
||
if
((
tokenValue
[
0
]
===
'
\'
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
\'
'
)
||
(
tokenValue
[
0
]
===
'
"
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
"
'
))
{
(
tokenValue
[
0
]
===
'
"
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
"
'
))
{
tokenValue
=
tokenValue
.
slice
(
1
,
tokenValue
.
length
-
1
);
tokenValue
=
tokenValue
.
slice
(
1
,
tokenValue
.
length
-
1
);
...
...
app/assets/javascripts/filtered_search/filtered_search_token_keys.js
View file @
b9f35f4e
...
@@ -23,6 +23,16 @@ export default class FilteredSearchTokenKeys {
...
@@ -23,6 +23,16 @@ export default class FilteredSearchTokenKeys {
return
this
.
conditions
;
return
this
.
conditions
;
}
}
shouldUppercaseTokenName
(
tokenKey
)
{
const
token
=
this
.
searchByKey
(
tokenKey
.
toLowerCase
());
return
token
&&
token
.
uppercaseTokenName
;
}
shouldCapitalizeTokenValue
(
tokenKey
)
{
const
token
=
this
.
searchByKey
(
tokenKey
.
toLowerCase
());
return
token
&&
token
.
capitalizeTokenValue
;
}
searchByKey
(
key
)
{
searchByKey
(
key
)
{
return
this
.
tokenKeys
.
find
(
tokenKey
=>
tokenKey
.
key
===
key
)
||
null
;
return
this
.
tokenKeys
.
find
(
tokenKey
=>
tokenKey
.
key
===
key
)
||
null
;
}
}
...
@@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys {
...
@@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys {
return
this
.
conditions
return
this
.
conditions
.
find
(
condition
=>
condition
.
tokenKey
===
key
&&
condition
.
value
===
value
)
||
null
;
.
find
(
condition
=>
condition
.
tokenKey
===
key
&&
condition
.
value
===
value
)
||
null
;
}
}
addExtraTokensForMergeRequests
()
{
const
wipToken
=
{
key
:
'
wip
'
,
type
:
'
string
'
,
param
:
''
,
symbol
:
''
,
icon
:
'
admin
'
,
tag
:
'
Yes or No
'
,
lowercaseValueOnSubmit
:
true
,
uppercaseTokenName
:
true
,
capitalizeTokenValue
:
true
,
};
this
.
tokenKeys
.
push
(
wipToken
);
this
.
tokenKeysWithAlternative
.
push
(
wipToken
);
}
}
}
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
View file @
b9f35f4e
...
@@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens {
...
@@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens {
}
}
}
}
static
createVisualTokenElementHTML
(
canEdit
=
true
)
{
static
createVisualTokenElementHTML
(
options
=
{})
{
const
{
canEdit
=
true
,
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
,
}
=
options
;
return
`
return
`
<div class="
${
canEdit
?
'
selectable
'
:
'
hidden
'
}
" role="button">
<div class="
${
canEdit
?
'
selectable
'
:
'
hidden
'
}
" role="button">
<div class="name"></div>
<div class="
${
uppercaseTokenName
?
'
text-uppercase
'
:
''
}
name"></div>
<div class="value-container">
<div class="value-container">
<div class="value"></div>
<div class="
${
capitalizeTokenValue
?
'
text-capitalize
'
:
''
}
value"></div>
<div class="remove-token" role="button">
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
<i class="fa fa-close"></i>
</div>
</div>
...
@@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens {
...
@@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens {
}
}
}
}
static
addVisualTokenElement
(
name
,
value
,
isSearchTerm
,
canEdit
)
{
static
addVisualTokenElement
(
name
,
value
,
options
=
{})
{
const
{
isSearchTerm
=
false
,
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
}
=
options
;
const
li
=
document
.
createElement
(
'
li
'
);
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
if
(
value
)
{
if
(
value
)
{
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
(
canEdit
);
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
({
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
FilteredSearchVisualTokens
.
renderVisualTokenValue
(
li
,
name
,
value
);
FilteredSearchVisualTokens
.
renderVisualTokenValue
(
li
,
name
,
value
);
}
else
{
}
else
{
li
.
innerHTML
=
'
<div class="name"></div>
'
;
li
.
innerHTML
=
`<div class="
${
uppercaseTokenName
?
'
text-uppercase
'
:
''
}
name"></div>`
;
}
}
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
...
@@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens {
...
@@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens {
}
}
}
}
static
addFilterVisualToken
(
tokenName
,
tokenValue
,
canEdit
)
{
static
addFilterVisualToken
(
tokenName
,
tokenValue
,
{
canEdit
,
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
,
}
=
{})
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
{
addVisualTokenElement
}
=
FilteredSearchVisualTokens
;
const
{
addVisualTokenElement
}
=
FilteredSearchVisualTokens
;
if
(
isLastVisualTokenValid
)
{
if
(
isLastVisualTokenValid
)
{
addVisualTokenElement
(
tokenName
,
tokenValue
,
false
,
canEdit
);
addVisualTokenElement
(
tokenName
,
tokenValue
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
}
else
{
}
else
{
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
tokensContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
const
tokensContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
tokensContainer
.
removeChild
(
lastVisualToken
);
tokensContainer
.
removeChild
(
lastVisualToken
);
const
value
=
tokenValue
||
tokenName
;
const
value
=
tokenValue
||
tokenName
;
addVisualTokenElement
(
previousTokenName
,
value
,
false
,
canEdit
);
addVisualTokenElement
(
previousTokenName
,
value
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
}
}
}
}
...
@@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens {
...
@@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens {
if
(
lastVisualToken
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-term
'
))
{
if
(
lastVisualToken
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-term
'
))
{
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
+=
`
${
searchTerm
}
`
;
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
+=
`
${
searchTerm
}
`
;
}
else
{
}
else
{
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
true
);
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
{
isSearchTerm
:
true
,
});
}
}
}
}
...
@@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens {
...
@@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens {
let
value
;
let
value
;
if
(
token
.
classList
.
contains
(
'
filtered-search-token
'
))
{
if
(
token
.
classList
.
contains
(
'
filtered-search-token
'
))
{
FilteredSearchVisualTokens
.
addFilterVisualToken
(
nameElement
.
innerText
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
nameElement
.
innerText
,
null
,
{
uppercaseTokenName
:
nameElement
.
classList
.
contains
(
'
text-uppercase
'
),
});
const
valueContainerElement
=
token
.
querySelector
(
'
.value-container
'
);
const
valueContainerElement
=
token
.
querySelector
(
'
.value-container
'
);
value
=
valueContainerElement
.
dataset
.
originalValue
;
value
=
valueContainerElement
.
dataset
.
originalValue
;
...
...
app/assets/javascripts/pages/groups/merge_requests/index.js
View file @
b9f35f4e
...
@@ -4,6 +4,8 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
...
@@ -4,6 +4,8 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import
{
FILTERED_SEARCH
}
from
'
~/pages/constants
'
;
import
{
FILTERED_SEARCH
}
from
'
~/pages/constants
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
IssuableFilteredSearchTokenKeys
.
addExtraTokensForMergeRequests
();
initFilteredSearch
({
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
isGroupDecendent
:
true
,
isGroupDecendent
:
true
,
...
...
app/assets/javascripts/pages/projects/merge_requests/index/index.js
View file @
b9f35f4e
...
@@ -7,10 +7,13 @@ import { FILTERED_SEARCH } from '~/pages/constants';
...
@@ -7,10 +7,13 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import
{
ISSUABLE_INDEX
}
from
'
~/pages/projects/constants
'
;
import
{
ISSUABLE_INDEX
}
from
'
~/pages/projects/constants
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
IssuableFilteredSearchTokenKeys
.
addExtraTokensForMergeRequests
();
initFilteredSearch
({
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
filteredSearchTokenKeys
:
IssuableFilteredSearchTokenKeys
,
filteredSearchTokenKeys
:
IssuableFilteredSearchTokenKeys
,
});
});
new
IssuableIndex
(
ISSUABLE_INDEX
.
MERGE_REQUEST
);
// eslint-disable-line no-new
new
IssuableIndex
(
ISSUABLE_INDEX
.
MERGE_REQUEST
);
// eslint-disable-line no-new
new
ShortcutsNavigation
();
// eslint-disable-line no-new
new
ShortcutsNavigation
();
// eslint-disable-line no-new
new
UsersSelect
();
// eslint-disable-line no-new
new
UsersSelect
();
// eslint-disable-line no-new
...
...
app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
View file @
b9f35f4e
...
@@ -51,10 +51,10 @@ export default {
...
@@ -51,10 +51,10 @@ export default {
<
template
>
<
template
>
<div
class=
"block"
>
<div
class=
"block"
>
<issuable-time-tracker
<issuable-time-tracker
:time
_
estimate=
"store.timeEstimate"
:time
-
estimate=
"store.timeEstimate"
:time
_
spent=
"store.totalTimeSpent"
:time
-
spent=
"store.totalTimeSpent"
:human
_time_
estimate=
"store.humanTimeEstimate"
:human
-time-
estimate=
"store.humanTimeEstimate"
:human
_time_
spent=
"store.humanTotalTimeSpent"
:human
-time-
spent=
"store.humanTotalTimeSpent"
:root-path=
"store.rootPath"
:root-path=
"store.rootPath"
/>
/>
</div>
</div>
...
...
app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
View file @
b9f35f4e
...
@@ -19,24 +19,20 @@ export default {
...
@@ -19,24 +19,20 @@ export default {
TimeTrackingHelpState
,
TimeTrackingHelpState
,
},
},
props
:
{
props
:
{
// eslint-disable-next-line vue/prop-name-casing
timeEstimate
:
{
time_estimate
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
},
},
// eslint-disable-next-line vue/prop-name-casing
timeSpent
:
{
time_spent
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
},
},
// eslint-disable-next-line vue/prop-name-casing
humanTimeEstimate
:
{
human_time_estimate
:
{
type
:
String
,
type
:
String
,
required
:
false
,
required
:
false
,
default
:
''
,
default
:
''
,
},
},
// eslint-disable-next-line vue/prop-name-casing
humanTimeSpent
:
{
human_time_spent
:
{
type
:
String
,
type
:
String
,
required
:
false
,
required
:
false
,
default
:
''
,
default
:
''
,
...
@@ -52,18 +48,6 @@ export default {
...
@@ -52,18 +48,6 @@ export default {
};
};
},
},
computed
:
{
computed
:
{
timeSpent
()
{
return
this
.
time_spent
;
},
timeEstimate
()
{
return
this
.
time_estimate
;
},
timeEstimateHumanReadable
()
{
return
this
.
human_time_estimate
;
},
timeSpentHumanReadable
()
{
return
this
.
human_time_spent
;
},
hasTimeSpent
()
{
hasTimeSpent
()
{
return
!!
this
.
timeSpent
;
return
!!
this
.
timeSpent
;
},
},
...
@@ -94,10 +78,12 @@ export default {
...
@@ -94,10 +78,12 @@ export default {
this
.
showHelp
=
show
;
this
.
showHelp
=
show
;
},
},
update
(
data
)
{
update
(
data
)
{
this
.
time_estimate
=
data
.
time_estimate
;
const
{
timeEstimate
,
timeSpent
,
humanTimeEstimate
,
humanTimeSpent
}
=
data
;
this
.
time_spent
=
data
.
time_spent
;
this
.
human_time_estimate
=
data
.
human_time_estimate
;
this
.
timeEstimate
=
timeEstimate
;
this
.
human_time_spent
=
data
.
human_time_spent
;
this
.
timeSpent
=
timeSpent
;
this
.
humanTimeEstimate
=
humanTimeEstimate
;
this
.
humanTimeSpent
=
humanTimeSpent
;
},
},
},
},
};
};
...
@@ -114,8 +100,8 @@ export default {
...
@@ -114,8 +100,8 @@ export default {
:show-help-state=
"showHelpState"
:show-help-state=
"showHelpState"
:show-spent-only-state=
"showSpentOnlyState"
:show-spent-only-state=
"showSpentOnlyState"
:show-estimate-only-state=
"showEstimateOnlyState"
:show-estimate-only-state=
"showEstimateOnlyState"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-spent-human-readable=
"
humanTimeSpent
"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
/>
<div
class=
"title hide-collapsed"
>
<div
class=
"title hide-collapsed"
>
{{
__
(
'
Time tracking
'
)
}}
{{
__
(
'
Time tracking
'
)
}}
...
@@ -145,11 +131,11 @@ export default {
...
@@ -145,11 +131,11 @@ export default {
<div
class=
"time-tracking-content hide-collapsed"
>
<div
class=
"time-tracking-content hide-collapsed"
>
<time-tracking-estimate-only-pane
<time-tracking-estimate-only-pane
v-if=
"showEstimateOnlyState"
v-if=
"showEstimateOnlyState"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
/>
<time-tracking-spent-only-pane
<time-tracking-spent-only-pane
v-if=
"showSpentOnlyState"
v-if=
"showSpentOnlyState"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-spent-human-readable=
"
humanTimeSpent
"
/>
/>
<time-tracking-no-tracking-pane
<time-tracking-no-tracking-pane
v-if=
"showNoTimeTrackingState"
v-if=
"showNoTimeTrackingState"
...
@@ -158,8 +144,8 @@ export default {
...
@@ -158,8 +144,8 @@ export default {
v-if=
"showComparisonState"
v-if=
"showComparisonState"
:time-estimate=
"timeEstimate"
:time-estimate=
"timeEstimate"
:time-spent=
"timeSpent"
:time-spent=
"timeSpent"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-spent-human-readable=
"
humanTimeSpent
"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
/>
<transition
name=
"help-state-toggle"
>
<transition
name=
"help-state-toggle"
>
<time-tracking-help-state
<time-tracking-help-state
...
...
app/assets/javascripts/sidebar/mount_milestone_sidebar.js
View file @
b9f35f4e
...
@@ -7,6 +7,8 @@ export default class SidebarMilestone {
...
@@ -7,6 +7,8 @@ export default class SidebarMilestone {
if
(
!
el
)
return
;
if
(
!
el
)
return
;
const
{
timeEstimate
,
timeSpent
,
humanTimeEstimate
,
humanTimeSpent
}
=
el
.
dataset
;
// eslint-disable-next-line no-new
// eslint-disable-next-line no-new
new
Vue
({
new
Vue
({
el
,
el
,
...
@@ -15,10 +17,10 @@ export default class SidebarMilestone {
...
@@ -15,10 +17,10 @@ export default class SidebarMilestone {
},
},
render
:
createElement
=>
createElement
(
'
timeTracker
'
,
{
render
:
createElement
=>
createElement
(
'
timeTracker
'
,
{
props
:
{
props
:
{
time
_estimate
:
parseInt
(
el
.
dataset
.
timeEstimate
,
10
),
time
Estimate
:
parseInt
(
timeEstimate
,
10
),
time
_spent
:
parseInt
(
el
.
dataset
.
timeSpent
,
10
),
time
Spent
:
parseInt
(
timeSpent
,
10
),
human
_time_estimate
:
el
.
dataset
.
human
TimeEstimate
,
humanTimeEstimate
,
human
_time_spent
:
el
.
dataset
.
human
TimeSpent
,
humanTimeSpent
,
rootPath
:
'
/
'
,
rootPath
:
'
/
'
,
},
},
}),
}),
...
...
app/assets/stylesheets/pages/commits.scss
View file @
b9f35f4e
...
@@ -223,6 +223,7 @@
...
@@ -223,6 +223,7 @@
}
}
}
}
.clipboard-group
,
.commit-sha-group
{
.commit-sha-group
{
display
:
inline-flex
;
display
:
inline-flex
;
...
...
app/finders/merge_requests_finder.rb
View file @
b9f35f4e
...
@@ -27,13 +27,17 @@
...
@@ -27,13 +27,17 @@
# updated_before: datetime
# updated_before: datetime
#
#
class
MergeRequestsFinder
<
IssuableFinder
class
MergeRequestsFinder
<
IssuableFinder
def
self
.
scalar_params
@scalar_params
||=
super
+
[
:wip
]
end
def
klass
def
klass
MergeRequest
MergeRequest
end
end
def
filter_items
(
_items
)
def
filter_items
(
_items
)
items
=
by_source_branch
(
super
)
items
=
by_source_branch
(
super
)
items
=
by_wip
(
items
)
by_target_branch
(
items
)
by_target_branch
(
items
)
end
end
...
@@ -61,5 +65,20 @@ class MergeRequestsFinder < IssuableFinder
...
@@ -61,5 +65,20 @@ class MergeRequestsFinder < IssuableFinder
items
.
where
(
target_branch:
target_branch
)
items
.
where
(
target_branch:
target_branch
)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def
by_wip
(
items
)
if
params
[
:wip
]
==
'yes'
items
.
where
(
wip_match
(
items
.
arel_table
))
elsif
params
[
:wip
]
==
'no'
items
.
where
.
not
(
wip_match
(
items
.
arel_table
))
else
items
end
end
def
wip_match
(
table
)
table
[
:title
].
matches
(
'WIP:%'
)
.
or
(
table
[
:title
].
matches
(
'WIP %'
))
.
or
(
table
[
:title
].
matches
(
'[WIP]%'
))
end
end
end
app/models/merge_request.rb
View file @
b9f35f4e
...
@@ -265,7 +265,7 @@ class MergeRequest < ActiveRecord::Base
...
@@ -265,7 +265,7 @@ class MergeRequest < ActiveRecord::Base
end
end
end
end
WIP_REGEX
=
/\A
\s
*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
.
freeze
WIP_REGEX
=
/\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
.
freeze
def
self
.
work_in_progress?
(
title
)
def
self
.
work_in_progress?
(
title
)
!!
(
title
=~
WIP_REGEX
)
!!
(
title
=~
WIP_REGEX
)
...
...
app/views/admin/applications/show.html.haml
View file @
b9f35f4e
-
page_title
@application
.
name
,
"Applications"
-
page_title
@application
.
name
,
"Applications"
%h3
.page-title
%h3
.page-title
Application:
#{
@application
.
name
}
Application:
#{
@application
.
name
}
...
@@ -6,23 +7,29 @@
...
@@ -6,23 +7,29 @@
%table
.table
%table
.table
%tr
%tr
%td
%td
Application Id
=
_
(
'Application ID'
)
%td
%td
%code
#application_id
=
@application
.
uid
.clipboard-group
.input-group
%input
.label.label-monospace
{
id:
"application_id"
,
type:
"text"
,
autocomplete:
'off'
,
value:
@application
.
uid
,
readonly:
true
}
.input-group-append
=
clipboard_button
(
target:
'#application_id'
,
title:
_
(
"Copy ID to clipboard"
),
class:
"btn btn btn-default"
)
%tr
%tr
%td
%td
Secret:
=
_
(
'Secret'
)
%td
%td
%code
#secret
=
@application
.
secret
.clipboard-group
.input-group
%input
.label.label-monospace
{
id:
"secret"
,
type:
"text"
,
autocomplete:
'off'
,
value:
@application
.
secret
,
readonly:
true
}
.input-group-append
=
clipboard_button
(
target:
'#application_id'
,
title:
_
(
"Copy secret to clipboard"
),
class:
"btn btn btn-default"
)
%tr
%tr
%td
%td
Callback url
=
_
(
'Callback URL'
)
%td
%td
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
%div
%div
%span
.monospace
=
uri
%span
.monospace
=
uri
%tr
%tr
%td
%td
Trusted
Trusted
...
...
app/views/doorkeeper/applications/show.html.haml
View file @
b9f35f4e
...
@@ -10,18 +10,25 @@
...
@@ -10,18 +10,25 @@
%table
.table
%table
.table
%tr
%tr
%td
%td
=
_
(
'Application I
d
'
)
=
_
(
'Application I
D
'
)
%td
%td
%code
#application_id
=
@application
.
uid
.clipboard-group
.input-group
%input
.label.label-monospace
{
id:
"application_id"
,
type:
"text"
,
autocomplete:
'off'
,
value:
@application
.
uid
,
readonly:
true
}
.input-group-append
=
clipboard_button
(
target:
'#application_id'
,
title:
_
(
"Copy ID to clipboard"
),
class:
"btn btn btn-default"
)
%tr
%tr
%td
%td
=
_
(
'Secret
:
'
)
=
_
(
'Secret'
)
%td
%td
%code
#secret
=
@application
.
secret
.clipboard-group
.input-group
%input
.label.label-monospace
{
id:
"secret"
,
type:
"text"
,
autocomplete:
'off'
,
value:
@application
.
secret
,
readonly:
true
}
.input-group-append
=
clipboard_button
(
target:
'#application_id'
,
title:
_
(
"Copy secret to clipboard"
),
class:
"btn btn btn-default"
)
%tr
%tr
%td
%td
=
_
(
'Callback
url
'
)
=
_
(
'Callback
URL
'
)
%td
%td
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
%div
%div
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
b9f35f4e
...
@@ -33,13 +33,13 @@
...
@@ -33,13 +33,13 @@
#js-dropdown-hint
.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
#js-dropdown-hint
.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul
{
data:
{
dropdown:
true
}
}
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
action:
'submit'
}
}
%li
.filter-dropdown-item
{
data:
{
action:
'submit'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
=
sprite_icon
(
'search'
)
=
sprite_icon
(
'search'
)
%span
%span
Press Enter or click to search
Press Enter or click to search
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
-# haml lint's ClassAttributeWithStaticValue
%svg
%svg
...
@@ -60,7 +60,7 @@
...
@@ -60,7 +60,7 @@
#js-dropdown-assignee
.filtered-search-input-dropdown-menu.dropdown-menu
#js-dropdown-assignee
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Assignee
No Assignee
%li
.divider.droplab-item-ignore
%li
.divider.droplab-item-ignore
-
if
current_user
-
if
current_user
...
@@ -73,38 +73,46 @@
...
@@ -73,38 +73,46 @@
#js-dropdown-milestone
.filtered-search-input-dropdown-menu.dropdown-menu
#js-dropdown-milestone
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Milestone
No Milestone
%li
.filter-dropdown-item
{
data:
{
value:
'upcoming'
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'upcoming'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
Upcoming
Upcoming
%li
.filter-dropdown-item
{
'data-value'
=>
'started'
}
%li
.filter-dropdown-item
{
'data-value'
=>
'started'
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
Started
Started
%li
.divider.droplab-item-ignore
%li
.divider.droplab-item-ignore
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%li
.filter-dropdown-item
%button
.btn.btn-link.js-data-value
%button
.btn.btn-link.js-data-value
{
type:
'button'
}
{{title}}
{{title}}
#js-dropdown-label
.filtered-search-input-dropdown-menu.dropdown-menu
#js-dropdown-label
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Label
No Label
%li
.divider.droplab-item-ignore
%li
.divider.droplab-item-ignore
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
%span
.dropdown-label-box
{
style:
'
background:
{{
color
}}
'
}
%span
.dropdown-label-box
{
style:
'
background:
{{
color
}}
'
}
%span
.label-title.js-data-value
%span
.label-title.js-data-value
{{title}}
{{title}}
#js-dropdown-my-reaction
.filtered-search-input-dropdown-menu.dropdown-menu
#js-dropdown-my-reaction
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
%gl-emoji
%gl-emoji
%span
.js-data-value.prepend-left-10
%span
.js-data-value.prepend-left-10
{{name}}
{{name}}
#js-dropdown-wip
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
.filter-dropdown
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'yes'
,
capitalize:
true
}
}
%button
.btn.btn-link
{
type:
'button'
}
=
_
(
'Yes'
)
%li
.filter-dropdown-item
{
data:
{
value:
'no'
,
capitalize:
true
}
}
%button
.btn.btn-link
{
type:
'button'
}
=
_
(
'No'
)
=
render_if_exists
'shared/issuable/filter_weight'
,
type:
type
=
render_if_exists
'shared/issuable/filter_weight'
,
type:
type
...
...
changelogs/unreleased/add-clipboard-button-to-application-id-and-secret.yml
0 → 100644
View file @
b9f35f4e
---
title
:
Add copy to clipboard button for application id and secret
merge_request
:
21978
author
:
George Tsiolis
type
:
other
changelogs/unreleased/ccr-wip_filter.yml
0 → 100644
View file @
b9f35f4e
---
title
:
Added search functionality for Work In Progress (WIP) merge requests
merge_request
:
18119
author
:
Chantal Rollison
type
:
added
doc/api/merge_requests.md
View file @
b9f35f4e
...
@@ -47,6 +47,7 @@ Parameters:
...
@@ -47,6 +47,7 @@ Parameters:
|
`source_branch`
| string | no | Return merge requests with the given source branch |
|
`source_branch`
| string | no | Return merge requests with the given source branch |
|
`target_branch`
| string | no | Return merge requests with the given target branch |
|
`target_branch`
| string | no | Return merge requests with the given target branch |
|
`search`
| string | no | Search merge requests against their
`title`
and
`description`
|
|
`search`
| string | no | Search merge requests against their
`title`
and
`description`
|
|
`wip`
| string | no | Filter merge requests against their
`wip`
status.
`yes`
to return
*only*
WIP merge requests,
`no`
to return
*non*
WIP merge requests |
```
json
```
json
[
[
...
...
doc/user/project/merge_requests/img/filter_wip_merge_requests.png
0 → 100644
View file @
b9f35f4e
16.9 KB
doc/user/project/merge_requests/work_in_progress_merge_requests.md
View file @
b9f35f4e
...
@@ -7,7 +7,7 @@ have been marked a **Work In Progress**.
...
@@ -7,7 +7,7 @@ have been marked a **Work In Progress**.
![
Blocked Accept Button
](
img/wip_blocked_accept_button.png
)
![
Blocked Accept Button
](
img/wip_blocked_accept_button.png
)
To mark a merge request a Work In Progress, simply start its title with
`[WIP]`
To mark a merge request a Work In Progress, simply start its title with
`[WIP]`
or
`WIP:`
. As an alternative, you're also able to do it by sending a commit
or
`WIP:`
. As an alternative, you're also able to do it by sending a commit
with its title starting with
`wip`
or
`WIP`
to the merge request's source branch.
with its title starting with
`wip`
or
`WIP`
to the merge request's source branch.
![
Mark as WIP
](
img/wip_mark_as_wip.png
)
![
Mark as WIP
](
img/wip_mark_as_wip.png
)
...
@@ -15,4 +15,11 @@ with its title starting with `wip` or `WIP` to the merge request's source branch
...
@@ -15,4 +15,11 @@ with its title starting with `wip` or `WIP` to the merge request's source branch
To allow a Work In Progress merge request to be accepted again when it's ready,
To allow a Work In Progress merge request to be accepted again when it's ready,
simply remove the
`WIP`
prefix.
simply remove the
`WIP`
prefix.
![
Unark as WIP
](
img/wip_unmark_as_wip.png
)
![
Unmark as WIP
](
img/wip_unmark_as_wip.png
)
## Filtering merge requests with WIP Status
To filter merge requests with the
`WIP`
status, you can type
`wip`
and select the value for your filter from the merge request search input.
![
Filter WIP MRs
](
img/filter_wip_merge_requests.png
)
lib/api/merge_requests.rb
View file @
b9f35f4e
...
@@ -35,7 +35,6 @@ module API
...
@@ -35,7 +35,6 @@ module API
# rubocop: disable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def
find_merge_requests
(
args
=
{})
def
find_merge_requests
(
args
=
{})
args
=
declared_params
.
merge
(
args
)
args
=
declared_params
.
merge
(
args
)
args
[
:milestone_title
]
=
args
.
delete
(
:milestone
)
args
[
:milestone_title
]
=
args
.
delete
(
:milestone
)
args
[
:label_name
]
=
args
.
delete
(
:labels
)
args
[
:label_name
]
=
args
.
delete
(
:labels
)
args
[
:scope
]
=
args
[
:scope
].
underscore
if
args
[
:scope
]
args
[
:scope
]
=
args
[
:scope
].
underscore
if
args
[
:scope
]
...
@@ -99,6 +98,7 @@ module API
...
@@ -99,6 +98,7 @@ module API
optional
:source_branch
,
type:
String
,
desc:
'Return merge requests with the given source branch'
optional
:source_branch
,
type:
String
,
desc:
'Return merge requests with the given source branch'
optional
:target_branch
,
type:
String
,
desc:
'Return merge requests with the given target branch'
optional
:target_branch
,
type:
String
,
desc:
'Return merge requests with the given target branch'
optional
:search
,
type:
String
,
desc:
'Search merge requests for text present in the title or description'
optional
:search
,
type:
String
,
desc:
'Search merge requests for text present in the title or description'
optional
:wip
,
type:
String
,
values:
%w[yes no]
,
desc:
'Search merge requests for WIP in the title'
use
:pagination
use
:pagination
end
end
end
end
...
...
locale/gitlab.pot
View file @
b9f35f4e
...
@@ -703,7 +703,7 @@ msgstr ""
...
@@ -703,7 +703,7 @@ msgstr ""
msgid "Application"
msgid "Application"
msgstr ""
msgstr ""
msgid "Application I
d
"
msgid "Application I
D
"
msgstr ""
msgstr ""
msgid "Application: %{name}"
msgid "Application: %{name}"
...
@@ -1320,9 +1320,6 @@ msgstr ""
...
@@ -1320,9 +1320,6 @@ msgstr ""
msgid "Callback URL"
msgid "Callback URL"
msgstr ""
msgstr ""
msgid "Callback url"
msgstr ""
msgid "Can't find HEAD commit for this branch"
msgid "Can't find HEAD commit for this branch"
msgstr ""
msgstr ""
...
@@ -2233,6 +2230,9 @@ msgstr ""
...
@@ -2233,6 +2230,9 @@ msgstr ""
msgid "Copy HTTPS clone URL"
msgid "Copy HTTPS clone URL"
msgstr ""
msgstr ""
msgid "Copy ID to clipboard"
msgstr ""
msgid "Copy SSH clone URL"
msgid "Copy SSH clone URL"
msgstr ""
msgstr ""
...
@@ -2260,6 +2260,9 @@ msgstr ""
...
@@ -2260,6 +2260,9 @@ msgstr ""
msgid "Copy reference to clipboard"
msgid "Copy reference to clipboard"
msgstr ""
msgstr ""
msgid "Copy secret to clipboard"
msgstr ""
msgid "Copy to clipboard"
msgid "Copy to clipboard"
msgstr ""
msgstr ""
...
@@ -6694,7 +6697,7 @@ msgstr ""
...
@@ -6694,7 +6697,7 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgid "Seconds to wait for a storage access attempt"
msgstr ""
msgstr ""
msgid "Secret
:
"
msgid "Secret"
msgstr ""
msgstr ""
msgid "Security"
msgid "Security"
...
...
spec/features/admin/admin_manage_applications_spec.rb
View file @
b9f35f4e
...
@@ -16,7 +16,7 @@ RSpec.describe 'admin manage applications' do
...
@@ -16,7 +16,7 @@ RSpec.describe 'admin manage applications' do
check
:doorkeeper_application_trusted
check
:doorkeeper_application_trusted
click_on
'Submit'
click_on
'Submit'
expect
(
page
).
to
have_content
(
'Application: test'
)
expect
(
page
).
to
have_content
(
'Application: test'
)
expect
(
page
).
to
have_content
(
'Application I
d
'
)
expect
(
page
).
to
have_content
(
'Application I
D
'
)
expect
(
page
).
to
have_content
(
'Secret'
)
expect
(
page
).
to
have_content
(
'Secret'
)
expect
(
page
).
to
have_content
(
'Trusted Y'
)
expect
(
page
).
to
have_content
(
'Trusted Y'
)
...
@@ -28,7 +28,7 @@ RSpec.describe 'admin manage applications' do
...
@@ -28,7 +28,7 @@ RSpec.describe 'admin manage applications' do
click_on
'Submit'
click_on
'Submit'
expect
(
page
).
to
have_content
(
'test_changed'
)
expect
(
page
).
to
have_content
(
'test_changed'
)
expect
(
page
).
to
have_content
(
'Application I
d
'
)
expect
(
page
).
to
have_content
(
'Application I
D
'
)
expect
(
page
).
to
have_content
(
'Secret'
)
expect
(
page
).
to
have_content
(
'Secret'
)
expect
(
page
).
to
have_content
(
'Trusted N'
)
expect
(
page
).
to
have_content
(
'Trusted N'
)
...
...
spec/features/issues/filtered_search/dropdown_hint_spec.rb
View file @
b9f35f4e
...
@@ -15,6 +15,7 @@ describe 'Dropdown hint', :js do
...
@@ -15,6 +15,7 @@ describe 'Dropdown hint', :js do
before
do
before
do
project
.
add_maintainer
(
user
)
project
.
add_maintainer
(
user
)
create
(
:issue
,
project:
project
)
create
(
:issue
,
project:
project
)
create
(
:merge_request
,
source_project:
project
,
target_project:
project
)
end
end
context
'when user not logged in'
do
context
'when user not logged in'
do
...
@@ -224,4 +225,21 @@ describe 'Dropdown hint', :js do
...
@@ -224,4 +225,21 @@ describe 'Dropdown hint', :js do
end
end
end
end
end
end
context
'merge request page'
do
before
do
sign_in
(
user
)
visit
project_merge_requests_path
(
project
)
filtered_search
.
click
end
it
'shows the WIP menu item and opens the WIP options dropdown'
do
click_hint
(
'wip'
)
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-wip'
,
visible:
true
)
expect_tokens
([{
name:
'wip'
}])
expect_filtered_search_input_empty
end
end
end
end
spec/features/profiles/user_manages_applications_spec.rb
View file @
b9f35f4e
...
@@ -16,7 +16,7 @@ describe 'User manages applications' do
...
@@ -16,7 +16,7 @@ describe 'User manages applications' do
click_on
'Save application'
click_on
'Save application'
expect
(
page
).
to
have_content
'Application: test'
expect
(
page
).
to
have_content
'Application: test'
expect
(
page
).
to
have_content
'Application I
d
'
expect
(
page
).
to
have_content
'Application I
D
'
expect
(
page
).
to
have_content
'Secret'
expect
(
page
).
to
have_content
'Secret'
click_on
'Edit'
click_on
'Edit'
...
@@ -26,7 +26,7 @@ describe 'User manages applications' do
...
@@ -26,7 +26,7 @@ describe 'User manages applications' do
click_on
'Save application'
click_on
'Save application'
expect
(
page
).
to
have_content
'test_changed'
expect
(
page
).
to
have_content
'test_changed'
expect
(
page
).
to
have_content
'Application I
d
'
expect
(
page
).
to
have_content
'Application I
D
'
expect
(
page
).
to
have_content
'Secret'
expect
(
page
).
to
have_content
'Secret'
visit
applications_profile_path
visit
applications_profile_path
...
...
spec/finders/merge_requests_finder_spec.rb
View file @
b9f35f4e
...
@@ -3,25 +3,47 @@ require 'spec_helper'
...
@@ -3,25 +3,47 @@ require 'spec_helper'
describe
MergeRequestsFinder
do
describe
MergeRequestsFinder
do
include
ProjectForksHelper
include
ProjectForksHelper
# We need to explicitly permit Gitaly N+1s because of the specs that use
# :request_store. Gitaly N+1 detection is only enabled when :request_store is,
# but we don't care about potential N+1s when we're just creating several
# projects in the setup phase.
def
create_project_without_n_plus_1
(
*
args
)
Gitlab
::
GitalyClient
.
allow_n_plus_1_calls
do
create
(
:project
,
:public
,
*
args
)
end
end
let
(
:user
)
{
create
:user
}
let
(
:user
)
{
create
:user
}
let
(
:user2
)
{
create
:user
}
let
(
:user2
)
{
create
:user
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:subgroup
)
{
create
(
:group
,
parent:
group
)
}
let
(
:subgroup
)
{
create
(
:group
,
parent:
group
)
}
let
(
:project1
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
(
:project1
)
{
create_project_without_n_plus_1
(
group:
group
)
}
let
(
:project2
)
{
fork_project
(
project1
,
user
)
}
let
(
:project2
)
do
Gitlab
::
GitalyClient
.
allow_n_plus_1_calls
do
fork_project
(
project1
,
user
)
end
end
let
(
:project3
)
do
let
(
:project3
)
do
p
=
fork_project
(
project1
,
user
)
Gitlab
::
GitalyClient
.
allow_n_plus_1_calls
do
p
.
update!
(
archived:
true
)
p
=
fork_project
(
project1
,
user
)
p
p
.
update!
(
archived:
true
)
p
end
end
end
let
(
:project4
)
{
create
(
:project
,
:public
,
group:
subgroup
)
}
let
(
:project4
)
{
create_project_without_n_plus_1
(
group:
subgroup
)
}
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
,
:simple
,
author:
user
,
source_project:
project2
,
target_project:
project1
)
}
let!
(
:merge_request2
)
{
create
(
:merge_request
,
:conflict
,
author:
user
,
source_project:
project2
,
target_project:
project1
,
state:
'closed'
)
}
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'
)
}
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
)
}
let!
(
:merge_request4
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project3
,
target_project:
project3
,
title:
'WIP thing'
)
}
let!
(
:merge_request5
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project4
,
target_project:
project4
)
}
let!
(
:merge_request5
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project4
,
target_project:
project4
,
title:
'[WIP]'
)
}
let!
(
:merge_request6
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project5
,
target_project:
project5
,
title:
'WIP: thing'
)
}
let!
(
:merge_request7
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project6
,
target_project:
project6
,
title:
'wip thing'
)
}
let!
(
:merge_request8
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project1
,
target_project:
project1
,
title:
'[wip] thing'
)
}
let!
(
:merge_request9
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project1
,
target_project:
project2
,
title:
'wip: thing'
)
}
before
do
before
do
project1
.
add_maintainer
(
user
)
project1
.
add_maintainer
(
user
)
...
@@ -29,25 +51,27 @@ describe MergeRequestsFinder do
...
@@ -29,25 +51,27 @@ describe MergeRequestsFinder do
project3
.
add_developer
(
user
)
project3
.
add_developer
(
user
)
project2
.
add_developer
(
user2
)
project2
.
add_developer
(
user2
)
project4
.
add_developer
(
user
)
project4
.
add_developer
(
user
)
project5
.
add_developer
(
user
)
project6
.
add_developer
(
user
)
end
end
describe
"#execute"
do
describe
"#execute"
do
it
'filters by scope'
do
it
'filters by scope'
do
params
=
{
scope:
'authored'
,
state:
'opened'
}
params
=
{
scope:
'authored'
,
state:
'opened'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
3
)
expect
(
merge_requests
.
size
).
to
eq
(
7
)
end
end
it
'filters by project'
do
it
'filters by project'
do
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
}
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
1
)
expect
(
merge_requests
.
size
).
to
eq
(
2
)
end
end
it
'ignores sorting by weight'
do
it
'ignores sorting by weight'
do
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
,
weight:
Issue
::
WEIGHT_ANY
}
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
,
weight:
Issue
::
WEIGHT_ANY
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
1
)
expect
(
merge_requests
.
size
).
to
eq
(
2
)
end
end
it
'filters by group'
do
it
'filters by group'
do
...
@@ -55,7 +79,7 @@ describe MergeRequestsFinder do
...
@@ -55,7 +79,7 @@ describe MergeRequestsFinder do
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
2
)
expect
(
merge_requests
.
size
).
to
eq
(
3
)
end
end
it
'filters by group including subgroups'
,
:nested_groups
do
it
'filters by group including subgroups'
,
:nested_groups
do
...
@@ -63,13 +87,13 @@ describe MergeRequestsFinder do
...
@@ -63,13 +87,13 @@ describe MergeRequestsFinder do
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
3
)
expect
(
merge_requests
.
size
).
to
eq
(
6
)
end
end
it
'filters by non_archived'
do
it
'filters by non_archived'
do
params
=
{
non_archived:
true
}
params
=
{
non_archived:
true
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
4
)
expect
(
merge_requests
.
size
).
to
eq
(
8
)
end
end
it
'filters by iid'
do
it
'filters by iid'
do
...
@@ -104,6 +128,36 @@ describe MergeRequestsFinder do
...
@@ -104,6 +128,36 @@ describe MergeRequestsFinder do
expect
(
merge_requests
).
to
contain_exactly
(
merge_request3
)
expect
(
merge_requests
).
to
contain_exactly
(
merge_request3
)
end
end
it
'filters by wip'
do
params
=
{
wip:
'yes'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
).
to
contain_exactly
(
merge_request4
,
merge_request5
,
merge_request6
,
merge_request7
,
merge_request8
,
merge_request9
)
end
it
'filters by not wip'
do
params
=
{
wip:
'no'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
).
to
contain_exactly
(
merge_request1
,
merge_request2
,
merge_request3
)
end
it
'returns all items if no valid wip param exists'
do
params
=
{
wip:
''
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
).
to
contain_exactly
(
merge_request1
,
merge_request2
,
merge_request3
,
merge_request4
,
merge_request5
,
merge_request6
,
merge_request7
,
merge_request8
,
merge_request9
)
end
it
'adds wip to scalar params'
do
scalar_params
=
described_class
.
scalar_params
expect
(
scalar_params
).
to
include
(
:wip
,
:assignee_id
)
end
context
'filtering by group milestone'
do
context
'filtering by group milestone'
do
let!
(
:group
)
{
create
(
:group
,
:public
)
}
let!
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:group_milestone
)
{
create
(
:milestone
,
group:
group
)
}
let
(
:group_milestone
)
{
create
(
:milestone
,
group:
group
)
}
...
@@ -213,7 +267,7 @@ describe MergeRequestsFinder do
...
@@ -213,7 +267,7 @@ describe MergeRequestsFinder do
it
'returns the number of rows for the default state'
do
it
'returns the number of rows for the default state'
do
finder
=
described_class
.
new
(
user
)
finder
=
described_class
.
new
(
user
)
expect
(
finder
.
row_count
).
to
eq
(
3
)
expect
(
finder
.
row_count
).
to
eq
(
7
)
end
end
it
'returns the number of rows for a given state'
do
it
'returns the number of rows for a given state'
do
...
...
spec/javascripts/filtered_search/dropdown_utils_spec.js
View file @
b9f35f4e
...
@@ -288,13 +288,13 @@ describe('Dropdown Utils', () => {
...
@@ -288,13 +288,13 @@ describe('Dropdown Utils', () => {
describe
(
'
setDataValueIfSelected
'
,
()
=>
{
describe
(
'
setDataValueIfSelected
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
spyOn
(
FilteredSearchDropdownManager
,
'
addWordToInput
'
)
spyOn
(
FilteredSearchDropdownManager
,
'
addWordToInput
'
).
and
.
callFake
(()
=>
{});
.
and
.
callFake
(()
=>
{});
});
});
it
(
'
calls addWordToInput when dataValue exists
'
,
()
=>
{
it
(
'
calls addWordToInput when dataValue exists
'
,
()
=>
{
const
selected
=
{
const
selected
=
{
getAttribute
:
()
=>
'
value
'
,
getAttribute
:
()
=>
'
value
'
,
hasAttribute
:
()
=>
false
,
};
};
DropdownUtils
.
setDataValueIfSelected
(
null
,
selected
);
DropdownUtils
.
setDataValueIfSelected
(
null
,
selected
);
...
@@ -304,6 +304,7 @@ describe('Dropdown Utils', () => {
...
@@ -304,6 +304,7 @@ describe('Dropdown Utils', () => {
it
(
'
returns true when dataValue exists
'
,
()
=>
{
it
(
'
returns true when dataValue exists
'
,
()
=>
{
const
selected
=
{
const
selected
=
{
getAttribute
:
()
=>
'
value
'
,
getAttribute
:
()
=>
'
value
'
,
hasAttribute
:
()
=>
false
,
};
};
const
result
=
DropdownUtils
.
setDataValueIfSelected
(
null
,
selected
);
const
result
=
DropdownUtils
.
setDataValueIfSelected
(
null
,
selected
);
...
...
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
View file @
b9f35f4e
...
@@ -240,13 +240,17 @@ describe('Filtered Search Visual Tokens', () => {
...
@@ -240,13 +240,17 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
setFixtures
(
`
setFixtures
(
`
<div class="test-area">
<div class="test-area">
${
subject
.
createVisualTokenElementHTML
()}
${
subject
.
createVisualTokenElementHTML
(
'
custom-token
'
)}
</div>
</div>
`
);
`
);
tokenElement
=
document
.
querySelector
(
'
.test-area
'
).
firstElementChild
;
tokenElement
=
document
.
querySelector
(
'
.test-area
'
).
firstElementChild
;
});
});
it
(
'
should add class name to token element
'
,
()
=>
{
expect
(
document
.
querySelector
(
'
.test-area .custom-token
'
)).
toBeDefined
();
});
it
(
'
contains name div
'
,
()
=>
{
it
(
'
contains name div
'
,
()
=>
{
expect
(
tokenElement
.
querySelector
(
'
.name
'
)).
toEqual
(
jasmine
.
anything
());
expect
(
tokenElement
.
querySelector
(
'
.name
'
)).
toEqual
(
jasmine
.
anything
());
});
});
...
@@ -280,7 +284,7 @@ describe('Filtered Search Visual Tokens', () => {
...
@@ -280,7 +284,7 @@ describe('Filtered Search Visual Tokens', () => {
describe
(
'
addVisualTokenElement
'
,
()
=>
{
describe
(
'
addVisualTokenElement
'
,
()
=>
{
it
(
'
renders search visual tokens
'
,
()
=>
{
it
(
'
renders search visual tokens
'
,
()
=>
{
subject
.
addVisualTokenElement
(
'
search term
'
,
null
,
true
);
subject
.
addVisualTokenElement
(
'
search term
'
,
null
,
{
isSearchTerm
:
true
}
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-term
'
)).
toEqual
(
true
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-term
'
)).
toEqual
(
true
);
...
...
spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
View file @
b9f35f4e
...
@@ -8,7 +8,10 @@ describe('Issuable Time Tracker', () => {
...
@@ -8,7 +8,10 @@ describe('Issuable Time Tracker', () => {
let
initialData
;
let
initialData
;
let
vm
;
let
vm
;
const
initTimeTrackingComponent
=
opts
=>
{
const
initTimeTrackingComponent
=
({
timeEstimate
,
timeSpent
,
timeEstimateHumanReadable
,
timeSpentHumanReadable
})
=>
{
setFixtures
(
`
setFixtures
(
`
<div>
<div>
<div id="mock-container"></div>
<div id="mock-container"></div>
...
@@ -16,10 +19,10 @@ describe('Issuable Time Tracker', () => {
...
@@ -16,10 +19,10 @@ describe('Issuable Time Tracker', () => {
`
);
`
);
initialData
=
{
initialData
=
{
time
_estimate
:
opts
.
time
Estimate
,
timeEstimate
,
time
_spent
:
opts
.
time
Spent
,
timeSpent
,
human
_time_estimate
:
opts
.
timeEstimateHumanReadable
,
human
TimeEstimate
:
timeEstimateHumanReadable
,
human
_time_spent
:
opts
.
timeSpentHumanReadable
,
human
TimeSpent
:
timeSpentHumanReadable
,
rootPath
:
'
/
'
,
rootPath
:
'
/
'
,
};
};
...
@@ -43,8 +46,8 @@ describe('Issuable Time Tracker', () => {
...
@@ -43,8 +46,8 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Initialization
'
,
()
=>
{
describe
(
'
Initialization
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
initTimeTrackingComponent
({
timeEstimate
:
10000
0
,
timeEstimate
:
10000
,
// 2h 46m
timeSpent
:
5000
,
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
});
});
...
@@ -56,14 +59,14 @@ describe('Issuable Time Tracker', () => {
...
@@ -56,14 +59,14 @@ describe('Issuable Time Tracker', () => {
it
(
'
should correctly set timeEstimate
'
,
done
=>
{
it
(
'
should correctly set timeEstimate
'
,
done
=>
{
Vue
.
nextTick
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
timeEstimate
).
toBe
(
initialData
.
time
_e
stimate
);
expect
(
vm
.
timeEstimate
).
toBe
(
initialData
.
time
E
stimate
);
done
();
done
();
});
});
});
});
it
(
'
should correctly set time_spent
'
,
done
=>
{
it
(
'
should correctly set time_spent
'
,
done
=>
{
Vue
.
nextTick
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
timeSpent
).
toBe
(
initialData
.
time
_s
pent
);
expect
(
vm
.
timeSpent
).
toBe
(
initialData
.
time
S
pent
);
done
();
done
();
});
});
});
});
...
@@ -74,8 +77,8 @@ describe('Issuable Time Tracker', () => {
...
@@ -74,8 +77,8 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Comparison pane
'
,
()
=>
{
describe
(
'
Comparison pane
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
initTimeTrackingComponent
({
timeEstimate
:
100000
,
timeEstimate
:
100000
,
// 1d 3h
timeSpent
:
5000
,
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
''
,
timeEstimateHumanReadable
:
''
,
timeSpentHumanReadable
:
''
,
timeSpentHumanReadable
:
''
,
});
});
...
@@ -106,8 +109,8 @@ describe('Issuable Time Tracker', () => {
...
@@ -106,8 +109,8 @@ describe('Issuable Time Tracker', () => {
});
});
it
(
'
should display the remaining meter with the correct background color when over estimate
'
,
done
=>
{
it
(
'
should display the remaining meter with the correct background color when over estimate
'
,
done
=>
{
vm
.
time
_estimate
=
100000
;
vm
.
time
Estimate
=
10000
;
// 2h 46m
vm
.
time
_spent
=
20000000
;
vm
.
time
Spent
=
20000000
;
// 231 days
Vue
.
nextTick
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.time-tracking-comparison-pane .progress[variant="danger"]
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.time-tracking-comparison-pane .progress[variant="danger"]
'
)).
not
.
toBeNull
();
done
();
done
();
...
@@ -119,7 +122,7 @@ describe('Issuable Time Tracker', () => {
...
@@ -119,7 +122,7 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Estimate only pane
'
,
()
=>
{
describe
(
'
Estimate only pane
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
initTimeTrackingComponent
({
timeEstimate
:
10000
0
,
timeEstimate
:
10000
,
// 2h 46m
timeSpent
:
0
,
timeSpent
:
0
,
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
''
,
timeSpentHumanReadable
:
''
,
...
@@ -142,7 +145,7 @@ describe('Issuable Time Tracker', () => {
...
@@ -142,7 +145,7 @@ describe('Issuable Time Tracker', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
initTimeTrackingComponent
({
timeEstimate
:
0
,
timeEstimate
:
0
,
timeSpent
:
5000
,
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
});
});
...
...
spec/models/merge_request_spec.rb
View file @
b9f35f4e
...
@@ -747,7 +747,7 @@ describe MergeRequest do
...
@@ -747,7 +747,7 @@ describe MergeRequest do
end
end
describe
"#wipless_title"
do
describe
"#wipless_title"
do
[
'WIP '
,
'WIP:'
,
'WIP: '
,
'[WIP]'
,
'[WIP] '
,
'
[WIP] WIP [WIP] WIP: WIP '
].
each
do
|
wip_prefix
|
[
'WIP '
,
'WIP:'
,
'WIP: '
,
'[WIP]'
,
'[WIP] '
,
'[WIP] WIP [WIP] WIP: WIP '
].
each
do
|
wip_prefix
|
it
"removes the '
#{
wip_prefix
}
' prefix"
do
it
"removes the '
#{
wip_prefix
}
' prefix"
do
wipless_title
=
subject
.
title
wipless_title
=
subject
.
title
subject
.
title
=
"
#{
wip_prefix
}#{
subject
.
title
}
"
subject
.
title
=
"
#{
wip_prefix
}#{
subject
.
title
}
"
...
...
spec/requests/api/merge_requests_spec.rb
View file @
b9f35f4e
...
@@ -81,6 +81,35 @@ describe API::MergeRequests do
...
@@ -81,6 +81,35 @@ describe API::MergeRequests do
let
(
:user2
)
{
create
(
:user
)
}
let
(
:user2
)
{
create
(
:user
)
}
it
'returns an array of all merge requests except unauthorized ones'
do
it
'returns an array of all merge requests except unauthorized ones'
do
get
api
(
'/merge_requests'
,
user
),
scope: :all
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
include_pagination_headers
expect
(
json_response
).
to
be_an
Array
expect
(
json_response
.
map
{
|
mr
|
mr
[
'id'
]
})
.
to
contain_exactly
(
merge_request
.
id
,
merge_request_closed
.
id
,
merge_request_merged
.
id
,
merge_request_locked
.
id
,
merge_request2
.
id
)
end
it
"returns an array of no merge_requests when wip=yes"
do
get
api
(
"/merge_requests"
,
user
),
wip:
'yes'
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
include_pagination_headers
expect
(
json_response
).
to
be_an
Array
expect
(
json_response
.
length
).
to
eq
(
0
)
end
it
"returns an array of no merge_requests when wip=no"
do
get
api
(
"/merge_requests"
,
user
),
wip:
'no'
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
include_pagination_headers
expect
(
json_response
).
to
be_an
Array
expect
(
json_response
.
map
{
|
mr
|
mr
[
'id'
]
})
.
to
contain_exactly
(
merge_request
.
id
,
merge_request_closed
.
id
,
merge_request_merged
.
id
,
merge_request_locked
.
id
,
merge_request2
.
id
)
end
it
'does not return unauthorized merge requests'
do
private_project
=
create
(
:project
,
:private
)
private_project
=
create
(
:project
,
:private
)
merge_request3
=
create
(
:merge_request
,
:simple
,
source_project:
private_project
,
target_project:
private_project
,
source_branch:
'other-branch'
)
merge_request3
=
create
(
:merge_request
,
:simple
,
source_project:
private_project
,
target_project:
private_project
,
source_branch:
'other-branch'
)
...
@@ -244,6 +273,15 @@ describe API::MergeRequests do
...
@@ -244,6 +273,15 @@ describe API::MergeRequests do
expect
(
response
).
to
have_gitlab_http_status
(
404
)
expect
(
response
).
to
have_gitlab_http_status
(
404
)
end
end
it
"returns an array of no merge_requests when wip=yes"
do
get
api
(
"/projects/
#{
project
.
id
}
/merge_requests"
,
user
),
wip:
'yes'
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
include_pagination_headers
expect
(
json_response
).
to
be_an
Array
expect
(
json_response
.
length
).
to
eq
(
0
)
end
it
'returns merge_request by "iids" array'
do
it
'returns merge_request by "iids" array'
do
get
api
(
endpoint_path
,
user
),
iids:
[
merge_request
.
iid
,
merge_request_closed
.
iid
]
get
api
(
endpoint_path
,
user
),
iids:
[
merge_request
.
iid
,
merge_request_closed
.
iid
]
...
...
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