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
154dd2c2
Commit
154dd2c2
authored
Jun 16, 2021
by
Coung Ngo
Committed by
Nathan Friend
Jun 16, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Convert to use GraphQL in issues page refactor
parent
475ba0d9
Changes
16
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
544 additions
and
298 deletions
+544
-298
app/assets/javascripts/issuable_list/components/issuable_item.vue
...ts/javascripts/issuable_list/components/issuable_item.vue
+8
-5
app/assets/javascripts/issuable_list/components/issuable_list_root.vue
...vascripts/issuable_list/components/issuable_list_root.vue
+28
-4
app/assets/javascripts/issues_list/components/issue_card_time_info.vue
...vascripts/issues_list/components/issue_card_time_info.vue
+5
-2
app/assets/javascripts/issues_list/components/issues_list_app.vue
...ts/javascripts/issues_list/components/issues_list_app.vue
+61
-71
app/assets/javascripts/issues_list/constants.js
app/assets/javascripts/issues_list/constants.js
+4
-1
app/assets/javascripts/issues_list/index.js
app/assets/javascripts/issues_list/index.js
+8
-5
app/assets/javascripts/issues_list/queries/get_issues.query.graphql
.../javascripts/issues_list/queries/get_issues.query.graphql
+45
-0
app/assets/javascripts/issues_list/queries/issue.fragment.graphql
...ts/javascripts/issues_list/queries/issue.fragment.graphql
+51
-0
app/helpers/issues_helper.rb
app/helpers/issues_helper.rb
+0
-1
ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
.../javascripts/issues_list/queries/get_issues.query.graphql
+56
-0
ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap
...mponents/__snapshots__/jira_issues_list_root_spec.js.snap
+3
-0
spec/frontend/issuable_list/components/issuable_list_root_spec.js
...ntend/issuable_list/components/issuable_list_root_spec.js
+82
-46
spec/frontend/issues_list/components/issue_card_time_info_spec.js
...ntend/issues_list/components/issue_card_time_info_spec.js
+4
-6
spec/frontend/issues_list/components/issues_list_app_spec.js
spec/frontend/issues_list/components/issues_list_app_spec.js
+122
-156
spec/frontend/issues_list/mock_data.js
spec/frontend/issues_list/mock_data.js
+67
-0
spec/helpers/issues_helper_spec.rb
spec/helpers/issues_helper_spec.rb
+0
-1
No files found.
app/assets/javascripts/issuable_list/components/issuable_item.vue
View file @
154dd2c2
...
...
@@ -50,6 +50,9 @@ export default {
},
},
computed
:
{
issuableId
()
{
return
getIdFromGraphQLId
(
this
.
issuable
.
id
);
},
createdInPastDay
()
{
const
createdSecondsAgo
=
differenceInSeconds
(
new
Date
(
this
.
issuable
.
createdAt
),
new
Date
());
return
createdSecondsAgo
<
SECONDS_IN_DAY
;
...
...
@@ -61,7 +64,7 @@ export default {
return
this
.
issuable
.
gitlabWebUrl
||
this
.
issuable
.
webUrl
;
},
authorId
()
{
return
getIdFromGraphQLId
(
`
${
this
.
author
.
id
}
`
);
return
getIdFromGraphQLId
(
this
.
author
.
id
);
},
isIssuableUrlExternal
()
{
return
isExternal
(
this
.
webUrl
);
...
...
@@ -70,10 +73,10 @@ export default {
return
this
.
issuable
.
labels
?.
nodes
||
this
.
issuable
.
labels
||
[];
},
labelIdsString
()
{
return
JSON
.
stringify
(
this
.
labels
.
map
((
label
)
=>
label
.
id
));
return
JSON
.
stringify
(
this
.
labels
.
map
((
label
)
=>
getIdFromGraphQLId
(
label
.
id
)
));
},
assignees
()
{
return
this
.
issuable
.
assignees
||
[];
return
this
.
issuable
.
assignees
?.
nodes
||
this
.
issuable
.
assignees
||
[];
},
createdAt
()
{
return
sprintf
(
__
(
'
created %{timeAgo}
'
),
{
...
...
@@ -157,7 +160,7 @@ export default {
<
template
>
<li
:id=
"`issuable_$
{issuable
.i
d}`"
:id=
"`issuable_$
{issuable
I
d}`"
class="issue gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString"
...
...
@@ -167,7 +170,7 @@ export default {
<gl-form-checkbox
class=
"gl-mr-0"
:checked=
"checked"
:data-id=
"issuable
.i
d"
:data-id=
"issuable
I
d"
@
input=
"$emit('checked-input', $event)"
>
<span
class=
"gl-sr-only"
>
{{
issuable
.
title
}}
</span>
...
...
app/assets/javascripts/issuable_list/components/issuable_list_root.vue
View file @
154dd2c2
<
script
>
import
{
GlSkeletonLoading
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
Gl
KeysetPagination
,
Gl
SkeletonLoading
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
uniqueId
}
from
'
lodash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
updateHistory
,
setUrlParams
}
from
'
~/lib/utils/url_utility
'
;
import
FilteredSearchBar
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
...
...
@@ -19,6 +19,7 @@ export default {
tag
:
'
ul
'
,
},
components
:
{
GlKeysetPagination
,
GlSkeletonLoading
,
IssuableTabs
,
FilteredSearchBar
,
...
...
@@ -140,6 +141,21 @@ export default {
required
:
false
,
default
:
false
,
},
useKeysetPagination
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
hasNextPage
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
hasPreviousPage
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
...
...
@@ -211,7 +227,7 @@ export default {
},
methods
:
{
issuableId
(
issuable
)
{
return
issuable
.
id
||
issuable
.
iid
||
uniqueId
();
return
getIdFromGraphQLId
(
issuable
.
id
)
||
issuable
.
iid
||
uniqueId
();
},
issuableChecked
(
issuable
)
{
return
this
.
checkedIssuables
[
this
.
issuableId
(
issuable
)]?.
checked
;
...
...
@@ -315,8 +331,16 @@ export default {
<slot
v-else
name=
"empty-state"
></slot>
</template>
<div
v-if=
"showPaginationControls && useKeysetPagination"
class=
"gl-text-center gl-mt-3"
>
<gl-keyset-pagination
:has-next-page=
"hasNextPage"
:has-previous-page=
"hasPreviousPage"
@
next=
"$emit('next-page')"
@
prev=
"$emit('previous-page')"
/>
</div>
<gl-pagination
v-if=
"showPaginationControls"
v-
else-
if=
"showPaginationControls"
:per-page=
"defaultPageSize"
:total-items=
"totalItems"
:value=
"currentPage"
...
...
app/assets/javascripts/issues_list/components/issue_card_time_info.vue
View file @
154dd2c2
...
...
@@ -42,6 +42,9 @@ export default {
}
return
__
(
'
Milestone
'
);
},
milestoneLink
()
{
return
this
.
issue
.
milestone
.
webPath
||
this
.
issue
.
milestone
.
webUrl
;
},
dueDate
()
{
return
this
.
issue
.
dueDate
&&
dateInWords
(
new
Date
(
this
.
issue
.
dueDate
),
true
);
},
...
...
@@ -49,7 +52,7 @@ export default {
return
isInPast
(
new
Date
(
this
.
issue
.
dueDate
));
},
timeEstimate
()
{
return
this
.
issue
.
timeStats
?.
humanTimeEstimate
;
return
this
.
issue
.
humanTimeEstimate
||
this
.
issue
.
timeStats
?.
humanTimeEstimate
;
},
showHealthStatus
()
{
return
this
.
hasIssuableHealthStatusFeature
&&
this
.
issue
.
healthStatus
;
...
...
@@ -85,7 +88,7 @@ export default {
class=
"issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
data-testid=
"issuable-milestone"
>
<gl-link
v-gl-tooltip
:href=
"
issue.milestone.webUrl
"
:title=
"milestoneDate"
>
<gl-link
v-gl-tooltip
:href=
"
milestoneLink
"
:title=
"milestoneDate"
>
<gl-icon
name=
"clock"
/>
{{
issue
.
milestone
.
title
}}
</gl-link>
...
...
app/assets/javascripts/issues_list/components/issues_list_app.vue
View file @
154dd2c2
...
...
@@ -9,7 +9,7 @@ import {
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
fuzzaldrinPlus
from
'
fuzzaldrin-plus
'
;
import
{
toNumber
}
from
'
lodash
'
;
import
getIssuesQuery
from
'
ee_else_ce/issues_list/queries/get_issues.query.graphql
'
;
import
createFlash
from
'
~/flash
'
;
import
CsvImportExportButtons
from
'
~/issuable/components/csv_import_export_buttons.vue
'
;
import
IssuableByEmail
from
'
~/issuable/components/issuable_by_email.vue
'
;
...
...
@@ -17,13 +17,12 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import
{
IssuableListTabs
,
IssuableStates
}
from
'
~/issuable_list/constants
'
;
import
{
API_PARAM
,
apiSortParams
,
CREATED_DESC
,
i18n
,
initialPageParams
,
MAX_LIST_SIZE
,
PAGE_SIZE
,
PARAM_DUE_DATE
,
PARAM_PAGE
,
PARAM_SORT
,
PARAM_STATE
,
RELATIVE_POSITION_DESC
,
...
...
@@ -49,7 +48,8 @@ import {
getSortOptions
,
}
from
'
~/issues_list/utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
convertObjectPropsToCamelCase
,
getParameterByName
}
from
'
~/lib/utils/common_utils
'
;
import
{
getParameterByName
}
from
'
~/lib/utils/common_utils
'
;
import
{
scrollUp
}
from
'
~/lib/utils/scroll_utils
'
;
import
{
DEFAULT_NONE_ANY
,
OPERATOR_IS_ONLY
,
...
...
@@ -107,9 +107,6 @@ export default {
emptyStateSvgPath
:
{
default
:
''
,
},
endpoint
:
{
default
:
''
,
},
exportCsvPath
:
{
default
:
''
,
},
...
...
@@ -173,15 +170,43 @@ export default {
dueDateFilter
:
getDueDateValue
(
getParameterByName
(
PARAM_DUE_DATE
)),
exportCsvPathWithQuery
:
this
.
getExportCsvPathWithQuery
(),
filterTokens
:
getFilterTokens
(
window
.
location
.
search
),
isLoading
:
false
,
issues
:
[],
page
:
toNumber
(
getParameterByName
(
PARAM_PAGE
))
||
1
,
pageInfo
:
{},
pageParams
:
initialPageParams
,
showBulkEditSidebar
:
false
,
sortKey
:
getSortKey
(
getParameterByName
(
PARAM_SORT
))
||
defaultSortKey
,
state
:
state
||
IssuableStates
.
Opened
,
totalIssues
:
0
,
};
},
apollo
:
{
issues
:
{
query
:
getIssuesQuery
,
variables
()
{
return
{
projectPath
:
this
.
projectPath
,
search
:
this
.
searchQuery
,
sort
:
this
.
sortKey
,
state
:
this
.
state
,
...
this
.
pageParams
,
...
this
.
apiFilterParams
,
};
},
update
:
({
project
})
=>
project
.
issues
.
nodes
,
result
({
data
})
{
this
.
pageInfo
=
data
.
project
.
issues
.
pageInfo
;
this
.
totalIssues
=
data
.
project
.
issues
.
count
;
this
.
exportCsvPathWithQuery
=
this
.
getExportCsvPathWithQuery
();
},
error
(
error
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
errorFetchingIssues
,
captureError
:
true
,
error
});
},
skip
()
{
return
!
this
.
hasProjectIssues
;
},
debounce
:
200
,
},
},
computed
:
{
hasSearch
()
{
return
this
.
searchQuery
||
Object
.
keys
(
this
.
urlFilterParams
).
length
;
...
...
@@ -348,7 +373,6 @@ export default {
return
{
due_date
:
this
.
dueDateFilter
,
page
:
this
.
page
,
search
:
this
.
searchQuery
,
state
:
this
.
state
,
...
urlSortParams
[
this
.
sortKey
],
...
...
@@ -361,7 +385,6 @@ export default {
},
mounted
()
{
eventHub
.
$on
(
'
issuables:toggleBulkEdit
'
,
this
.
toggleBulkEditSidebar
);
this
.
fetchIssues
();
},
beforeDestroy
()
{
eventHub
.
$off
(
'
issuables:toggleBulkEdit
'
,
this
.
toggleBulkEditSidebar
);
...
...
@@ -406,54 +429,11 @@ export default {
fetchUsers
(
search
)
{
return
axios
.
get
(
this
.
autocompleteUsersPath
,
{
params
:
{
search
}
});
},
fetchIssues
()
{
if
(
!
this
.
hasProjectIssues
)
{
return
undefined
;
}
this
.
isLoading
=
true
;
const
filterParams
=
{
...
this
.
apiFilterParams
,
};
if
(
filterParams
.
epic_id
)
{
filterParams
.
epic_id
=
filterParams
.
epic_id
.
split
(
'
::&
'
).
pop
();
}
else
if
(
filterParams
[
'
not[epic_id]
'
])
{
filterParams
[
'
not[epic_id]
'
]
=
filterParams
[
'
not[epic_id]
'
].
split
(
'
::&
'
).
pop
();
}
return
axios
.
get
(
this
.
endpoint
,
{
params
:
{
due_date
:
this
.
dueDateFilter
,
page
:
this
.
page
,
per_page
:
PAGE_SIZE
,
search
:
this
.
searchQuery
,
state
:
this
.
state
,
with_labels_details
:
true
,
...
apiSortParams
[
this
.
sortKey
],
...
filterParams
,
},
})
.
then
(({
data
,
headers
})
=>
{
this
.
page
=
Number
(
headers
[
'
x-page
'
]);
this
.
totalIssues
=
Number
(
headers
[
'
x-total
'
]);
this
.
issues
=
data
.
map
((
issue
)
=>
convertObjectPropsToCamelCase
(
issue
,
{
deep
:
true
}));
this
.
exportCsvPathWithQuery
=
this
.
getExportCsvPathWithQuery
();
})
.
catch
(()
=>
{
createFlash
({
message
:
this
.
$options
.
i18n
.
errorFetchingIssues
});
})
.
finally
(()
=>
{
this
.
isLoading
=
false
;
});
},
getExportCsvPathWithQuery
()
{
return
`
${
this
.
exportCsvPath
}${
window
.
location
.
search
}
`
;
},
getStatus
(
issue
)
{
if
(
issue
.
closedAt
&&
issue
.
moved
ToId
)
{
if
(
issue
.
closedAt
&&
issue
.
moved
)
{
return
this
.
$options
.
i18n
.
closedMoved
;
}
if
(
issue
.
closedAt
)
{
...
...
@@ -484,18 +464,26 @@ export default {
},
handleClickTab
(
state
)
{
if
(
this
.
state
!==
state
)
{
this
.
page
=
1
;
this
.
page
Params
=
initialPageParams
;
}
this
.
state
=
state
;
this
.
fetchIssues
();
},
handleFilter
(
filter
)
{
this
.
filterTokens
=
filter
;
this
.
fetchIssues
();
},
handlePageChange
(
page
)
{
this
.
page
=
page
;
this
.
fetchIssues
();
handleNextPage
()
{
this
.
pageParams
=
{
afterCursor
:
this
.
pageInfo
.
endCursor
,
firstPageSize
:
PAGE_SIZE
,
};
scrollUp
();
},
handlePreviousPage
()
{
this
.
pageParams
=
{
beforeCursor
:
this
.
pageInfo
.
startCursor
,
lastPageSize
:
PAGE_SIZE
,
};
scrollUp
();
},
handleReorder
({
newIndex
,
oldIndex
})
{
const
issueToMove
=
this
.
issues
[
oldIndex
];
...
...
@@ -530,9 +518,11 @@ export default {
createFlash
({
message
:
this
.
$options
.
i18n
.
reorderError
});
});
},
handleSort
(
value
)
{
this
.
sortKey
=
value
;
this
.
fetchIssues
();
handleSort
(
sortKey
)
{
if
(
this
.
sortKey
!==
sortKey
)
{
this
.
pageParams
=
initialPageParams
;
}
this
.
sortKey
=
sortKey
;
},
toggleBulkEditSidebar
(
showBulkEditSidebar
)
{
this
.
showBulkEditSidebar
=
showBulkEditSidebar
;
...
...
@@ -556,18 +546,18 @@ export default {
:tabs=
"$options.IssuableListTabs"
:current-tab=
"state"
:tab-counts=
"tabCounts"
:issuables-loading=
"
isL
oading"
:issuables-loading=
"
$apollo.queries.issues.l
oading"
:is-manual-ordering=
"isManualOrdering"
:show-bulk-edit-sidebar=
"showBulkEditSidebar"
:show-pagination-controls=
"showPaginationControls"
:total-items=
"totalIssues"
:current-page=
"page"
:previous-page=
"page - 1"
:next-page=
"page + 1"
:use-keyset-pagination=
"true"
:has-next-page=
"pageInfo.hasNextPage"
:has-previous-page=
"pageInfo.hasPreviousPage"
:url-params=
"urlParams"
@
click-tab=
"handleClickTab"
@
filter=
"handleFilter"
@
page-change=
"handlePageChange"
@
next-page=
"handleNextPage"
@
previous-page=
"handlePreviousPage"
@
reorder=
"handleReorder"
@
sort=
"handleSort"
@
update-legacy-bulk-edit=
"handleUpdateLegacyBulkEdit"
...
...
@@ -646,7 +636,7 @@ export default {
</li>
<blocking-issues-count
class=
"gl-display-none gl-sm-display-block"
:blocking-issues-count=
"issuable.block
ingIssues
Count"
:blocking-issues-count=
"issuable.block
edBy
Count"
:is-list-item=
"true"
/>
</
template
>
...
...
app/assets/javascripts/issues_list/constants.js
View file @
154dd2c2
...
...
@@ -101,10 +101,13 @@ export const i18n = {
export
const
JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY
=
'
jira-import-success-alert-hide-map
'
;
export
const
PARAM_DUE_DATE
=
'
due_date
'
;
export
const
PARAM_PAGE
=
'
page
'
;
export
const
PARAM_SORT
=
'
sort
'
;
export
const
PARAM_STATE
=
'
state
'
;
export
const
initialPageParams
=
{
firstPageSize
:
PAGE_SIZE
,
};
export
const
DUE_DATE_NONE
=
'
0
'
;
export
const
DUE_DATE_ANY
=
''
;
export
const
DUE_DATE_OVERDUE
=
'
overdue
'
;
...
...
app/assets/javascripts/issues_list/index.js
View file @
154dd2c2
...
...
@@ -73,6 +73,13 @@ export function mountIssuesListApp() {
return
false
;
}
Vue
.
use
(
VueApollo
);
const
defaultClient
=
createDefaultClient
({},
{
assumeImmutableResults
:
true
});
const
apolloProvider
=
new
VueApollo
({
defaultClient
,
});
const
{
autocompleteAwardEmojisPath
,
autocompleteUsersPath
,
...
...
@@ -83,7 +90,6 @@ export function mountIssuesListApp() {
email
,
emailsHelpPagePath
,
emptyStateSvgPath
,
endpoint
,
exportCsvPath
,
groupEpicsPath
,
hasBlockedIssuesFeature
,
...
...
@@ -113,16 +119,13 @@ export function mountIssuesListApp() {
return
new
Vue
({
el
,
// Currently does not use Vue Apollo, but need to provide {} for now until the
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider
:
{},
apolloProvider
,
provide
:
{
autocompleteAwardEmojisPath
,
autocompleteUsersPath
,
calendarPath
,
canBulkUpdate
:
parseBoolean
(
canBulkUpdate
),
emptyStateSvgPath
,
endpoint
,
groupEpicsPath
,
hasBlockedIssuesFeature
:
parseBoolean
(
hasBlockedIssuesFeature
),
hasIssuableHealthStatusFeature
:
parseBoolean
(
hasIssuableHealthStatusFeature
),
...
...
app/assets/javascripts/issues_list/queries/get_issues.query.graphql
0 → 100644
View file @
154dd2c2
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./issue.fragment.graphql"
query
getProjectIssues
(
$projectPath
:
ID
!
$search
:
String
$sort
:
IssueSort
$state
:
IssuableState
$assigneeId
:
String
$assigneeUsernames
:
[
String
!]
$authorUsername
:
String
$labelName
:
[
String
]
$milestoneTitle
:
[
String
]
$not
:
NegatedIssueFilterInput
$beforeCursor
:
String
$afterCursor
:
String
$firstPageSize
:
Int
$lastPageSize
:
Int
)
{
project
(
fullPath
:
$projectPath
)
{
issues
(
search
:
$search
sort
:
$sort
state
:
$state
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
not
:
$not
before
:
$beforeCursor
after
:
$afterCursor
first
:
$firstPageSize
last
:
$lastPageSize
)
{
count
pageInfo
{
...
PageInfo
}
nodes
{
...
IssueFragment
}
}
}
}
app/assets/javascripts/issues_list/queries/issue.fragment.graphql
0 → 100644
View file @
154dd2c2
fragment
IssueFragment
on
Issue
{
id
iid
closedAt
confidential
createdAt
downvotes
dueDate
humanTimeEstimate
moved
title
updatedAt
upvotes
userDiscussionsCount
webUrl
assignees
{
nodes
{
id
avatarUrl
name
username
webUrl
}
}
author
{
id
avatarUrl
name
username
webUrl
}
labels
{
nodes
{
id
color
title
description
}
}
milestone
{
id
dueDate
startDate
webPath
title
}
taskCompletionStatus
{
completedCount
count
}
}
app/helpers/issues_helper.rb
View file @
154dd2c2
...
...
@@ -190,7 +190,6 @@ module IssuesHelper
email:
current_user
&
.
notification_email
,
emails_help_page_path:
help_page_path
(
'development/emails'
,
anchor:
'email-namespace'
),
empty_state_svg_path:
image_path
(
'illustrations/issues.svg'
),
endpoint:
expose_path
(
api_v4_projects_issues_path
(
id:
project
.
id
)),
export_csv_path:
export_csv_project_issues_path
(
project
),
has_project_issues:
project_issues
(
project
).
exists?
.
to_s
,
import_csv_issues_path:
import_csv_namespace_project_issues_path
,
...
...
ee/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
0 → 100644
View file @
154dd2c2
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "~/issues_list/queries/issue.fragment.graphql"
query
getProjectIssues
(
$projectPath
:
ID
!
$search
:
String
$sort
:
IssueSort
$state
:
IssuableState
$assigneeId
:
String
$assigneeUsernames
:
[
String
!]
$authorUsername
:
String
$labelName
:
[
String
]
$milestoneTitle
:
[
String
]
$epicId
:
String
$iterationId
:
[
ID
]
$iterationWildcardId
:
IterationWildcardId
$weight
:
String
$not
:
NegatedIssueFilterInput
$beforeCursor
:
String
$afterCursor
:
String
$firstPageSize
:
Int
$lastPageSize
:
Int
)
{
project
(
fullPath
:
$projectPath
)
{
issues
(
search
:
$search
sort
:
$sort
state
:
$state
assigneeId
:
$assigneeId
assigneeUsernames
:
$assigneeUsernames
authorUsername
:
$authorUsername
labelName
:
$labelName
milestoneTitle
:
$milestoneTitle
epicId
:
$epicId
iterationId
:
$iterationId
iterationWildcardId
:
$iterationWildcardId
weight
:
$weight
not
:
$not
before
:
$beforeCursor
after
:
$afterCursor
first
:
$firstPageSize
last
:
$lastPageSize
)
{
count
pageInfo
{
...
PageInfo
}
nodes
{
...
IssueFragment
blockedByCount
healthStatus
weight
}
}
}
}
ee/spec/frontend/integrations/jira/issues_list/components/__snapshots__/jira_issues_list_root_spec.js.snap
View file @
154dd2c2
...
...
@@ -6,6 +6,8 @@ Object {
"currentTab": "opened",
"defaultPageSize": 2,
"enableLabelPermalinks": true,
"hasNextPage": false,
"hasPreviousPage": false,
"initialFilterValue": Array [
Object {
"type": "filtered-search-term",
...
...
@@ -75,5 +77,6 @@ Object {
"sort": "created_desc",
"state": "opened",
},
"useKeysetPagination": false,
}
`;
spec/frontend/issuable_list/components/issuable_list_root_spec.js
View file @
154dd2c2
import
{
GlSkeletonLoading
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
Gl
KeysetPagination
,
Gl
SkeletonLoading
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
VueDraggable
from
'
vuedraggable
'
;
...
...
@@ -11,9 +11,12 @@ import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filte
import
{
mockIssuableListProps
,
mockIssuables
}
from
'
../mock_data
'
;
const
createComponent
=
({
props
=
mockIssuableListProps
,
data
=
{}
}
=
{})
=>
const
createComponent
=
({
props
=
{}
,
data
=
{}
}
=
{})
=>
shallowMount
(
IssuableListRoot
,
{
propsData
:
props
,
propsData
:
{
...
mockIssuableListProps
,
...
props
,
},
data
()
{
return
data
;
},
...
...
@@ -34,6 +37,7 @@ describe('IssuableListRoot', () => {
let
wrapper
;
const
findFilteredSearchBar
=
()
=>
wrapper
.
findComponent
(
FilteredSearchBar
);
const
findGlKeysetPagination
=
()
=>
wrapper
.
findComponent
(
GlKeysetPagination
);
const
findGlPagination
=
()
=>
wrapper
.
findComponent
(
GlPagination
);
const
findIssuableItem
=
()
=>
wrapper
.
findComponent
(
IssuableItem
);
const
findIssuableTabs
=
()
=>
wrapper
.
findComponent
(
IssuableTabs
);
...
...
@@ -189,15 +193,15 @@ describe('IssuableListRoot', () => {
});
describe
(
'
template
'
,
()
=>
{
beforeEach
(
()
=>
{
it
(
'
renders component container element with class "issuable-list-container"
'
,
()
=>
{
wrapper
=
createComponent
();
});
it
(
'
renders component container element with class "issuable-list-container"
'
,
()
=>
{
expect
(
wrapper
.
classes
()).
toContain
(
'
issuable-list-container
'
);
});
it
(
'
renders issuable-tabs component
'
,
()
=>
{
wrapper
=
createComponent
();
const
tabsEl
=
findIssuableTabs
();
expect
(
tabsEl
.
exists
()).
toBe
(
true
);
...
...
@@ -209,6 +213,8 @@ describe('IssuableListRoot', () => {
});
it
(
'
renders contents for slot "nav-actions" within issuable-tab component
'
,
()
=>
{
wrapper
=
createComponent
();
const
buttonEl
=
findIssuableTabs
().
find
(
'
button.js-new-issuable
'
);
expect
(
buttonEl
.
exists
()).
toBe
(
true
);
...
...
@@ -216,6 +222,8 @@ describe('IssuableListRoot', () => {
});
it
(
'
renders filtered-search-bar component
'
,
()
=>
{
wrapper
=
createComponent
();
const
searchEl
=
findFilteredSearchBar
();
const
{
namespace
,
...
...
@@ -239,12 +247,8 @@ describe('IssuableListRoot', () => {
});
});
it
(
'
renders gl-loading-icon when `issuablesLoading` prop is true
'
,
async
()
=>
{
wrapper
.
setProps
({
issuablesLoading
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
it
(
'
renders gl-loading-icon when `issuablesLoading` prop is true
'
,
()
=>
{
wrapper
=
createComponent
({
props
:
{
issuablesLoading
:
true
}
});
expect
(
wrapper
.
findAllComponents
(
GlSkeletonLoading
)).
toHaveLength
(
wrapper
.
vm
.
skeletonItemCount
,
...
...
@@ -252,6 +256,8 @@ describe('IssuableListRoot', () => {
});
it
(
'
renders issuable-item component for each item within `issuables` array
'
,
()
=>
{
wrapper
=
createComponent
();
const
itemsEl
=
wrapper
.
findAllComponents
(
IssuableItem
);
const
mockIssuable
=
mockIssuableListProps
.
issuables
[
0
];
...
...
@@ -262,28 +268,23 @@ describe('IssuableListRoot', () => {
});
});
it
(
'
renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty
'
,
async
()
=>
{
wrapper
.
setProps
({
issuables
:
[],
});
await
wrapper
.
vm
.
$nextTick
();
it
(
'
renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty
'
,
()
=>
{
wrapper
=
createComponent
({
props
:
{
issuables
:
[]
}
});
expect
(
wrapper
.
find
(
'
p.js-issuable-empty-state
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
find
(
'
p.js-issuable-empty-state
'
).
text
()).
toBe
(
'
Issuable empty state
'
);
});
it
(
'
renders gl-pagination when `showPaginationControls` prop is true
'
,
async
()
=>
{
wrapper
.
setProps
({
showPaginationControls
:
true
,
totalItems
:
10
,
it
(
'
renders only gl-pagination when `showPaginationControls` prop is true
'
,
()
=>
{
wrapper
=
createComponent
({
props
:
{
showPaginationControls
:
true
,
totalItems
:
10
,
},
});
await
wrapper
.
vm
.
$nextTick
();
const
paginationEl
=
findGlPagination
();
expect
(
paginationEl
.
exists
()).
toBe
(
true
);
expect
(
paginationEl
.
props
()).
toMatchObject
({
expect
(
findGlKeysetPagination
().
exists
()).
toBe
(
false
);
expect
(
findGlPagination
().
props
()).
toMatchObject
({
perPage
:
20
,
value
:
1
,
prevPage
:
0
,
...
...
@@ -292,32 +293,47 @@ describe('IssuableListRoot', () => {
align
:
'
center
'
,
});
});
});
describe
(
'
events
'
,
()
=>
{
beforeEach
(()
=>
{
it
(
'
renders only gl-keyset-pagination when `showPaginationControls` and `useKeysetPagination` props are true
'
,
()
=>
{
wrapper
=
createComponent
({
data
:
{
checkedIssuables
:
{
[
mockIssuables
[
0
].
iid
]:
{
checked
:
true
,
issuable
:
mockIssuables
[
0
]
},
},
props
:
{
hasNextPage
:
true
,
hasPreviousPage
:
true
,
showPaginationControls
:
true
,
useKeysetPagination
:
true
,
},
});
expect
(
findGlPagination
().
exists
()).
toBe
(
false
);
expect
(
findGlKeysetPagination
().
props
()).
toMatchObject
({
hasNextPage
:
true
,
hasPreviousPage
:
true
,
});
});
});
describe
(
'
events
'
,
()
=>
{
const
data
=
{
checkedIssuables
:
{
[
mockIssuables
[
0
].
iid
]:
{
checked
:
true
,
issuable
:
mockIssuables
[
0
]
},
},
};
it
(
'
issuable-tabs component emits `click-tab` event on `click-tab` event
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
findIssuableTabs
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
click-tab
'
)).
toBeTruthy
();
});
it
(
'
sets all issuables as checked when filtered-search-bar component emits `checked-input` event
'
,
async
()
=>
{
it
(
'
sets all issuables as checked when filtered-search-bar component emits `checked-input` event
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
const
searchEl
=
findFilteredSearchBar
();
searchEl
.
vm
.
$emit
(
'
checked-input
'
,
true
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
searchEl
.
emitted
(
'
checked-input
'
)).
toBeTruthy
();
expect
(
searchEl
.
emitted
(
'
checked-input
'
).
length
).
toBe
(
1
);
...
...
@@ -328,6 +344,8 @@ describe('IssuableListRoot', () => {
});
it
(
'
filtered-search-bar component emits `filter` event on `onFilter` & `sort` event on `onSort` events
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
const
searchEl
=
findFilteredSearchBar
();
searchEl
.
vm
.
$emit
(
'
onFilter
'
);
...
...
@@ -336,13 +354,13 @@ describe('IssuableListRoot', () => {
expect
(
wrapper
.
emitted
(
'
sort
'
)).
toBeTruthy
();
});
it
(
'
sets an issuable as checked when issuable-item component emits `checked-input` event
'
,
async
()
=>
{
it
(
'
sets an issuable as checked when issuable-item component emits `checked-input` event
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
const
issuableItem
=
wrapper
.
findAllComponents
(
IssuableItem
).
at
(
0
);
issuableItem
.
vm
.
$emit
(
'
checked-input
'
,
true
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
issuableItem
.
emitted
(
'
checked-input
'
)).
toBeTruthy
();
expect
(
issuableItem
.
emitted
(
'
checked-input
'
).
length
).
toBe
(
1
);
...
...
@@ -353,27 +371,45 @@ describe('IssuableListRoot', () => {
});
it
(
'
emits `update-legacy-bulk-edit` when filtered-search-bar checkbox is checked
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
findFilteredSearchBar
().
vm
.
$emit
(
'
checked-input
'
);
expect
(
wrapper
.
emitted
(
'
update-legacy-bulk-edit
'
)).
toEqual
([[]]);
});
it
(
'
emits `update-legacy-bulk-edit` when issuable-item checkbox is checked
'
,
()
=>
{
wrapper
=
createComponent
({
data
});
findIssuableItem
().
vm
.
$emit
(
'
checked-input
'
);
expect
(
wrapper
.
emitted
(
'
update-legacy-bulk-edit
'
)).
toEqual
([[]]);
});
it
(
'
gl-pagination component emits `page-change` event on `input` event
'
,
async
()
=>
{
wrapper
.
setProps
({
showPaginationControls
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
it
(
'
gl-pagination component emits `page-change` event on `input` event
'
,
()
=>
{
wrapper
=
createComponent
({
data
,
props
:
{
showPaginationControls
:
true
}
});
findGlPagination
().
vm
.
$emit
(
'
input
'
);
expect
(
wrapper
.
emitted
(
'
page-change
'
)).
toBeTruthy
();
});
it
.
each
`
event | glKeysetPaginationEvent
${
'
next-page
'
}
|
${
'
next
'
}
${
'
previous-page
'
}
|
${
'
prev
'
}
`
(
'
emits `$event` event when gl-keyset-pagination emits `$glKeysetPaginationEvent` event
'
,
({
event
,
glKeysetPaginationEvent
})
=>
{
wrapper
=
createComponent
({
data
,
props
:
{
showPaginationControls
:
true
,
useKeysetPagination
:
true
},
});
findGlKeysetPagination
().
vm
.
$emit
(
glKeysetPaginationEvent
);
expect
(
wrapper
.
emitted
(
event
)).
toEqual
([[]]);
},
);
});
describe
(
'
manual sorting
'
,
()
=>
{
...
...
spec/frontend/issues_list/components/issue_card_time_info_spec.js
View file @
154dd2c2
...
...
@@ -13,12 +13,10 @@ describe('IssuesListApp component', () => {
dueDate
:
'
2020-12-17
'
,
startDate
:
'
2020-12-10
'
,
title
:
'
My milestone
'
,
web
Url
:
'
/milestone/webUrl
'
,
web
Path
:
'
/milestone/webPath
'
,
},
dueDate
:
'
2020-12-12
'
,
timeStats
:
{
humanTimeEstimate
:
'
1w
'
,
},
humanTimeEstimate
:
'
1w
'
,
};
const
findMilestone
=
()
=>
wrapper
.
find
(
'
[data-testid="issuable-milestone"]
'
);
...
...
@@ -56,7 +54,7 @@ describe('IssuesListApp component', () => {
expect
(
milestone
.
text
()).
toBe
(
issue
.
milestone
.
title
);
expect
(
milestone
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
clock
'
);
expect
(
milestone
.
find
(
GlLink
).
attributes
(
'
href
'
)).
toBe
(
issue
.
milestone
.
web
Url
);
expect
(
milestone
.
find
(
GlLink
).
attributes
(
'
href
'
)).
toBe
(
issue
.
milestone
.
web
Path
);
});
describe
.
each
`
...
...
@@ -102,7 +100,7 @@ describe('IssuesListApp component', () => {
const
timeEstimate
=
wrapper
.
find
(
'
[data-testid="time-estimate"]
'
);
expect
(
timeEstimate
.
text
()).
toBe
(
issue
.
timeStats
.
humanTimeEstimate
);
expect
(
timeEstimate
.
text
()).
toBe
(
issue
.
humanTimeEstimate
);
expect
(
timeEstimate
.
attributes
(
'
title
'
)).
toBe
(
'
Estimate
'
);
expect
(
timeEstimate
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
timer
'
);
});
...
...
spec/frontend/issues_list/components/issues_list_app_spec.js
View file @
154dd2c2
This diff is collapsed.
Click to expand it.
spec/frontend/issues_list/mock_data.js
View file @
154dd2c2
...
...
@@ -3,6 +3,73 @@ import {
OPERATOR_IS_NOT
,
}
from
'
~/vue_shared/components/filtered_search_bar/constants
'
;
export
const
getIssuesQueryResponse
=
{
data
:
{
project
:
{
issues
:
{
count
:
1
,
pageInfo
:
{
hasNextPage
:
false
,
hasPreviousPage
:
false
,
startCursor
:
'
startcursor
'
,
endCursor
:
'
endcursor
'
,
},
nodes
:
[
{
id
:
'
gid://gitlab/Issue/123456
'
,
iid
:
'
789
'
,
closedAt
:
null
,
confidential
:
false
,
createdAt
:
'
2021-05-22T04:08:01Z
'
,
downvotes
:
2
,
dueDate
:
'
2021-05-29
'
,
humanTimeEstimate
:
null
,
moved
:
false
,
title
:
'
Issue title
'
,
updatedAt
:
'
2021-05-22T04:08:01Z
'
,
upvotes
:
3
,
userDiscussionsCount
:
4
,
webUrl
:
'
project/-/issues/789
'
,
assignees
:
{
nodes
:
[
{
id
:
'
gid://gitlab/User/234
'
,
avatarUrl
:
'
avatar/url
'
,
name
:
'
Marge Simpson
'
,
username
:
'
msimpson
'
,
webUrl
:
'
url/msimpson
'
,
},
],
},
author
:
{
id
:
'
gid://gitlab/User/456
'
,
avatarUrl
:
'
avatar/url
'
,
name
:
'
Homer Simpson
'
,
username
:
'
hsimpson
'
,
webUrl
:
'
url/hsimpson
'
,
},
labels
:
{
nodes
:
[
{
id
:
'
gid://gitlab/ProjectLabel/456
'
,
color
:
'
#333
'
,
title
:
'
Label title
'
,
description
:
'
Label description
'
,
},
],
},
milestone
:
null
,
taskCompletionStatus
:
{
completedCount
:
1
,
count
:
2
,
},
},
],
},
},
},
};
export
const
locationSearch
=
[
'
?search=find+issues
'
,
'
author_username=homer
'
,
...
...
spec/helpers/issues_helper_spec.rb
View file @
154dd2c2
...
...
@@ -302,7 +302,6 @@ RSpec.describe IssuesHelper do
email:
current_user
&
.
notification_email
,
emails_help_page_path:
help_page_path
(
'development/emails'
,
anchor:
'email-namespace'
),
empty_state_svg_path:
'#'
,
endpoint:
expose_path
(
api_v4_projects_issues_path
(
id:
project
.
id
)),
export_csv_path:
export_csv_project_issues_path
(
project
),
has_project_issues:
project_issues
(
project
).
exists?
.
to_s
,
import_csv_issues_path:
'#'
,
...
...
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