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
ecc70809
Commit
ecc70809
authored
Jun 14, 2021
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to create epic from boards
Adds ability to create epic from within Epic Boards.
parent
5dea2af8
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
645 additions
and
181 deletions
+645
-181
app/assets/javascripts/boards/components/board_list.vue
app/assets/javascripts/boards/components/board_list.vue
+23
-5
app/assets/javascripts/boards/components/board_list_header.vue
...ssets/javascripts/boards/components/board_list_header.vue
+21
-3
app/assets/javascripts/boards/components/board_new_issue.vue
app/assets/javascripts/boards/components/board_new_issue.vue
+15
-9
app/assets/javascripts/boards/constants.js
app/assets/javascripts/boards/constants.js
+5
-0
ee/app/assets/javascripts/boards/boards_util.js
ee/app/assets/javascripts/boards/boards_util.js
+12
-0
ee/app/assets/javascripts/boards/components/board_new_epic.vue
...p/assets/javascripts/boards/components/board_new_epic.vue
+66
-0
ee/app/assets/javascripts/boards/graphql/epic_create.mutation.graphql
...s/javascripts/boards/graphql/epic_create.mutation.graphql
+15
-0
ee/app/assets/javascripts/boards/stores/actions.js
ee/app/assets/javascripts/boards/stores/actions.js
+47
-0
ee/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
...avascripts/graphql_shared/fragments/epic.fragment.graphql
+1
-0
ee/spec/frontend/boards/boards_util_spec.js
ee/spec/frontend/boards/boards_util_spec.js
+28
-0
ee/spec/frontend/boards/components/board_list_header_spec.js
ee/spec/frontend/boards/components/board_list_header_spec.js
+58
-15
ee/spec/frontend/boards/components/board_list_spec.js
ee/spec/frontend/boards/components/board_list_spec.js
+114
-24
ee/spec/frontend/boards/stores/actions_spec.js
ee/spec/frontend/boards/stores/actions_spec.js
+165
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/boards/board_list_helper.js
spec/frontend/boards/board_list_helper.js
+59
-36
spec/frontend/boards/board_list_spec.js
spec/frontend/boards/board_list_spec.js
+7
-89
No files found.
app/assets/javascripts/boards/components/board_list.vue
View file @
ecc70809
...
...
@@ -6,6 +6,7 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import
{
sprintf
,
__
}
from
'
~/locale
'
;
import
defaultSortableConfig
from
'
~/sortable/sortable_config
'
;
import
Tracking
from
'
~/tracking
'
;
import
{
toggleFormEventPrefix
}
from
'
../constants
'
;
import
eventHub
from
'
../eventhub
'
;
import
BoardCard
from
'
./board_card.vue
'
;
import
BoardNewIssue
from
'
./board_new_issue.vue
'
;
...
...
@@ -21,6 +22,7 @@ export default {
components
:
{
BoardCard
,
BoardNewIssue
,
BoardNewEpic
:
()
=>
import
(
'
ee_component/boards/components/board_new_epic.vue
'
),
GlLoadingIcon
,
GlIntersectionObserver
,
},
...
...
@@ -49,6 +51,7 @@ export default {
scrollOffset
:
250
,
showCount
:
false
,
showIssueForm
:
false
,
showEpicForm
:
false
,
};
},
computed
:
{
...
...
@@ -64,6 +67,9 @@ export default {
issuableType
:
this
.
isEpicBoard
?
'
epics
'
:
'
issues
'
,
});
},
toggleFormEventPrefix
()
{
return
this
.
isEpicBoard
?
toggleFormEventPrefix
.
epic
:
toggleFormEventPrefix
.
issue
;
},
boardItemsSizeExceedsMax
()
{
return
this
.
list
.
maxIssueCount
>
0
&&
this
.
listItemsCount
>
this
.
list
.
maxIssueCount
;
},
...
...
@@ -76,6 +82,12 @@ export default {
loadingMore
()
{
return
this
.
listsFlags
[
this
.
list
.
id
]?.
isLoadingMore
;
},
epicCreateFormVisible
()
{
return
this
.
isEpicBoard
&&
this
.
list
.
listType
!==
'
closed
'
&&
this
.
showEpicForm
;
},
issueCreateFormVisible
()
{
return
!
this
.
isEpicBoard
&&
this
.
list
.
listType
!==
'
closed
'
&&
this
.
showIssueForm
;
},
listRef
()
{
// When list is draggable, the reference to the list needs to be accessed differently
return
this
.
canAdminList
?
this
.
$refs
.
list
.
$el
:
this
.
$refs
.
list
;
...
...
@@ -116,9 +128,10 @@ export default {
'
list.id
'
:
{
handler
(
id
,
oldVal
)
{
if
(
id
)
{
eventHub
.
$on
(
`
toggle-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$on
(
`
${
this
.
toggleFormEventPrefix
}
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$on
(
`scroll-board-list-
${
this
.
list
.
id
}
`
,
this
.
scrollToTop
);
eventHub
.
$off
(
`toggle-issue-form-
${
oldVal
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`
${
this
.
toggleFormEventPrefix
}${
oldVal
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`scroll-board-list-
${
oldVal
}
`
,
this
.
scrollToTop
);
}
},
...
...
@@ -126,7 +139,7 @@ export default {
},
},
beforeDestroy
()
{
eventHub
.
$off
(
`
toggle-issue-form-
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`
${
this
.
toggleFormEventPrefix
}
${
this
.
list
.
id
}
`
,
this
.
toggleForm
);
eventHub
.
$off
(
`scroll-board-list-
${
this
.
list
.
id
}
`
,
this
.
scrollToTop
);
},
methods
:
{
...
...
@@ -147,7 +160,11 @@ export default {
this
.
fetchItemsForList
({
listId
:
this
.
list
.
id
,
fetchNext
:
true
});
},
toggleForm
()
{
this
.
showIssueForm
=
!
this
.
showIssueForm
;
if
(
this
.
isEpicBoard
)
{
this
.
showEpicForm
=
!
this
.
showEpicForm
;
}
else
{
this
.
showIssueForm
=
!
this
.
showIssueForm
;
}
},
onReachingListBottom
()
{
if
(
!
this
.
loadingMore
&&
this
.
hasNextPage
)
{
...
...
@@ -227,7 +244,8 @@ export default {
>
<gl-loading-icon
/>
</div>
<board-new-issue
v-if=
"list.listType !== 'closed' && showIssueForm"
:list=
"list"
/>
<board-new-issue
v-if=
"issueCreateFormVisible"
:list=
"list"
/>
<board-new-epic
v-if=
"epicCreateFormVisible"
:list=
"list"
/>
<component
:is=
"treeRootWrapper"
v-show=
"!loading"
...
...
app/assets/javascripts/boards/components/board_list_header.vue
View file @
ecc70809
...
...
@@ -16,13 +16,14 @@ import { n__, s__, __ } from '~/locale';
import
sidebarEventHub
from
'
~/sidebar/event_hub
'
;
import
Tracking
from
'
~/tracking
'
;
import
AccessorUtilities
from
'
../../lib/utils/accessor
'
;
import
{
inactiveId
,
LIST
,
ListType
}
from
'
../constants
'
;
import
{
inactiveId
,
LIST
,
ListType
,
toggleFormEventPrefix
}
from
'
../constants
'
;
import
eventHub
from
'
../eventhub
'
;
import
ItemCount
from
'
./item_count.vue
'
;
export
default
{
i18n
:
{
newIssue
:
__
(
'
New issue
'
),
newEpic
:
s__
(
'
Boards|New epic
'
),
listSettings
:
__
(
'
List settings
'
),
expand
:
s__
(
'
Boards|Expand
'
),
collapse
:
s__
(
'
Boards|Collapse
'
),
...
...
@@ -102,7 +103,7 @@ export default {
},
showListHeaderActions
()
{
if
(
this
.
isLoggedIn
)
{
return
this
.
isNewIssueShown
||
this
.
isSettingsShown
;
return
this
.
isNewIssueShown
||
this
.
is
NewEpicShown
||
this
.
is
SettingsShown
;
}
return
false
;
},
...
...
@@ -124,6 +125,9 @@ export default {
isNewIssueShown
()
{
return
(
this
.
listType
===
ListType
.
backlog
||
this
.
showListHeaderButton
)
&&
!
this
.
isEpicBoard
;
},
isNewEpicShown
()
{
return
this
.
isEpicBoard
&&
this
.
listType
!==
ListType
.
closed
;
},
isSettingsShown
()
{
return
(
this
.
listType
!==
ListType
.
backlog
&&
this
.
showListHeaderButton
&&
!
this
.
list
.
collapsed
...
...
@@ -165,7 +169,10 @@ export default {
},
showNewIssueForm
()
{
eventHub
.
$emit
(
`toggle-issue-form-
${
this
.
list
.
id
}
`
);
eventHub
.
$emit
(
`
${
toggleFormEventPrefix
.
issue
}${
this
.
list
.
id
}
`
);
},
showNewEpicForm
()
{
eventHub
.
$emit
(
`
${
toggleFormEventPrefix
.
epic
}${
this
.
list
.
id
}
`
);
},
toggleExpanded
()
{
const
collapsed
=
!
this
.
list
.
collapsed
;
...
...
@@ -379,6 +386,17 @@ export default {
@
click=
"showNewIssueForm"
/>
<gl-button
v-if=
"isNewEpicShown"
v-show=
"!list.collapsed"
v-gl-tooltip
.
hover
:aria-label=
"$options.i18n.newEpic"
:title=
"$options.i18n.newEpic"
class=
"no-drag"
icon=
"plus"
@
click=
"showNewEpicForm"
/>
<gl-button
v-if=
"isSettingsShown"
ref=
"settingsBtn"
...
...
app/assets/javascripts/boards/components/board_new_issue.vue
View file @
ecc70809
...
...
@@ -4,13 +4,13 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import
{
getMilestone
}
from
'
ee_else_ce/boards/boards_util
'
;
import
BoardNewIssueMixin
from
'
ee_else_ce/boards/mixins/board_new_issue
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
toggleFormEventPrefix
}
from
'
../constants
'
;
import
eventHub
from
'
../eventhub
'
;
import
ProjectSelect
from
'
./project_select.vue
'
;
export
default
{
name
:
'
BoardNewIssue
'
,
i18n
:
{
submit
:
__
(
'
Create issue
'
),
cancel
:
__
(
'
Cancel
'
),
},
components
:
{
...
...
@@ -32,7 +32,15 @@ export default {
},
computed
:
{
...
mapState
([
'
selectedProject
'
]),
...
mapGetters
([
'
isGroupBoard
'
]),
...
mapGetters
([
'
isGroupBoard
'
,
'
isEpicBoard
'
]),
/**
* We've extended this component in EE where
* submitButtonTitle returns a different string
* hence this is kept as a computed prop.
*/
submitButtonTitle
()
{
return
__
(
'
Create issue
'
);
},
disabled
()
{
if
(
this
.
isGroupBoard
)
{
return
this
.
title
===
''
||
!
this
.
selectedProject
.
name
;
...
...
@@ -50,9 +58,7 @@ export default {
},
methods
:
{
...
mapActions
([
'
addListNewIssue
'
]),
submit
(
e
)
{
e
.
preventDefault
();
submit
()
{
const
{
title
}
=
this
;
const
labels
=
this
.
list
.
label
?
[
this
.
list
.
label
]
:
[];
const
assignees
=
this
.
list
.
assignee
?
[
this
.
list
.
assignee
]
:
[];
...
...
@@ -76,7 +82,7 @@ export default {
},
reset
()
{
this
.
title
=
''
;
eventHub
.
$emit
(
`
toggle-issue-form-
${
this
.
list
.
id
}
`
);
eventHub
.
$emit
(
`
${
toggleFormEventPrefix
.
issue
}
${
this
.
list
.
id
}
`
);
},
},
};
...
...
@@ -85,7 +91,7 @@ export default {
<
template
>
<div
class=
"board-new-issue-form"
>
<div
class=
"board-card position-relative p-3 rounded"
>
<form
ref=
"submitForm"
@
submit=
"submit"
>
<form
ref=
"submitForm"
@
submit
.prevent
=
"submit"
>
<label
:for=
"inputFieldId"
class=
"label-bold"
>
{{
__
(
'
Title
'
)
}}
</label>
<input
:id=
"inputFieldId"
...
...
@@ -96,7 +102,7 @@ export default {
name=
"issue_title"
autocomplete=
"off"
/>
<project-select
v-if=
"isGroupBoard"
:group-id=
"groupId"
:list=
"list"
/>
<project-select
v-if=
"isGroupBoard
&& !isEpicBoard
"
:group-id=
"groupId"
:list=
"list"
/>
<div
class=
"clearfix gl-mt-3"
>
<gl-button
ref=
"submitButton"
...
...
@@ -106,7 +112,7 @@ export default {
category=
"primary"
type=
"submit"
>
{{
$options
.
i18n
.
submit
}}
{{
submitButtonTitle
}}
</gl-button>
<gl-button
ref=
"cancelButton"
...
...
app/assets/javascripts/boards/constants.js
View file @
ecc70809
...
...
@@ -45,6 +45,11 @@ export const formType = {
edit
:
'
edit
'
,
};
export
const
toggleFormEventPrefix
=
{
epic
:
'
toggle-epic-form-
'
,
issue
:
'
toggle-issue-form-
'
,
};
export
const
inactiveId
=
0
;
export
const
ISSUABLE
=
'
issuable
'
;
...
...
ee/app/assets/javascripts/boards/boards_util.js
View file @
ecc70809
...
...
@@ -37,6 +37,18 @@ export function calculateSwimlanesBufferSize(listTopCoordinate) {
return
Math
.
ceil
((
window
.
innerHeight
-
listTopCoordinate
)
/
EPIC_LANE_BASE_HEIGHT
);
}
export
function
formatEpic
(
epic
)
{
return
{
...
epic
,
labels
:
epic
.
labels
?.
nodes
||
[],
// Epics don't support assignees as of now
// but `<board-card-inner>` expects it.
// So until https://gitlab.com/gitlab-org/gitlab/-/issues/238444
// is addressed, we need to pass empty array.
assignees
:
[],
};
}
export
function
formatListEpics
(
listEpics
)
{
const
boardItems
=
{};
let
listItemsCount
;
...
...
ee/app/assets/javascripts/boards/components/board_new_epic.vue
0 → 100644
View file @
ecc70809
<
script
>
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
BoardNewIssueFoss
from
'
~/boards/components/board_new_issue.vue
'
;
import
{
toggleFormEventPrefix
}
from
'
~/boards/constants
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
fullEpicBoardId
}
from
'
../boards_util
'
;
export
default
{
extends
:
BoardNewIssueFoss
,
inject
:
{
boardId
:
{
default
:
''
,
},
},
computed
:
{
...
mapGetters
([
'
isGroupBoard
'
]),
submitButtonTitle
()
{
return
__
(
'
Create epic
'
);
},
disabled
()
{
return
this
.
title
===
''
;
},
},
methods
:
{
...
mapActions
([
'
addListNewEpic
'
]),
submit
()
{
const
{
title
,
boardId
,
list
:
{
id
},
}
=
this
;
eventHub
.
$emit
(
`scroll-board-list-
${
id
}
`
);
this
.
addListNewEpic
({
epicInput
:
{
title
,
boardId
:
fullEpicBoardId
(
boardId
),
listId
:
id
,
},
list
:
this
.
list
,
})
.
then
(()
=>
{
this
.
reset
();
})
.
catch
((
error
)
=>
{
createFlash
({
message
:
s__
(
'
Board|Failed to create epic. Please try again.
'
),
captureError
:
true
,
error
,
});
});
},
reset
()
{
this
.
title
=
''
;
eventHub
.
$emit
(
`
${
toggleFormEventPrefix
.
epic
}${
this
.
list
.
id
}
`
);
},
},
};
</
script
>
ee/app/assets/javascripts/boards/graphql/epic_create.mutation.graphql
0 → 100644
View file @
ecc70809
#import "ee/graphql_shared/fragments/epic.fragment.graphql"
#import "~/graphql_shared/fragments/label.fragment.graphql"
mutation
CreateEpic
(
$input
:
BoardEpicCreateInput
!)
{
boardEpicCreate
(
input
:
$input
)
{
epic
{
...
EpicNode
labels
{
nodes
{
...
Label
}
}
}
}
}
ee/app/assets/javascripts/boards/stores/actions.js
View file @
ecc70809
...
...
@@ -13,6 +13,7 @@ import projectBoardMembersQuery from '~/boards/graphql/project_board_members.que
import
actionsCE
,
{
gqlClient
}
from
'
~/boards/stores/actions
'
;
import
boardsStore
from
'
~/boards/stores/boards_store
'
;
import
*
as
typesCE
from
'
~/boards/stores/mutation_types
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
historyPushState
,
...
...
@@ -20,9 +21,11 @@ import {
urlParamsToObject
,
}
from
'
~/lib/utils/common_utils
'
;
import
{
mergeUrlParams
,
removeParams
}
from
'
~/lib/utils/url_utility
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
fullEpicId
,
fullEpicBoardId
,
formatEpic
,
formatListEpics
,
formatEpicListsPageInfo
,
FiltersInfo
,
...
...
@@ -30,6 +33,7 @@ import {
import
{
EpicFilterType
,
GroupByParamType
,
FilterFields
}
from
'
../constants
'
;
import
createEpicBoardListMutation
from
'
../graphql/epic_board_list_create.mutation.graphql
'
;
import
epicCreateMutation
from
'
../graphql/epic_create.mutation.graphql
'
;
import
epicMoveListMutation
from
'
../graphql/epic_move_list.mutation.graphql
'
;
import
epicsSwimlanesQuery
from
'
../graphql/epics_swimlanes.query.graphql
'
;
import
groupBoardIterationsQuery
from
'
../graphql/group_board_iterations.query.graphql
'
;
...
...
@@ -604,6 +608,49 @@ export default {
});
},
addListNewEpic
:
(
{
state
:
{
fullPath
},
dispatch
,
commit
},
{
epicInput
,
list
,
placeholderId
=
`tmp-
${
new
Date
().
getTime
()}
`
},
)
=>
{
const
input
=
{
...
epicInput
,
groupPath
:
fullPath
,
};
const
placeholderEpic
=
{
...
epicInput
,
id
:
placeholderId
,
isLoading
:
true
,
labels
:
[],
assignees
:
[],
};
dispatch
(
'
addListItem
'
,
{
list
,
item
:
placeholderEpic
,
position
:
0
,
inProgress
:
true
});
gqlClient
.
mutate
({
mutation
:
epicCreateMutation
,
variables
:
{
input
},
})
.
then
(({
data
})
=>
{
if
(
data
.
boardEpicCreate
.
errors
?.
length
)
{
throw
new
Error
();
}
const
rawEpic
=
data
.
boardEpicCreate
?.
epic
;
const
formattedEpic
=
formatEpic
({
...
rawEpic
,
id
:
getIdFromGraphQLId
(
rawEpic
.
id
)
});
dispatch
(
'
removeListItem
'
,
{
listId
:
list
.
id
,
itemId
:
placeholderId
});
dispatch
(
'
addListItem
'
,
{
list
,
item
:
formattedEpic
,
position
:
0
});
})
.
catch
(()
=>
{
dispatch
(
'
removeListItem
'
,
{
listId
:
list
.
id
,
itemId
:
placeholderId
});
commit
(
types
.
SET_ERROR
,
s__
(
'
Boards|An error occurred while creating the epic. Please try again.
'
),
);
});
},
setActiveBoardItemLabels
:
({
getters
,
dispatch
},
params
)
=>
{
if
(
!
getters
.
isEpicBoard
)
{
dispatch
(
'
setActiveIssueLabels
'
,
params
);
...
...
ee/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
View file @
ecc70809
...
...
@@ -7,6 +7,7 @@ fragment EpicNode on Epic {
title
state
reference
referencePath
:
reference
(
full
:
true
)
webPath
webUrl
createdAt
...
...
ee/spec/frontend/boards/boards_util_spec.js
View file @
ecc70809
import
{
formatEpic
,
formatListEpics
,
formatEpicListsPageInfo
,
transformBoardConfig
,
...
...
@@ -7,6 +8,33 @@ import { mockLabel } from './mock_data';
const
listId
=
'
gid://gitlab/Boards::EpicList/3
'
;
describe
(
'
formatEpic
'
,
()
=>
{
it
(
'
formats raw epic object for state
'
,
()
=>
{
const
labels
=
[
{
id
:
1
,
title
:
'
bug
'
,
},
];
const
rawEpic
=
{
id
:
1
,
title
:
'
Foo
'
,
labels
:
{
nodes
:
labels
,
},
};
expect
(
formatEpic
(
rawEpic
)).
toEqual
({
...
rawEpic
,
labels
,
// Until we add support for assignees within Epics,
// we need to pass it as an empty array.
assignees
:
[],
});
});
});
describe
(
'
formatListEpics
'
,
()
=>
{
it
(
'
formats raw response from list epics for state
'
,
()
=>
{
const
rawEpicsInLists
=
{
...
...
ee/spec/frontend/boards/components/board_list_header_spec.js
View file @
ecc70809
import
{
GlButton
,
GlButtonGroup
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
BoardListHeader
from
'
ee/boards/components/board_list_header.vue
'
;
import
g
etters
from
'
ee/boards/stores/getters
'
;
import
defaultG
etters
from
'
ee/boards/stores/getters
'
;
import
{
mockLabelList
}
from
'
jest/boards/mock_data
'
;
import
{
ListType
,
inactiveId
}
from
'
~/boards/constants
'
;
import
boardsEventHub
from
'
~/boards/eventhub
'
;
import
sidebarEventHub
from
'
~/sidebar/event_hub
'
;
const
localVue
=
createLocalVue
();
...
...
@@ -15,18 +17,6 @@ describe('Board List Header Component', () => {
let
store
;
let
wrapper
;
beforeEach
(()
=>
{
store
=
new
Vuex
.
Store
({
state
:
{
activeId
:
inactiveId
},
getters
});
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockImplementation
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
localStorage
.
clear
();
});
const
createComponent
=
({
listType
=
ListType
.
backlog
,
collapsed
=
false
,
...
...
@@ -34,6 +24,8 @@ describe('Board List Header Component', () => {
isSwimlanesHeader
=
false
,
weightFeatureAvailable
=
false
,
currentUserId
=
1
,
state
=
{
activeId
:
inactiveId
},
getters
=
{},
}
=
{})
=>
{
const
boardId
=
'
1
'
;
...
...
@@ -55,6 +47,16 @@ describe('Board List Header Component', () => {
);
}
store
=
new
Vuex
.
Store
({
state
,
getters
:
{
...
defaultGetters
,
...
getters
,
},
});
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockImplementation
();
wrapper
=
shallowMount
(
BoardListHeader
,
{
store
,
localVue
,
...
...
@@ -73,6 +75,43 @@ describe('Board List Header Component', () => {
const
findSettingsButton
=
()
=>
wrapper
.
find
({
ref
:
'
settingsBtn
'
});
afterEach
(()
=>
{
wrapper
.
destroy
();
localStorage
.
clear
();
});
describe
(
'
New epic button
'
,
()
=>
{
let
newEpicButton
;
beforeEach
(()
=>
{
jest
.
spyOn
(
boardsEventHub
,
'
$emit
'
);
createComponent
({
getters
:
{
isIssueBoard
:
()
=>
false
,
isEpicBoard
:
()
=>
true
,
isGroupBoard
:
()
=>
true
,
},
});
newEpicButton
=
wrapper
.
findComponent
(
GlButtonGroup
).
findComponent
(
GlButton
);
});
it
(
'
renders New epic button
'
,
()
=>
{
expect
(
newEpicButton
.
exists
()).
toBe
(
true
);
expect
(
newEpicButton
.
attributes
()).
toMatchObject
({
title
:
'
New epic
'
,
'
aria-label
'
:
'
New epic
'
,
});
});
it
(
'
emits `toggle-epic-form` event on Sidebar eventHub when clicked
'
,
async
()
=>
{
await
newEpicButton
.
vm
.
$emit
(
'
click
'
);
expect
(
boardsEventHub
.
$emit
).
toHaveBeenCalledWith
(
`toggle-epic-form-
${
mockLabelList
.
id
}
`
);
expect
(
boardsEventHub
.
$emit
).
toHaveBeenCalledTimes
(
1
);
});
});
describe
(
'
Settings Button
'
,
()
=>
{
const
hasSettings
=
[
ListType
.
assignee
,
ListType
.
milestone
,
ListType
.
iteration
,
ListType
.
label
];
const
hasNoSettings
=
[
ListType
.
backlog
,
ListType
.
closed
];
...
...
@@ -111,8 +150,12 @@ describe('Board List Header Component', () => {
});
it
(
'
does not emit event when there is an active List
'
,
()
=>
{
store
.
state
.
activeId
=
mockLabelList
.
id
;
createComponent
({
listType
:
hasSettings
[
0
]
});
createComponent
({
listType
:
hasSettings
[
0
],
state
:
{
activeId
:
mockLabelList
.
id
,
},
});
wrapper
.
vm
.
openSidebarSettings
();
expect
(
sidebarEventHub
.
$emit
).
not
.
toHaveBeenCalled
();
...
...
ee/spec/frontend/boards/components/board_list_spec.js
View file @
ecc70809
import
BoardNewEpic
from
'
ee/boards/components/board_new_epic.vue
'
;
import
createComponent
from
'
jest/boards/board_list_helper
'
;
describe
(
'
BoardList Component
'
,
()
=>
{
let
mock
;
let
component
;
beforeEach
((
done
)
=>
{
const
listIssueProps
=
{
project
:
{
path
:
'
/test
'
,
},
real_path
:
''
,
webUrl
:
''
,
};
import
BoardCard
from
'
~/boards/components/board_card.vue
'
;
import
BoardCardInner
from
'
~/boards/components/board_card_inner.vue
'
;
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
createFlash
from
'
~/flash
'
;
const
componentProps
=
{
groupId
:
undefined
,
};
jest
.
mock
(
'
~/flash
'
);
const
listIssueProps
=
{
project
:
{
path
:
'
/test
'
,
},
real_path
:
''
,
webUrl
:
''
,
};
const
componentProps
=
{
groupId
:
undefined
,
};
const
actions
=
{
addListNewEpic
:
jest
.
fn
().
mockResolvedValue
(),
};
({
mock
,
component
}
=
createComponent
({
done
,
componentProps
,
listIssueProps
,
}));
const
componentConfig
=
{
listIssueProps
,
componentProps
,
getters
:
{
isGroupBoard
:
()
=>
true
,
isProjectBoard
:
()
=>
false
,
isEpicBoard
:
()
=>
true
,
},
state
:
{
issuableType
:
issuableTypes
.
epic
,
},
actions
,
stubs
:
{
BoardCard
,
BoardCardInner
,
BoardNewEpic
,
},
provide
:
{
scopedLabelsAvailable
:
true
,
},
};
describe
(
'
BoardList Component
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
(
componentConfig
);
});
afterEach
(()
=>
{
mock
.
restore
();
wrapper
.
destroy
();
});
it
(
'
renders link properly in issue
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-card .board-card-title a
'
).
getAttribute
(
'
href
'
),
).
not
.
toContain
(
'
:project_path
'
);
expect
(
wrapper
.
find
(
'
.board-card .board-card-title a
'
).
attributes
(
'
href
'
)).
not
.
toContain
(
'
:project_path
'
,
);
});
describe
(
'
board-new-epic component
'
,
()
=>
{
const
submitForm
=
async
(
w
)
=>
{
const
newEpicForm
=
w
.
findComponent
(
BoardNewEpic
);
newEpicForm
.
find
(
'
input
'
).
setValue
(
'
Foo
'
);
newEpicForm
.
find
(
'
form
'
).
trigger
(
'
submit
'
);
await
wrapper
.
vm
.
$nextTick
();
};
beforeEach
(
async
()
=>
{
eventHub
.
$emit
(
`toggle-epic-form-
${
wrapper
.
vm
.
list
.
id
}
`
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
renders component
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
BoardNewEpic
).
exists
()).
toBe
(
true
);
});
it
(
'
calls action `addListNewEpic` when "Create epic" button is clicked
'
,
async
()
=>
{
await
submitForm
(
wrapper
);
expect
(
actions
.
addListNewEpic
).
toHaveBeenCalledWith
(
expect
.
any
(
Object
),
expect
.
objectContaining
({
epicInput
:
{
title
:
'
Foo
'
,
boardId
:
'
gid://gitlab/Boards::EpicBoard/
'
,
listId
:
'
gid://gitlab/List/1
'
,
},
}),
);
});
it
(
'
calls `createFlash` when form submission fails
'
,
async
()
=>
{
const
mockActions
=
{
addListNewEpic
:
jest
.
fn
().
mockRejectedValue
(),
};
wrapper
=
createComponent
({
...
componentConfig
,
actions
:
mockActions
,
});
eventHub
.
$emit
(
`toggle-epic-form-
${
wrapper
.
vm
.
list
.
id
}
`
);
await
wrapper
.
vm
.
$nextTick
();
await
submitForm
(
wrapper
);
return
mockActions
.
addListNewEpic
().
catch
((
error
)
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Failed to create epic. Please try again.
'
,
captureError
:
true
,
error
,
});
});
});
});
});
ee/spec/frontend/boards/stores/actions_spec.js
View file @
ecc70809
...
...
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
BoardType
,
GroupByParamType
,
listsQuery
,
issuableTypes
}
from
'
ee/boards/constants
'
;
import
epicCreateMutation
from
'
ee/boards/graphql/epic_create.mutation.graphql
'
;
import
actions
,
{
gqlClient
}
from
'
ee/boards/stores/actions
'
;
import
boardsStoreEE
from
'
ee/boards/stores/boards_store_ee
'
;
import
*
as
types
from
'
ee/boards/stores/mutation_types
'
;
...
...
@@ -13,6 +14,7 @@ import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mo
import
{
formatListIssues
}
from
'
~/boards/boards_util
'
;
import
listsIssuesQuery
from
'
~/boards/graphql/lists_issues.query.graphql
'
;
import
*
as
typesCE
from
'
~/boards/stores/mutation_types
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
*
as
commonUtils
from
'
~/lib/utils/common_utils
'
;
import
{
mergeUrlParams
,
removeParams
}
from
'
~/lib/utils/url_utility
'
;
import
{
...
...
@@ -876,6 +878,169 @@ describe('createEpicList', () => {
});
});
describe
(
'
addListNewEpic
'
,
()
=>
{
const
state
=
{
boardType
:
'
group
'
,
fullPath
:
'
gitlab-org/gitlab
'
,
boardConfig
:
{
labelIds
:
[],
assigneeId
:
null
,
milestoneId
:
-
1
,
},
};
const
fakeList
=
{
id
:
'
gid://gitlab/List/123
'
};
it
(
'
should add board scope to the epic being created
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
boardEpicCreate
:
{
epic
:
mockEpic
,
errors
:
[],
},
},
});
await
actions
.
addListNewEpic
(
{
dispatch
:
jest
.
fn
(),
commit
:
jest
.
fn
(),
state
},
{
epicInput
:
mockEpic
,
list
:
fakeList
},
);
expect
(
gqlClient
.
mutate
).
toHaveBeenCalledWith
({
mutation
:
epicCreateMutation
,
variables
:
{
input
:
{
...
mockEpic
,
groupPath
:
state
.
fullPath
,
id
:
'
gid://gitlab/Epic/41
'
,
labels
:
[],
},
},
});
});
it
(
'
should add board scope by merging attributes to the epic being created
'
,
async
()
=>
{
const
epic
=
{
...
mockEpic
,
labelIds
:
[
'
gid://gitlab/GroupLabel/4
'
],
};
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
boardEpicCreate
:
{
epic
,
errors
:
[],
},
},
});
const
payload
=
{
...
epic
,
labelIds
:
[...
epic
.
labelIds
,
'
gid://gitlab/GroupLabel/5
'
],
};
await
actions
.
addListNewEpic
(
{
dispatch
:
jest
.
fn
(),
commit
:
jest
.
fn
(),
state
},
{
epicInput
:
epic
,
list
:
fakeList
},
);
expect
(
gqlClient
.
mutate
).
toHaveBeenCalledWith
({
mutation
:
epicCreateMutation
,
variables
:
{
input
:
{
...
epic
,
groupPath
:
state
.
fullPath
,
},
},
});
expect
(
payload
.
labelIds
).
toEqual
([
'
gid://gitlab/GroupLabel/4
'
,
'
gid://gitlab/GroupLabel/5
'
]);
});
describe
(
'
when issue creation mutation request succeeds
'
,
()
=>
{
it
(
'
dispatches a correct set of mutations
'
,
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
boardEpicCreate
:
{
epic
:
mockEpic
,
errors
:
[],
},
},
});
testAction
({
action
:
actions
.
addListNewEpic
,
payload
:
{
epicInput
:
mockEpic
,
list
:
fakeList
,
placeholderId
:
'
tmp
'
,
},
state
,
expectedActions
:
[
{
type
:
'
addListItem
'
,
payload
:
{
list
:
fakeList
,
item
:
{
...
mockEpic
,
id
:
'
tmp
'
,
isLoading
:
true
,
labels
:
[],
assignees
:
[]
},
position
:
0
,
inProgress
:
true
,
},
},
{
type
:
'
removeListItem
'
,
payload
:
{
listId
:
fakeList
.
id
,
itemId
:
'
tmp
'
}
},
{
type
:
'
addListItem
'
,
payload
:
{
list
:
fakeList
,
item
:
{
...
mockEpic
,
id
:
getIdFromGraphQLId
(
mockEpic
.
id
),
assignees
:
[]
},
position
:
0
,
},
},
],
});
});
});
describe
(
'
when issue creation mutation request fails
'
,
()
=>
{
it
(
'
dispatches a correct set of mutations
'
,
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
boardEpicCreate
:
{
epic
:
mockEpic
,
errors
:
[{
foo
:
'
bar
'
}],
},
},
});
testAction
({
action
:
actions
.
addListNewEpic
,
payload
:
{
epicInput
:
mockEpic
,
list
:
fakeList
,
placeholderId
:
'
tmp
'
,
},
state
,
expectedActions
:
[
{
type
:
'
addListItem
'
,
payload
:
{
list
:
fakeList
,
item
:
{
...
mockEpic
,
id
:
'
tmp
'
,
isLoading
:
true
,
labels
:
[],
assignees
:
[]
},
position
:
0
,
inProgress
:
true
,
},
},
{
type
:
'
removeListItem
'
,
payload
:
{
listId
:
fakeList
.
id
,
itemId
:
'
tmp
'
}
},
],
expectedMutations
:
[
{
type
:
types
.
SET_ERROR
,
payload
:
'
An error occurred while creating the epic. Please try again.
'
,
},
],
});
});
});
});
describe
(
'
fetchMilestones
'
,
()
=>
{
const
queryResponse
=
{
data
:
{
...
...
locale/gitlab.pot
View file @
ecc70809
...
...
@@ -5247,6 +5247,9 @@ msgid_plural "Boards|+ %{displayedIssuablesCount} more %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
msgid "Boards|An error occurred while creating the epic. Please try again."
msgstr ""
msgid "Boards|An error occurred while creating the issue. Please try again."
msgstr ""
...
...
@@ -5306,6 +5309,9 @@ msgstr ""
msgid "Boards|Failed to fetch blocking %{issuableType}s"
msgstr ""
msgid "Boards|New epic"
msgstr ""
msgid "Boards|Retrieving blocking %{issuableType}s"
msgstr ""
...
...
@@ -5336,6 +5342,9 @@ msgstr ""
msgid "Board|Enter board name"
msgstr ""
msgid "Board|Failed to create epic. Please try again."
msgstr ""
msgid "Board|Failed to delete board. Please try again."
msgstr ""
...
...
spec/frontend/boards/board_list_helper.js
View file @
ecc70809
/* global List */
/* global ListIssue */
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Sortable
from
'
sortablejs
'
;
import
Vue
from
'
vue
'
;
import
BoardList
from
'
~/boards/components/board_list_deprecated.vue
'
;
import
'
~/boards/models/issue
'
;
import
'
~/boards/models/list
'
;
import
store
from
'
~/boards/stores
'
;
import
boardsStore
from
'
~/boards/stores/boards_store
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
listObj
,
boardsMockInterceptor
}
from
'
./mock_data
'
;
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
window
.
Sortable
=
Sortable
;
import
BoardCard
from
'
~/boards/components/board_card.vue
'
;
import
BoardList
from
'
~/boards/components/board_list.vue
'
;
import
BoardNewIssue
from
'
~/boards/components/board_new_issue.vue
'
;
import
defaultState
from
'
~/boards/stores/state
'
;
import
{
mockList
,
mockIssuesByListId
,
issues
}
from
'
./mock_data
'
;
export
default
function
createComponent
({
done
,
listIssueProps
=
{},
componentProps
=
{},
listProps
=
{},
})
{
const
el
=
document
.
createElement
(
'
div
'
);
actions
=
{},
getters
=
{},
provide
=
{},
state
=
defaultState
,
stubs
=
{
BoardNewIssue
,
BoardCard
,
},
}
=
{})
{
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
document
.
body
.
appendChild
(
el
);
const
mock
=
new
MockAdapter
(
axios
);
mock
.
onAny
().
reply
(
boardsMockInterceptor
);
boardsStore
.
create
();
const
store
=
new
Vuex
.
Store
({
state
:
{
boardItemsByListId
:
mockIssuesByListId
,
boardItems
:
issues
,
pageInfoByListId
:
{
'
gid://gitlab/List/1
'
:
{
hasNextPage
:
true
},
'
gid://gitlab/List/2
'
:
{},
},
listsFlags
:
{
'
gid://gitlab/List/1
'
:
{},
'
gid://gitlab/List/2
'
:
{},
},
selectedBoardItems
:
[],
...
state
,
},
getters
:
{
isGroupBoard
:
()
=>
false
,
isProjectBoard
:
()
=>
true
,
isEpicBoard
:
()
=>
false
,
...
getters
,
},
actions
,
});
const
BoardListComp
=
Vue
.
extend
(
BoardList
);
const
list
=
new
List
({
...
listObj
,
...
listProps
});
const
issue
=
new
ListIssue
({
const
list
=
{
...
mockList
,
...
listProps
,
};
const
issue
=
{
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
...
...
@@ -36,31 +59,31 @@ export default function createComponent({
labels
:
[],
assignees
:
[],
...
listIssueProps
,
}
)
;
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
listProps
,
'
issues
Size
'
))
{
list
.
issues
Size
=
1
;
};
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
listProps
,
'
issues
Count
'
))
{
list
.
issues
Count
=
1
;
}
list
.
issues
.
push
(
issue
);
const
component
=
new
BoardListComp
(
{
el
,
const
component
=
shallowMount
(
BoardList
,
{
localVue
,
store
,
propsData
:
{
disabled
:
false
,
list
,
issues
:
list
.
issues
,
loading
:
fals
e
,
boardItems
:
[
issue
]
,
canAdminList
:
tru
e
,
...
componentProps
,
},
provide
:
{
groupId
:
null
,
rootPath
:
'
/
'
,
weightFeatureAvailable
:
false
,
boardWeight
:
null
,
canAdminList
:
true
,
...
provide
,
},
}).
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
stubs
,
});
return
{
component
,
mock
}
;
return
component
;
}
spec/frontend/boards/board_list_spec.js
View file @
ecc70809
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
{
useFakeRequestAnimationFrame
}
from
'
helpers/fake_request_animation_frame
'
;
import
createComponent
from
'
jest/boards/board_list_helper
'
;
import
BoardCard
from
'
~/boards/components/board_card.vue
'
;
import
BoardList
from
'
~/boards/components/board_list.vue
'
;
import
BoardNewIssue
from
'
~/boards/components/board_new_issue.vue
'
;
import
eventHub
from
'
~/boards/eventhub
'
;
import
defaultState
from
'
~/boards/stores/state
'
;
import
{
mockList
,
mockIssuesByListId
,
issues
,
mockIssues
}
from
'
./mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
actions
=
{
fetchItemsForList
:
jest
.
fn
(),
};
const
createStore
=
(
state
=
defaultState
)
=>
{
return
new
Vuex
.
Store
({
state
,
actions
,
getters
:
{
isGroupBoard
:
()
=>
false
,
isProjectBoard
:
()
=>
true
,
isEpicBoard
:
()
=>
false
,
},
});
};
const
createComponent
=
({
listIssueProps
=
{},
componentProps
=
{},
listProps
=
{},
state
=
{},
}
=
{})
=>
{
const
store
=
createStore
({
boardItemsByListId
:
mockIssuesByListId
,
boardItems
:
issues
,
pageInfoByListId
:
{
'
gid://gitlab/List/1
'
:
{
hasNextPage
:
true
},
'
gid://gitlab/List/2
'
:
{},
},
listsFlags
:
{
'
gid://gitlab/List/1
'
:
{},
'
gid://gitlab/List/2
'
:
{},
},
selectedBoardItems
:
[],
...
state
,
});
const
list
=
{
...
mockList
,
...
listProps
,
};
const
issue
=
{
title
:
'
Testing
'
,
id
:
1
,
iid
:
1
,
confidential
:
false
,
labels
:
[],
assignees
:
[],
...
listIssueProps
,
};
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
listProps
,
'
issuesCount
'
))
{
list
.
issuesCount
=
1
;
}
const
component
=
shallowMount
(
BoardList
,
{
localVue
,
propsData
:
{
disabled
:
false
,
list
,
boardItems
:
[
issue
],
canAdminList
:
true
,
...
componentProps
,
},
store
,
provide
:
{
groupId
:
null
,
rootPath
:
'
/
'
,
weightFeatureAvailable
:
false
,
boardWeight
:
null
,
canAdminList
:
true
,
},
stubs
:
{
BoardCard
,
BoardNewIssue
,
},
});
return
component
;
};
import
{
mockIssues
}
from
'
./mock_data
'
;
describe
(
'
Board list component
'
,
()
=>
{
let
wrapper
;
...
...
@@ -101,7 +15,6 @@ describe('Board list component', () => {
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
When Expanded
'
,
()
=>
{
...
...
@@ -176,6 +89,10 @@ describe('Board list component', () => {
});
describe
(
'
load more issues
'
,
()
=>
{
const
actions
=
{
fetchItemsForList
:
jest
.
fn
(),
};
beforeEach
(()
=>
{
wrapper
=
createComponent
({
listProps
:
{
issuesCount
:
25
},
...
...
@@ -184,6 +101,7 @@ describe('Board list component', () => {
it
(
'
does not load issues if already loading
'
,
()
=>
{
wrapper
=
createComponent
({
actions
,
state
:
{
listsFlags
:
{
'
gid://gitlab/List/1
'
:
{
isLoadingMore
:
true
}
}
},
});
wrapper
.
vm
.
listRef
.
dispatchEvent
(
new
Event
(
'
scroll
'
));
...
...
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