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
2563b031
Commit
2563b031
authored
Feb 24, 2020
by
James Lopez
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'add-epic-filter' into 'master'
Add epic in filtered search See merge request gitlab-org/gitlab!22958
parents
f9a3c233
eb12ab58
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
219 additions
and
30 deletions
+219
-30
app/assets/javascripts/filtered_search/available_dropdown_mappings.js
...avascripts/filtered_search/available_dropdown_mappings.js
+2
-2
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+4
-10
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+18
-5
app/assets/javascripts/filtered_search/visual_token_value.js
app/assets/javascripts/filtered_search/visual_token_value.js
+35
-0
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+9
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+2
-0
doc/user/search/index.md
doc/user/search/index.md
+1
-0
ee/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
...avascripts/filtered_search/available_dropdown_mappings.js
+19
-12
ee/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
...ts/filtered_search/issuable_filtered_search_token_keys.js
+45
-1
ee/app/helpers/ee/search_helper.rb
ee/app/helpers/ee/search_helper.rb
+6
-0
ee/app/views/shared/issuable/_filter_epic.html.haml
ee/app/views/shared/issuable/_filter_epic.html.haml
+17
-0
ee/changelogs/unreleased/add-epic-filter.yml
ee/changelogs/unreleased/add-epic-filter.yml
+5
-0
ee/spec/features/issues/filtered_search/dropdown_epic_spec.rb
...pec/features/issues/filtered_search/dropdown_epic_spec.rb
+53
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
No files found.
app/assets/javascripts/filtered_search/available_dropdown_mappings.js
View file @
2563b031
...
...
@@ -9,7 +9,7 @@ import DropdownUtils from './dropdown_utils';
import
{
mergeUrlParams
}
from
'
../lib/utils/url_utility
'
;
export
default
class
AvailableDropdownMappings
{
constructor
(
constructor
(
{
container
,
runnerTagsEndpoint
,
labelsEndpoint
,
...
...
@@ -18,7 +18,7 @@ export default class AvailableDropdownMappings {
groupsOnly
,
includeAncestorGroups
,
includeDescendantGroups
,
)
{
}
)
{
this
.
container
=
container
;
this
.
runnerTagsEndpoint
=
runnerTagsEndpoint
;
this
.
labelsEndpoint
=
labelsEndpoint
;
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
2563b031
...
...
@@ -13,6 +13,7 @@ export default class FilteredSearchDropdownManager {
labelsEndpoint
=
''
,
milestonesEndpoint
=
''
,
releasesEndpoint
=
''
,
epicsEndpoint
=
''
,
tokenizer
,
page
,
isGroup
,
...
...
@@ -27,6 +28,7 @@ export default class FilteredSearchDropdownManager {
this
.
labelsEndpoint
=
removeTrailingSlash
(
labelsEndpoint
);
this
.
milestonesEndpoint
=
removeTrailingSlash
(
milestonesEndpoint
);
this
.
releasesEndpoint
=
removeTrailingSlash
(
releasesEndpoint
);
this
.
epicsEndpoint
=
removeTrailingSlash
(
epicsEndpoint
);
this
.
tokenizer
=
tokenizer
;
this
.
filteredSearchTokenKeys
=
filteredSearchTokenKeys
||
FilteredSearchTokenKeys
;
this
.
filteredSearchInput
=
this
.
container
.
querySelector
(
'
.filtered-search
'
);
...
...
@@ -54,16 +56,8 @@ export default class FilteredSearchDropdownManager {
setupMapping
()
{
const
supportedTokens
=
this
.
filteredSearchTokenKeys
.
getKeys
();
const
availableMappings
=
new
AvailableDropdownMappings
(
this
.
container
,
this
.
runnerTagsEndpoint
,
this
.
labelsEndpoint
,
this
.
milestonesEndpoint
,
this
.
releasesEndpoint
,
this
.
groupsOnly
,
this
.
includeAncestorGroups
,
this
.
includeDescendantGroups
,
);
const
availableMappings
=
new
AvailableDropdownMappings
({
...
this
});
this
.
mapping
=
availableMappings
.
getAllowedMappings
(
supportedTokens
);
}
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
2563b031
...
...
@@ -45,6 +45,11 @@ export default class FilteredSearchManager {
this
.
filteredSearchTokenKeys
.
enableMultipleAssignees
();
}
const
{
epicsEndpoint
}
=
this
.
filteredSearchInput
.
dataset
;
if
(
!
epicsEndpoint
&&
this
.
filteredSearchTokenKeys
.
removeEpicToken
)
{
this
.
filteredSearchTokenKeys
.
removeEpicToken
();
}
this
.
recentSearchesStore
=
new
RecentSearchesStore
({
isLocalStorageAvailable
:
RecentSearchesService
.
isAvailable
(),
allowedKeys
:
this
.
filteredSearchTokenKeys
.
getKeys
(),
...
...
@@ -88,12 +93,20 @@ export default class FilteredSearchManager {
if
(
this
.
filteredSearchInput
)
{
this
.
tokenizer
=
FilteredSearchTokenizer
;
const
{
runnerTagsEndpoint
=
''
,
labelsEndpoint
=
''
,
milestonesEndpoint
=
''
,
releasesEndpoint
=
''
,
epicsEndpoint
=
''
,
}
=
this
.
filteredSearchInput
.
dataset
;
this
.
dropdownManager
=
new
FilteredSearchDropdownManager
({
runnerTagsEndpoint
:
this
.
filteredSearchInput
.
getAttribute
(
'
data-runner-tags-endpoint
'
)
||
''
,
labelsEndpoint
:
this
.
filteredSearchInput
.
getAttribute
(
'
data-labels-endpoint
'
)
||
''
,
milestonesEndpoint
:
this
.
filteredSearchInput
.
getAttribute
(
'
data-milestones-endpoint
'
)
||
''
,
releasesEndpoint
:
this
.
filteredSearchInput
.
getAttribute
(
'
data-releases-endpoint
'
)
||
''
,
runnerTagsEndpoint
,
labelsEndpoint
,
milestonesEndpoint
,
releasesEndpoint
,
epicsEndpoint
,
tokenizer
:
this
.
tokenizer
,
page
:
this
.
page
,
isGroup
:
this
.
isGroup
,
...
...
app/assets/javascripts/filtered_search/visual_token_value.js
View file @
2563b031
...
...
@@ -28,6 +28,8 @@ export default class VisualTokenValue {
this
.
updateUserTokenAppearance
(
tokenValueContainer
,
tokenValueElement
);
}
else
if
(
tokenType
===
'
my-reaction
'
)
{
this
.
updateEmojiTokenAppearance
(
tokenValueContainer
,
tokenValueElement
);
}
else
if
(
tokenType
===
'
epic
'
)
{
this
.
updateEpicLabel
(
tokenValueContainer
,
tokenValueElement
);
}
}
...
...
@@ -83,6 +85,39 @@ export default class VisualTokenValue {
.
catch
(()
=>
new
Flash
(
__
(
'
An error occurred while fetching label colors.
'
)));
}
updateEpicLabel
(
tokenValueContainer
)
{
const
tokenValue
=
this
.
tokenValue
.
replace
(
/^&/
,
''
);
const
filteredSearchInput
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
const
{
epicsEndpoint
}
=
filteredSearchInput
.
dataset
;
const
epicsEndpointWithParams
=
FilteredSearchVisualTokens
.
getEndpointWithQueryParams
(
`
${
epicsEndpoint
}
.json`
,
filteredSearchInput
.
dataset
.
endpointQueryParams
,
);
return
AjaxCache
.
retrieve
(
epicsEndpointWithParams
)
.
then
(
epics
=>
{
const
matchingEpic
=
(
epics
||
[]).
find
(
epic
=>
epic
.
id
===
Number
(
tokenValue
));
if
(
!
matchingEpic
)
{
return
;
}
VisualTokenValue
.
replaceEpicTitle
(
tokenValueContainer
,
matchingEpic
.
title
,
matchingEpic
.
id
);
})
.
catch
(()
=>
new
Flash
(
__
(
'
An error occurred while adding formatted title for epic
'
)));
}
static
replaceEpicTitle
(
tokenValueContainer
,
epicTitle
,
epicId
)
{
const
tokenContainer
=
tokenValueContainer
;
const
valueContainer
=
tokenContainer
.
querySelector
(
'
.value
'
);
if
(
valueContainer
)
{
tokenContainer
.
dataset
.
originalValue
=
valueContainer
.
innerText
;
valueContainer
.
innerText
=
`"
${
epicTitle
}
"::&
${
epicId
}
`
;
}
}
static
setTokenStyle
(
tokenValueContainer
,
backgroundColor
,
textColor
)
{
const
token
=
tokenValueContainer
;
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
2563b031
...
...
@@ -410,6 +410,15 @@
}
}
>
button
.dropdown-epic-button
{
flex-direction
:
column
;
.reference
{
color
:
$gl-gray-400
;
margin-top
:
$gl-padding-4
;
}
}
&
.droplab-item-selected
i
{
visibility
:
visible
;
}
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
2563b031
...
...
@@ -159,6 +159,8 @@
=
render_if_exists
'shared/issuable/filter_weight'
,
type:
type
=
render_if_exists
'shared/issuable/filter_epic'
,
type:
type
%button
.clear-search.hidden
{
type:
'button'
}
=
icon
(
'times'
)
.filter-dropdown-container.d-flex.flex-column.flex-md-row
...
...
doc/user/search/index.md
View file @
2563b031
...
...
@@ -41,6 +41,7 @@ groups:
-
[
Label
](
../project/labels.md
)
-
My-reaction
-
Confidential
-
Epic (
[
Introduced
](
https://gitlab.com/gitlab-org/gitlab/issues/195704
)
in GitLab 12.8)
-
Search for this text
1.
Select or type the operator to use for filtering the attribute. The following operators are
available:
...
...
ee/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
View file @
2563b031
...
...
@@ -4,35 +4,28 @@ import DropdownWeight from './dropdown_weight';
import
AvailableDropdownMappingsCE
from
'
~/filtered_search/available_dropdown_mappings
'
;
export
default
class
AvailableDropdownMappings
{
constructor
(
constructor
(
{
container
,
runnerTagsEndpoint
,
labelsEndpoint
,
milestonesEndpoint
,
epicsEndpoint
,
releasesEndpoint
,
groupsOnly
,
includeAncestorGroups
,
includeDescendantGroups
,
)
{
}
)
{
this
.
container
=
container
;
this
.
runnerTagsEndpoint
=
runnerTagsEndpoint
;
this
.
labelsEndpoint
=
labelsEndpoint
;
this
.
milestonesEndpoint
=
milestonesEndpoint
;
this
.
epicsEndpoint
=
epicsEndpoint
;
this
.
releasesEndpoint
=
releasesEndpoint
;
this
.
groupsOnly
=
groupsOnly
;
this
.
includeAncestorGroups
=
includeAncestorGroups
;
this
.
includeDescendantGroups
=
includeDescendantGroups
;
this
.
ceAvailableMappings
=
new
AvailableDropdownMappingsCE
(
container
,
runnerTagsEndpoint
,
labelsEndpoint
,
milestonesEndpoint
,
releasesEndpoint
,
groupsOnly
,
includeAncestorGroups
,
includeDescendantGroups
,
);
this
.
ceAvailableMappings
=
new
AvailableDropdownMappingsCE
({
...
this
});
}
getAllowedMappings
(
supportedTokens
)
{
...
...
@@ -60,6 +53,16 @@ export default class AvailableDropdownMappings {
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-weight
'
),
};
ceMappings
.
epic
=
{
reference
:
null
,
gl
:
DropdownNonUser
,
extraArguments
:
{
endpoint
:
this
.
getEpicEndpoint
(),
symbol
:
'
&
'
,
},
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-epic
'
),
};
return
this
.
ceAvailableMappings
.
buildMappings
(
supportedTokens
,
ceMappings
);
}
...
...
@@ -72,4 +75,8 @@ export default class AvailableDropdownMappings {
return
endpoint
;
}
getEpicEndpoint
()
{
return
`
${
this
.
epicsEndpoint
}
.json`
;
}
}
ee/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
View file @
2563b031
...
...
@@ -16,6 +16,15 @@ const weightTokenKey = {
tag
:
'
number
'
,
};
const
epicTokenKey
=
{
formattedKey
:
__
(
'
Epic
'
),
key
:
'
epic
'
,
type
:
'
string
'
,
param
:
'
id
'
,
symbol
:
'
&
'
,
icon
:
'
epic
'
,
};
const
weightConditions
=
[
{
url
:
'
weight=None
'
,
...
...
@@ -43,14 +52,42 @@ const weightConditions = [
},
];
const
epicConditions
=
[
{
url
:
'
epic_id=None
'
,
operator
:
'
=
'
,
tokenKey
:
'
epic
'
,
value
:
__
(
'
None
'
),
},
{
url
:
'
epic_id=Any
'
,
operator
:
'
=
'
,
tokenKey
:
'
epic
'
,
value
:
__
(
'
Any
'
),
},
{
url
:
'
not[epic_id]=None
'
,
operator
:
'
!=
'
,
tokenKey
:
'
epic
'
,
value
:
__
(
'
None
'
),
},
{
url
:
'
not[epic_id]=Any
'
,
operator
:
'
!=
'
,
tokenKey
:
'
epic
'
,
value
:
__
(
'
Any
'
),
},
];
/**
* Filter tokens for issues in EE.
*/
class
IssuesFilteredSearchTokenKeysEE
extends
FilteredSearchTokenKeys
{
constructor
()
{
super
([...
tokenKeys
,
weightTokenKey
],
alternativeTokenKeys
,
[
super
([...
tokenKeys
,
epicTokenKey
,
weightTokenKey
],
alternativeTokenKeys
,
[
...
conditions
,
...
weightConditions
,
...
epicConditions
,
]);
}
...
...
@@ -66,6 +103,13 @@ class IssuesFilteredSearchTokenKeysEE extends FilteredSearchTokenKeys {
assigneeTokenKey
.
type
=
'
array
'
;
assigneeTokenKey
.
param
=
'
username[]
'
;
}
removeEpicToken
()
{
const
index
=
this
.
tokenKeys
.
findIndex
(
token
=>
token
.
key
===
epicTokenKey
.
key
);
if
(
index
>=
0
)
{
this
.
tokenKeys
.
splice
(
index
,
1
);
}
}
}
export
default
new
IssuesFilteredSearchTokenKeysEE
();
ee/app/helpers/ee/search_helper.rb
View file @
2563b031
...
...
@@ -10,6 +10,12 @@ module EE
options
=
super
options
[
:data
][
:'multiple-assignees'
]
=
'true'
if
search_multiple_assignees?
(
type
)
if
@project
&
.
group
options
[
:data
][
'epics-endpoint'
]
=
group_epics_path
(
@project
.
group
)
elsif
@group
.
present?
options
[
:data
][
'epics-endpoint'
]
=
group_epics_path
(
@group
)
end
options
end
...
...
ee/app/views/shared/issuable/_filter_epic.html.haml
0 → 100644
View file @
2563b031
-
type
=
local_assigns
.
fetch
(
:type
)
-
return
unless
type
==
:issues
||
type
==
:boards
||
type
==
:boards_modal
||
type
==
:issues_analytics
#js-dropdown-epic
.filtered-search-input-dropdown-menu.dropdown-menu
%ul
{
'data-dropdown'
=>
true
}
%li
.filter-dropdown-item
{
'data-value'
=>
'None'
}
%button
.btn.btn-link
=
_
(
'None'
)
%li
.filter-dropdown-item
{
'data-value'
=>
'Any'
}
%button
.btn.btn-link
=
_
(
'Any'
)
%li
.divider.droplab-item-ignore
%ul
.filter-dropdown
{
data:
{
dropdown:
true
,
dynamic:
true
}
}
%li
.filter-dropdown-item
{
data:
{
value:
'
{{
id
}}
'
}
}
%button
.btn.btn-link.dropdown-epic-button
%span
=
"{{title}}"
%span
.reference
=
"&"
+
"{{id}}"
ee/changelogs/unreleased/add-epic-filter.yml
0 → 100644
View file @
2563b031
---
title
:
Add epic in filtered search
merge_request
:
22958
author
:
type
:
added
ee/spec/features/issues/filtered_search/dropdown_epic_spec.rb
0 → 100644
View file @
2563b031
# frozen_string_literal: true
require
'spec_helper'
describe
'Dropdown epic'
,
:js
do
include
FilteredSearchHelpers
let
(
:group
)
{
create
(
:group
)
}
let
(
:project
)
{
create
(
:project
,
group:
group
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:filtered_search
)
{
find
(
'.filtered-search'
)
}
let
(
:js_dropdown_epic
)
{
'#js-dropdown-epic'
}
let
(
:filter_dropdown
)
{
find
(
"
#{
js_dropdown_epic
}
.filter-dropdown"
)
}
let!
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let!
(
:epic2
)
{
create
(
:epic
,
group:
group
)
}
let!
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
before
do
stub_licensed_features
(
epics:
true
)
group
.
add_maintainer
(
user
)
sign_in
(
user
)
visit
issues_group_path
(
group
)
end
describe
'behavior'
do
it
'loads all the epics when opened'
do
input_filtered_search
(
'epic='
,
submit:
false
,
extra_space:
false
)
expect_filtered_search_dropdown_results
(
filter_dropdown
,
2
)
end
it
'selects epic and correct title is loaded'
do
input_filtered_search
(
'epic='
,
submit:
false
,
extra_space:
false
)
wait_for_requests
find
(
'li'
,
text:
epic
.
title
).
click
expect
(
find
(
'.filtered-search-token .value'
).
text
).
to
eq
(
"
\"
#{
epic
.
title
}
\"
::&
#{
epic
.
id
}
"
)
end
it
'filters issues by epic'
do
input_filtered_search
(
'epic='
,
submit:
false
,
extra_space:
false
)
wait_for_requests
find
(
'li'
,
text:
epic2
.
title
).
click
expect
(
find
(
'.issue-title-text'
).
text
).
to
eq
(
"
#{
issue
.
title
}
"
)
end
end
end
locale/gitlab.pot
View file @
2563b031
...
...
@@ -1728,6 +1728,9 @@ msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
msgid "An error occurred while adding formatted title for epic"
msgstr ""
msgid "An error occurred while checking group path"
msgstr ""
...
...
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