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
f79ed8ad
Commit
f79ed8ad
authored
Mar 11, 2021
by
Eulyeon Ko
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add blocked popover feature for boards
Also - Add vuex action for setting error state
parent
1632d456
Changes
23
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
777 additions
and
86 deletions
+777
-86
app/assets/javascripts/boards/components/board_blocked_icon.vue
...sets/javascripts/boards/components/board_blocked_icon.vue
+192
-0
app/assets/javascripts/boards/components/board_card_inner.vue
...assets/javascripts/boards/components/board_card_inner.vue
+20
-16
app/assets/javascripts/boards/components/board_content.vue
app/assets/javascripts/boards/components/board_content.vue
+2
-2
app/assets/javascripts/boards/constants.js
app/assets/javascripts/boards/constants.js
+7
-0
app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
...cripts/boards/graphql/board_blocking_issues.query.graphql
+16
-0
app/assets/javascripts/boards/index.js
app/assets/javascripts/boards/index.js
+1
-0
app/assets/javascripts/boards/stores/actions.js
app/assets/javascripts/boards/stores/actions.js
+13
-0
app/assets/javascripts/boards/stores/mutation_types.js
app/assets/javascripts/boards/stores/mutation_types.js
+1
-0
app/assets/javascripts/boards/stores/mutations.js
app/assets/javascripts/boards/stores/mutations.js
+4
-0
app/assets/javascripts/boards/stores/state.js
app/assets/javascripts/boards/stores/state.js
+1
-0
app/assets/javascripts/graphql_shared/constants.js
app/assets/javascripts/graphql_shared/constants.js
+2
-0
changelogs/unreleased/implement-blocking-issue-popover-for-boards.yml
...nreleased/implement-blocking-issue-popover-for-boards.yml
+5
-0
doc/user/project/img/issue_boards_blocked_icon_v13_10.png
doc/user/project/img/issue_boards_blocked_icon_v13_10.png
+0
-0
doc/user/project/img/issue_boards_blocked_icon_v13_6.png
doc/user/project/img/issue_boards_blocked_icon_v13_6.png
+0
-0
doc/user/project/issue_board.md
doc/user/project/issue_board.md
+9
-2
ee/spec/frontend/boards/board_card_inner_spec.js
ee/spec/frontend/boards/board_card_inner_spec.js
+27
-38
locale/gitlab.pot
locale/gitlab.pot
+19
-0
spec/frontend/boards/board_card_inner_spec.js
spec/frontend/boards/board_card_inner_spec.js
+59
-28
spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
.../components/__snapshots__/board_blocked_icon_spec.js.snap
+30
-0
spec/frontend/boards/components/board_blocked_icon_spec.js
spec/frontend/boards/components/board_blocked_icon_spec.js
+226
-0
spec/frontend/boards/mock_data.js
spec/frontend/boards/mock_data.js
+87
-0
spec/frontend/boards/stores/actions_spec.js
spec/frontend/boards/stores/actions_spec.js
+46
-0
spec/frontend/boards/stores/mutations_spec.js
spec/frontend/boards/stores/mutations_spec.js
+10
-0
No files found.
app/assets/javascripts/boards/components/board_blocked_icon.vue
0 → 100644
View file @
f79ed8ad
<
script
>
import
{
GlIcon
,
GlLink
,
GlPopover
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
blockingIssuablesQueries
,
issuableTypes
}
from
'
~/boards/constants
'
;
import
{
IssueType
}
from
'
~/graphql_shared/constants
'
;
import
{
convertToGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
truncate
}
from
'
~/lib/utils/text_utility
'
;
import
{
__
,
n__
,
s__
,
sprintf
}
from
'
~/locale
'
;
export
default
{
i18n
:
{
issuableType
:
{
[
issuableTypes
.
issue
]:
__
(
'
issue
'
),
},
},
graphQLIdType
:
{
[
issuableTypes
.
issue
]:
IssueType
,
},
referenceFormatter
:
{
[
issuableTypes
.
issue
]:
(
r
)
=>
r
.
split
(
'
/
'
)[
1
],
},
defaultDisplayLimit
:
3
,
textTruncateWidth
:
80
,
components
:
{
GlIcon
,
GlPopover
,
GlLink
,
GlLoadingIcon
,
},
blockingIssuablesQueries
,
props
:
{
item
:
{
type
:
Object
,
required
:
true
,
},
uniqueId
:
{
type
:
String
,
required
:
true
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
validator
(
value
)
{
return
[
issuableTypes
.
issue
].
includes
(
value
);
},
},
},
apollo
:
{
blockingIssuables
:
{
skip
()
{
return
this
.
skip
;
},
query
()
{
return
blockingIssuablesQueries
[
this
.
issuableType
].
query
;
},
variables
()
{
return
{
id
:
convertToGraphQLId
(
this
.
$options
.
graphQLIdType
[
this
.
issuableType
],
this
.
item
.
id
),
};
},
update
(
data
)
{
this
.
skip
=
true
;
return
data
?.
issuable
?.
blockingIssuables
?.
nodes
||
[];
},
error
(
error
)
{
const
message
=
sprintf
(
s__
(
'
Boards|Failed to fetch blocking %{issuableType}s
'
),
{
issuableType
:
this
.
issuableTypeText
,
});
this
.
$emit
(
'
blocking-issuables-error
'
,
{
error
,
message
});
},
},
},
data
()
{
return
{
skip
:
true
,
blockingIssuables
:
[],
};
},
computed
:
{
displayedIssuables
()
{
const
{
defaultDisplayLimit
,
referenceFormatter
}
=
this
.
$options
;
return
this
.
blockingIssuables
.
slice
(
0
,
defaultDisplayLimit
).
map
((
i
)
=>
{
return
{
...
i
,
title
:
truncate
(
i
.
title
,
this
.
$options
.
textTruncateWidth
),
reference
:
referenceFormatter
[
this
.
issuableType
](
i
.
reference
),
};
});
},
loading
()
{
return
this
.
$apollo
.
queries
.
blockingIssuables
.
loading
;
},
issuableTypeText
()
{
return
this
.
$options
.
i18n
.
issuableType
[
this
.
issuableType
];
},
blockedLabel
()
{
return
sprintf
(
n__
(
'
Boards|Blocked by %{blockedByCount} %{issuableType}
'
,
'
Boards|Blocked by %{blockedByCount} %{issuableType}s
'
,
this
.
item
.
blockedByCount
,
),
{
blockedByCount
:
this
.
item
.
blockedByCount
,
issuableType
:
this
.
issuableTypeText
,
},
);
},
glIconId
()
{
return
`blocked-icon-
${
this
.
uniqueId
}
`
;
},
hasMoreIssuables
()
{
return
this
.
item
.
blockedByCount
>
this
.
$options
.
defaultDisplayLimit
;
},
displayedIssuablesCount
()
{
return
this
.
hasMoreIssuables
?
this
.
item
.
blockedByCount
-
this
.
$options
.
defaultDisplayLimit
:
this
.
item
.
blockedByCount
;
},
moreIssuablesText
()
{
return
sprintf
(
n__
(
'
Boards|+ %{displayedIssuablesCount} more %{issuableType}
'
,
'
Boards|+ %{displayedIssuablesCount} more %{issuableType}s
'
,
this
.
displayedIssuablesCount
,
),
{
displayedIssuablesCount
:
this
.
displayedIssuablesCount
,
issuableType
:
this
.
issuableTypeText
,
},
);
},
viewAllIssuablesText
()
{
return
sprintf
(
s__
(
'
Boards|View all blocking %{issuableType}s
'
),
{
issuableType
:
this
.
issuableTypeText
,
});
},
loadingMessage
()
{
return
sprintf
(
s__
(
'
Boards|Retrieving blocking %{issuableType}s
'
),
{
issuableType
:
this
.
issuableTypeText
,
});
},
},
methods
:
{
handleMouseEnter
()
{
this
.
skip
=
false
;
},
},
};
</
script
>
<
template
>
<div
class=
"gl-display-inline"
>
<gl-icon
:id=
"glIconId"
ref=
"icon"
name=
"issue-block"
class=
"issue-blocked-icon gl-mr-2 gl-cursor-pointer"
data-testid=
"issue-blocked-icon"
@
mouseenter=
"handleMouseEnter"
/>
<gl-popover
:target=
"glIconId"
placement=
"top"
triggers=
"hover"
>
<template
#title
><span
data-testid=
"popover-title"
>
{{
blockedLabel
}}
</span></
template
>
<
template
v-if=
"loading"
>
<gl-loading-icon
/>
<p
class=
"gl-mt-4 gl-mb-0 gl-font-small"
>
{{
loadingMessage
}}
</p>
</
template
>
<
template
v-else
>
<ul
class=
"gl-list-style-none gl-p-0"
>
<li
v-for=
"issuable in displayedIssuables"
:key=
"issuable.id"
>
<gl-link
:href=
"issuable.webUrl"
class=
"gl-text-blue-500! gl-font-sm"
>
{{
issuable
.
reference
}}
</gl-link>
<p
class=
"gl-mb-3 gl-display-block!"
data-testid=
"issuable-title"
>
{{
issuable
.
title
}}
</p>
</li>
</ul>
<div
v-if=
"hasMoreIssuables"
class=
"gl-mt-4"
>
<p
class=
"gl-mb-3"
data-testid=
"hidden-blocking-count"
>
{{
moreIssuablesText
}}
</p>
<gl-link
data-testid=
"view-all-issues"
:href=
"`$
{item.webUrl}#related-issues`"
class="gl-text-blue-500! gl-font-sm"
>
{{
viewAllIssuablesText
}}
</gl-link
>
</div>
</
template
>
</gl-popover>
</div>
</template>
app/assets/javascripts/boards/components/board_card_inner.vue
View file @
f79ed8ad
...
@@ -10,6 +10,7 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
...
@@ -10,6 +10,7 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import
UserAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
UserAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
{
ListType
}
from
'
../constants
'
;
import
{
ListType
}
from
'
../constants
'
;
import
eventHub
from
'
../eventhub
'
;
import
eventHub
from
'
../eventhub
'
;
import
BoardBlockedIcon
from
'
./board_blocked_icon.vue
'
;
import
IssueDueDate
from
'
./issue_due_date.vue
'
;
import
IssueDueDate
from
'
./issue_due_date.vue
'
;
import
IssueTimeEstimate
from
'
./issue_time_estimate.vue
'
;
import
IssueTimeEstimate
from
'
./issue_time_estimate.vue
'
;
...
@@ -22,6 +23,7 @@ export default {
...
@@ -22,6 +23,7 @@ export default {
IssueDueDate
,
IssueDueDate
,
IssueTimeEstimate
,
IssueTimeEstimate
,
IssueCardWeight
:
()
=>
import
(
'
ee_component/boards/components/issue_card_weight.vue
'
),
IssueCardWeight
:
()
=>
import
(
'
ee_component/boards/components/issue_card_weight.vue
'
),
BoardBlockedIcon
,
},
},
directives
:
{
directives
:
{
GlTooltip
:
GlTooltipDirective
,
GlTooltip
:
GlTooltipDirective
,
...
@@ -52,7 +54,7 @@ export default {
...
@@ -52,7 +54,7 @@ export default {
};
};
},
},
computed
:
{
computed
:
{
...
mapState
([
'
isShowingLabels
'
]),
...
mapState
([
'
isShowingLabels
'
,
'
issuableType
'
]),
...
mapGetters
([
'
isEpicBoard
'
]),
...
mapGetters
([
'
isEpicBoard
'
]),
cappedAssignees
()
{
cappedAssignees
()
{
// e.g. maxRender is 4,
// e.g. maxRender is 4,
...
@@ -114,7 +116,13 @@ export default {
...
@@ -114,7 +116,13 @@ export default {
},
},
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
performSearch
'
]),
...
mapActions
([
'
performSearch
'
,
'
setError
'
,
'
setBlockingIssuables
'
,
'
unsetBlockingIssuables
'
,
'
toggleBlockedPopover
'
,
]),
isIndexLessThanlimit
(
index
)
{
isIndexLessThanlimit
(
index
)
{
return
index
<
this
.
limitBeforeCounter
;
return
index
<
this
.
limitBeforeCounter
;
},
},
...
@@ -164,14 +172,14 @@ export default {
...
@@ -164,14 +172,14 @@ export default {
<div>
<div>
<div
class=
"gl-display-flex"
dir=
"auto"
>
<div
class=
"gl-display-flex"
dir=
"auto"
>
<h4
class=
"board-card-title gl-mb-0 gl-mt-0"
>
<h4
class=
"board-card-title gl-mb-0 gl-mt-0"
>
<
gl
-icon
<
board-blocked
-icon
v-if=
"item.blocked"
v-if=
"item.blocked"
v-gl-tooltip
:item=
"item"
name=
"issue-block
"
:unique-id=
"`$
{item.id}${list.id}`
"
:
title=
"blockedLabel
"
:
issuable-type="issuableType
"
class=
"issue-blocked-icon gl-mr-2
"
@blocking-issuables-error="setError
"
:aria-label=
"blockedLabel
"
@blocking-issuables="setBlockingIssuables
"
data-testid=
"issue-blocked-icon
"
@hidden="unsetBlockingIssuables
"
/>
/>
<gl-icon
<gl-icon
v-if=
"item.confidential"
v-if=
"item.confidential"
...
@@ -181,13 +189,9 @@ export default {
...
@@ -181,13 +189,9 @@ export default {
class=
"confidential-icon gl-mr-2"
class=
"confidential-icon gl-mr-2"
:aria-label=
"__('Confidential')"
:aria-label=
"__('Confidential')"
/>
/>
<a
<a
:href=
"item.path || item.webUrl || ''"
:title=
"item.title"
@
mousemove
.
stop
>
{{
:href=
"item.path || item.webUrl || ''"
item
.
title
:title=
"item.title"
}}
</a>
class=
"js-no-trigger"
@
mousemove
.
stop
>
{{
item
.
title
}}
</a
>
</h4>
</h4>
</div>
</div>
<div
v-if=
"showLabelFooter"
class=
"board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"
>
<div
v-if=
"showLabelFooter"
class=
"board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"
>
...
...
app/assets/javascripts/boards/components/board_content.vue
View file @
f79ed8ad
...
@@ -69,7 +69,7 @@ export default {
...
@@ -69,7 +69,7 @@ export default {
},
},
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
moveList
'
]),
...
mapActions
([
'
moveList
'
,
'
unsetError
'
]),
afterFormEnters
()
{
afterFormEnters
()
{
const
el
=
this
.
canDragColumns
?
this
.
$refs
.
list
.
$el
:
this
.
$refs
.
list
;
const
el
=
this
.
canDragColumns
?
this
.
$refs
.
list
.
$el
:
this
.
$refs
.
list
;
el
.
scrollTo
({
left
:
el
.
scrollWidth
,
behavior
:
'
smooth
'
});
el
.
scrollTo
({
left
:
el
.
scrollWidth
,
behavior
:
'
smooth
'
});
...
@@ -100,7 +100,7 @@ export default {
...
@@ -100,7 +100,7 @@ export default {
<
template
>
<
template
>
<div>
<div>
<gl-alert
v-if=
"error"
variant=
"danger"
:dismissible=
"
false
"
>
<gl-alert
v-if=
"error"
variant=
"danger"
:dismissible=
"
true"
@
dismiss=
"unsetError
"
>
{{
error
}}
{{
error
}}
</gl-alert>
</gl-alert>
<component
<component
...
...
app/assets/javascripts/boards/constants.js
View file @
f79ed8ad
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
boardBlockingIssuesQuery
from
'
./graphql/board_blocking_issues.query.graphql
'
;
export
const
issuableTypes
=
{
export
const
issuableTypes
=
{
issue
:
'
issue
'
,
issue
:
'
issue
'
,
...
@@ -45,3 +46,9 @@ export default {
...
@@ -45,3 +46,9 @@ export default {
BoardType
,
BoardType
,
ListType
,
ListType
,
};
};
export
const
blockingIssuablesQueries
=
{
[
issuableTypes
.
issue
]:
{
query
:
boardBlockingIssuesQuery
,
},
};
app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
0 → 100644
View file @
f79ed8ad
query
BoardBlockingIssues
(
$id
:
IssueID
!)
{
issuable
:
issue
(
id
:
$id
)
{
__typename
id
blockingIssuables
:
blockedByIssues
{
__typename
nodes
{
id
iid
title
reference
(
full
:
true
)
webUrl
}
}
}
}
app/assets/javascripts/boards/index.js
View file @
f79ed8ad
...
@@ -107,6 +107,7 @@ export default () => {
...
@@ -107,6 +107,7 @@ export default () => {
milestoneListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
milestoneListsAvailable
),
milestoneListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
milestoneListsAvailable
),
assigneeListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
assigneeListsAvailable
),
assigneeListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
assigneeListsAvailable
),
iterationListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
iterationListsAvailable
),
iterationListsAvailable
:
parseBoolean
(
$boardApp
.
dataset
.
iterationListsAvailable
),
issuableType
:
issuableTypes
.
issue
,
},
},
store
,
store
,
apolloProvider
,
apolloProvider
,
...
...
app/assets/javascripts/boards/stores/actions.js
View file @
f79ed8ad
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
pick
}
from
'
lodash
'
;
import
{
pick
}
from
'
lodash
'
;
import
createBoardListMutation
from
'
ee_else_ce/boards/graphql/board_list_create.mutation.graphql
'
;
import
createBoardListMutation
from
'
ee_else_ce/boards/graphql/board_list_create.mutation.graphql
'
;
import
boardListsQuery
from
'
ee_else_ce/boards/graphql/board_lists.query.graphql
'
;
import
boardListsQuery
from
'
ee_else_ce/boards/graphql/board_lists.query.graphql
'
;
...
@@ -608,6 +609,18 @@ export default {
...
@@ -608,6 +609,18 @@ export default {
}
}
},
},
setError
:
({
commit
},
{
message
,
error
,
captureError
=
false
})
=>
{
commit
(
types
.
SET_ERROR
,
message
);
if
(
captureError
)
{
Sentry
.
captureException
(
error
);
}
},
unsetError
:
({
commit
})
=>
{
commit
(
types
.
SET_ERROR
,
undefined
);
},
fetchBacklog
:
()
=>
{
fetchBacklog
:
()
=>
{
notImplemented
();
notImplemented
();
},
},
...
...
app/assets/javascripts/boards/stores/mutation_types.js
View file @
f79ed8ad
...
@@ -49,3 +49,4 @@ export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
...
@@ -49,3 +49,4 @@ export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
export
const
ADD_LIST_TO_HIGHLIGHTED_LISTS
=
'
ADD_LIST_TO_HIGHLIGHTED_LISTS
'
;
export
const
ADD_LIST_TO_HIGHLIGHTED_LISTS
=
'
ADD_LIST_TO_HIGHLIGHTED_LISTS
'
;
export
const
REMOVE_LIST_FROM_HIGHLIGHTED_LISTS
=
'
REMOVE_LIST_FROM_HIGHLIGHTED_LISTS
'
;
export
const
REMOVE_LIST_FROM_HIGHLIGHTED_LISTS
=
'
REMOVE_LIST_FROM_HIGHLIGHTED_LISTS
'
;
export
const
RESET_BOARD_ITEM_SELECTION
=
'
RESET_BOARD_ITEM_SELECTION
'
;
export
const
RESET_BOARD_ITEM_SELECTION
=
'
RESET_BOARD_ITEM_SELECTION
'
;
export
const
SET_ERROR
=
'
SET_ERROR
'
;
app/assets/javascripts/boards/stores/mutations.js
View file @
f79ed8ad
...
@@ -309,4 +309,8 @@ export default {
...
@@ -309,4 +309,8 @@ export default {
[
mutationTypes
.
RESET_BOARD_ITEM_SELECTION
]:
(
state
)
=>
{
[
mutationTypes
.
RESET_BOARD_ITEM_SELECTION
]:
(
state
)
=>
{
state
.
selectedBoardItems
=
[];
state
.
selectedBoardItems
=
[];
},
},
[
mutationTypes
.
SET_ERROR
]:
(
state
,
error
)
=>
{
state
.
error
=
error
;
},
};
};
app/assets/javascripts/boards/stores/state.js
View file @
f79ed8ad
...
@@ -34,4 +34,5 @@ export default () => ({
...
@@ -34,4 +34,5 @@ export default () => ({
},
},
// TODO: remove after ce/ee split of board_content.vue
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes
:
false
,
isShowingEpicsSwimlanes
:
false
,
activeBlockingIssuables
:
[],
});
});
app/assets/javascripts/graphql_shared/constants.js
0 → 100644
View file @
f79ed8ad
/* eslint-disable @gitlab/require-i18n-strings */
export
const
IssueType
=
'
Issue
'
;
changelogs/unreleased/implement-blocking-issue-popover-for-boards.yml
0 → 100644
View file @
f79ed8ad
---
title
:
Add blocked issues detail popover for boards cards
merge_request
:
55821
author
:
type
:
added
doc/user/project/img/issue_boards_blocked_icon_v13_10.png
0 → 100644
View file @
f79ed8ad
41.5 KB
doc/user/project/img/issue_boards_blocked_icon_v13_6.png
deleted
100644 → 0
View file @
1632d456
16.3 KB
doc/user/project/issue_board.md
View file @
f79ed8ad
...
@@ -280,6 +280,7 @@ group-level objects are available.
...
@@ -280,6 +280,7 @@ group-level objects are available.
#### GraphQL-based sidebar for group issue boards **(PREMIUM)**
#### GraphQL-based sidebar for group issue boards **(PREMIUM)**
<!-- When the feature flag is removed, integrate this section into the above ("Group issue boards"). -->
<!-- When the feature flag is removed, integrate this section into the above ("Group issue boards"). -->
<!-- This anchor is linked from #blocked-issues as well. -->
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285074) in GitLab 13.9.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285074) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
...
@@ -407,12 +408,18 @@ To set a WIP limit for a list:
...
@@ -407,12 +408,18 @@ To set a WIP limit for a list:
## Blocked issues
## Blocked issues
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34723) in GitLab 12.8.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34723) in GitLab 12.8.
> - [View blocking issues when hovering over blocked icon](https://gitlab.com/gitlab-org/gitlab/-/issues/210452) in GitLab 13.10.
If an issue is blocked by another issue, an icon appears next to its title to indicate its blocked
If an issue is blocked by another issue, an icon appears next to its title to indicate its blocked
status.
status.
![
Blocked issues
](
img/issue_boards_blocked_icon_v13_6.png
)
When you hover over the blocked icon (
**{issue-block}**
), a detailed information popover is displayed.
To enable this in group issue boards, enable the
[
GraphQL-based sidebar
](
#graphql-based-sidebar-for-group-issue-boards
)
.
The feature is enabled by default when you use group issue boards with epic swimlanes.
![
Blocked issues
](
img/issue_boards_blocked_icon_v13_10.png
)
## Actions you can take on an issue board
## Actions you can take on an issue board
...
...
ee/spec/frontend/boards/board_card_inner_spec.js
View file @
f79ed8ad
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
IssueCardWeight
from
'
ee/boards/components/issue_card_weight.vue
'
;
import
IssueCardWeight
from
'
ee/boards/components/issue_card_weight.vue
'
;
import
BoardCardInner
from
'
~/boards/components/board_card_inner.vue
'
;
import
BoardCardInner
from
'
~/boards/components/board_card_inner.vue
'
;
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
defaultStore
from
'
~/boards/stores
'
;
import
defaultStore
from
'
~/boards/stores
'
;
describe
(
'
Board card component
'
,
()
=>
{
describe
(
'
Board card component
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
issue
;
let
issue
;
let
list
;
let
list
;
let
store
;
const
createStore
=
({
isShowingLabels
=
true
}
=
{})
=>
{
store
=
new
Vuex
.
Store
({
...
defaultStore
,
state
:
{
...
defaultStore
.
state
,
issuableType
:
issuableTypes
.
issue
,
isShowingLabels
,
},
getters
:
{
isGroupBoard
:
()
=>
true
,
isEpicBoard
:
()
=>
false
,
isProjectBoard
:
()
=>
false
,
},
});
};
const
createComponent
=
(
props
=
{}
,
store
=
defaultStore
)
=>
{
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
BoardCardInner
,
{
wrapper
=
shallowMount
(
BoardCardInner
,
{
store
,
store
,
propsData
:
{
propsData
:
{
...
@@ -55,9 +74,14 @@ describe('Board card component', () => {
...
@@ -55,9 +74,14 @@ describe('Board card component', () => {
};
};
});
});
beforeEach
(()
=>
{
createStore
();
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
wrapper
=
null
;
store
=
null
;
});
});
describe
(
'
labels
'
,
()
=>
{
describe
(
'
labels
'
,
()
=>
{
...
@@ -95,48 +119,13 @@ describe('Board card component', () => {
...
@@ -95,48 +119,13 @@ describe('Board card component', () => {
});
});
it
(
'
shows no labels when the isShowingLabels state is false
'
,
()
=>
{
it
(
'
shows no labels when the isShowingLabels state is false
'
,
()
=>
{
const
store
=
{
createStore
({
isShowingLabels
:
false
});
...
defaultStore
,
createComponent
({});
state
:
{
...
defaultStore
.
state
,
isShowingLabels
:
false
,
},
};
createComponent
({},
store
);
expect
(
wrapper
.
findAll
(
'
.board-card-labels
'
)).
toHaveLength
(
0
);
expect
(
wrapper
.
findAll
(
'
.board-card-labels
'
)).
toHaveLength
(
0
);
});
});
});
});
describe
(
'
blocked
'
,
()
=>
{
const
findBlockedIcon
=
()
=>
wrapper
.
find
(
'
[data-testid="issue-blocked-icon"
'
);
it
(
'
shows blocked icon if issue is blocked, when blocked by multiple issues
'
,
()
=>
{
createComponent
();
const
blockedIcon
=
findBlockedIcon
();
expect
(
blockedIcon
.
exists
()).
toBe
(
true
);
expect
(
blockedIcon
.
attributes
(
'
title
'
)).
toBe
(
'
Blocked by 2 issues
'
);
});
it
(
'
shows blocked icon if issue is blocked, when blocked by one issue
'
,
()
=>
{
issue
.
blockedByCount
=
1
;
createComponent
();
const
blockedIcon
=
findBlockedIcon
();
expect
(
blockedIcon
.
exists
()).
toBe
(
true
);
expect
(
blockedIcon
.
attributes
(
'
title
'
)).
toBe
(
'
Blocked by 1 issue
'
);
});
it
(
'
does not show blocked icon if issue is not blocked
'
,
()
=>
{
issue
.
blocked
=
false
;
issue
.
blockedByCount
=
0
;
createComponent
();
expect
(
findBlockedIcon
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
weight
'
,
()
=>
{
describe
(
'
weight
'
,
()
=>
{
it
(
'
shows weight component
'
,
()
=>
{
it
(
'
shows weight component
'
,
()
=>
{
createComponent
();
createComponent
();
...
...
locale/gitlab.pot
View file @
f79ed8ad
...
@@ -4880,6 +4880,11 @@ msgstr ""
...
@@ -4880,6 +4880,11 @@ msgstr ""
msgid "Boards and Board Lists"
msgid "Boards and Board Lists"
msgstr ""
msgstr ""
msgid "Boards|+ %{displayedIssuablesCount} more %{issuableType}"
msgid_plural "Boards|+ %{displayedIssuablesCount} more %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
msgid "Boards|An error occurred while creating the issue. Please try again."
msgid "Boards|An error occurred while creating the issue. Please try again."
msgstr ""
msgstr ""
...
@@ -4922,6 +4927,11 @@ msgstr ""
...
@@ -4922,6 +4927,11 @@ msgstr ""
msgid "Boards|An error occurred while updating the list. Please try again."
msgid "Boards|An error occurred while updating the list. Please try again."
msgstr ""
msgstr ""
msgid "Boards|Blocked by %{blockedByCount} %{issuableType}"
msgid_plural "Boards|Blocked by %{blockedByCount} %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
msgid "Boards|Board"
msgid "Boards|Board"
msgstr ""
msgstr ""
...
@@ -4934,6 +4944,15 @@ msgstr ""
...
@@ -4934,6 +4944,15 @@ msgstr ""
msgid "Boards|Expand"
msgid "Boards|Expand"
msgstr ""
msgstr ""
msgid "Boards|Failed to fetch blocking %{issuableType}s"
msgstr ""
msgid "Boards|Retrieving blocking %{issuableType}s"
msgstr ""
msgid "Boards|View all blocking %{issuableType}s"
msgstr ""
msgid "Boards|View scope"
msgid "Boards|View scope"
msgstr ""
msgstr ""
...
...
spec/frontend/boards/board_card_inner_spec.js
View file @
f79ed8ad
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
GlLabel
}
from
'
@gitlab/ui
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
range
}
from
'
lodash
'
;
import
{
range
}
from
'
lodash
'
;
import
Vuex
from
'
vuex
'
;
import
BoardBlockedIcon
from
'
~/boards/components/board_blocked_icon.vue
'
;
import
BoardCardInner
from
'
~/boards/components/board_card_inner.vue
'
;
import
BoardCardInner
from
'
~/boards/components/board_card_inner.vue
'
;
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
defaultStore
from
'
~/boards/stores
'
;
import
defaultStore
from
'
~/boards/stores
'
;
import
{
updateHistory
}
from
'
~/lib/utils/url_utility
'
;
import
{
updateHistory
}
from
'
~/lib/utils/url_utility
'
;
import
{
mockLabelList
}
from
'
./mock_data
'
;
import
{
mockLabelList
,
mockIssue
}
from
'
./mock_data
'
;
jest
.
mock
(
'
~/lib/utils/url_utility
'
);
jest
.
mock
(
'
~/lib/utils/url_utility
'
);
jest
.
mock
(
'
~/boards/eventhub
'
);
jest
.
mock
(
'
~/boards/eventhub
'
);
...
@@ -29,8 +32,28 @@ describe('Board card component', () => {
...
@@ -29,8 +32,28 @@ describe('Board card component', () => {
let
wrapper
;
let
wrapper
;
let
issue
;
let
issue
;
let
list
;
let
list
;
let
store
;
const
findBoardBlockedIcon
=
()
=>
wrapper
.
find
(
BoardBlockedIcon
);
const
createStore
=
()
=>
{
store
=
new
Vuex
.
Store
({
...
defaultStore
,
state
:
{
...
defaultStore
.
state
,
issuableType
:
issuableTypes
.
issue
,
},
getters
:
{
isGroupBoard
:
()
=>
true
,
isEpicBoard
:
()
=>
false
,
isProjectBoard
:
()
=>
false
,
},
});
};
const
createWrapper
=
(
props
=
{})
=>
{
createStore
();
const
createWrapper
=
(
props
=
{},
store
=
defaultStore
)
=>
{
wrapper
=
mount
(
BoardCardInner
,
{
wrapper
=
mount
(
BoardCardInner
,
{
store
,
store
,
propsData
:
{
propsData
:
{
...
@@ -41,6 +64,13 @@ describe('Board card component', () => {
...
@@ -41,6 +64,13 @@ describe('Board card component', () => {
stubs
:
{
stubs
:
{
GlLabel
:
true
,
GlLabel
:
true
,
},
},
mocks
:
{
$apollo
:
{
queries
:
{
blockingIssuables
:
{
loading
:
false
},
},
},
},
provide
:
{
provide
:
{
rootPath
:
'
/
'
,
rootPath
:
'
/
'
,
scopedLabelsAvailable
:
false
,
scopedLabelsAvailable
:
false
,
...
@@ -51,14 +81,9 @@ describe('Board card component', () => {
...
@@ -51,14 +81,9 @@ describe('Board card component', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
list
=
mockLabelList
;
list
=
mockLabelList
;
issue
=
{
issue
=
{
title
:
'
Testing
'
,
...
mockIssue
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[
list
.
label
],
labels
:
[
list
.
label
],
assignees
:
[],
assignees
:
[],
referencePath
:
'
#1
'
,
webUrl
:
'
/test/1
'
,
weight
:
1
,
weight
:
1
,
};
};
...
@@ -68,6 +93,7 @@ describe('Board card component', () => {
...
@@ -68,6 +93,7 @@ describe('Board card component', () => {
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
wrapper
=
null
;
store
=
null
;
jest
.
clearAllMocks
();
jest
.
clearAllMocks
();
});
});
...
@@ -87,18 +113,38 @@ describe('Board card component', () => {
...
@@ -87,18 +113,38 @@ describe('Board card component', () => {
expect
(
wrapper
.
find
(
'
.confidential-icon
'
).
exists
()).
toBe
(
false
);
expect
(
wrapper
.
find
(
'
.confidential-icon
'
).
exists
()).
toBe
(
false
);
});
});
it
(
'
does not render blocked icon
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.issue-blocked-icon
'
).
exists
()).
toBe
(
false
);
});
it
(
'
renders issue ID with #
'
,
()
=>
{
it
(
'
renders issue ID with #
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.board-card-number
'
).
text
()).
toContain
(
`#
${
issue
.
id
}
`
);
expect
(
wrapper
.
find
(
'
.board-card-number
'
).
text
()).
toContain
(
`#
${
issue
.
i
i
d
}
`
);
});
});
it
(
'
does not render assignee
'
,
()
=>
{
it
(
'
does not render assignee
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.board-card-assignee .avatar
'
).
exists
()).
toBe
(
false
);
expect
(
wrapper
.
find
(
'
.board-card-assignee .avatar
'
).
exists
()).
toBe
(
false
);
});
});
describe
(
'
blocked
'
,
()
=>
{
it
(
'
renders blocked icon if issue is blocked
'
,
async
()
=>
{
createWrapper
({
item
:
{
...
issue
,
blocked
:
true
,
},
});
expect
(
findBoardBlockedIcon
().
exists
()).
toBe
(
true
);
});
it
(
'
does not show blocked icon if issue is not blocked
'
,
()
=>
{
createWrapper
({
item
:
{
...
issue
,
blocked
:
false
,
},
});
expect
(
findBoardBlockedIcon
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
confidential issue
'
,
()
=>
{
describe
(
'
confidential issue
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
.
setProps
({
wrapper
.
setProps
({
...
@@ -303,21 +349,6 @@ describe('Board card component', () => {
...
@@ -303,21 +349,6 @@ describe('Board card component', () => {
});
});
});
});
describe
(
'
blocked
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
.
setProps
({
item
:
{
...
wrapper
.
props
(
'
item
'
),
blocked
:
true
,
},
});
});
it
(
'
renders blocked icon if issue is blocked
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.issue-blocked-icon
'
).
exists
()).
toBe
(
true
);
});
});
describe
(
'
filterByLabel method
'
,
()
=>
{
describe
(
'
filterByLabel method
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
delete
window
.
location
;
delete
window
.
location
;
...
...
spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
0 → 100644
View file @
f79ed8ad
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
<use href=\\"#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
<ul class=\\"gl-list-style-none gl-p-0\\">
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a>
<p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
blocking issue title 1
</p>
</li>
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a>
<p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc…
</p>
</li>
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a>
<p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
blocking issue title 3
</p>
</li>
</ul>
<div class=\\"gl-mt-4\\">
<p data-testid=\\"hidden-blocking-count\\" class=\\"gl-mb-3\\">+ 1 more issue</p> <a data-testid=\\"view-all-issues\\" href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0#related-issues\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">View all blocking issues</a>
</div><span data-testid=\\"popover-title\\">Blocked by 4 issues</span>
</div>
</div>"
`;
spec/frontend/boards/components/board_blocked_icon_spec.js
0 → 100644
View file @
f79ed8ad
import
{
GlIcon
,
GlLink
,
GlPopover
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
mount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
BoardBlockedIcon
from
'
~/boards/components/board_blocked_icon.vue
'
;
import
{
blockingIssuablesQueries
,
issuableTypes
}
from
'
~/boards/constants
'
;
import
{
truncate
}
from
'
~/lib/utils/text_utility
'
;
import
{
mockIssue
,
mockBlockingIssue1
,
mockBlockingIssue2
,
mockBlockingIssuablesResponse1
,
mockBlockingIssuablesResponse2
,
mockBlockingIssuablesResponse3
,
mockBlockedIssue1
,
mockBlockedIssue2
,
}
from
'
../mock_data
'
;
describe
(
'
BoardBlockedIcon
'
,
()
=>
{
let
wrapper
;
let
mockApollo
;
const
findGlIcon
=
()
=>
wrapper
.
find
(
GlIcon
);
const
findGlPopover
=
()
=>
wrapper
.
find
(
GlPopover
);
const
findGlLink
=
()
=>
wrapper
.
find
(
GlLink
);
const
findPopoverTitle
=
()
=>
wrapper
.
findByTestId
(
'
popover-title
'
);
const
findIssuableTitle
=
()
=>
wrapper
.
findByTestId
(
'
issuable-title
'
);
const
findHiddenBlockingCount
=
()
=>
wrapper
.
findByTestId
(
'
hidden-blocking-count
'
);
const
findViewAllIssuableLink
=
()
=>
wrapper
.
findByTestId
(
'
view-all-issues
'
);
const
waitForApollo
=
async
()
=>
{
jest
.
runOnlyPendingTimers
();
await
waitForPromises
();
};
const
mouseenter
=
async
()
=>
{
findGlIcon
().
vm
.
$emit
(
'
mouseenter
'
);
await
wrapper
.
vm
.
$nextTick
();
await
waitForApollo
();
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
const
createWrapperWithApollo
=
({
item
=
mockBlockedIssue1
,
blockingIssuablesSpy
=
jest
.
fn
().
mockResolvedValue
(
mockBlockingIssuablesResponse1
),
}
=
{})
=>
{
mockApollo
=
createMockApollo
([
[
blockingIssuablesQueries
[
issuableTypes
.
issue
].
query
,
blockingIssuablesSpy
],
]);
Vue
.
use
(
VueApollo
);
wrapper
=
extendedWrapper
(
mount
(
BoardBlockedIcon
,
{
apolloProvider
:
mockApollo
,
propsData
:
{
item
:
{
...
mockIssue
,
...
item
,
},
uniqueId
:
'
uniqueId
'
,
issuableType
:
issuableTypes
.
issue
,
},
attachTo
:
document
.
body
,
}),
);
};
const
createWrapper
=
({
item
=
{},
queries
=
{},
data
=
{},
loading
=
false
}
=
{})
=>
{
wrapper
=
extendedWrapper
(
shallowMount
(
BoardBlockedIcon
,
{
propsData
:
{
item
:
{
...
mockIssue
,
...
item
,
},
uniqueId
:
'
uniqueid
'
,
issuableType
:
issuableTypes
.
issue
,
},
data
()
{
return
{
...
data
,
};
},
mocks
:
{
$apollo
:
{
queries
:
{
blockingIssuables
:
{
loading
},
...
queries
,
},
},
},
stubs
:
{
GlPopover
,
},
attachTo
:
document
.
body
,
}),
);
};
it
(
'
should render blocked icon
'
,
()
=>
{
createWrapper
();
expect
(
findGlIcon
().
exists
()).
toBe
(
true
);
});
it
(
'
should display a loading spinner while loading
'
,
()
=>
{
createWrapper
({
loading
:
true
});
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
});
it
(
'
should not query for blocking issuables by default
'
,
async
()
=>
{
createWrapperWithApollo
();
expect
(
findGlPopover
().
text
()).
not
.
toContain
(
mockBlockingIssue1
.
title
);
});
describe
(
'
on mouseenter on blocked icon
'
,
()
=>
{
it
(
'
should query for blocking issuables and render the result
'
,
async
()
=>
{
createWrapperWithApollo
();
expect
(
findGlPopover
().
text
()).
not
.
toContain
(
mockBlockingIssue1
.
title
);
await
mouseenter
();
expect
(
findGlPopover
().
exists
()).
toBe
(
true
);
expect
(
findIssuableTitle
().
text
()).
toContain
(
mockBlockingIssue1
.
title
);
expect
(
wrapper
.
vm
.
skip
).
toBe
(
true
);
});
it
(
'
should emit "blocking-issuables-error" event on query error
'
,
async
()
=>
{
const
mockError
=
new
Error
(
'
mayday
'
);
createWrapperWithApollo
({
blockingIssuablesSpy
:
jest
.
fn
().
mockRejectedValue
(
mockError
)
});
await
mouseenter
();
const
[
[
{
message
,
error
:
{
networkError
},
},
],
]
=
wrapper
.
emitted
(
'
blocking-issuables-error
'
);
expect
(
message
).
toBe
(
'
Failed to fetch blocking issues
'
);
expect
(
networkError
).
toBe
(
mockError
);
});
describe
(
'
with a single blocking issue
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapperWithApollo
();
await
mouseenter
();
});
it
(
'
should render a title of the issuable
'
,
async
()
=>
{
expect
(
findIssuableTitle
().
text
()).
toBe
(
mockBlockingIssue1
.
title
);
});
it
(
'
should render issuable reference and link to the issuable
'
,
async
()
=>
{
const
formattedRef
=
mockBlockingIssue1
.
reference
.
split
(
'
/
'
)[
1
];
expect
(
findGlLink
().
text
()).
toBe
(
formattedRef
);
expect
(
findGlLink
().
attributes
(
'
href
'
)).
toBe
(
mockBlockingIssue1
.
webUrl
);
});
it
(
'
should render popover title with correct blocking issuable count
'
,
async
()
=>
{
expect
(
findPopoverTitle
().
text
()).
toBe
(
'
Blocked by 1 issue
'
);
});
});
describe
(
'
when issue has a long title
'
,
()
=>
{
it
(
'
should render a truncated title
'
,
async
()
=>
{
createWrapperWithApollo
({
blockingIssuablesSpy
:
jest
.
fn
().
mockResolvedValue
(
mockBlockingIssuablesResponse2
),
});
await
mouseenter
();
const
truncatedTitle
=
truncate
(
mockBlockingIssue2
.
title
,
wrapper
.
vm
.
$options
.
textTruncateWidth
,
);
expect
(
findIssuableTitle
().
text
()).
toBe
(
truncatedTitle
);
});
});
describe
(
'
with more than three blocking issues
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createWrapperWithApollo
({
item
:
mockBlockedIssue2
,
blockingIssuablesSpy
:
jest
.
fn
().
mockResolvedValue
(
mockBlockingIssuablesResponse3
),
});
await
mouseenter
();
});
it
(
'
matches the snapshot
'
,
()
=>
{
expect
(
wrapper
.
html
()).
toMatchSnapshot
();
});
it
(
'
should render popover title with correct blocking issuable count
'
,
async
()
=>
{
expect
(
findPopoverTitle
().
text
()).
toBe
(
'
Blocked by 4 issues
'
);
});
it
(
'
should render the number of hidden blocking issuables
'
,
()
=>
{
expect
(
findHiddenBlockingCount
().
text
()).
toBe
(
'
+ 1 more issue
'
);
});
it
(
'
should link to the blocked issue page at the related issue anchor
'
,
async
()
=>
{
expect
(
findViewAllIssuableLink
().
text
()).
toBe
(
'
View all blocking issues
'
);
expect
(
findViewAllIssuableLink
().
attributes
(
'
href
'
)).
toBe
(
`
${
mockBlockedIssue2
.
webUrl
}
#related-issues`
,
);
});
});
});
});
spec/frontend/boards/mock_data.js
View file @
f79ed8ad
...
@@ -398,3 +398,90 @@ export const mockActiveGroupProjects = [
...
@@ -398,3 +398,90 @@ export const mockActiveGroupProjects = [
{
...
mockGroupProject1
,
archived
:
false
},
{
...
mockGroupProject1
,
archived
:
false
},
{
...
mockGroupProject2
,
archived
:
false
},
{
...
mockGroupProject2
,
archived
:
false
},
];
];
export
const
mockBlockingIssue1
=
{
id
:
'
gid://gitlab/Issue/525
'
,
iid
:
'
6
'
,
title
:
'
blocking issue title 1
'
,
reference
:
'
gitlab-org/my-project-1#6
'
,
webUrl
:
'
http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6
'
,
__typename
:
'
Issue
'
,
};
export
const
mockBlockingIssue2
=
{
id
:
'
gid://gitlab/Issue/524
'
,
iid
:
'
5
'
,
title
:
'
blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + blocking issue title 2
'
,
reference
:
'
gitlab-org/my-project-1#5
'
,
webUrl
:
'
http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5
'
,
__typename
:
'
Issue
'
,
};
export
const
mockBlockingIssue3
=
{
id
:
'
gid://gitlab/Issue/523
'
,
iid
:
'
4
'
,
title
:
'
blocking issue title 3
'
,
reference
:
'
gitlab-org/my-project-1#4
'
,
webUrl
:
'
http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4
'
,
__typename
:
'
Issue
'
,
};
export
const
mockBlockingIssue4
=
{
id
:
'
gid://gitlab/Issue/522
'
,
iid
:
'
3
'
,
title
:
'
blocking issue title 4
'
,
reference
:
'
gitlab-org/my-project-1#3
'
,
webUrl
:
'
http://gdk.test:3000/gitlab-org/my-project-1/-/issues/3
'
,
__typename
:
'
Issue
'
,
};
export
const
mockBlockingIssuablesResponse1
=
{
data
:
{
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/527
'
,
blockingIssuables
:
{
__typename
:
'
IssueConnection
'
,
nodes
:
[
mockBlockingIssue1
],
},
},
},
};
export
const
mockBlockingIssuablesResponse2
=
{
data
:
{
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/527
'
,
blockingIssuables
:
{
__typename
:
'
IssueConnection
'
,
nodes
:
[
mockBlockingIssue2
],
},
},
},
};
export
const
mockBlockingIssuablesResponse3
=
{
data
:
{
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/527
'
,
blockingIssuables
:
{
__typename
:
'
IssueConnection
'
,
nodes
:
[
mockBlockingIssue1
,
mockBlockingIssue2
,
mockBlockingIssue3
,
mockBlockingIssue4
],
},
},
},
};
export
const
mockBlockedIssue1
=
{
id
:
'
527
'
,
blockedByCount
:
1
,
};
export
const
mockBlockedIssue2
=
{
id
:
'
527
'
,
blockedByCount
:
4
,
webUrl
:
'
http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0
'
,
};
spec/frontend/boards/stores/actions_spec.js
View file @
f79ed8ad
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
{
import
{
fullBoardId
,
fullBoardId
,
...
@@ -1378,6 +1379,51 @@ describe('toggleBoardItem', () => {
...
@@ -1378,6 +1379,51 @@ describe('toggleBoardItem', () => {
});
});
});
});
describe
(
'
setError
'
,
()
=>
{
it
(
'
should commit mutation SET_ERROR
'
,
()
=>
{
testAction
({
action
:
actions
.
setError
,
payload
:
{
message
:
'
mayday
'
},
expectedMutations
:
[
{
payload
:
'
mayday
'
,
type
:
types
.
SET_ERROR
,
},
],
});
});
it
(
'
should capture error using Sentry when captureError is true
'
,
()
=>
{
jest
.
spyOn
(
Sentry
,
'
captureException
'
);
const
mockError
=
new
Error
();
actions
.
setError
(
{
commit
:
()
=>
{}
},
{
message
:
'
mayday
'
,
error
:
mockError
,
captureError
:
true
,
},
);
expect
(
Sentry
.
captureException
).
toHaveBeenNthCalledWith
(
1
,
mockError
);
});
});
describe
(
'
unsetError
'
,
()
=>
{
it
(
'
should commit mutation SET_ERROR with undefined as payload
'
,
()
=>
{
testAction
({
action
:
actions
.
unsetError
,
expectedMutations
:
[
{
payload
:
undefined
,
type
:
types
.
SET_ERROR
,
},
],
});
});
});
describe
(
'
fetchBacklog
'
,
()
=>
{
describe
(
'
fetchBacklog
'
,
()
=>
{
expectNotImplemented
(
actions
.
fetchBacklog
);
expectNotImplemented
(
actions
.
fetchBacklog
);
});
});
...
...
spec/frontend/boards/stores/mutations_spec.js
View file @
f79ed8ad
...
@@ -666,4 +666,14 @@ describe('Board Store Mutations', () => {
...
@@ -666,4 +666,14 @@ describe('Board Store Mutations', () => {
expect
(
state
.
selectedBoardItems
).
toEqual
([]);
expect
(
state
.
selectedBoardItems
).
toEqual
([]);
});
});
});
});
describe
(
'
SET_ERROR
'
,
()
=>
{
it
(
'
Should set error state
'
,
()
=>
{
state
.
error
=
undefined
;
mutations
[
types
.
SET_ERROR
](
state
,
'
mayday
'
);
expect
(
state
.
error
).
toBe
(
'
mayday
'
);
});
});
});
});
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