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
fb3b8945
Commit
fb3b8945
authored
Aug 25, 2021
by
Coung Ngo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor group issues page from Haml to Vue
https://gitlab.com/gitlab-org/gitlab/-/issues/322755
parent
ca17296d
Changes
24
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
443 additions
and
88 deletions
+443
-88
app/assets/javascripts/issuable_list/components/issuable_item.vue
...ts/javascripts/issuable_list/components/issuable_item.vue
+6
-3
app/assets/javascripts/issues_list/components/issues_list_app.vue
...ts/javascripts/issues_list/components/issues_list_app.vue
+30
-19
app/assets/javascripts/issues_list/index.js
app/assets/javascripts/issues_list/index.js
+5
-5
app/assets/javascripts/issues_list/queries/get_issues.query.graphql
.../javascripts/issues_list/queries/get_issues.query.graphql
+30
-1
app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
...ripts/issues_list/queries/get_issues_counts.query.graphql
+49
-1
app/assets/javascripts/issues_list/queries/issue.fragment.graphql
...ts/javascripts/issues_list/queries/issue.fragment.graphql
+1
-0
app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
...ripts/issues_list/queries/reorder_issues.mutation.graphql
+7
-1
app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
...ripts/issues_list/queries/search_iterations.query.graphql
+11
-3
app/assets/javascripts/issues_list/queries/search_labels.query.graphql
...vascripts/issues_list/queries/search_labels.query.graphql
+12
-2
app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
...ripts/issues_list/queries/search_milestones.query.graphql
+10
-2
app/assets/javascripts/issues_list/queries/search_users.query.graphql
...avascripts/issues_list/queries/search_users.query.graphql
+14
-2
app/assets/javascripts/pages/groups/issues/index.js
app/assets/javascripts/pages/groups/issues/index.js
+19
-15
app/controllers/groups_controller.rb
app/controllers/groups_controller.rb
+1
-0
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+1
-1
app/helpers/issues_helper.rb
app/helpers/issues_helper.rb
+15
-1
app/views/groups/issues.html.haml
app/views/groups/issues.html.haml
+26
-21
app/views/projects/issues/index.html.haml
app/views/projects/issues/index.html.haml
+1
-1
ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
.../javascripts/issues_list/queries/get_issues.query.graphql
+37
-1
ee/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
...ripts/issues_list/queries/get_issues_counts.query.graphql
+61
-1
ee/app/helpers/ee/issues_helper.rb
ee/app/helpers/ee/issues_helper.rb
+18
-0
ee/spec/helpers/ee/issues_helper_spec.rb
ee/spec/helpers/ee/issues_helper_spec.rb
+53
-0
spec/frontend/issues_list/components/issues_list_app_spec.js
spec/frontend/issues_list/components/issues_list_app_spec.js
+7
-7
spec/frontend/issues_list/mock_data.js
spec/frontend/issues_list/mock_data.js
+1
-0
spec/helpers/issues_helper_spec.rb
spec/helpers/issues_helper_spec.rb
+28
-1
No files found.
app/assets/javascripts/issuable_list/components/issuable_item.vue
View file @
fb3b8945
...
...
@@ -69,6 +69,9 @@ export default {
isIssuableUrlExternal
()
{
return
isExternal
(
this
.
webUrl
);
},
reference
()
{
return
this
.
issuable
.
reference
||
`
${
this
.
issuableSymbol
}${
this
.
issuable
.
iid
}
`
;
},
labels
()
{
return
this
.
issuable
.
labels
?.
nodes
||
this
.
issuable
.
labels
||
[];
},
...
...
@@ -201,9 +204,9 @@ export default {
</div>
<div
class=
"issuable-info"
>
<slot
v-if=
"hasSlotContents('reference')"
name=
"reference"
></slot>
<span
v-else
data-testid=
"issuable-reference"
class=
"issuable-reference"
>
{{
issuableSymbol
}}{{
issuable
.
iid
}}
</span
>
<span
v-else
data-testid=
"issuable-reference"
class=
"issuable-reference"
>
{{
reference
}}
</span
>
<span
class=
"issuable-authored gl-display-none gl-sm-display-inline-block! gl-mr-3"
>
<span
aria-hidden=
"true"
>
·
</span>
<span
...
...
app/assets/javascripts/issues_list/components/issues_list_app.vue
View file @
fb3b8945
...
...
@@ -140,11 +140,11 @@ export default {
initialEmail
:
{
default
:
''
,
},
is
SignedIn
:
{
is
Project
:
{
default
:
false
,
},
is
suesPath
:
{
default
:
''
,
is
SignedIn
:
{
default
:
false
,
},
jiraIntegrationPath
:
{
default
:
''
,
...
...
@@ -186,9 +186,11 @@ export default {
variables
()
{
return
this
.
queryVariables
;
},
update
:
({
project
})
=>
project
?.
issues
.
nodes
??
[],
update
(
data
)
{
return
data
[
this
.
namespace
]?.
issues
.
nodes
??
[];
},
result
({
data
})
{
this
.
pageInfo
=
data
.
project
?.
issues
.
pageInfo
??
{};
this
.
pageInfo
=
data
[
this
.
namespace
]
?.
issues
.
pageInfo
??
{};
this
.
exportCsvPathWithQuery
=
this
.
getExportCsvPathWithQuery
();
},
error
(
error
)
{
...
...
@@ -204,7 +206,9 @@ export default {
variables
()
{
return
this
.
queryVariables
;
},
update
:
({
project
})
=>
project
??
{},
update
(
data
)
{
return
data
[
this
.
namespace
]
??
{};
},
error
(
error
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
errorFetchingCounts
,
captureError
:
true
,
error
});
},
...
...
@@ -220,8 +224,9 @@ export default {
computed
:
{
queryVariables
()
{
return
{
isSignedIn
:
this
.
isSignedIn
,
fullPath
:
this
.
fullPath
,
isProject
:
this
.
isProject
,
isSignedIn
:
this
.
isSignedIn
,
search
:
this
.
searchQuery
,
sort
:
this
.
sortKey
,
state
:
this
.
state
,
...
...
@@ -229,6 +234,9 @@ export default {
...
this
.
apiFilterParams
,
};
},
namespace
()
{
return
this
.
isProject
?
'
project
'
:
'
group
'
;
},
hasSearch
()
{
return
this
.
searchQuery
||
Object
.
keys
(
this
.
urlFilterParams
).
length
;
},
...
...
@@ -242,7 +250,7 @@ export default {
return
this
.
state
===
IssuableStates
.
Opened
;
},
showCsvButtons
()
{
return
this
.
isSignedIn
;
return
this
.
is
Project
&&
this
.
is
SignedIn
;
},
apiFilterParams
()
{
return
convertToApiParams
(
this
.
filterTokens
);
...
...
@@ -447,39 +455,41 @@ export default {
return
this
.
$apollo
.
query
({
query
:
searchLabelsQuery
,
variables
:
{
fullPath
:
this
.
fullPath
,
search
},
variables
:
{
fullPath
:
this
.
fullPath
,
search
,
isProject
:
this
.
isProject
},
})
.
then
(({
data
})
=>
data
.
project
.
labels
.
nodes
);
.
then
(({
data
})
=>
data
[
this
.
namespace
]?
.
labels
.
nodes
);
},
fetchMilestones
(
search
)
{
return
this
.
$apollo
.
query
({
query
:
searchMilestonesQuery
,
variables
:
{
fullPath
:
this
.
fullPath
,
search
},
variables
:
{
fullPath
:
this
.
fullPath
,
search
,
isProject
:
this
.
isProject
},
})
.
then
(({
data
})
=>
data
.
project
.
milestones
.
nodes
);
.
then
(({
data
})
=>
data
[
this
.
namespace
]?
.
milestones
.
nodes
);
},
fetchIterations
(
search
)
{
const
id
=
Number
(
search
);
const
variables
=
!
search
||
Number
.
isNaN
(
id
)
?
{
fullPath
:
this
.
fullPath
,
search
}
:
{
fullPath
:
this
.
fullPath
,
id
};
?
{
fullPath
:
this
.
fullPath
,
search
,
isProject
:
this
.
isProject
}
:
{
fullPath
:
this
.
fullPath
,
id
,
isProject
:
this
.
isProject
};
return
this
.
$apollo
.
query
({
query
:
searchIterationsQuery
,
variables
,
})
.
then
(({
data
})
=>
data
.
project
.
iterations
.
nodes
);
.
then
(({
data
})
=>
data
[
this
.
namespace
]?
.
iterations
.
nodes
);
},
fetchUsers
(
search
)
{
return
this
.
$apollo
.
query
({
query
:
searchUsersQuery
,
variables
:
{
fullPath
:
this
.
fullPath
,
search
},
variables
:
{
fullPath
:
this
.
fullPath
,
search
,
isProject
:
this
.
isProject
},
})
.
then
(({
data
})
=>
data
.
project
.
projectMembers
.
nodes
.
map
((
member
)
=>
member
.
user
));
.
then
(({
data
})
=>
data
[
this
.
namespace
]?.[
`
${
this
.
namespace
}
Members`
].
nodes
.
map
((
member
)
=>
member
.
user
),
);
},
getExportCsvPathWithQuery
()
{
return
`
${
this
.
exportCsvPath
}${
window
.
location
.
search
}
`
;
...
...
@@ -560,15 +570,16 @@ export default {
}
return
axios
.
put
(
joinPaths
(
this
.
issuesPath
,
issueToMove
.
iid
,
'
reorder
'
),
{
.
put
(
joinPaths
(
issueToMove
.
webPath
,
'
reorder
'
),
{
move_before_id
:
isMovingToBeginning
?
null
:
getIdFromGraphQLId
(
moveBeforeId
),
move_after_id
:
isMovingToEnd
?
null
:
getIdFromGraphQLId
(
moveAfterId
),
group_full_path
:
this
.
isProject
?
undefined
:
this
.
fullPath
,
})
.
then
(()
=>
{
const
serializedVariables
=
JSON
.
stringify
(
this
.
queryVariables
);
return
this
.
$apollo
.
mutate
({
mutation
:
reorderIssuesMutation
,
variables
:
{
oldIndex
,
newIndex
,
serializedVariables
},
variables
:
{
oldIndex
,
newIndex
,
namespace
:
this
.
namespace
,
serializedVariables
},
});
})
.
catch
((
error
)
=>
{
...
...
app/assets/javascripts/issues_list/index.js
View file @
fb3b8945
...
...
@@ -85,17 +85,17 @@ export function mountIssuesListApp() {
const
resolvers
=
{
Mutation
:
{
reorderIssues
:
(
_
,
{
oldIndex
,
newIndex
,
serializedVariables
},
{
cache
})
=>
{
reorderIssues
:
(
_
,
{
oldIndex
,
newIndex
,
namespace
,
serializedVariables
},
{
cache
})
=>
{
const
variables
=
JSON
.
parse
(
serializedVariables
);
const
sourceData
=
cache
.
readQuery
({
query
:
getIssuesQuery
,
variables
});
const
data
=
produce
(
sourceData
,
(
draftData
)
=>
{
const
issues
=
draftData
.
project
.
issues
.
nodes
.
slice
();
const
issues
=
draftData
[
namespace
]
.
issues
.
nodes
.
slice
();
const
issueToMove
=
issues
[
oldIndex
];
issues
.
splice
(
oldIndex
,
1
);
issues
.
splice
(
newIndex
,
0
,
issueToMove
);
draftData
.
project
.
issues
.
nodes
=
issues
;
draftData
[
namespace
]
.
issues
.
nodes
=
issues
;
});
cache
.
writeQuery
({
query
:
getIssuesQuery
,
variables
,
data
});
...
...
@@ -128,8 +128,8 @@ export function mountIssuesListApp() {
hasMultipleIssueAssigneesFeature
,
importCsvIssuesPath
,
initialEmail
,
isProject
,
isSignedIn
,
issuesPath
,
jiraIntegrationPath
,
markdownHelpPath
,
maxAttachmentSize
,
...
...
@@ -158,8 +158,8 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature
:
parseBoolean
(
hasIssueWeightsFeature
),
hasIterationsFeature
:
parseBoolean
(
hasIterationsFeature
),
hasMultipleIssueAssigneesFeature
:
parseBoolean
(
hasMultipleIssueAssigneesFeature
),
isProject
:
parseBoolean
(
isProject
),
isSignedIn
:
parseBoolean
(
isSignedIn
),
issuesPath
,
jiraIntegrationPath
,
newIssuePath
,
rssPath
,
...
...
app/assets/javascripts/issues_list/queries/get_issues.query.graphql
View file @
fb3b8945
...
...
@@ -2,6 +2,7 @@
#import "./issue.fragment.graphql"
query
getIssues
(
$isProject
:
Boolean
=
false
$isSignedIn
:
Boolean
=
false
$fullPath
:
ID
!
$search
:
String
...
...
@@ -20,7 +21,35 @@ query getIssues(
$firstPageSize
:
Int
$lastPageSize
:
Int
)
{
project
(
fullPath
:
$fullPath
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
issues
(
includeSubgroups
:
true
search
:
$search
sort
:
$sort
state
:
$state
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
not
:
$not
before
:
$beforeCursor
after
:
$afterCursor
first
:
$firstPageSize
last
:
$lastPageSize
)
{
pageInfo
{
...
PageInfo
}
nodes
{
...
IssueFragment
reference
(
full
:
true
)
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
issues
(
search
:
$search
sort
:
$sort
...
...
app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
View file @
fb3b8945
query
getIssuesCount
(
$isProject
:
Boolean
=
false
$fullPath
:
ID
!
$search
:
String
$assigneeId
:
String
...
...
@@ -10,7 +11,54 @@ query getIssuesCount(
$types
:
[
IssueType
!]
$not
:
NegatedIssueFilterInput
)
{
project
(
fullPath
:
$fullPath
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
openedIssues
:
issues
(
includeSubgroups
:
true
state
:
opened
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
not
:
$not
)
{
count
}
closedIssues
:
issues
(
includeSubgroups
:
true
state
:
closed
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
not
:
$not
)
{
count
}
allIssues
:
issues
(
includeSubgroups
:
true
state
:
all
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
not
:
$not
)
{
count
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
openedIssues
:
issues
(
state
:
opened
search
:
$search
...
...
app/assets/javascripts/issues_list/queries/issue.fragment.graphql
View file @
fb3b8945
...
...
@@ -13,6 +13,7 @@ fragment IssueFragment on Issue {
updatedAt
upvotes
userDiscussionsCount
@include
(
if
:
$isSignedIn
)
webPath
webUrl
assignees
{
nodes
{
...
...
app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
View file @
fb3b8945
mutation
reorderIssues
(
$oldIndex
:
Int
,
$newIndex
:
Int
,
$serializedVariables
:
String
)
{
mutation
reorderIssues
(
$oldIndex
:
Int
$newIndex
:
Int
$namespace
:
String
$serializedVariables
:
String
)
{
reorderIssues
(
oldIndex
:
$oldIndex
newIndex
:
$newIndex
namespace
:
$namespace
serializedVariables
:
$serializedVariables
)
@client
}
app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
View file @
fb3b8945
query
searchIterations
(
$fullPath
:
ID
!,
$search
:
String
,
$id
:
ID
)
{
project
(
fullPath
:
$fullPath
)
{
iterations
(
title
:
$search
,
id
:
$id
)
{
query
searchIterations
(
$fullPath
:
ID
!,
$search
:
String
,
$id
:
ID
,
$isProject
:
Boolean
=
false
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
iterations
(
title
:
$search
,
id
:
$id
,
includeAncestors
:
true
)
{
nodes
{
id
title
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
iterations
(
title
:
$search
,
id
:
$id
,
includeAncestors
:
true
)
{
nodes
{
id
title
...
...
app/assets/javascripts/issues_list/queries/search_labels.query.graphql
View file @
fb3b8945
query
searchLabels
(
$fullPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
query
searchLabels
(
$fullPath
:
ID
!,
$search
:
String
,
$isProject
:
Boolean
=
false
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
labels
(
searchTerm
:
$search
,
includeAncestorGroups
:
true
,
includeDescendantGroups
:
true
)
{
nodes
{
id
color
textColor
title
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
labels
(
searchTerm
:
$search
,
includeAncestorGroups
:
true
)
{
nodes
{
id
...
...
app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
View file @
fb3b8945
query
searchMilestones
(
$fullPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
query
searchMilestones
(
$fullPath
:
ID
!,
$search
:
String
,
$isProject
:
Boolean
=
false
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
milestones
(
searchTitle
:
$search
,
includeAncestors
:
true
,
includeDescendants
:
true
)
{
nodes
{
id
title
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
milestones
(
searchTitle
:
$search
,
includeAncestors
:
true
)
{
nodes
{
id
...
...
app/assets/javascripts/issues_list/queries/search_users.query.graphql
View file @
fb3b8945
query
searchUsers
(
$fullPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
query
searchUsers
(
$fullPath
:
ID
!,
$search
:
String
,
$isProject
:
Boolean
=
false
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
groupMembers
(
search
:
$search
)
{
nodes
{
user
{
id
avatarUrl
name
username
}
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
projectMembers
(
search
:
$search
)
{
nodes
{
user
{
...
...
app/assets/javascripts/pages/groups/issues/index.js
View file @
fb3b8945
import
IssuableFilteredSearchTokenKeys
from
'
ee_else_ce/filtered_search/issuable_filtered_search_token_keys
'
;
import
issuableInitBulkUpdateSidebar
from
'
~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar
'
;
import
{
mountIssuablesListApp
}
from
'
~/issues_list
'
;
import
{
mountIssuablesListApp
,
mountIssuesListApp
}
from
'
~/issues_list
'
;
import
initManualOrdering
from
'
~/manual_ordering
'
;
import
{
FILTERED_SEARCH
}
from
'
~/pages/constants
'
;
import
initFilteredSearch
from
'
~/pages/search/init_filtered_search
'
;
import
projectSelect
from
'
~/project_select
'
;
const
ISSUE_BULK_UPDATE_PREFIX
=
'
issue_
'
;
if
(
gon
.
features
?.
vueIssuesList
)
{
mountIssuesListApp
();
}
else
{
const
ISSUE_BULK_UPDATE_PREFIX
=
'
issue_
'
;
IssuableFilteredSearchTokenKeys
.
addExtraTokensForIssues
();
IssuableFilteredSearchTokenKeys
.
removeTokensForKeys
(
'
release
'
);
issuableInitBulkUpdateSidebar
.
init
(
ISSUE_BULK_UPDATE_PREFIX
);
IssuableFilteredSearchTokenKeys
.
addExtraTokensForIssues
();
IssuableFilteredSearchTokenKeys
.
removeTokensForKeys
(
'
release
'
);
issuableInitBulkUpdateSidebar
.
init
(
ISSUE_BULK_UPDATE_PREFIX
);
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
ISSUES
,
isGroupDecendent
:
true
,
useDefaultState
:
true
,
filteredSearchTokenKeys
:
IssuableFilteredSearchTokenKeys
,
});
projectSelect
();
initManualOrdering
();
initFilteredSearch
({
page
:
FILTERED_SEARCH
.
ISSUES
,
isGroupDecendent
:
true
,
useDefaultState
:
true
,
filteredSearchTokenKeys
:
IssuableFilteredSearchTokenKeys
,
});
projectSelect
();
initManualOrdering
();
if
(
gon
.
features
?.
vueIssuablesList
)
{
mountIssuablesListApp
();
if
(
gon
.
features
?.
vueIssuablesList
)
{
mountIssuablesListApp
();
}
}
app/controllers/groups_controller.rb
View file @
fb3b8945
...
...
@@ -33,6 +33,7 @@ class GroupsController < Groups::ApplicationController
before_action
do
push_frontend_feature_flag
(
:vue_issuables_list
,
@group
)
push_frontend_feature_flag
(
:vue_issues_list
,
@group
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:iteration_cadences
,
@group
,
default_enabled: :yaml
)
end
...
...
app/controllers/projects/issues_controller.rb
View file @
fb3b8945
...
...
@@ -43,7 +43,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag
(
:tribute_autocomplete
,
@project
)
push_frontend_feature_flag
(
:vue_issuables_list
,
project
)
push_frontend_feature_flag
(
:improved_emoji_picker
,
project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:vue_issues_list
,
project
)
push_frontend_feature_flag
(
:vue_issues_list
,
project
&
.
group
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:iteration_cadences
,
project
&
.
group
,
default_enabled: :yaml
)
end
...
...
app/helpers/issues_helper.rb
View file @
fb3b8945
...
...
@@ -218,8 +218,8 @@ module IssuesHelper
has_any_issues:
project_issues
(
project
).
exists?
.
to_s
,
import_csv_issues_path:
import_csv_namespace_project_issues_path
,
initial_email:
project
.
new_issuable_address
(
current_user
,
'issue'
),
is_project:
true
.
to_s
,
is_signed_in:
current_user
.
present?
.
to_s
,
issues_path:
project_issues_path
(
project
),
jira_integration_path:
help_page_url
(
'integration/jira/issues'
,
anchor:
'view-jira-issues'
),
markdown_help_path:
help_page_path
(
'user/markdown'
),
max_attachment_size:
number_to_human_size
(
Gitlab
::
CurrentSettings
.
max_attachment_size
.
megabytes
),
...
...
@@ -233,6 +233,20 @@ module IssuesHelper
}
end
def
group_issues_list_data
(
group
,
current_user
,
issues
)
{
autocomplete_award_emojis_path:
autocomplete_award_emojis_path
,
calendar_path:
url_for
(
safe_params
.
merge
(
calendar_url_options
)),
empty_state_svg_path:
image_path
(
'illustrations/issues.svg'
),
full_path:
group
.
full_path
,
has_any_issues:
issues
.
to_a
.
any?
.
to_s
,
is_signed_in:
current_user
.
present?
.
to_s
,
jira_integration_path:
help_page_url
(
'integration/jira/issues'
,
anchor:
'view-jira-issues'
),
rss_path:
url_for
(
safe_params
.
merge
(
rss_url_options
)),
sign_in_path:
new_user_session_path
}
end
# Overridden in EE
def
scoped_labels_available?
(
parent
)
false
...
...
app/views/groups/issues.html.haml
View file @
fb3b8945
...
...
@@ -5,29 +5,34 @@
=
content_for
:meta_tags
do
=
auto_discovery_link_tag
(
:atom
,
safe_params
.
merge
(
rss_url_options
).
to_h
,
title:
"
#{
@group
.
name
}
issues"
)
.top-area
=
render
'shared/issuable/nav'
,
type: :issues
.nav-controls
=
render
'shared/issuable/feed_buttons'
-
if
Feature
.
enabled?
(
:vue_issues_list
,
@group
,
default_enabled: :yaml
)
.js-issues-list
{
data:
group_issues_list_data
(
@group
,
current_user
,
@issues
)
}
-
if
@can_bulk_update
=
render_if_exists
'shared/issuable/group_bulk_update_sidebar'
,
group:
@group
,
type: :issues
-
else
.top-area
=
render
'shared/issuable/nav'
,
type: :issues
.nav-controls
=
render
'shared/issuable/feed_buttons'
-
if
@can_bulk_update
=
render_if_exists
'shared/issuable/bulk_update_button'
,
type: :issues
-
if
@can_bulk_update
=
render_if_exists
'shared/issuable/bulk_update_button'
,
type: :issues
=
render
'shared/new_project_item_select'
,
path:
'issues/new'
,
label:
"New issue"
,
type: :issues
,
with_feature_enabled:
'issues'
,
with_shared:
false
,
include_projects_in_subgroups:
true
=
render
'shared/new_project_item_select'
,
path:
'issues/new'
,
label:
"New issue"
,
type: :issues
,
with_feature_enabled:
'issues'
,
with_shared:
false
,
include_projects_in_subgroups:
true
=
render
'shared/issuable/search_bar'
,
type: :issues
=
render
'shared/issuable/search_bar'
,
type: :issues
-
if
@can_bulk_update
=
render_if_exists
'shared/issuable/group_bulk_update_sidebar'
,
group:
@group
,
type: :issues
-
if
@can_bulk_update
=
render_if_exists
'shared/issuable/group_bulk_update_sidebar'
,
group:
@group
,
type: :issues
-
if
Feature
.
enabled?
(
:vue_issuables_list
,
@group
)
&&
@issues
.
to_a
.
any?
-
if
use_startup_call?
-
add_page_startup_api_call
(
api_v4_groups_issues_path
(
id:
@group
.
id
,
params:
startup_call_params
))
.js-issuables-list
{
data:
{
endpoint:
expose_url
(
api_v4_groups_issues_path
(
id:
@group
.
id
)),
'can-bulk-edit'
:
@can_bulk_update
.
to_json
,
'empty-state-meta'
:
{
svg_path:
image_path
(
'illustrations/issues.svg'
)
},
'sort-key'
:
@sort
,
type:
'issues'
,
'scoped-labels-available'
:
scoped_labels_available?
(
@group
).
to_json
}
}
-
else
=
render
'shared/issues'
,
project_select_button:
true
-
if
Feature
.
enabled?
(
:vue_issuables_list
,
@group
)
&&
@issues
.
to_a
.
any?
-
if
use_startup_call?
-
add_page_startup_api_call
(
api_v4_groups_issues_path
(
id:
@group
.
id
,
params:
startup_call_params
))
.js-issuables-list
{
data:
{
endpoint:
expose_url
(
api_v4_groups_issues_path
(
id:
@group
.
id
)),
'can-bulk-edit'
:
@can_bulk_update
.
to_json
,
'empty-state-meta'
:
{
svg_path:
image_path
(
'illustrations/issues.svg'
)
},
'sort-key'
:
@sort
,
type:
'issues'
,
'scoped-labels-available'
:
scoped_labels_available?
(
@group
).
to_json
}
}
-
else
=
render
'shared/issues'
,
project_select_button:
true
app/views/projects/issues/index.html.haml
View file @
fb3b8945
...
...
@@ -13,7 +13,7 @@
issues_path:
project_issues_path
(
@project
),
project_path:
@project
.
full_path
}
}
-
if
Feature
.
enabled?
(
:vue_issues_list
,
@project
)
-
if
Feature
.
enabled?
(
:vue_issues_list
,
@project
&
.
group
,
default_enabled: :yaml
)
.js-issues-list
{
data:
issues_list_data
(
@project
,
current_user
,
finder
)
}
-
if
@can_bulk_update
=
render
'shared/issuable/bulk_update_sidebar'
,
type: :issues
...
...
ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
View file @
fb3b8945
...
...
@@ -2,6 +2,7 @@
#import "~/issues_list/queries/issue.fragment.graphql"
query
getIssues
(
$isProject
:
Boolean
=
false
$isSignedIn
:
Boolean
=
false
$fullPath
:
ID
!
$search
:
String
...
...
@@ -24,7 +25,42 @@ query getIssues(
$firstPageSize
:
Int
$lastPageSize
:
Int
)
{
project
(
fullPath
:
$fullPath
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
issues
(
includeSubgroups
:
true
search
:
$search
sort
:
$sort
state
:
$state
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
epicId
:
$epicId
iterationId
:
$iterationId
iterationWildcardId
:
$iterationWildcardId
weight
:
$weight
not
:
$not
before
:
$beforeCursor
after
:
$afterCursor
first
:
$firstPageSize
last
:
$lastPageSize
)
{
pageInfo
{
...
PageInfo
}
nodes
{
...
IssueFragment
reference
(
full
:
true
)
blockingCount
healthStatus
weight
}
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
issues
(
search
:
$search
sort
:
$sort
...
...
ee/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
View file @
fb3b8945
query
getIssuesCount
(
$isProject
:
Boolean
=
false
$fullPath
:
ID
!
$search
:
String
$assigneeId
:
String
...
...
@@ -14,7 +15,66 @@ query getIssuesCount(
$weight
:
String
$not
:
NegatedIssueFilterInput
)
{
project
(
fullPath
:
$fullPath
)
{
group
(
fullPath
:
$fullPath
)
@skip
(
if
:
$isProject
)
{
openedIssues
:
issues
(
includeSubgroups
:
true
state
:
opened
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
epicId
:
$epicId
iterationId
:
$iterationId
iterationWildcardId
:
$iterationWildcardId
weight
:
$weight
not
:
$not
)
{
count
}
closedIssues
:
issues
(
includeSubgroups
:
true
state
:
closed
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
epicId
:
$epicId
iterationId
:
$iterationId
iterationWildcardId
:
$iterationWildcardId
weight
:
$weight
not
:
$not
)
{
count
}
allIssues
:
issues
(
includeSubgroups
:
true
state
:
all
search
:
$search
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
milestoneWildcardId
:
$milestoneWildcardId
types
:
$types
epicId
:
$epicId
iterationId
:
$iterationId
iterationWildcardId
:
$iterationWildcardId
weight
:
$weight
not
:
$not
)
{
count
}
}
project
(
fullPath
:
$fullPath
)
@include
(
if
:
$isProject
)
{
openedIssues
:
issues
(
state
:
opened
search
:
$search
...
...
ee/app/helpers/ee/issues_helper.rb
View file @
fb3b8945
...
...
@@ -58,5 +58,23 @@ module EE
data
end
override
:group_issues_list_data
def
group_issues_list_data
(
group
,
current_user
,
issues
)
data
=
super
.
merge!
(
can_bulk_update:
(
can?
(
current_user
,
:admin_issue
,
group
)
&&
group
.
feature_available?
(
:group_bulk_edit
)).
to_s
,
has_blocked_issues_feature:
group
.
feature_available?
(
:blocked_issues
).
to_s
,
has_issuable_health_status_feature:
group
.
feature_available?
(
:issuable_health_status
).
to_s
,
has_issue_weights_feature:
group
.
feature_available?
(
:issue_weights
).
to_s
,
has_iterations_feature:
group
.
feature_available?
(
:iterations
).
to_s
,
has_multiple_issue_assignees_feature:
group
.
feature_available?
(
:multiple_issue_assignees
).
to_s
)
if
group
.
feature_available?
(
:epics
)
data
[
:group_epics_path
]
=
group_epics_path
(
group
,
format: :json
)
end
data
end
end
end
ee/spec/helpers/ee/issues_helper_spec.rb
View file @
fb3b8945
...
...
@@ -183,4 +183,57 @@ RSpec.describe EE::IssuesHelper do
end
end
end
describe
'#group_issues_list_data'
do
let
(
:current_user
)
{
double
.
as_null_object
}
let
(
:issues
)
{
[]
}
before
do
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
current_user
)
allow
(
helper
).
to
receive
(
:can?
).
and_return
(
true
)
allow
(
helper
).
to
receive
(
:url_for
).
and_return
(
'#'
)
end
context
'when features are enabled'
do
before
do
stub_licensed_features
(
blocked_issues:
true
,
epics:
true
,
group_bulk_edit:
true
,
issuable_health_status:
true
,
issue_weights:
true
,
iterations:
true
,
multiple_issue_assignees:
true
)
end
it
'returns data with licensed features enabled'
do
expected
=
{
can_bulk_update:
'true'
,
has_blocked_issues_feature:
'true'
,
has_issuable_health_status_feature:
'true'
,
has_issue_weights_feature:
'true'
,
has_iterations_feature:
'true'
,
has_multiple_issue_assignees_feature:
'true'
,
group_epics_path:
group_epics_path
(
project
.
group
,
format: :json
)
}
expect
(
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
)).
to
include
(
expected
)
end
end
context
'when features are disabled'
do
before
do
stub_licensed_features
(
blocked_issues:
false
,
epics:
false
,
group_bulk_edit:
false
,
issuable_health_status:
false
,
issue_weights:
false
,
iterations:
false
,
multiple_issue_assignees:
false
)
end
it
'returns data with licensed features disabled'
do
expected
=
{
can_bulk_update:
'false'
,
has_blocked_issues_feature:
'false'
,
has_issuable_health_status_feature:
'false'
,
has_issue_weights_feature:
'false'
,
has_iterations_feature:
'false'
,
has_multiple_issue_assignees_feature:
'false'
}
result
=
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
)
expect
(
result
).
to
include
(
expected
)
expect
(
result
).
not_to
include
(
:group_epics_path
)
end
end
end
end
spec/frontend/issues_list/components/issues_list_app_spec.js
View file @
fb3b8945
...
...
@@ -68,8 +68,8 @@ describe('IssuesListApp component', () => {
hasBlockedIssuesFeature
:
true
,
hasIssueWeightsFeature
:
true
,
hasIterationsFeature
:
true
,
isProject
:
true
,
isSignedIn
:
true
,
issuesPath
:
'
path/to/issues
'
,
jiraIntegrationPath
:
'
jira/integration/path
'
,
newIssuePath
:
'
new/issue/path
'
,
rssPath
:
'
rss/path
'
,
...
...
@@ -625,25 +625,25 @@ describe('IssuesListApp component', () => {
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
'
101
'
,
title
:
'
Issue one
'
,
webPath
:
'
/group/project/-/issues/1
'
,
};
const
issueTwo
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/2
'
,
iid
:
'
102
'
,
title
:
'
Issue two
'
,
webPath
:
'
/group/project/-/issues/2
'
,
};
const
issueThree
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/3
'
,
iid
:
'
103
'
,
title
:
'
Issue three
'
,
webPath
:
'
/group/project/-/issues/3
'
,
};
const
issueFour
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/4
'
,
iid
:
'
104
'
,
title
:
'
Issue four
'
,
webPath
:
'
/group/project/-/issues/4
'
,
};
const
response
=
{
data
:
{
...
...
@@ -677,7 +677,7 @@ describe('IssuesListApp component', () => {
await
waitForPromises
();
expect
(
axiosMock
.
history
.
put
[
0
]).
toMatchObject
({
url
:
joinPaths
(
defaultProvide
.
issuesPath
,
issueToMove
.
iid
,
'
reorder
'
),
url
:
joinPaths
(
issueToMove
.
webPath
,
'
reorder
'
),
data
:
JSON
.
stringify
({
move_before_id
:
getIdFromGraphQLId
(
moveBeforeId
),
move_after_id
:
getIdFromGraphQLId
(
moveAfterId
),
...
...
@@ -690,7 +690,7 @@ describe('IssuesListApp component', () => {
describe
(
'
when unsuccessful
'
,
()
=>
{
it
(
'
displays an error message
'
,
async
()
=>
{
axiosMock
.
onPut
(
joinPaths
(
defaultProvide
.
issuesPath
,
issueOne
.
iid
,
'
reorder
'
)).
reply
(
500
);
axiosMock
.
onPut
(
joinPaths
(
issueOne
.
webPath
,
'
reorder
'
)).
reply
(
500
);
findIssuableList
().
vm
.
$emit
(
'
reorder
'
,
{
oldIndex
:
0
,
newIndex
:
1
});
...
...
spec/frontend/issues_list/mock_data.js
View file @
fb3b8945
...
...
@@ -29,6 +29,7 @@ export const getIssuesQueryResponse = {
updatedAt
:
'
2021-05-22T04:08:01Z
'
,
upvotes
:
3
,
userDiscussionsCount
:
4
,
webPath
:
'
project/-/issues/789
'
,
webUrl
:
'
project/-/issues/789
'
,
assignees
:
{
nodes
:
[
...
...
spec/helpers/issues_helper_spec.rb
View file @
fb3b8945
...
...
@@ -318,8 +318,8 @@ RSpec.describe IssuesHelper do
has_any_issues:
project_issues
(
project
).
exists?
.
to_s
,
import_csv_issues_path:
'#'
,
initial_email:
project
.
new_issuable_address
(
current_user
,
'issue'
),
is_project:
'true'
,
is_signed_in:
current_user
.
present?
.
to_s
,
issues_path:
project_issues_path
(
project
),
jira_integration_path:
help_page_url
(
'integration/jira/issues'
,
anchor:
'view-jira-issues'
),
markdown_help_path:
help_page_path
(
'user/markdown'
),
max_attachment_size:
number_to_human_size
(
Gitlab
::
CurrentSettings
.
max_attachment_size
.
megabytes
),
...
...
@@ -350,6 +350,33 @@ RSpec.describe IssuesHelper do
end
end
describe
'#group_issues_list_data'
do
let
(
:group
)
{
create
(
:group
)
}
let
(
:current_user
)
{
double
.
as_null_object
}
let
(
:issues
)
{
[]
}
it
'returns expected result'
do
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
current_user
)
allow
(
helper
).
to
receive
(
:can?
).
and_return
(
true
)
allow
(
helper
).
to
receive
(
:image_path
).
and_return
(
'#'
)
allow
(
helper
).
to
receive
(
:url_for
).
and_return
(
'#'
)
expected
=
{
autocomplete_award_emojis_path:
autocomplete_award_emojis_path
,
calendar_path:
'#'
,
empty_state_svg_path:
'#'
,
full_path:
group
.
full_path
,
has_any_issues:
issues
.
to_a
.
any?
.
to_s
,
is_signed_in:
current_user
.
present?
.
to_s
,
jira_integration_path:
help_page_url
(
'integration/jira/issues'
,
anchor:
'view-jira-issues'
),
rss_path:
'#'
,
sign_in_path:
new_user_session_path
}
expect
(
helper
.
group_issues_list_data
(
group
,
current_user
,
issues
)).
to
include
(
expected
)
end
end
describe
'#issue_manual_ordering_class'
do
context
'when sorting by relative position'
do
before
do
...
...
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