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 {
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
.
dispatchInputEvent
();
...
...
app/assets/javascripts/filtered_search/dropdown_utils.js
View file @
b9f35f4e
...
...
@@ -143,7 +143,9 @@ export default class DropdownUtils {
const
dataValue
=
selected
.
getAttribute
(
'
data-value
'
);
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
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
b9f35f4e
...
...
@@ -92,6 +92,11 @@ export default class FilteredSearchDropdownManager {
gl
:
DropdownEmoji
,
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-my-reaction
'
),
},
wip
:
{
reference
:
null
,
gl
:
DropdownNonUser
,
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-wip
'
),
},
status
:
{
reference
:
null
,
gl
:
NullDropdown
,
...
...
@@ -150,10 +155,16 @@ export default class FilteredSearchDropdownManager {
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
'
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
,
{
uppercaseTokenName
,
capitalizeTokenValue
,
});
input
.
value
=
''
;
if
(
clicked
)
{
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
b9f35f4e
...
...
@@ -428,7 +428,10 @@ export default class FilteredSearchManager {
if
(
isLastVisualTokenValid
)
{
tokens
.
forEach
((
t
)
=>
{
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
(
'
:
'
);
...
...
@@ -444,7 +447,10 @@ export default class FilteredSearchManager {
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
}
:`
,
''
);
}
}
else
{
...
...
@@ -452,7 +458,10 @@ export default class FilteredSearchManager {
const
valueCompletedRegex
=
/
([
~%@
]{0,1}
".+"
)
|
([
~%@
]{0,1}
'.+'
)
|^
((?![
~%@
]
'
)(?![
~%@
]
"
)(?!
'
)(?!
"
))
.*/g
;
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
input
.
value
=
input
.
value
.
replace
(
searchToken
,
''
).
trim
();
...
...
@@ -503,7 +512,7 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens
.
addFilterVisualToken
(
condition
.
tokenKey
,
condition
.
value
,
canEdit
,
{
canEdit
}
,
);
}
else
{
// Sanitize value since URL converts spaces into +
...
...
@@ -529,10 +538,15 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
sanitizedKey
,
sanitizedValue
);
const
{
uppercaseTokenName
,
capitalizeTokenValue
}
=
match
;
FilteredSearchVisualTokens
.
addFilterVisualToken
(
sanitizedKey
,
`
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
,
canEdit
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
},
);
}
else
if
(
!
match
&&
keyParam
===
'
assignee_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
...
...
@@ -540,7 +554,7 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
const
tokenName
=
'
assignee
'
;
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
'
)
{
const
id
=
parseInt
(
value
,
10
);
...
...
@@ -548,7 +562,7 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
const
tokenName
=
'
author
'
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
canEdit
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
}
);
}
}
else
if
(
!
match
&&
keyParam
===
'
search
'
)
{
hasFilteredSearch
=
true
;
...
...
@@ -584,15 +598,17 @@ export default class FilteredSearchManager {
this
.
saveCurrentSearchQuery
();
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
searchQuery
,
this
.
filteredSearchTokenKeys
.
getKeys
()
);
const
tokenKeys
=
this
.
filteredSearchTokenKeys
.
getKeys
();
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
searchQuery
,
tokenKeys
);
const
currentState
=
state
||
getParameterByName
(
'
state
'
)
||
'
opened
'
;
paths
.
push
(
`state=
${
currentState
}
`
);
tokens
.
forEach
((
token
)
=>
{
const
condition
=
this
.
filteredSearchTokenKeys
.
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
// e.g. 'my-reaction' => 'my_reaction'
const
underscoredKey
=
token
.
key
.
replace
(
'
-
'
,
'
_
'
);
...
...
@@ -604,6 +620,10 @@ export default class FilteredSearchManager {
}
else
{
let
tokenValue
=
token
.
value
;
if
(
tokenConfig
.
lowercaseValueOnSubmit
)
{
tokenValue
=
tokenValue
.
toLowerCase
();
}
if
((
tokenValue
[
0
]
===
'
\'
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
\'
'
)
||
(
tokenValue
[
0
]
===
'
"
'
&&
tokenValue
[
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 {
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
)
{
return
this
.
tokenKeys
.
find
(
tokenKey
=>
tokenKey
.
key
===
key
)
||
null
;
}
...
...
@@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys {
return
this
.
conditions
.
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 {
}
}
static
createVisualTokenElementHTML
(
canEdit
=
true
)
{
static
createVisualTokenElementHTML
(
options
=
{})
{
const
{
canEdit
=
true
,
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
,
}
=
options
;
return
`
<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"></div>
<div class="
${
capitalizeTokenValue
?
'
text-capitalize
'
:
''
}
value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
...
...
@@ -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
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
if
(
value
)
{
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
(
canEdit
);
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
({
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
FilteredSearchVisualTokens
.
renderVisualTokenValue
(
li
,
name
,
value
);
}
else
{
li
.
innerHTML
=
'
<div class="name"></div>
'
;
li
.
innerHTML
=
`<div class="
${
uppercaseTokenName
?
'
text-uppercase
'
:
''
}
name"></div>`
;
}
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
...
...
@@ -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
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
{
addVisualTokenElement
}
=
FilteredSearchVisualTokens
;
if
(
isLastVisualTokenValid
)
{
addVisualTokenElement
(
tokenName
,
tokenValue
,
false
,
canEdit
);
addVisualTokenElement
(
tokenName
,
tokenValue
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
}
else
{
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
tokensContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
tokensContainer
.
removeChild
(
lastVisualToken
);
const
value
=
tokenValue
||
tokenName
;
addVisualTokenElement
(
previousTokenName
,
value
,
false
,
canEdit
);
addVisualTokenElement
(
previousTokenName
,
value
,
{
canEdit
,
uppercaseTokenName
,
capitalizeTokenValue
,
});
}
}
...
...
@@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens {
if
(
lastVisualToken
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-term
'
))
{
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
+=
`
${
searchTerm
}
`
;
}
else
{
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
true
);
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
{
isSearchTerm
:
true
,
});
}
}
...
...
@@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens {
let
value
;
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
'
);
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
import
{
FILTERED_SEARCH
}
from
'
~/pages/constants
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
IssuableFilteredSearchTokenKeys
.
addExtraTokensForMergeRequests
();
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
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';
import
{
ISSUABLE_INDEX
}
from
'
~/pages/projects/constants
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
IssuableFilteredSearchTokenKeys
.
addExtraTokensForMergeRequests
();
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
MERGE_REQUESTS
,
filteredSearchTokenKeys
:
IssuableFilteredSearchTokenKeys
,
});
new
IssuableIndex
(
ISSUABLE_INDEX
.
MERGE_REQUEST
);
// eslint-disable-line no-new
new
ShortcutsNavigation
();
// 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 {
<
template
>
<div
class=
"block"
>
<issuable-time-tracker
:time
_
estimate=
"store.timeEstimate"
:time
_
spent=
"store.totalTimeSpent"
:human
_time_
estimate=
"store.humanTimeEstimate"
:human
_time_
spent=
"store.humanTotalTimeSpent"
:time
-
estimate=
"store.timeEstimate"
:time
-
spent=
"store.totalTimeSpent"
:human
-time-
estimate=
"store.humanTimeEstimate"
:human
-time-
spent=
"store.humanTotalTimeSpent"
:root-path=
"store.rootPath"
/>
</div>
...
...
app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
View file @
b9f35f4e
...
...
@@ -19,24 +19,20 @@ export default {
TimeTrackingHelpState
,
},
props
:
{
// eslint-disable-next-line vue/prop-name-casing
time_estimate
:
{
timeEstimate
:
{
type
:
Number
,
required
:
true
,
},
// eslint-disable-next-line vue/prop-name-casing
time_spent
:
{
timeSpent
:
{
type
:
Number
,
required
:
true
,
},
// eslint-disable-next-line vue/prop-name-casing
human_time_estimate
:
{
humanTimeEstimate
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
// eslint-disable-next-line vue/prop-name-casing
human_time_spent
:
{
humanTimeSpent
:
{
type
:
String
,
required
:
false
,
default
:
''
,
...
...
@@ -52,18 +48,6 @@ export default {
};
},
computed
:
{
timeSpent
()
{
return
this
.
time_spent
;
},
timeEstimate
()
{
return
this
.
time_estimate
;
},
timeEstimateHumanReadable
()
{
return
this
.
human_time_estimate
;
},
timeSpentHumanReadable
()
{
return
this
.
human_time_spent
;
},
hasTimeSpent
()
{
return
!!
this
.
timeSpent
;
},
...
...
@@ -94,10 +78,12 @@ export default {
this
.
showHelp
=
show
;
},
update
(
data
)
{
this
.
time_estimate
=
data
.
time_estimate
;
this
.
time_spent
=
data
.
time_spent
;
this
.
human_time_estimate
=
data
.
human_time_estimate
;
this
.
human_time_spent
=
data
.
human_time_spent
;
const
{
timeEstimate
,
timeSpent
,
humanTimeEstimate
,
humanTimeSpent
}
=
data
;
this
.
timeEstimate
=
timeEstimate
;
this
.
timeSpent
=
timeSpent
;
this
.
humanTimeEstimate
=
humanTimeEstimate
;
this
.
humanTimeSpent
=
humanTimeSpent
;
},
},
};
...
...
@@ -114,8 +100,8 @@ export default {
:show-help-state=
"showHelpState"
:show-spent-only-state=
"showSpentOnlyState"
:show-estimate-only-state=
"showEstimateOnlyState"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-spent-human-readable=
"
humanTimeSpent
"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
<div
class=
"title hide-collapsed"
>
{{
__
(
'
Time tracking
'
)
}}
...
...
@@ -145,11 +131,11 @@ export default {
<div
class=
"time-tracking-content hide-collapsed"
>
<time-tracking-estimate-only-pane
v-if=
"showEstimateOnlyState"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
<time-tracking-spent-only-pane
v-if=
"showSpentOnlyState"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-spent-human-readable=
"
humanTimeSpent
"
/>
<time-tracking-no-tracking-pane
v-if=
"showNoTimeTrackingState"
...
...
@@ -158,8 +144,8 @@ export default {
v-if=
"showComparisonState"
:time-estimate=
"timeEstimate"
:time-spent=
"timeSpent"
:time-spent-human-readable=
"
timeSpentHumanReadable
"
:time-estimate-human-readable=
"
timeEstimateHumanReadabl
e"
:time-spent-human-readable=
"
humanTimeSpent
"
:time-estimate-human-readable=
"
humanTimeEstimat
e"
/>
<transition
name=
"help-state-toggle"
>
<time-tracking-help-state
...
...
app/assets/javascripts/sidebar/mount_milestone_sidebar.js
View file @
b9f35f4e
...
...
@@ -7,6 +7,8 @@ export default class SidebarMilestone {
if
(
!
el
)
return
;
const
{
timeEstimate
,
timeSpent
,
humanTimeEstimate
,
humanTimeSpent
}
=
el
.
dataset
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
...
...
@@ -15,10 +17,10 @@ export default class SidebarMilestone {
},
render
:
createElement
=>
createElement
(
'
timeTracker
'
,
{
props
:
{
time
_estimate
:
parseInt
(
el
.
dataset
.
timeEstimate
,
10
),
time
_spent
:
parseInt
(
el
.
dataset
.
timeSpent
,
10
),
human
_time_estimate
:
el
.
dataset
.
human
TimeEstimate
,
human
_time_spent
:
el
.
dataset
.
human
TimeSpent
,
time
Estimate
:
parseInt
(
timeEstimate
,
10
),
time
Spent
:
parseInt
(
timeSpent
,
10
),
humanTimeEstimate
,
humanTimeSpent
,
rootPath
:
'
/
'
,
},
}),
...
...
app/assets/stylesheets/pages/commits.scss
View file @
b9f35f4e
...
...
@@ -223,6 +223,7 @@
}
}
.clipboard-group
,
.commit-sha-group
{
display
:
inline-flex
;
...
...
app/finders/merge_requests_finder.rb
View file @
b9f35f4e
...
...
@@ -27,13 +27,17 @@
# updated_before: datetime
#
class
MergeRequestsFinder
<
IssuableFinder
def
self
.
scalar_params
@scalar_params
||=
super
+
[
:wip
]
end
def
klass
MergeRequest
end
def
filter_items
(
_items
)
items
=
by_source_branch
(
super
)
items
=
by_wip
(
items
)
by_target_branch
(
items
)
end
...
...
@@ -61,5 +65,20 @@ class MergeRequestsFinder < IssuableFinder
items
.
where
(
target_branch:
target_branch
)
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
app/models/merge_request.rb
View file @
b9f35f4e
...
...
@@ -265,7 +265,7 @@ class MergeRequest < ActiveRecord::Base
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
)
!!
(
title
=~
WIP_REGEX
)
...
...
app/views/admin/applications/show.html.haml
View file @
b9f35f4e
-
page_title
@application
.
name
,
"Applications"
%h3
.page-title
Application:
#{
@application
.
name
}
...
...
@@ -6,23 +7,29 @@
%table
.table
%tr
%td
Application Id
=
_
(
'Application ID'
)
%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
%td
Secret:
=
_
(
'Secret'
)
%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
%td
Callback url
=
_
(
'Callback URL'
)
%td
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
%div
%span
.monospace
=
uri
%tr
%td
Trusted
...
...
app/views/doorkeeper/applications/show.html.haml
View file @
b9f35f4e
...
...
@@ -10,18 +10,25 @@
%table
.table
%tr
%td
=
_
(
'Application I
d
'
)
=
_
(
'Application I
D
'
)
%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
%td
=
_
(
'Secret
:
'
)
=
_
(
'Secret'
)
%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
%td
=
_
(
'Callback
url
'
)
=
_
(
'Callback
URL
'
)
%td
-
@application
.
redirect_uri
.
split
.
each
do
|
uri
|
%div
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
b9f35f4e
...
...
@@ -33,13 +33,13 @@
#js-dropdown-hint
.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
action:
'submit'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
=
sprite_icon
(
'search'
)
%span
Press Enter or click to search
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
...
...
@@ -60,7 +60,7 @@
#js-dropdown-assignee
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Assignee
%li
.divider.droplab-item-ignore
-
if
current_user
...
...
@@ -73,38 +73,46 @@
#js-dropdown-milestone
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Milestone
%li
.filter-dropdown-item
{
data:
{
value:
'upcoming'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
Upcoming
%li
.filter-dropdown-item
{
'data-value'
=>
'started'
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
Started
%li
.divider.droplab-item-ignore
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link.js-data-value
%button
.btn.btn-link.js-data-value
{
type:
'button'
}
{{title}}
#js-dropdown-label
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
data:
{
dropdown:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'none'
}
}
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
No Label
%li
.divider.droplab-item-ignore
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
%span
.dropdown-label-box
{
style:
'
background:
{{
color
}}
'
}
%span
.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
.filter-dropdown
{
data:
{
dynamic:
true
,
dropdown:
true
}
}
%li
.filter-dropdown-item
%button
.btn.btn-link
%button
.btn.btn-link
{
type:
'button'
}
%gl-emoji
%span
.js-data-value.prepend-left-10
{{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
...
...
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:
|
`source_branch`
| string | no | Return merge requests with the given source branch |
|
`target_branch`
| string | no | Return merge requests with the given target branch |
|
`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
[
...
...
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**.
![
Blocked Accept Button
](
img/wip_blocked_accept_button.png
)
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.
![
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
To allow a Work In Progress merge request to be accepted again when it's ready,
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
# rubocop: disable CodeReuse/ActiveRecord
def
find_merge_requests
(
args
=
{})
args
=
declared_params
.
merge
(
args
)
args
[
:milestone_title
]
=
args
.
delete
(
:milestone
)
args
[
:label_name
]
=
args
.
delete
(
:labels
)
args
[
:scope
]
=
args
[
:scope
].
underscore
if
args
[
:scope
]
...
...
@@ -99,6 +98,7 @@ module API
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
: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
end
end
...
...
locale/gitlab.pot
View file @
b9f35f4e
...
...
@@ -703,7 +703,7 @@ msgstr ""
msgid "Application"
msgstr ""
msgid "Application I
d
"
msgid "Application I
D
"
msgstr ""
msgid "Application: %{name}"
...
...
@@ -1320,9 +1320,6 @@ msgstr ""
msgid "Callback URL"
msgstr ""
msgid "Callback url"
msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
...
...
@@ -2233,6 +2230,9 @@ msgstr ""
msgid "Copy HTTPS clone URL"
msgstr ""
msgid "Copy ID to clipboard"
msgstr ""
msgid "Copy SSH clone URL"
msgstr ""
...
...
@@ -2260,6 +2260,9 @@ msgstr ""
msgid "Copy reference to clipboard"
msgstr ""
msgid "Copy secret to clipboard"
msgstr ""
msgid "Copy to clipboard"
msgstr ""
...
...
@@ -6694,7 +6697,7 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
msgid "Secret
:
"
msgid "Secret"
msgstr ""
msgid "Security"
...
...
spec/features/admin/admin_manage_applications_spec.rb
View file @
b9f35f4e
...
...
@@ -16,7 +16,7 @@ RSpec.describe 'admin manage applications' do
check
:doorkeeper_application_trusted
click_on
'Submit'
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
(
'Trusted Y'
)
...
...
@@ -28,7 +28,7 @@ RSpec.describe 'admin manage applications' do
click_on
'Submit'
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
(
'Trusted N'
)
...
...
spec/features/issues/filtered_search/dropdown_hint_spec.rb
View file @
b9f35f4e
...
...
@@ -15,6 +15,7 @@ describe 'Dropdown hint', :js do
before
do
project
.
add_maintainer
(
user
)
create
(
:issue
,
project:
project
)
create
(
:merge_request
,
source_project:
project
,
target_project:
project
)
end
context
'when user not logged in'
do
...
...
@@ -224,4 +225,21 @@ describe 'Dropdown hint', :js do
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
spec/features/profiles/user_manages_applications_spec.rb
View file @
b9f35f4e
...
...
@@ -16,7 +16,7 @@ describe 'User manages applications' do
click_on
'Save application'
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'
click_on
'Edit'
...
...
@@ -26,7 +26,7 @@ describe 'User manages applications' do
click_on
'Save application'
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'
visit
applications_profile_path
...
...
spec/finders/merge_requests_finder_spec.rb
View file @
b9f35f4e
...
...
@@ -3,25 +3,47 @@ require 'spec_helper'
describe
MergeRequestsFinder
do
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
(
:user2
)
{
create
:user
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:subgroup
)
{
create
(
:group
,
parent:
group
)
}
let
(
:project1
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
(
:project2
)
{
fork_project
(
project1
,
user
)
}
let
(
:project1
)
{
create_project_without_n_plus_1
(
group:
group
)
}
let
(
:project2
)
do
Gitlab
::
GitalyClient
.
allow_n_plus_1_calls
do
fork_project
(
project1
,
user
)
end
end
let
(
:project3
)
do
p
=
fork_project
(
project1
,
user
)
p
.
update!
(
archived:
true
)
p
Gitlab
::
GitalyClient
.
allow_n_plus_1_calls
do
p
=
fork_project
(
project1
,
user
)
p
.
update!
(
archived:
true
)
p
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_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_request4
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project3
,
target_project:
project3
)
}
let!
(
:merge_request5
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
source_project:
project4
,
target_project:
project4
)
}
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'
)
}
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
project1
.
add_maintainer
(
user
)
...
...
@@ -29,25 +51,27 @@ describe MergeRequestsFinder do
project3
.
add_developer
(
user
)
project2
.
add_developer
(
user2
)
project4
.
add_developer
(
user
)
project5
.
add_developer
(
user
)
project6
.
add_developer
(
user
)
end
describe
"#execute"
do
it
'filters by scope'
do
params
=
{
scope:
'authored'
,
state:
'opened'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
3
)
expect
(
merge_requests
.
size
).
to
eq
(
7
)
end
it
'filters by project'
do
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
1
)
expect
(
merge_requests
.
size
).
to
eq
(
2
)
end
it
'ignores sorting by weight'
do
params
=
{
project_id:
project1
.
id
,
scope:
'authored'
,
state:
'opened'
,
weight:
Issue
::
WEIGHT_ANY
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
1
)
expect
(
merge_requests
.
size
).
to
eq
(
2
)
end
it
'filters by group'
do
...
...
@@ -55,7 +79,7 @@ describe MergeRequestsFinder do
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
2
)
expect
(
merge_requests
.
size
).
to
eq
(
3
)
end
it
'filters by group including subgroups'
,
:nested_groups
do
...
...
@@ -63,13 +87,13 @@ describe MergeRequestsFinder do
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
3
)
expect
(
merge_requests
.
size
).
to
eq
(
6
)
end
it
'filters by non_archived'
do
params
=
{
non_archived:
true
}
merge_requests
=
described_class
.
new
(
user
,
params
).
execute
expect
(
merge_requests
.
size
).
to
eq
(
4
)
expect
(
merge_requests
.
size
).
to
eq
(
8
)
end
it
'filters by iid'
do
...
...
@@ -104,6 +128,36 @@ describe MergeRequestsFinder do
expect
(
merge_requests
).
to
contain_exactly
(
merge_request3
)
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
let!
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:group_milestone
)
{
create
(
:milestone
,
group:
group
)
}
...
...
@@ -213,7 +267,7 @@ describe MergeRequestsFinder do
it
'returns the number of rows for the default state'
do
finder
=
described_class
.
new
(
user
)
expect
(
finder
.
row_count
).
to
eq
(
3
)
expect
(
finder
.
row_count
).
to
eq
(
7
)
end
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', () => {
describe
(
'
setDataValueIfSelected
'
,
()
=>
{
beforeEach
(()
=>
{
spyOn
(
FilteredSearchDropdownManager
,
'
addWordToInput
'
)
.
and
.
callFake
(()
=>
{});
spyOn
(
FilteredSearchDropdownManager
,
'
addWordToInput
'
).
and
.
callFake
(()
=>
{});
});
it
(
'
calls addWordToInput when dataValue exists
'
,
()
=>
{
const
selected
=
{
getAttribute
:
()
=>
'
value
'
,
hasAttribute
:
()
=>
false
,
};
DropdownUtils
.
setDataValueIfSelected
(
null
,
selected
);
...
...
@@ -304,6 +304,7 @@ describe('Dropdown Utils', () => {
it
(
'
returns true when dataValue exists
'
,
()
=>
{
const
selected
=
{
getAttribute
:
()
=>
'
value
'
,
hasAttribute
:
()
=>
false
,
};
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', () => {
beforeEach
(()
=>
{
setFixtures
(
`
<div class="test-area">
${
subject
.
createVisualTokenElementHTML
()}
${
subject
.
createVisualTokenElementHTML
(
'
custom-token
'
)}
</div>
`
);
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
'
,
()
=>
{
expect
(
tokenElement
.
querySelector
(
'
.name
'
)).
toEqual
(
jasmine
.
anything
());
});
...
...
@@ -280,7 +284,7 @@ describe('Filtered Search Visual Tokens', () => {
describe
(
'
addVisualTokenElement
'
,
()
=>
{
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
'
);
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', () => {
let
initialData
;
let
vm
;
const
initTimeTrackingComponent
=
opts
=>
{
const
initTimeTrackingComponent
=
({
timeEstimate
,
timeSpent
,
timeEstimateHumanReadable
,
timeSpentHumanReadable
})
=>
{
setFixtures
(
`
<div>
<div id="mock-container"></div>
...
...
@@ -16,10 +19,10 @@ describe('Issuable Time Tracker', () => {
`
);
initialData
=
{
time
_estimate
:
opts
.
time
Estimate
,
time
_spent
:
opts
.
time
Spent
,
human
_time_estimate
:
opts
.
timeEstimateHumanReadable
,
human
_time_spent
:
opts
.
timeSpentHumanReadable
,
timeEstimate
,
timeSpent
,
human
TimeEstimate
:
timeEstimateHumanReadable
,
human
TimeSpent
:
timeSpentHumanReadable
,
rootPath
:
'
/
'
,
};
...
...
@@ -43,8 +46,8 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Initialization
'
,
()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
timeEstimate
:
10000
0
,
timeSpent
:
5000
,
timeEstimate
:
10000
,
// 2h 46m
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
});
...
...
@@ -56,14 +59,14 @@ describe('Issuable Time Tracker', () => {
it
(
'
should correctly set timeEstimate
'
,
done
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
timeEstimate
).
toBe
(
initialData
.
time
_e
stimate
);
expect
(
vm
.
timeEstimate
).
toBe
(
initialData
.
time
E
stimate
);
done
();
});
});
it
(
'
should correctly set time_spent
'
,
done
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
timeSpent
).
toBe
(
initialData
.
time
_s
pent
);
expect
(
vm
.
timeSpent
).
toBe
(
initialData
.
time
S
pent
);
done
();
});
});
...
...
@@ -74,8 +77,8 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Comparison pane
'
,
()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
timeEstimate
:
100000
,
timeSpent
:
5000
,
timeEstimate
:
100000
,
// 1d 3h
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
''
,
timeSpentHumanReadable
:
''
,
});
...
...
@@ -106,8 +109,8 @@ describe('Issuable Time Tracker', () => {
});
it
(
'
should display the remaining meter with the correct background color when over estimate
'
,
done
=>
{
vm
.
time
_estimate
=
100000
;
vm
.
time
_spent
=
20000000
;
vm
.
time
Estimate
=
10000
;
// 2h 46m
vm
.
time
Spent
=
20000000
;
// 231 days
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.time-tracking-comparison-pane .progress[variant="danger"]
'
)).
not
.
toBeNull
();
done
();
...
...
@@ -119,7 +122,7 @@ describe('Issuable Time Tracker', () => {
describe
(
'
Estimate only pane
'
,
()
=>
{
beforeEach
(()
=>
{
initTimeTrackingComponent
({
timeEstimate
:
10000
0
,
timeEstimate
:
10000
,
// 2h 46m
timeSpent
:
0
,
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
''
,
...
...
@@ -142,7 +145,7 @@ describe('Issuable Time Tracker', () => {
beforeEach
(()
=>
{
initTimeTrackingComponent
({
timeEstimate
:
0
,
timeSpent
:
5000
,
timeSpent
:
5000
,
// 1h 23m
timeEstimateHumanReadable
:
'
2h 46m
'
,
timeSpentHumanReadable
:
'
1h 23m
'
,
});
...
...
spec/models/merge_request_spec.rb
View file @
b9f35f4e
...
...
@@ -747,7 +747,7 @@ describe MergeRequest do
end
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
wipless_title
=
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
let
(
:user2
)
{
create
(
:user
)
}
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
)
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
expect
(
response
).
to
have_gitlab_http_status
(
404
)
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
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