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
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
import
{
GlButton
,
GlEmptyState
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
createLocalVue
,
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
AxiosMockAdapter
from
'
axios-mock-adapter
'
;
import
{
cloneDeep
}
from
'
lodash
'
;
import
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
getIssuesQuery
from
'
ee_else_ce/issues_list/queries/get_issues.query.graphql
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
apiParams
,
filteredTokens
,
locationSearch
,
urlParams
}
from
'
jest/issues_list/mock_data
'
;
import
{
getIssuesQueryResponse
,
filteredTokens
,
locationSearch
,
urlParams
,
}
from
'
jest/issues_list/mock_data
'
;
import
createFlash
from
'
~/flash
'
;
import
CsvImportExportButtons
from
'
~/issuable/components/csv_import_export_buttons.vue
'
;
import
IssuableByEmail
from
'
~/issuable/components/issuable_by_email.vue
'
;
...
...
@@ -14,10 +24,7 @@ import {
apiSortParams
,
CREATED_DESC
,
DUE_DATE_OVERDUE
,
PAGE_SIZE
,
PAGE_SIZE_MANUAL
,
PARAM_DUE_DATE
,
RELATIVE_POSITION_DESC
,
TOKEN_TYPE_ASSIGNEE
,
TOKEN_TYPE_AUTHOR
,
TOKEN_TYPE_CONFIDENTIAL
,
...
...
@@ -32,20 +39,26 @@ import {
import
eventHub
from
'
~/issues_list/eventhub
'
;
import
{
getSortOptions
}
from
'
~/issues_list/utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
scrollUp
}
from
'
~/lib/utils/scroll_utils
'
;
import
{
setUrlParams
}
from
'
~/lib/utils/url_utility
'
;
jest
.
mock
(
'
~/flash
'
);
jest
.
mock
(
'
~/lib/utils/scroll_utils
'
,
()
=>
({
scrollUp
:
jest
.
fn
().
mockName
(
'
scrollUpMock
'
),
}));
describe
(
'
IssuesListApp component
'
,
()
=>
{
let
axiosMock
;
let
wrapper
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueApollo
);
const
defaultProvide
=
{
autocompleteUsersPath
:
'
autocomplete/users/path
'
,
calendarPath
:
'
calendar/path
'
,
canBulkUpdate
:
false
,
emptyStateSvgPath
:
'
empty-state.svg
'
,
endpoint
:
'
api/endpoint
'
,
exportCsvPath
:
'
export/csv/path
'
,
hasBlockedIssuesFeature
:
true
,
hasIssueWeightsFeature
:
true
,
...
...
@@ -61,21 +74,13 @@ describe('IssuesListApp component', () => {
signInPath
:
'
sign/in/path
'
,
};
const
state
=
'
opened
'
;
const
xPage
=
1
;
const
xTotal
=
25
;
const
tabCounts
=
{
opened
:
xTotal
,
closed
:
undefined
,
all
:
undefined
,
};
const
fetchIssuesResponse
=
{
data
:
[],
headers
:
{
'
x-page
'
:
xPage
,
'
x-total
'
:
xTotal
,
},
};
let
defaultQueryResponse
=
getIssuesQueryResponse
;
if
(
IS_EE
)
{
defaultQueryResponse
=
cloneDeep
(
getIssuesQueryResponse
);
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
].
blockedByCount
=
1
;
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
].
healthStatus
=
null
;
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
].
weight
=
5
;
}
const
findCsvImportExportButtons
=
()
=>
wrapper
.
findComponent
(
CsvImportExportButtons
);
const
findIssuableByEmail
=
()
=>
wrapper
.
findComponent
(
IssuableByEmail
);
...
...
@@ -86,19 +91,26 @@ describe('IssuesListApp component', () => {
const
findGlLink
=
()
=>
wrapper
.
findComponent
(
GlLink
);
const
findIssuableList
=
()
=>
wrapper
.
findComponent
(
IssuableList
);
const
mountComponent
=
({
provide
=
{},
mountFn
=
shallowMount
}
=
{})
=>
mountFn
(
IssuesListApp
,
{
const
mountComponent
=
({
provide
=
{},
response
=
defaultQueryResponse
,
mountFn
=
shallowMount
,
}
=
{})
=>
{
const
requestHandlers
=
[[
getIssuesQuery
,
jest
.
fn
().
mockResolvedValue
(
response
)]];
const
apolloProvider
=
createMockApollo
(
requestHandlers
);
return
mountFn
(
IssuesListApp
,
{
localVue
,
apolloProvider
,
provide
:
{
...
defaultProvide
,
...
provide
,
},
});
};
beforeEach
(()
=>
{
axiosMock
=
new
AxiosMockAdapter
(
axios
);
axiosMock
.
onGet
(
defaultProvide
.
endpoint
)
.
reply
(
200
,
fetchIssuesResponse
.
data
,
fetchIssuesResponse
.
headers
);
});
afterEach
(()
=>
{
...
...
@@ -108,28 +120,37 @@ describe('IssuesListApp component', () => {
});
describe
(
'
IssuableList
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
wrapper
=
mountComponent
();
await
waitForPromise
s
();
jest
.
runOnlyPendingTimer
s
();
});
it
(
'
renders
'
,
()
=>
{
expect
(
findIssuableList
().
props
()).
toMatchObject
({
namespace
:
defaultProvide
.
projectPath
,
recentSearchesStorageKey
:
'
issues
'
,
searchInputPlaceholder
:
'
Search or filter results…
'
,
searchInputPlaceholder
:
IssuesListApp
.
i18n
.
searchPlaceholder
,
sortOptions
:
getSortOptions
(
true
,
true
),
initialSortBy
:
CREATED_DESC
,
issuables
:
getIssuesQueryResponse
.
data
.
project
.
issues
.
nodes
,
tabs
:
IssuableListTabs
,
currentTab
:
IssuableStates
.
Opened
,
tabCounts
,
showPaginationControls
:
false
,
issuables
:
[],
totalItems
:
xTotal
,
currentPage
:
xPage
,
previousPage
:
xPage
-
1
,
nextPage
:
xPage
+
1
,
urlParams
:
{
page
:
xPage
,
state
},
tabCounts
:
{
opened
:
1
,
closed
:
undefined
,
all
:
undefined
,
},
issuablesLoading
:
false
,
isManualOrdering
:
false
,
showBulkEditSidebar
:
false
,
showPaginationControls
:
true
,
useKeysetPagination
:
true
,
hasPreviousPage
:
getIssuesQueryResponse
.
data
.
project
.
issues
.
pageInfo
.
hasPreviousPage
,
hasNextPage
:
getIssuesQueryResponse
.
data
.
project
.
issues
.
pageInfo
.
hasNextPage
,
urlParams
:
{
state
:
IssuableStates
.
Opened
,
...
urlSortParams
[
CREATED_DESC
],
},
});
});
});
...
...
@@ -157,9 +178,9 @@ describe('IssuesListApp component', () => {
describe
(
'
csv import/export component
'
,
()
=>
{
describe
(
'
when user is signed in
'
,
()
=>
{
it
(
'
renders
'
,
async
()
=>
{
const
search
=
'
?page=1&search=refactor&state=opened&sort=created_date
'
;
const
search
=
'
?search=refactor&state=opened&sort=created_date
'
;
beforeEach
(()
=>
{
global
.
jsdom
.
reconfigure
({
url
:
`
${
TEST_HOST
}${
search
}
`
});
wrapper
=
mountComponent
({
...
...
@@ -167,11 +188,13 @@ describe('IssuesListApp component', () => {
mountFn
:
mount
,
});
await
waitForPromises
();
jest
.
runOnlyPendingTimers
();
});
it
(
'
renders
'
,
()
=>
{
expect
(
findCsvImportExportButtons
().
props
()).
toMatchObject
({
exportCsvPath
:
`
${
defaultProvide
.
exportCsvPath
}${
search
}
`
,
issuableCount
:
xTotal
,
issuableCount
:
1
,
});
});
});
...
...
@@ -238,18 +261,6 @@ describe('IssuesListApp component', () => {
});
});
describe
(
'
page
'
,
()
=>
{
it
(
'
is set from the url params
'
,
()
=>
{
const
page
=
5
;
global
.
jsdom
.
reconfigure
({
url
:
setUrlParams
({
page
},
TEST_HOST
)
});
wrapper
=
mountComponent
();
expect
(
findIssuableList
().
props
(
'
currentPage
'
)).
toBe
(
page
);
});
});
describe
(
'
search
'
,
()
=>
{
it
(
'
is set from the url params
'
,
()
=>
{
global
.
jsdom
.
reconfigure
({
url
:
`
${
TEST_HOST
}${
locationSearch
}
`
});
...
...
@@ -326,12 +337,10 @@ describe('IssuesListApp component', () => {
describe
(
'
empty states
'
,
()
=>
{
describe
(
'
when there are issues
'
,
()
=>
{
describe
(
'
when search returns no results
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
global
.
jsdom
.
reconfigure
({
url
:
`
${
TEST_HOST
}
?search=no+results`
});
wrapper
=
mountComponent
({
provide
:
{
hasProjectIssues
:
true
},
mountFn
:
mount
});
await
waitForPromises
();
});
it
(
'
shows empty state
'
,
()
=>
{
...
...
@@ -344,10 +353,8 @@ describe('IssuesListApp component', () => {
});
describe
(
'
when "Open" tab has no issues
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
wrapper
=
mountComponent
({
provide
:
{
hasProjectIssues
:
true
},
mountFn
:
mount
});
await
waitForPromises
();
});
it
(
'
shows empty state
'
,
()
=>
{
...
...
@@ -360,14 +367,12 @@ describe('IssuesListApp component', () => {
});
describe
(
'
when "Closed" tab has no issues
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(()
=>
{
global
.
jsdom
.
reconfigure
({
url
:
setUrlParams
({
state
:
IssuableStates
.
Closed
},
TEST_HOST
),
});
wrapper
=
mountComponent
({
provide
:
{
hasProjectIssues
:
true
},
mountFn
:
mount
});
await
waitForPromises
();
});
it
(
'
shows empty state
'
,
()
=>
{
...
...
@@ -555,98 +560,70 @@ describe('IssuesListApp component', () => {
describe
(
'
events
'
,
()
=>
{
describe
(
'
when "click-tab" event is emitted by IssuableList
'
,
()
=>
{
beforeEach
(()
=>
{
axiosMock
.
onGet
(
defaultProvide
.
endpoint
).
reply
(
200
,
fetchIssuesResponse
.
data
,
{
'
x-page
'
:
2
,
'
x-total
'
:
xTotal
,
});
wrapper
=
mountComponent
();
findIssuableList
().
vm
.
$emit
(
'
click-tab
'
,
IssuableStates
.
Closed
);
});
it
(
'
makes API call to filter the list by the new state and resets the page to 1
'
,
()
=>
{
expect
(
axiosMock
.
history
.
get
[
1
].
params
).
toMatchObject
({
page
:
1
,
state
:
IssuableStates
.
Closed
,
});
it
(
'
updates to the new tab
'
,
()
=>
{
expect
(
findIssuableList
().
props
(
'
currentTab
'
)).
toBe
(
IssuableStates
.
Closed
);
});
});
describe
(
'
when "page-change" event is emitted by IssuableList
'
,
()
=>
{
const
data
=
[{
id
:
10
,
title
:
'
title
'
,
state
}];
const
page
=
2
;
const
totalItems
=
21
;
beforeEach
(
async
()
=>
{
axiosMock
.
onGet
(
defaultProvide
.
endpoint
).
reply
(
200
,
data
,
{
'
x-page
'
:
page
,
'
x-total
'
:
totalItems
,
});
wrapper
=
mountComponent
();
findIssuableList
().
vm
.
$emit
(
'
page-change
'
,
page
);
await
waitForPromises
();
});
describe
.
each
([
'
next-page
'
,
'
previous-page
'
])(
'
when "%s" event is emitted by IssuableList
'
,
(
event
)
=>
{
beforeEach
(()
=>
{
wrapper
=
mountComponent
();
it
(
'
fetches issues with expected params
'
,
()
=>
{
expect
(
axiosMock
.
history
.
get
[
1
].
params
).
toMatchObject
({
page
,
per_page
:
PAGE_SIZE
,
state
,
with_labels_details
:
true
,
findIssuableList
().
vm
.
$emit
(
event
);
});
});
it
(
'
updates IssuableList with response data
'
,
()
=>
{
expect
(
findIssuableList
().
props
()).
toMatchObject
({
issuables
:
data
,
totalItems
,
currentPage
:
page
,
previousPage
:
page
-
1
,
nextPage
:
page
+
1
,
urlParams
:
{
page
,
state
},
it
(
'
scrolls to the top
'
,
()
=>
{
expect
(
scrollUp
).
toHaveBeenCalled
();
});
}
);
}
);
}
,
);
describe
(
'
when "reorder" event is emitted by IssuableList
'
,
()
=>
{
const
issueOne
=
{
id
:
1
,
iid
:
101
,
title
:
'
Issue one
'
};
const
issueTwo
=
{
id
:
2
,
iid
:
102
,
title
:
'
Issue two
'
};
const
issueThree
=
{
id
:
3
,
iid
:
103
,
title
:
'
Issue three
'
};
const
issueFour
=
{
id
:
4
,
iid
:
104
,
title
:
'
Issue four
'
};
const
issues
=
[
issueOne
,
issueTwo
,
issueThree
,
issueFour
];
beforeEach
(
async
()
=>
{
axiosMock
.
onGet
(
defaultProvide
.
endpoint
).
reply
(
200
,
issues
,
fetchIssuesResponse
.
headers
);
wrapper
=
mountComponent
();
await
waitForPromises
();
});
describe
(
'
when successful
'
,
()
=>
{
describe
.
each
`
description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
${
'
to the beginning of the list
'
}
|
${
issueThree
}
|
${
2
}
|
${
0
}
|
${
null
}
|
${
issueOne
.
id
}
${
'
down the list
'
}
|
${
issueOne
}
|
${
0
}
|
${
1
}
|
${
issueTwo
.
id
}
|
${
issueThree
.
id
}
${
'
up the list
'
}
|
${
issueThree
}
|
${
2
}
|
${
1
}
|
${
issueOne
.
id
}
|
${
issueTwo
.
id
}
${
'
to the end of the list
'
}
|
${
issueTwo
}
|
${
1
}
|
${
3
}
|
${
issueFour
.
id
}
|
${
null
}
`
(
'
when moving issue $description
'
,
({
issueToMove
,
oldIndex
,
newIndex
,
moveBeforeId
,
moveAfterId
})
=>
{
it
(
'
makes API call to reorder the issue
'
,
async
()
=>
{
findIssuableList
().
vm
.
$emit
(
'
reorder
'
,
{
oldIndex
,
newIndex
});
await
waitForPromises
();
expect
(
axiosMock
.
history
.
put
[
0
]).
toMatchObject
({
url
:
`
${
defaultProvide
.
issuesPath
}
/
${
issueToMove
.
iid
}
/reorder`
,
data
:
JSON
.
stringify
({
move_before_id
:
moveBeforeId
,
move_after_id
:
moveAfterId
}),
});
});
const
issueOne
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/1
'
,
iid
:
101
,
title
:
'
Issue one
'
,
};
const
issueTwo
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/2
'
,
iid
:
102
,
title
:
'
Issue two
'
,
};
const
issueThree
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/3
'
,
iid
:
103
,
title
:
'
Issue three
'
,
};
const
issueFour
=
{
...
defaultQueryResponse
.
data
.
project
.
issues
.
nodes
[
0
],
id
:
'
gid://gitlab/Issue/4
'
,
iid
:
104
,
title
:
'
Issue four
'
,
};
const
response
=
{
data
:
{
project
:
{
issues
:
{
...
defaultQueryResponse
.
data
.
project
.
issues
,
nodes
:
[
issueOne
,
issueTwo
,
issueThree
,
issueFour
],
},
},
);
},
};
beforeEach
(()
=>
{
wrapper
=
mountComponent
({
response
});
jest
.
runOnlyPendingTimers
();
});
describe
(
'
when unsuccessful
'
,
()
=>
{
...
...
@@ -664,21 +641,16 @@ describe('IssuesListApp component', () => {
describe
(
'
when "sort" event is emitted by IssuableList
'
,
()
=>
{
it
.
each
(
Object
.
keys
(
apiSortParams
))(
'
fetches issues with correct params with payload
`%s`
'
,
'
updates to the new sort when payload is
`%s`
'
,
async
(
sortKey
)
=>
{
wrapper
=
mountComponent
();
findIssuableList
().
vm
.
$emit
(
'
sort
'
,
sortKey
);
await
waitForPromises
();
jest
.
runOnlyPendingTimers
();
await
nextTick
();
expect
(
axiosMock
.
history
.
get
[
1
].
params
).
toEqual
({
page
:
xPage
,
per_page
:
sortKey
===
RELATIVE_POSITION_DESC
?
PAGE_SIZE_MANUAL
:
PAGE_SIZE
,
state
,
with_labels_details
:
true
,
...
apiSortParams
[
sortKey
],
});
expect
(
findIssuableList
().
props
(
'
urlParams
'
)).
toMatchObject
(
urlSortParams
[
sortKey
]);
},
);
});
...
...
@@ -687,13 +659,11 @@ describe('IssuesListApp component', () => {
beforeEach
(()
=>
{
wrapper
=
mountComponent
();
jest
.
spyOn
(
eventHub
,
'
$emit
'
);
});
it
(
'
emits an "issuables:updateBulkEdit" event to the legacy bulk edit class
'
,
async
()
=>
{
findIssuableList
().
vm
.
$emit
(
'
update-legacy-bulk-edit
'
);
});
await
waitForPromises
();
it
(
'
emits an "issuables:updateBulkEdit" event to the legacy bulk edit class
'
,
()
=>
{
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
issuables:updateBulkEdit
'
);
});
});
...
...
@@ -705,10 +675,6 @@ describe('IssuesListApp component', () => {
findIssuableList
().
vm
.
$emit
(
'
filter
'
,
filteredTokens
);
});
it
(
'
makes an API call to search for issues with the search term
'
,
()
=>
{
expect
(
axiosMock
.
history
.
get
[
1
].
params
).
toMatchObject
(
apiParams
);
});
it
(
'
updates IssuableList with url params
'
,
()
=>
{
expect
(
findIssuableList
().
props
(
'
urlParams
'
)).
toMatchObject
(
urlParams
);
});
...
...
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