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
fb245442
Commit
fb245442
authored
Oct 12, 2021
by
Florie Guibert
Committed by
Simon Knox
Oct 12, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor EpicsToken to use BaseToken
parent
3b3dce4a
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
253 additions
and
181 deletions
+253
-181
app/assets/javascripts/issues_list/components/issues_list_app.vue
...ts/javascripts/issues_list/components/issues_list_app.vue
+5
-13
app/assets/javascripts/issues_list/index.js
app/assets/javascripts/issues_list/index.js
+2
-2
app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
...ponents/filtered_search_bar/queries/epic.fragment.graphql
+15
-0
app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
...ts/filtered_search_bar/queries/search_epics.query.graphql
+16
-0
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
...ared/components/filtered_search_bar/tokens/base_token.vue
+20
-6
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
...ared/components/filtered_search_bar/tokens/epic_token.vue
+57
-81
ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js
...ssets/javascripts/roadmap/mixins/filtered_search_mixin.js
+4
-21
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
+9
-1
ee/app/helpers/ee/issues_helper.rb
ee/app/helpers/ee/issues_helper.rb
+2
-2
ee/spec/frontend/roadmap/mock_data.js
ee/spec/frontend/roadmap/mock_data.js
+4
-2
ee/spec/helpers/ee/issues_helper_spec.rb
ee/spec/helpers/ee/issues_helper_spec.rb
+6
-6
spec/frontend/issues_list/components/issues_list_app_spec.js
spec/frontend/issues_list/components/issues_list_app_spec.js
+3
-3
spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
...nd/vue_shared/components/filtered_search_bar/mock_data.js
+56
-1
spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
.../components/filtered_search_bar/tokens/epic_token_spec.js
+54
-43
No files found.
app/assets/javascripts/issues_list/components/issues_list_app.vue
View file @
fb245442
...
@@ -122,7 +122,7 @@ export default {
...
@@ -122,7 +122,7 @@ export default {
fullPath
:
{
fullPath
:
{
default
:
''
,
default
:
''
,
},
},
group
Epics
Path
:
{
groupPath
:
{
default
:
''
,
default
:
''
,
},
},
hasAnyIssues
:
{
hasAnyIssues
:
{
...
@@ -371,16 +371,18 @@ export default {
...
@@ -371,16 +371,18 @@ export default {
});
});
}
}
if
(
this
.
group
Epics
Path
)
{
if
(
this
.
groupPath
)
{
tokens
.
push
({
tokens
.
push
({
type
:
TOKEN_TYPE_EPIC
,
type
:
TOKEN_TYPE_EPIC
,
title
:
TOKEN_TITLE_EPIC
,
title
:
TOKEN_TITLE_EPIC
,
icon
:
'
epic
'
,
icon
:
'
epic
'
,
token
:
EpicToken
,
token
:
EpicToken
,
unique
:
true
,
unique
:
true
,
symbol
:
'
&
'
,
idProperty
:
'
id
'
,
idProperty
:
'
id
'
,
useIdValue
:
true
,
useIdValue
:
true
,
fetchEpics
:
this
.
fetchEpics
,
recentSuggestionsStorageKey
:
`
${
this
.
fullPath
}
-issues-recent-tokens-epic_id`
,
fullPath
:
this
.
groupPath
,
});
});
}
}
...
@@ -450,16 +452,6 @@ export default {
...
@@ -450,16 +452,6 @@ export default {
fetchEmojis
(
search
)
{
fetchEmojis
(
search
)
{
return
this
.
fetchWithCache
(
this
.
autocompleteAwardEmojisPath
,
'
emojis
'
,
'
name
'
,
search
);
return
this
.
fetchWithCache
(
this
.
autocompleteAwardEmojisPath
,
'
emojis
'
,
'
name
'
,
search
);
},
},
async
fetchEpics
({
search
})
{
const
epics
=
await
this
.
fetchWithCache
(
this
.
groupEpicsPath
,
'
epics
'
);
if
(
!
search
)
{
return
epics
.
slice
(
0
,
MAX_LIST_SIZE
);
}
const
number
=
Number
(
search
);
return
Number
.
isNaN
(
number
)
?
fuzzaldrinPlus
.
filter
(
epics
,
search
,
{
key
:
'
title
'
})
:
epics
.
filter
((
epic
)
=>
epic
.
id
===
number
);
},
fetchLabels
(
search
)
{
fetchLabels
(
search
)
{
return
this
.
$apollo
return
this
.
$apollo
.
query
({
.
query
({
...
...
app/assets/javascripts/issues_list/index.js
View file @
fb245442
...
@@ -119,7 +119,7 @@ export function mountIssuesListApp() {
...
@@ -119,7 +119,7 @@ export function mountIssuesListApp() {
emptyStateSvgPath
,
emptyStateSvgPath
,
exportCsvPath
,
exportCsvPath
,
fullPath
,
fullPath
,
group
Epics
Path
,
groupPath
,
hasAnyIssues
,
hasAnyIssues
,
hasAnyProjects
,
hasAnyProjects
,
hasBlockedIssuesFeature
,
hasBlockedIssuesFeature
,
...
@@ -152,7 +152,7 @@ export function mountIssuesListApp() {
...
@@ -152,7 +152,7 @@ export function mountIssuesListApp() {
canBulkUpdate
:
parseBoolean
(
canBulkUpdate
),
canBulkUpdate
:
parseBoolean
(
canBulkUpdate
),
emptyStateSvgPath
,
emptyStateSvgPath
,
fullPath
,
fullPath
,
group
Epics
Path
,
groupPath
,
hasAnyIssues
:
parseBoolean
(
hasAnyIssues
),
hasAnyIssues
:
parseBoolean
(
hasAnyIssues
),
hasAnyProjects
:
parseBoolean
(
hasAnyProjects
),
hasAnyProjects
:
parseBoolean
(
hasAnyProjects
),
hasBlockedIssuesFeature
:
parseBoolean
(
hasBlockedIssuesFeature
),
hasBlockedIssuesFeature
:
parseBoolean
(
hasBlockedIssuesFeature
),
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
0 → 100644
View file @
fb245442
fragment
EpicNode
on
Epic
{
id
iid
group
{
fullPath
}
title
state
reference
referencePath
:
reference
(
full
:
true
)
webPath
webUrl
createdAt
closedAt
}
app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
0 → 100644
View file @
fb245442
#import "./epic.fragment.graphql"
query
searchEpics
(
$fullPath
:
ID
!,
$search
:
String
,
$state
:
EpicState
)
{
group
(
fullPath
:
$fullPath
)
{
epics
(
search
:
$search
state
:
$state
includeAncestorGroups
:
true
includeDescendantGroups
:
false
)
{
nodes
{
...
EpicNode
}
}
}
}
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
View file @
fb245442
...
@@ -67,6 +67,11 @@ export default {
...
@@ -67,6 +67,11 @@ export default {
required
:
false
,
required
:
false
,
default
:
'
id
'
,
default
:
'
id
'
,
},
},
searchBy
:
{
type
:
String
,
required
:
false
,
default
:
undefined
,
},
},
},
data
()
{
data
()
{
return
{
return
{
...
@@ -112,16 +117,18 @@ export default {
...
@@ -112,16 +117,18 @@ export default {
);
);
},
},
showDefaultSuggestions
()
{
showDefaultSuggestions
()
{
return
this
.
availableDefaultSuggestions
.
length
;
return
this
.
availableDefaultSuggestions
.
length
>
0
;
},
},
showRecentSuggestions
()
{
showRecentSuggestions
()
{
return
this
.
isRecentSuggestionsEnabled
&&
this
.
recentSuggestions
.
length
&&
!
this
.
searchKey
;
return
(
this
.
isRecentSuggestionsEnabled
&&
this
.
recentSuggestions
.
length
>
0
&&
!
this
.
searchKey
);
},
},
showPreloadedSuggestions
()
{
showPreloadedSuggestions
()
{
return
this
.
preloadedSuggestions
.
length
&&
!
this
.
searchKey
;
return
this
.
preloadedSuggestions
.
length
>
0
&&
!
this
.
searchKey
;
},
},
showAvailableSuggestions
()
{
showAvailableSuggestions
()
{
return
this
.
availableSuggestions
.
length
;
return
this
.
availableSuggestions
.
length
>
0
;
},
},
showSuggestions
()
{
showSuggestions
()
{
// These conditions must match the template under `#suggestions` slot
// These conditions must match the template under `#suggestions` slot
...
@@ -134,13 +141,19 @@ export default {
...
@@ -134,13 +141,19 @@ export default {
this
.
showAvailableSuggestions
this
.
showAvailableSuggestions
);
);
},
},
searchTerm
()
{
return
this
.
searchBy
&&
this
.
activeTokenValue
?
this
.
activeTokenValue
[
this
.
searchBy
]
:
undefined
;
},
},
},
watch
:
{
watch
:
{
active
:
{
active
:
{
immediate
:
true
,
immediate
:
true
,
handler
(
newValue
)
{
handler
(
newValue
)
{
if
(
!
newValue
&&
!
this
.
suggestions
.
length
)
{
if
(
!
newValue
&&
!
this
.
suggestions
.
length
)
{
this
.
$emit
(
'
fetch-suggestions
'
,
this
.
value
.
data
);
const
search
=
this
.
searchTerm
?
this
.
searchTerm
:
this
.
value
.
data
;
this
.
$emit
(
'
fetch-suggestions
'
,
search
);
}
}
},
},
},
},
...
@@ -150,7 +163,8 @@ export default {
...
@@ -150,7 +163,8 @@ export default {
this
.
searchKey
=
data
;
this
.
searchKey
=
data
;
if
(
!
this
.
suggestionsLoading
&&
!
this
.
activeTokenValue
)
{
if
(
!
this
.
suggestionsLoading
&&
!
this
.
activeTokenValue
)
{
this
.
$emit
(
'
fetch-suggestions
'
,
data
);
const
search
=
this
.
searchTerm
?
this
.
searchTerm
:
data
;
this
.
$emit
(
'
fetch-suggestions
'
,
search
);
}
}
},
DEBOUNCE_DELAY
),
},
DEBOUNCE_DELAY
),
handleTokenValueSelected
(
activeTokenValue
)
{
handleTokenValueSelected
(
activeTokenValue
)
{
...
...
app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
View file @
fb245442
<
script
>
<
script
>
import
{
import
{
GlFilteredSearchSuggestion
}
from
'
@gitlab/ui
'
;
GlDropdownDivider
,
GlFilteredSearchSuggestion
,
GlFilteredSearchToken
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
DEBOUNCE_DELAY
,
DEFAULT_NONE_ANY
,
FILTER_NONE_ANY
,
OPERATOR_IS_NOT
}
from
'
../constants
'
;
import
{
DEFAULT_NONE_ANY
,
FILTER_NONE_ANY
,
OPERATOR_IS_NOT
}
from
'
../constants
'
;
import
searchEpicsQuery
from
'
../queries/search_epics.query.graphql
'
;
import
BaseToken
from
'
./base_token.vue
'
;
export
default
{
export
default
{
separator
:
'
::&
'
,
prefix
:
'
&
'
,
separator
:
'
::
'
,
components
:
{
components
:
{
GlDropdownDivider
,
BaseToken
,
GlFilteredSearchToken
,
GlFilteredSearchSuggestion
,
GlFilteredSearchSuggestion
,
GlLoadingIcon
,
},
},
props
:
{
props
:
{
config
:
{
config
:
{
...
@@ -27,11 +24,15 @@ export default {
...
@@ -27,11 +24,15 @@ export default {
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
active
:
{
type
:
Boolean
,
required
:
true
,
},
},
},
data
()
{
data
()
{
return
{
return
{
epics
:
this
.
config
.
initialEpics
||
[],
epics
:
this
.
config
.
initialEpics
||
[],
loading
:
tru
e
,
loading
:
fals
e
,
};
};
},
},
computed
:
{
computed
:
{
...
@@ -56,98 +57,73 @@ export default {
...
@@ -56,98 +57,73 @@ export default {
}
}
return
this
.
defaultEpics
;
return
this
.
defaultEpics
;
},
},
activeEpic
()
{
if
(
this
.
currentValue
&&
this
.
epics
.
length
)
{
// Check if current value is an epic ID.
if
(
typeof
this
.
currentValue
===
'
number
'
)
{
return
this
.
epics
.
find
((
epic
)
=>
epic
[
this
.
idProperty
]
===
this
.
currentValue
);
}
// Current value is a string.
const
[
groupPath
,
idProperty
]
=
this
.
currentValue
?.
split
(
this
.
$options
.
separator
);
return
this
.
epics
.
find
(
(
epic
)
=>
epic
.
group_full_path
===
groupPath
&&
epic
[
this
.
idProperty
]
===
parseInt
(
idProperty
,
10
),
);
}
return
null
;
},
displayText
()
{
return
`
${
this
.
activeEpic
?.
title
}
$
{
this
.
$options
.
separator
}
$
{
this
.
activeEpic
?.
iid
}
`;
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.epics.length) {
this.searchEpics({ data: this.currentValue });
}
},
},
},
},
methods
:
{
methods
:
{
fetchEpicsBySearchTerm({ epicPath = '', search = '' }) {
fetchEpics
(
search
=
''
)
{
return
this
.
$apollo
.
query
({
query
:
searchEpicsQuery
,
variables
:
{
fullPath
:
this
.
config
.
fullPath
,
search
},
})
.
then
(({
data
})
=>
data
.
group
?.
epics
.
nodes
);
},
fetchEpicsBySearchTerm
(
search
)
{
this
.
loading
=
true
;
this
.
loading
=
true
;
this.config
this
.
fetchEpics
(
search
)
.fetchEpics({ epicPath, search })
.
then
((
response
)
=>
{
.
then
((
response
)
=>
{
this.epics = Array.isArray(response) ? response : response.data;
this
.
epics
=
Array
.
isArray
(
response
)
?
response
:
response
?
.
data
;
})
})
.
catch
(()
=>
createFlash
({
message
:
__
(
'
There was a problem fetching epics.
'
)
}))
.
catch
(()
=>
createFlash
({
message
:
__
(
'
There was a problem fetching epics.
'
)
}))
.
finally
(()
=>
{
.
finally
(()
=>
{
this
.
loading
=
false
;
this
.
loading
=
false
;
});
});
},
},
searchEpics: debounce(function debouncedSearch({ data }) {
getActiveEpic
(
epics
,
data
)
{
let epicPath = this.activeEpic?.web_url;
if
(
data
&&
epics
.
length
)
{
return
epics
.
find
((
epic
)
=>
this
.
getValue
(
epic
)
===
data
);
// When user visits the page with token value already included in filters
// We don't have any information about selected token except for its
// group path and iid joined by separator, so we need to manually
// compose epic path from it.
if (data.includes?.(this.$options.separator)) {
const [groupPath, epicIid] = data.split(this.$options.separator);
epicPath = `
/
groups
/
$
{
groupPath
}
/-/
epics
/
$
{
epicIid
}
`;
}
}
this.fetchEpicsBySearchTerm({ epicPath, search: data });
return
undefined
;
}, DEBOUNCE_DELAY),
},
getValue
(
epic
)
{
getValue
(
epic
)
{
return this.config.useIdValue
return
this
.
getEpicIdProperty
(
epic
).
toString
();
? String(epic[this.idProperty])
},
: `
$
{
epic
.
group_full_path
}
$
{
this
.
$options
.
separator
}
$
{
epic
[
this
.
idProperty
]}
`;
displayValue
(
epic
)
{
return
`
${
this
.
$options
.
prefix
}${
this
.
getEpicIdProperty
(
epic
)}${
this
.
$options
.
separator
}${
epic
?.
title
}
`;
},
getEpicIdProperty(epic) {
return getIdFromGraphQLId(epic[this.idProperty]);
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<
gl-filtered-search
-token
<
base
-token
:config=
"config"
:config=
"config"
v-bind=
"
{ ...$props, ...$attrs }"
:value=
"value"
:active=
"active"
:suggestions-loading=
"loading"
:suggestions=
"epics"
:get-active-token-value=
"getActiveEpic"
:default-suggestions=
"availableDefaultEpics"
:recent-suggestions-storage-key=
"config.recentSuggestionsStorageKey"
search-by=
"title"
@
fetch-suggestions=
"fetchEpicsBySearchTerm"
v-on=
"$listeners"
v-on=
"$listeners"
@input="searchEpics"
>
>
<template
#view
="
{
inputValue
}">
<template
#view
="
{
viewTokenProps: { inputValue, activeTokenValue }
}">
{{
active
Epic
?
displayText
:
inputValue
}}
{{
active
TokenValue
?
displayValue
(
activeTokenValue
)
:
inputValue
}}
</
template
>
</
template
>
<
template
#suggestions
>
<
template
#suggestions
-list=
"{ suggestions }"
>
<gl-filtered-search-suggestion
<gl-filtered-search-suggestion
v-for=
"epic in
availableDefaultEpic
s"
v-for=
"epic in
suggestion
s"
:key=
"epic.
value
"
:key=
"epic.
id
"
:value=
"
epic.value
"
:value=
"
getValue(epic)
"
>
>
{{
epic
.
t
ext
}}
{{
epic
.
t
itle
}}
</gl-filtered-search-suggestion>
</gl-filtered-search-suggestion>
<gl-dropdown-divider
v-if=
"availableDefaultEpics.length"
/>
<gl-loading-icon
v-if=
"loading"
size=
"sm"
/>
<template
v-else
>
<gl-filtered-search-suggestion
v-for=
"epic in epics"
:key=
"epic.id"
:value=
"getValue(epic)"
>
{{
epic
.
title
}}
</gl-filtered-search-suggestion>
</
template
>
</
template
>
</
template
>
</
gl-filtered-search
-token>
</
base
-token>
</template>
</template>
ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js
View file @
fb245442
...
@@ -2,7 +2,6 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
...
@@ -2,7 +2,6 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import
Api
from
'
~/api
'
;
import
Api
from
'
~/api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
import
{
...
@@ -150,29 +149,13 @@ export default {
...
@@ -150,29 +149,13 @@ export default {
icon
:
'
epic
'
,
icon
:
'
epic
'
,
title
:
__
(
'
Epic
'
),
title
:
__
(
'
Epic
'
),
unique
:
true
,
unique
:
true
,
idProperty
:
'
iid
'
,
useIdValue
:
true
,
symbol
:
'
&
'
,
symbol
:
'
&
'
,
token
:
EpicToken
,
token
:
EpicToken
,
operators
:
OPERATOR_IS_ONLY
,
operators
:
OPERATOR_IS_ONLY
,
defaultEpics
:
[],
recentSuggestionsStorageKey
:
`
${
this
.
groupFullPath
}
-epics-recent-tokens-epic_iid`
,
fetchEpics
:
({
epicPath
=
''
,
search
=
''
})
=>
{
fullPath
:
this
.
groupFullPath
,
const
epicId
=
Number
(
search
)
||
null
;
// No search criteria or path has been provided, fetch all epics.
if
(
!
epicPath
&&
!
search
)
{
return
axios
.
get
(
this
.
listEpicsPath
);
}
else
if
(
epicPath
)
{
// Just epicPath has been provided, fetch a specific epic.
return
axios
.
get
(
epicPath
).
then
(({
data
})
=>
[
data
]);
}
else
if
(
!
epicPath
&&
epicId
)
{
// Exact epic ID provided, fetch the epic.
return
axios
.
get
(
joinPaths
(
this
.
listEpicsPath
,
String
(
epicId
)))
.
then
(({
data
})
=>
[
data
]);
}
// Search for an epic.
return
axios
.
get
(
this
.
listEpicsPath
,
{
params
:
{
search
}
});
},
});
});
}
}
...
...
ee/app/assets/javascripts/roadmap/roadmap_bundle.js
View file @
fb245442
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
{
parseBoolean
,
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
parseBoolean
,
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
visitUrl
,
mergeUrlParams
,
queryToObject
}
from
'
~/lib/utils/url_utility
'
;
import
{
visitUrl
,
mergeUrlParams
,
queryToObject
}
from
'
~/lib/utils/url_utility
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
...
@@ -28,6 +30,12 @@ export default () => {
...
@@ -28,6 +30,12 @@ export default () => {
return
false
;
return
false
;
}
}
Vue
.
use
(
VueApollo
);
const
defaultClient
=
createDefaultClient
({},
{
assumeImmutableResults
:
true
});
const
apolloProvider
=
new
VueApollo
({
defaultClient
,
});
// This event handler is to be removed in 11.1 once
// This event handler is to be removed in 11.1 once
// we allow user to save selected preset in db
// we allow user to save selected preset in db
if
(
presetButtonsContainer
)
{
if
(
presetButtonsContainer
)
{
...
@@ -43,7 +51,7 @@ export default () => {
...
@@ -43,7 +51,7 @@ export default () => {
return
new
Vue
({
return
new
Vue
({
el
,
el
,
apolloProvider
:
{}
,
apolloProvider
,
store
:
createStore
(),
store
:
createStore
(),
components
:
{
components
:
{
roadmapApp
,
roadmapApp
,
...
...
ee/app/helpers/ee/issues_helper.rb
View file @
fb245442
...
@@ -57,7 +57,7 @@ module EE
...
@@ -57,7 +57,7 @@ module EE
def
project_issues_list_data
(
project
,
current_user
,
finder
)
def
project_issues_list_data
(
project
,
current_user
,
finder
)
super
.
tap
do
|
data
|
super
.
tap
do
|
data
|
if
project
.
feature_available?
(
:epics
)
&&
project
.
group
if
project
.
feature_available?
(
:epics
)
&&
project
.
group
data
[
:group_
epics_path
]
=
group_epics_path
(
project
.
group
,
format: :json
)
data
[
:group_
path
]
=
project
.
group
.
full_path
end
end
end
end
end
end
...
@@ -68,7 +68,7 @@ module EE
...
@@ -68,7 +68,7 @@ module EE
data
[
:can_bulk_update
]
=
(
can?
(
current_user
,
:admin_issue
,
group
)
&&
group
.
feature_available?
(
:group_bulk_edit
)).
to_s
data
[
:can_bulk_update
]
=
(
can?
(
current_user
,
:admin_issue
,
group
)
&&
group
.
feature_available?
(
:group_bulk_edit
)).
to_s
if
group
.
feature_available?
(
:epics
)
if
group
.
feature_available?
(
:epics
)
data
[
:group_
epics_path
]
=
group_epics_path
(
group
,
format: :json
)
data
[
:group_
path
]
=
group
.
full_path
end
end
end
end
end
end
...
...
ee/spec/frontend/roadmap/mock_data.js
View file @
fb245442
...
@@ -838,8 +838,10 @@ export const mockEpicTokenConfig = {
...
@@ -838,8 +838,10 @@ export const mockEpicTokenConfig = {
symbol
:
'
&
'
,
symbol
:
'
&
'
,
token
:
EpicToken
,
token
:
EpicToken
,
operators
:
OPERATOR_IS_ONLY
,
operators
:
OPERATOR_IS_ONLY
,
defaultEpics
:
[],
idProperty
:
'
iid
'
,
fetchEpics
:
expect
.
any
(
Function
),
useIdValue
:
true
,
recentSuggestionsStorageKey
:
'
gitlab-org-epics-recent-tokens-epic_iid
'
,
fullPath
:
'
gitlab-org
'
,
};
};
export
const
mockReactionEmojiTokenConfig
=
{
export
const
mockReactionEmojiTokenConfig
=
{
...
...
ee/spec/helpers/ee/issues_helper_spec.rb
View file @
fb245442
...
@@ -147,7 +147,7 @@ RSpec.describe EE::IssuesHelper do
...
@@ -147,7 +147,7 @@ RSpec.describe EE::IssuesHelper do
has_issue_weights_feature:
'true'
,
has_issue_weights_feature:
'true'
,
has_iterations_feature:
'true'
,
has_iterations_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
group_
epics_path:
group_epics_path
(
project
.
group
,
format: :json
)
group_
path:
project
.
group
.
full_path
}
}
expect
(
helper
.
project_issues_list_data
(
project
,
current_user
,
finder
)).
to
include
(
expected
)
expect
(
helper
.
project_issues_list_data
(
project
,
current_user
,
finder
)).
to
include
(
expected
)
...
@@ -156,8 +156,8 @@ RSpec.describe EE::IssuesHelper do
...
@@ -156,8 +156,8 @@ RSpec.describe EE::IssuesHelper do
context
'when project does not have group'
do
context
'when project does not have group'
do
let
(
:project_with_no_group
)
{
create
:project
}
let
(
:project_with_no_group
)
{
create
:project
}
it
'does not return group_
epics_
path'
do
it
'does not return group_path'
do
expect
(
helper
.
project_issues_list_data
(
project_with_no_group
,
current_user
,
finder
)).
not_to
include
(
:group_
epics_
path
)
expect
(
helper
.
project_issues_list_data
(
project_with_no_group
,
current_user
,
finder
)).
not_to
include
(
:group_path
)
end
end
end
end
end
end
...
@@ -179,7 +179,7 @@ RSpec.describe EE::IssuesHelper do
...
@@ -179,7 +179,7 @@ RSpec.describe EE::IssuesHelper do
result
=
helper
.
project_issues_list_data
(
project
,
current_user
,
finder
)
result
=
helper
.
project_issues_list_data
(
project
,
current_user
,
finder
)
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
not_to
include
(
:group_
epics_
path
)
expect
(
result
).
not_to
include
(
:group_path
)
end
end
end
end
end
end
...
@@ -208,7 +208,7 @@ RSpec.describe EE::IssuesHelper do
...
@@ -208,7 +208,7 @@ RSpec.describe EE::IssuesHelper do
has_issue_weights_feature:
'true'
,
has_issue_weights_feature:
'true'
,
has_iterations_feature:
'true'
,
has_iterations_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
group_
epics_path:
group_epics_path
(
project
.
group
,
format: :json
)
group_
path:
project
.
group
.
full_path
}
}
expect
(
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
,
projects
)).
to
include
(
expected
)
expect
(
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
,
projects
)).
to
include
(
expected
)
...
@@ -233,7 +233,7 @@ RSpec.describe EE::IssuesHelper do
...
@@ -233,7 +233,7 @@ RSpec.describe EE::IssuesHelper do
result
=
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
,
projects
)
result
=
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
,
projects
)
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
not_to
include
(
:group_
epics_
path
)
expect
(
result
).
not_to
include
(
:group_path
)
end
end
end
end
end
end
...
...
spec/frontend/issues_list/components/issues_list_app_spec.js
View file @
fb245442
...
@@ -520,7 +520,7 @@ describe('IssuesListApp component', () => {
...
@@ -520,7 +520,7 @@ describe('IssuesListApp component', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
=
mountComponent
({
wrapper
=
mountComponent
({
provide
:
{
provide
:
{
group
Epics
Path
:
''
,
groupPath
:
''
,
},
},
});
});
});
});
...
@@ -536,7 +536,7 @@ describe('IssuesListApp component', () => {
...
@@ -536,7 +536,7 @@ describe('IssuesListApp component', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
=
mountComponent
({
wrapper
=
mountComponent
({
provide
:
{
provide
:
{
group
Epics
Path
:
''
,
groupPath
:
''
,
},
},
});
});
});
});
...
@@ -564,7 +564,7 @@ describe('IssuesListApp component', () => {
...
@@ -564,7 +564,7 @@ describe('IssuesListApp component', () => {
provide
:
{
provide
:
{
isSignedIn
:
true
,
isSignedIn
:
true
,
projectIterationsPath
:
'
project/iterations/path
'
,
projectIterationsPath
:
'
project/iterations/path
'
,
group
EpicsPath
:
'
group/epics
/path
'
,
group
Path
:
'
group
/path
'
,
hasIssueWeightsFeature
:
true
,
hasIssueWeightsFeature
:
true
,
},
},
});
});
...
...
spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
View file @
fb245442
...
@@ -141,7 +141,62 @@ export const mockEpicToken = {
...
@@ -141,7 +141,62 @@ export const mockEpicToken = {
token
:
EpicToken
,
token
:
EpicToken
,
operators
:
OPERATOR_IS_ONLY
,
operators
:
OPERATOR_IS_ONLY
,
idProperty
:
'
iid
'
,
idProperty
:
'
iid
'
,
fetchEpics
:
()
=>
Promise
.
resolve
({
data
:
mockEpics
}),
fullPath
:
'
gitlab-org
'
,
};
export
const
mockEpicNode1
=
{
__typename
:
'
Epic
'
,
parent
:
null
,
id
:
'
gid://gitlab/Epic/40
'
,
iid
:
'
2
'
,
title
:
'
Marketing epic
'
,
description
:
'
Mock epic description
'
,
state
:
'
opened
'
,
startDate
:
'
2017-12-25
'
,
dueDate
:
'
2018-02-15
'
,
webUrl
:
'
http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1
'
,
hasChildren
:
false
,
hasParent
:
false
,
confidential
:
false
,
};
export
const
mockEpicNode2
=
{
__typename
:
'
Epic
'
,
parent
:
null
,
id
:
'
gid://gitlab/Epic/41
'
,
iid
:
'
3
'
,
title
:
'
Another marketing
'
,
startDate
:
'
2017-12-26
'
,
dueDate
:
'
2018-03-10
'
,
state
:
'
opened
'
,
webUrl
:
'
http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2
'
,
};
export
const
mockGroupEpicsQueryResponse
=
{
data
:
{
group
:
{
id
:
'
gid://gitlab/Group/1
'
,
name
:
'
Gitlab Org
'
,
epics
:
{
edges
:
[
{
node
:
{
...
mockEpicNode1
,
},
__typename
:
'
EpicEdge
'
,
},
{
node
:
{
...
mockEpicNode2
,
},
__typename
:
'
EpicEdge
'
,
},
],
__typename
:
'
EpicConnection
'
,
},
__typename
:
'
Group
'
,
},
},
};
};
export
const
mockReactionEmojiToken
=
{
export
const
mockReactionEmojiToken
=
{
...
...
spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
View file @
fb245442
import
{
GlFilteredSearchToken
,
GlFilteredSearchToken
Segment
}
from
'
@gitlab/ui
'
;
import
{
GlFilteredSearchTokenSegment
}
from
'
@gitlab/ui
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
searchEpicsQuery
from
'
~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
'
;
import
EpicToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
'
;
import
EpicToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
'
;
import
BaseToken
from
'
~/vue_shared/components/filtered_search_bar/tokens/base_token.vue
'
;
import
{
mockEpicToken
,
mockEpics
}
from
'
../mock_data
'
;
import
{
mockEpicToken
,
mockEpics
,
mockGroupEpicsQueryResponse
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
jest
.
mock
(
'
~/flash
'
);
Vue
.
use
(
VueApollo
);
const
defaultStubs
=
{
const
defaultStubs
=
{
Portal
:
true
,
Portal
:
true
,
...
@@ -21,31 +27,39 @@ const defaultStubs = {
...
@@ -21,31 +27,39 @@ const defaultStubs = {
},
},
};
};
function
createComponent
(
options
=
{})
{
const
{
config
=
mockEpicToken
,
value
=
{
data
:
''
},
active
=
false
,
stubs
=
defaultStubs
,
}
=
options
;
return
mount
(
EpicToken
,
{
propsData
:
{
config
,
value
,
active
,
},
provide
:
{
portalName
:
'
fake target
'
,
alignSuggestions
:
function
fakeAlignSuggestions
()
{},
suggestionsListClass
:
()
=>
'
custom-class
'
,
},
stubs
,
});
}
describe
(
'
EpicToken
'
,
()
=>
{
describe
(
'
EpicToken
'
,
()
=>
{
let
mock
;
let
mock
;
let
wrapper
;
let
wrapper
;
let
fakeApollo
;
const
findBaseToken
=
()
=>
wrapper
.
findComponent
(
BaseToken
);
function
createComponent
(
options
=
{},
epicsQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
mockGroupEpicsQueryResponse
),
)
{
fakeApollo
=
createMockApollo
([[
searchEpicsQuery
,
epicsQueryHandler
]]);
const
{
config
=
mockEpicToken
,
value
=
{
data
:
''
},
active
=
false
,
stubs
=
defaultStubs
,
}
=
options
;
return
mount
(
EpicToken
,
{
apolloProvider
:
fakeApollo
,
propsData
:
{
config
,
value
,
active
,
},
provide
:
{
portalName
:
'
fake target
'
,
alignSuggestions
:
function
fakeAlignSuggestions
()
{},
suggestionsListClass
:
'
custom-class
'
,
},
stubs
,
});
}
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
=
new
MockAdapter
(
axios
);
...
@@ -71,23 +85,20 @@ describe('EpicToken', () => {
...
@@ -71,23 +85,20 @@ describe('EpicToken', () => {
describe
(
'
methods
'
,
()
=>
{
describe
(
'
methods
'
,
()
=>
{
describe
(
'
fetchEpicsBySearchTerm
'
,
()
=>
{
describe
(
'
fetchEpicsBySearchTerm
'
,
()
=>
{
it
(
'
calls
`config.fetchEpics`
with provided searchTerm param
'
,
()
=>
{
it
(
'
calls
fetchEpics
with provided searchTerm param
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
config
,
'
fetchEpics
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
);
wrapper
.
vm
.
fetchEpicsBySearchTerm
({
search
:
'
foo
'
}
);
findBaseToken
().
vm
.
$emit
(
'
fetch-suggestions
'
,
'
foo
'
);
expect
(
wrapper
.
vm
.
config
.
fetchEpics
).
toHaveBeenCalledWith
({
expect
(
wrapper
.
vm
.
fetchEpics
).
toHaveBeenCalledWith
(
'
foo
'
);
epicPath
:
''
,
search
:
'
foo
'
,
});
});
});
it
(
'
sets response to `epics` when request is successful
'
,
async
()
=>
{
it
(
'
sets response to `epics` when request is successful
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
config
,
'
fetchEpics
'
).
mockResolvedValue
({
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
).
mockResolvedValue
({
data
:
mockEpics
,
data
:
mockEpics
,
});
});
wrapper
.
vm
.
fetchEpicsBySearchTerm
({}
);
findBaseToken
().
vm
.
$emit
(
'
fetch-suggestions
'
);
await
waitForPromises
();
await
waitForPromises
();
...
@@ -95,9 +106,9 @@ describe('EpicToken', () => {
...
@@ -95,9 +106,9 @@ describe('EpicToken', () => {
});
});
it
(
'
calls `createFlash` with flash error message when request fails
'
,
async
()
=>
{
it
(
'
calls `createFlash` with flash error message when request fails
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
config
,
'
fetchEpics
'
).
mockRejectedValue
({});
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
).
mockRejectedValue
({});
wrapper
.
vm
.
fetchEpicsBySearchTerm
({
search
:
'
foo
'
}
);
findBaseToken
().
vm
.
$emit
(
'
fetch-suggestions
'
,
'
foo
'
);
await
waitForPromises
();
await
waitForPromises
();
...
@@ -107,9 +118,9 @@ describe('EpicToken', () => {
...
@@ -107,9 +118,9 @@ describe('EpicToken', () => {
});
});
it
(
'
sets `loading` to false when request completes
'
,
async
()
=>
{
it
(
'
sets `loading` to false when request completes
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
config
,
'
fetchEpics
'
).
mockRejectedValue
({});
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpics
'
).
mockRejectedValue
({});
wrapper
.
vm
.
fetchEpicsBySearchTerm
({
search
:
'
foo
'
}
);
findBaseToken
().
vm
.
$emit
(
'
fetch-suggestions
'
,
'
foo
'
);
await
waitForPromises
();
await
waitForPromises
();
...
@@ -123,15 +134,15 @@ describe('EpicToken', () => {
...
@@ -123,15 +134,15 @@ describe('EpicToken', () => {
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
wrapper
=
createComponent
({
wrapper
=
createComponent
({
value
:
{
data
:
`
${
mockEpics
[
0
].
group_full_path
}
::&
${
mockEpics
[
0
].
iid
}
`
},
value
:
{
data
:
`
${
mockEpics
[
0
].
title
}
::&
${
mockEpics
[
0
].
iid
}
`
},
data
:
{
epics
:
mockEpics
},
data
:
{
epics
:
mockEpics
},
});
});
await
wrapper
.
vm
.
$nextTick
();
await
wrapper
.
vm
.
$nextTick
();
});
});
it
(
'
renders
gl-filtered-search-t
oken component
'
,
()
=>
{
it
(
'
renders
BaseT
oken component
'
,
()
=>
{
expect
(
wrapper
.
find
(
GlFilteredSearchToken
).
exists
()).
toBe
(
true
);
expect
(
findBaseToken
(
).
exists
()).
toBe
(
true
);
});
});
it
(
'
renders token item when value is selected
'
,
()
=>
{
it
(
'
renders token item when value is selected
'
,
()
=>
{
...
@@ -142,9 +153,9 @@ describe('EpicToken', () => {
...
@@ -142,9 +153,9 @@ describe('EpicToken', () => {
});
});
it
.
each
`
it
.
each
`
value
| valueType | tokenValueString
value | valueType | tokenValueString
${
`
${
mockEpics
[
0
].
group_full_path
}
::&
${
mockEpics
[
0
].
iid
}
`
}
|
${
'
string
'
}
|
${
`
${
mockEpics
[
0
].
title
}
::&
${
mockEpics
[
0
].
iid
}
`
}
${
`
${
mockEpics
[
0
].
title
}
::&
${
mockEpics
[
0
].
iid
}
`
}
|
${
'
string
'
}
|
${
`
${
mockEpics
[
0
].
title
}
::&
${
mockEpics
[
0
].
iid
}
`
}
${
`
${
mockEpics
[
1
].
group_full_path
}
::&
${
mockEpics
[
1
].
iid
}
`
}
|
${
'
number
'
}
|
${
`
${
mockEpics
[
1
].
title
}
::&
${
mockEpics
[
1
].
iid
}
`
}
${
`
${
mockEpics
[
1
].
title
}
::&
${
mockEpics
[
1
].
iid
}
`
}
|
${
'
number
'
}
|
${
`
${
mockEpics
[
1
].
title
}
::&
${
mockEpics
[
1
].
iid
}
`
}
`
(
'
renders token item when selection is a $valueType
'
,
async
({
value
,
tokenValueString
})
=>
{
`
(
'
renders token item when selection is a $valueType
'
,
async
({
value
,
tokenValueString
})
=>
{
wrapper
.
setProps
({
wrapper
.
setProps
({
value
:
{
data
:
value
},
value
:
{
data
:
value
},
...
...
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