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
798e516f
Commit
798e516f
authored
Mar 10, 2021
by
Florie Guibert
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Epic Boards - Drag & Drop epic between lists
Add ability to move epic between lists using Drag & Drop
parent
6213f056
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
497 additions
and
194 deletions
+497
-194
app/assets/javascripts/boards/boards_util.js
app/assets/javascripts/boards/boards_util.js
+9
-9
app/assets/javascripts/boards/components/board_card.vue
app/assets/javascripts/boards/components/board_card.vue
+11
-11
app/assets/javascripts/boards/components/board_content.vue
app/assets/javascripts/boards/components/board_content.vue
+2
-1
app/assets/javascripts/boards/components/board_list.vue
app/assets/javascripts/boards/components/board_list.vue
+21
-22
app/assets/javascripts/boards/stores/actions.js
app/assets/javascripts/boards/stores/actions.js
+9
-5
app/assets/javascripts/boards/stores/mutations.js
app/assets/javascripts/boards/stores/mutations.js
+26
-18
ee/app/assets/javascripts/boards/components/issues_lane_list.vue
...assets/javascripts/boards/components/issues_lane_list.vue
+9
-9
ee/app/assets/javascripts/boards/graphql/epic_move_list.mutation.graphql
...avascripts/boards/graphql/epic_move_list.mutation.graphql
+12
-0
ee/app/assets/javascripts/boards/stores/actions.js
ee/app/assets/javascripts/boards/stores/actions.js
+48
-5
ee/app/assets/javascripts/boards/stores/mutation_types.js
ee/app/assets/javascripts/boards/stores/mutation_types.js
+2
-0
ee/app/assets/javascripts/boards/stores/mutations.js
ee/app/assets/javascripts/boards/stores/mutations.js
+34
-5
ee/spec/features/epic_boards/epic_boards_spec.rb
ee/spec/features/epic_boards/epic_boards_spec.rb
+27
-1
ee/spec/frontend/boards/mock_data.js
ee/spec/frontend/boards/mock_data.js
+6
-0
ee/spec/frontend/boards/stores/actions_spec.js
ee/spec/frontend/boards/stores/actions_spec.js
+218
-91
ee/spec/frontend/boards/stores/mutations_spec.js
ee/spec/frontend/boards/stores/mutations_spec.js
+34
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/boards/board_list_spec.js
spec/frontend/boards/board_list_spec.js
+5
-5
spec/frontend/boards/components/board_card_spec.js
spec/frontend/boards/components/board_card_spec.js
+3
-3
spec/frontend/boards/stores/actions_spec.js
spec/frontend/boards/stores/actions_spec.js
+18
-9
No files found.
app/assets/javascripts/boards/boards_util.js
View file @
798e516f
...
...
@@ -113,31 +113,31 @@ export function formatIssueInput(issueInput, boardConfig) {
};
}
export
function
moveI
ssueListHelper
(
issue
,
fromList
,
toList
)
{
const
updatedI
ssue
=
issue
;
export
function
moveI
temListHelper
(
item
,
fromList
,
toList
)
{
const
updatedI
tem
=
item
;
if
(
toList
.
listType
===
ListType
.
label
&&
!
updatedI
ssue
.
labels
.
find
((
label
)
=>
label
.
id
===
toList
.
label
.
id
)
!
updatedI
tem
.
labels
.
find
((
label
)
=>
label
.
id
===
toList
.
label
.
id
)
)
{
updatedI
ssue
.
labels
.
push
(
toList
.
label
);
updatedI
tem
.
labels
.
push
(
toList
.
label
);
}
if
(
fromList
?.
label
&&
fromList
.
listType
===
ListType
.
label
)
{
updatedI
ssue
.
labels
=
updatedIssue
.
labels
.
filter
((
label
)
=>
fromList
.
label
.
id
!==
label
.
id
);
updatedI
tem
.
labels
=
updatedItem
.
labels
.
filter
((
label
)
=>
fromList
.
label
.
id
!==
label
.
id
);
}
if
(
toList
.
listType
===
ListType
.
assignee
&&
!
updatedI
ssue
.
assignees
.
find
((
assignee
)
=>
assignee
.
id
===
toList
.
assignee
.
id
)
!
updatedI
tem
.
assignees
.
find
((
assignee
)
=>
assignee
.
id
===
toList
.
assignee
.
id
)
)
{
updatedI
ssue
.
assignees
.
push
(
toList
.
assignee
);
updatedI
tem
.
assignees
.
push
(
toList
.
assignee
);
}
if
(
fromList
?.
assignee
&&
fromList
.
listType
===
ListType
.
assignee
)
{
updatedI
ssue
.
assignees
=
updatedIssue
.
assignees
.
filter
(
updatedI
tem
.
assignees
=
updatedItem
.
assignees
.
filter
(
(
assignee
)
=>
assignee
.
id
!==
fromList
.
assignee
.
id
,
);
}
return
updatedI
ssue
;
return
updatedI
tem
;
}
export
function
isListDraggable
(
list
)
{
...
...
app/assets/javascripts/boards/components/board_card.vue
View file @
798e516f
...
...
@@ -13,7 +13,7 @@ export default {
default
:
()
=>
({}),
required
:
false
,
},
i
ssue
:
{
i
tem
:
{
type
:
Object
,
default
:
()
=>
({}),
required
:
false
,
...
...
@@ -33,12 +33,12 @@ export default {
...
mapState
([
'
selectedBoardItems
'
,
'
activeId
'
]),
...
mapGetters
([
'
isSwimlanesOn
'
]),
isActive
()
{
return
this
.
i
ssue
.
id
===
this
.
activeId
;
return
this
.
i
tem
.
id
===
this
.
activeId
;
},
multiSelectVisible
()
{
return
(
!
this
.
activeId
&&
this
.
selectedBoardItems
.
findIndex
((
boardItem
)
=>
boardItem
.
id
===
this
.
i
ssue
.
id
)
>
-
1
this
.
selectedBoardItems
.
findIndex
((
boardItem
)
=>
boardItem
.
id
===
this
.
i
tem
.
id
)
>
-
1
);
},
},
...
...
@@ -50,9 +50,9 @@ export default {
const
isMultiSelect
=
e
.
ctrlKey
||
e
.
metaKey
;
if
(
isMultiSelect
)
{
this
.
toggleBoardItemMultiSelection
(
this
.
i
ssue
);
this
.
toggleBoardItemMultiSelection
(
this
.
i
tem
);
}
else
{
this
.
toggleBoardItem
({
boardItem
:
this
.
i
ssue
});
this
.
toggleBoardItem
({
boardItem
:
this
.
i
tem
});
}
},
},
...
...
@@ -64,18 +64,18 @@ export default {
data-qa-selector=
"board_card"
:class=
"
{
'multi-select': multiSelectVisible,
'user-can-drag': !disabled
&&
i
ssue
.id,
'is-disabled': disabled || !i
ssue
.id,
'user-can-drag': !disabled
&&
i
tem
.id,
'is-disabled': disabled || !i
tem
.id,
'is-active': isActive,
}"
:index="index"
:data-i
ssue-id="issue
.id"
:data-i
ssue-iid="issue
.iid"
:data-i
ssue-path="issue
.referencePath"
:data-i
tem-id="item
.id"
:data-i
tem-iid="item
.iid"
:data-i
tem-path="item
.referencePath"
data-testid="board_card"
class="board-card gl-p-5 gl-rounded-base"
@mouseup="toggleIssue($event)"
>
<board-card-inner
:list=
"list"
:item=
"i
ssue
"
:update-filters=
"true"
/>
<board-card-inner
:list=
"list"
:item=
"i
tem
"
:update-filters=
"true"
/>
</li>
</
template
>
app/assets/javascripts/boards/components/board_content.vue
View file @
798e516f
...
...
@@ -49,7 +49,7 @@ export default {
:
this
.
lists
;
},
canDragColumns
()
{
return
this
.
glFeatures
.
graphqlBoardLists
&&
this
.
canAdminList
;
return
!
this
.
isEpicBoard
&&
this
.
glFeatures
.
graphqlBoardLists
&&
this
.
canAdminList
;
},
boardColumnWrapper
()
{
return
this
.
canDragColumns
?
Draggable
:
'
div
'
;
...
...
@@ -80,6 +80,7 @@ export default {
handleDragOnEnd
(
params
)
{
sortableEnd
();
if
(
this
.
isEpicBoard
)
return
;
const
{
item
,
newIndex
,
oldIndex
,
to
}
=
params
;
...
...
app/assets/javascripts/boards/components/board_list.vue
View file @
798e516f
<
script
>
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
Draggable
from
'
vuedraggable
'
;
import
{
mapActions
,
map
Getters
,
map
State
}
from
'
vuex
'
;
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
sortableStart
,
sortableEnd
}
from
'
~/boards/mixins/sortable_default_options
'
;
import
{
sprintf
,
__
}
from
'
~/locale
'
;
import
defaultSortableConfig
from
'
~/sortable/sortable_config
'
;
...
...
@@ -49,7 +49,6 @@ export default {
},
computed
:
{
...
mapState
([
'
pageInfoByListId
'
,
'
listsFlags
'
]),
...
mapGetters
([
'
isEpicBoard
'
]),
paginatedIssueText
()
{
return
sprintf
(
__
(
'
Showing %{pageSize} of %{total} issues
'
),
{
pageSize
:
this
.
boardItems
.
length
,
...
...
@@ -70,13 +69,13 @@ export default {
},
listRef
()
{
// When list is draggable, the reference to the list needs to be accessed differently
return
this
.
canAdminList
&&
!
this
.
isEpicBoard
?
this
.
$refs
.
list
.
$el
:
this
.
$refs
.
list
;
return
this
.
canAdminList
?
this
.
$refs
.
list
.
$el
:
this
.
$refs
.
list
;
},
showingAllIssues
()
{
return
this
.
boardItems
.
length
===
this
.
list
.
issuesCount
;
},
treeRootWrapper
()
{
return
this
.
canAdminList
&&
!
this
.
isEpicBoard
?
Draggable
:
'
ul
'
;
return
this
.
canAdminList
?
Draggable
:
'
ul
'
;
},
treeRootOptions
()
{
const
options
=
{
...
...
@@ -113,7 +112,7 @@ export default {
this
.
listRef
.
removeEventListener
(
'
scroll
'
,
this
.
onScroll
);
},
methods
:
{
...
mapActions
([
'
fetchItemsForList
'
,
'
moveI
ssue
'
]),
...
mapActions
([
'
fetchItemsForList
'
,
'
moveI
tem
'
]),
listHeight
()
{
return
this
.
listRef
.
getBoundingClientRect
().
height
;
},
...
...
@@ -149,40 +148,40 @@ export default {
handleDragOnEnd
(
params
)
{
sortableEnd
();
const
{
newIndex
,
oldIndex
,
from
,
to
,
item
}
=
params
;
const
{
i
ssueId
,
issueIid
,
issue
Path
}
=
item
.
dataset
;
const
{
i
temId
,
itemIid
,
item
Path
}
=
item
.
dataset
;
const
{
children
}
=
to
;
let
moveBeforeId
;
let
moveAfterId
;
const
getI
ssueId
=
(
el
)
=>
Number
(
el
.
dataset
.
issue
Id
);
const
getI
temId
=
(
el
)
=>
Number
(
el
.
dataset
.
item
Id
);
// If i
ssue
is being moved within the same list
// If i
tem
is being moved within the same list
if
(
from
===
to
)
{
if
(
newIndex
>
oldIndex
&&
children
.
length
>
1
)
{
// If i
ssue is being moved down we look for the issue
that ends up before
moveBeforeId
=
getI
ssue
Id
(
children
[
newIndex
]);
// If i
tem is being moved down we look for the item
that ends up before
moveBeforeId
=
getI
tem
Id
(
children
[
newIndex
]);
}
else
if
(
newIndex
<
oldIndex
&&
children
.
length
>
1
)
{
// If i
ssue is being moved up we look for the issue
that ends up after
moveAfterId
=
getI
ssue
Id
(
children
[
newIndex
]);
// If i
tem is being moved up we look for the item
that ends up after
moveAfterId
=
getI
tem
Id
(
children
[
newIndex
]);
}
else
{
// If i
ssue
remains in the same list at the same position we do nothing
// If i
tem
remains in the same list at the same position we do nothing
return
;
}
}
else
{
// We look for the i
ssue that ends up before the moved issue
if it exists
// We look for the i
tem that ends up before the moved item
if it exists
if
(
children
[
newIndex
-
1
])
{
moveBeforeId
=
getI
ssue
Id
(
children
[
newIndex
-
1
]);
moveBeforeId
=
getI
tem
Id
(
children
[
newIndex
-
1
]);
}
// We look for the i
ssue that ends up after the moved issue
if it exists
// We look for the i
tem that ends up after the moved item
if it exists
if
(
children
[
newIndex
])
{
moveAfterId
=
getI
ssue
Id
(
children
[
newIndex
]);
moveAfterId
=
getI
tem
Id
(
children
[
newIndex
]);
}
}
this
.
moveI
ssue
({
i
ssue
Id
,
i
ssue
Iid
,
i
ssue
Path
,
this
.
moveI
tem
({
i
tem
Id
,
i
tem
Iid
,
i
tem
Path
,
fromListId
:
from
.
dataset
.
listId
,
toListId
:
to
.
dataset
.
listId
,
moveBeforeId
,
...
...
@@ -227,7 +226,7 @@ export default {
:key=
"item.id"
:index=
"index"
:list=
"list"
:i
ssue
=
"item"
:i
tem
=
"item"
:disabled=
"disabled"
/>
<li
v-if=
"showCount"
class=
"board-list-count gl-text-center"
data-issue-id=
"-1"
>
...
...
app/assets/javascripts/boards/stores/actions.js
View file @
798e516f
...
...
@@ -325,17 +325,21 @@ export default {
commit
(
types
.
RESET_ISSUES
);
},
moveItem
:
({
dispatch
})
=>
{
dispatch
(
'
moveIssue
'
);
},
moveIssue
:
(
{
state
,
commit
},
{
i
ssueId
,
issueIid
,
issue
Path
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
},
{
i
temId
,
itemIid
,
item
Path
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
},
)
=>
{
const
originalIssue
=
state
.
boardItems
[
i
ssue
Id
];
const
originalIssue
=
state
.
boardItems
[
i
tem
Id
];
const
fromList
=
state
.
boardItemsByListId
[
fromListId
];
const
originalIndex
=
fromList
.
indexOf
(
Number
(
i
ssue
Id
));
const
originalIndex
=
fromList
.
indexOf
(
Number
(
i
tem
Id
));
commit
(
types
.
MOVE_ISSUE
,
{
originalIssue
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
});
const
{
boardId
}
=
state
;
const
[
fullProjectPath
]
=
i
ssue
Path
.
split
(
/
[
#
]
/
);
const
[
fullProjectPath
]
=
i
tem
Path
.
split
(
/
[
#
]
/
);
gqlClient
.
mutate
({
...
...
@@ -343,7 +347,7 @@ export default {
variables
:
{
projectPath
:
fullProjectPath
,
boardId
:
fullBoardId
(
boardId
),
iid
:
i
ssue
Iid
,
iid
:
i
tem
Iid
,
fromListId
:
getIdFromGraphQLId
(
fromListId
),
toListId
:
getIdFromGraphQLId
(
toListId
),
moveBeforeId
,
...
...
app/assets/javascripts/boards/stores/mutations.js
View file @
798e516f
...
...
@@ -2,7 +2,8 @@ import { pull, union } from 'lodash';
import
Vue
from
'
vue
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
formatIssue
,
moveIssueListHelper
}
from
'
../boards_util
'
;
import
{
formatIssue
,
moveItemListHelper
}
from
'
../boards_util
'
;
import
{
issuableTypes
}
from
'
../constants
'
;
import
*
as
mutationTypes
from
'
./mutation_types
'
;
const
notImplemented
=
()
=>
{
...
...
@@ -10,13 +11,21 @@ const notImplemented = () => {
throw
new
Error
(
'
Not implemented!
'
);
};
export
const
removeIssueFromList
=
({
state
,
listId
,
issueId
})
=>
{
Vue
.
set
(
state
.
boardItemsByListId
,
listId
,
pull
(
state
.
boardItemsByListId
[
listId
],
issueId
));
const
updateListItemsCount
=
({
state
,
listId
,
value
})
=>
{
const
list
=
state
.
boardLists
[
listId
];
Vue
.
set
(
state
.
boardLists
,
listId
,
{
...
list
,
issuesCount
:
list
.
issuesCount
-
1
});
if
(
state
.
issuableType
===
issuableTypes
.
epic
)
{
Vue
.
set
(
state
.
boardLists
,
listId
,
{
...
list
,
epicsCount
:
list
.
epicsCount
+
value
});
}
else
{
Vue
.
set
(
state
.
boardLists
,
listId
,
{
...
list
,
issuesCount
:
list
.
issuesCount
+
value
});
}
};
export
const
removeItemFromList
=
({
state
,
listId
,
itemId
})
=>
{
Vue
.
set
(
state
.
boardItemsByListId
,
listId
,
pull
(
state
.
boardItemsByListId
[
listId
],
itemId
));
updateListItemsCount
({
state
,
listId
,
value
:
-
1
});
};
export
const
addI
ssueToList
=
({
state
,
listId
,
issue
Id
,
moveBeforeId
,
moveAfterId
,
atIndex
})
=>
{
export
const
addI
temToList
=
({
state
,
listId
,
item
Id
,
moveBeforeId
,
moveAfterId
,
atIndex
})
=>
{
const
listIssues
=
state
.
boardItemsByListId
[
listId
];
let
newIndex
=
atIndex
||
0
;
if
(
moveBeforeId
)
{
...
...
@@ -24,10 +33,9 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
}
else
if
(
moveAfterId
)
{
newIndex
=
listIssues
.
indexOf
(
moveAfterId
);
}
listIssues
.
splice
(
newIndex
,
0
,
i
ssue
Id
);
listIssues
.
splice
(
newIndex
,
0
,
i
tem
Id
);
Vue
.
set
(
state
.
boardItemsByListId
,
listId
,
listIssues
);
const
list
=
state
.
boardLists
[
listId
];
Vue
.
set
(
state
.
boardLists
,
listId
,
{
...
list
,
issuesCount
:
list
.
issuesCount
+
1
});
updateListItemsCount
({
state
,
listId
,
value
:
1
});
};
export
default
{
...
...
@@ -182,11 +190,11 @@ export default {
const
fromList
=
state
.
boardLists
[
fromListId
];
const
toList
=
state
.
boardLists
[
toListId
];
const
issue
=
moveI
ssue
ListHelper
(
originalIssue
,
fromList
,
toList
);
const
issue
=
moveI
tem
ListHelper
(
originalIssue
,
fromList
,
toList
);
Vue
.
set
(
state
.
boardItems
,
issue
.
id
,
issue
);
removeI
ssueFromList
({
state
,
listId
:
fromListId
,
issue
Id
:
issue
.
id
});
addI
ssueToList
({
state
,
listId
:
toListId
,
issue
Id
:
issue
.
id
,
moveBeforeId
,
moveAfterId
});
removeI
temFromList
({
state
,
listId
:
fromListId
,
item
Id
:
issue
.
id
});
addI
temToList
({
state
,
listId
:
toListId
,
item
Id
:
issue
.
id
,
moveBeforeId
,
moveAfterId
});
},
[
mutationTypes
.
MOVE_ISSUE_SUCCESS
]:
(
state
,
{
issue
})
=>
{
...
...
@@ -200,11 +208,11 @@ export default {
)
=>
{
state
.
error
=
s__
(
'
Boards|An error occurred while moving the issue. Please try again.
'
);
Vue
.
set
(
state
.
boardItems
,
originalIssue
.
id
,
originalIssue
);
removeI
ssueFromList
({
state
,
listId
:
toListId
,
issue
Id
:
originalIssue
.
id
});
addI
ssue
ToList
({
removeI
temFromList
({
state
,
listId
:
toListId
,
item
Id
:
originalIssue
.
id
});
addI
tem
ToList
({
state
,
listId
:
fromListId
,
i
ssue
Id
:
originalIssue
.
id
,
i
tem
Id
:
originalIssue
.
id
,
atIndex
:
originalIndex
,
});
},
...
...
@@ -226,10 +234,10 @@ export default {
},
[
mutationTypes
.
ADD_ISSUE_TO_LIST
]:
(
state
,
{
list
,
issue
,
position
})
=>
{
addI
ssue
ToList
({
addI
tem
ToList
({
state
,
listId
:
list
.
id
,
i
ssue
Id
:
issue
.
id
,
i
tem
Id
:
issue
.
id
,
atIndex
:
position
,
});
Vue
.
set
(
state
.
boardItems
,
issue
.
id
,
issue
);
...
...
@@ -237,11 +245,11 @@ export default {
[
mutationTypes
.
ADD_ISSUE_TO_LIST_FAILURE
]:
(
state
,
{
list
,
issueId
})
=>
{
state
.
error
=
s__
(
'
Boards|An error occurred while creating the issue. Please try again.
'
);
removeI
ssueFromList
({
state
,
listId
:
list
.
id
,
issueId
});
removeI
temFromList
({
state
,
listId
:
list
.
id
,
itemId
:
issueId
});
},
[
mutationTypes
.
REMOVE_ISSUE_FROM_LIST
]:
(
state
,
{
list
,
issue
})
=>
{
removeI
ssueFromList
({
state
,
listId
:
list
.
id
,
issue
Id
:
issue
.
id
});
removeI
temFromList
({
state
,
listId
:
list
.
id
,
item
Id
:
issue
.
id
});
Vue
.
delete
(
state
.
boardItems
,
issue
.
id
);
},
...
...
ee/app/assets/javascripts/boards/components/issues_lane_list.vue
View file @
798e516f
...
...
@@ -118,7 +118,7 @@ export default {
handleDragOnEnd
(
params
)
{
document
.
body
.
classList
.
remove
(
'
is-dragging
'
);
const
{
newIndex
,
oldIndex
,
from
,
to
,
item
}
=
params
;
const
{
i
ssueId
,
issueIid
,
issue
Path
}
=
item
.
dataset
;
const
{
i
temId
,
itemIid
,
item
Path
}
=
item
.
dataset
;
const
{
children
}
=
to
;
let
moveBeforeId
;
let
moveAfterId
;
...
...
@@ -127,10 +127,10 @@ export default {
if
(
from
===
to
)
{
if
(
newIndex
>
oldIndex
&&
children
.
length
>
1
)
{
// If issue is being moved down we look for the issue that ends up before
moveBeforeId
=
Number
(
children
[
newIndex
].
dataset
.
i
ssue
Id
);
moveBeforeId
=
Number
(
children
[
newIndex
].
dataset
.
i
tem
Id
);
}
else
if
(
newIndex
<
oldIndex
&&
children
.
length
>
1
)
{
// If issue is being moved up we look for the issue that ends up after
moveAfterId
=
Number
(
children
[
newIndex
].
dataset
.
i
ssue
Id
);
moveAfterId
=
Number
(
children
[
newIndex
].
dataset
.
i
tem
Id
);
}
else
{
// If issue remains in the same list at the same position we do nothing
return
;
...
...
@@ -138,18 +138,18 @@ export default {
}
else
{
// We look for the issue that ends up before the moved issue if it exists
if
(
children
[
newIndex
-
1
])
{
moveBeforeId
=
Number
(
children
[
newIndex
-
1
].
dataset
.
i
ssue
Id
);
moveBeforeId
=
Number
(
children
[
newIndex
-
1
].
dataset
.
i
tem
Id
);
}
// We look for the issue that ends up after the moved issue if it exists
if
(
children
[
newIndex
])
{
moveAfterId
=
Number
(
children
[
newIndex
].
dataset
.
i
ssue
Id
);
moveAfterId
=
Number
(
children
[
newIndex
].
dataset
.
i
tem
Id
);
}
}
this
.
moveIssue
({
i
ssue
Id
,
i
ssue
Iid
,
i
ssue
Path
,
i
tem
Id
,
i
tem
Iid
,
i
tem
Path
,
fromListId
:
from
.
dataset
.
listId
,
toListId
:
to
.
dataset
.
listId
,
moveBeforeId
,
...
...
@@ -187,7 +187,7 @@ export default {
:key=
"issue.id"
:index=
"index"
:list=
"list"
:i
ssue
=
"issue"
:i
tem
=
"issue"
:disabled=
"disabled || !canAdminEpic"
/>
<gl-loading-icon
v-if=
"isLoadingMore && isUnassignedIssuesLane"
size=
"sm"
class=
"gl-py-3"
/>
...
...
ee/app/assets/javascripts/boards/graphql/epic_move_list.mutation.graphql
0 → 100644
View file @
798e516f
mutation
EpicMoveList
(
$epicId
:
EpicID
!
$boardId
:
BoardsEpicBoardID
!
$fromListId
:
BoardsEpicListID
!
$toListId
:
BoardsEpicListID
!
)
{
epicMoveList
(
input
:
{
epicId
:
$epicId
,
boardId
:
$boardId
,
fromListId
:
$fromListId
,
toListId
:
$toListId
}
)
{
errors
}
}
ee/app/assets/javascripts/boards/stores/actions.js
View file @
798e516f
...
...
@@ -32,6 +32,7 @@ import { EpicFilterType, IterationFilterType, GroupByParamType } from '../consta
import
epicQuery
from
'
../graphql/epic.query.graphql
'
;
import
createEpicBoardListMutation
from
'
../graphql/epic_board_list_create.mutation.graphql
'
;
import
epicBoardListsQuery
from
'
../graphql/epic_board_lists.query.graphql
'
;
import
epicMoveListMutation
from
'
../graphql/epic_move_list.mutation.graphql
'
;
import
epicsSwimlanesQuery
from
'
../graphql/epics_swimlanes.query.graphql
'
;
import
groupBoardMilestonesQuery
from
'
../graphql/group_board_milestones.query.graphql
'
;
import
issueMoveListMutation
from
'
../graphql/issue_move_list.mutation.graphql
'
;
...
...
@@ -484,13 +485,21 @@ export default {
});
},
moveItem
:
({
getters
,
dispatch
},
params
)
=>
{
if
(
!
getters
.
isEpicBoard
)
{
dispatch
(
'
moveIssue
'
,
params
);
}
else
{
dispatch
(
'
moveEpic
'
,
params
);
}
},
moveIssue
:
(
{
state
,
commit
},
{
i
ssueId
,
issueIid
,
issue
Path
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
,
epicId
},
{
i
temId
,
itemIid
,
item
Path
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
,
epicId
},
)
=>
{
const
originalIssue
=
state
.
boardItems
[
i
ssue
Id
];
const
originalIssue
=
state
.
boardItems
[
i
tem
Id
];
const
fromList
=
state
.
boardItemsByListId
[
fromListId
];
const
originalIndex
=
fromList
.
indexOf
(
Number
(
i
ssue
Id
));
const
originalIndex
=
fromList
.
indexOf
(
Number
(
i
tem
Id
));
commit
(
types
.
MOVE_ISSUE
,
{
originalIssue
,
fromListId
,
...
...
@@ -501,7 +510,7 @@ export default {
});
const
{
boardId
}
=
state
;
const
[
fullProjectPath
]
=
i
ssue
Path
.
split
(
/
[
#
]
/
);
const
[
fullProjectPath
]
=
i
tem
Path
.
split
(
/
[
#
]
/
);
gqlClient
.
mutate
({
...
...
@@ -509,7 +518,7 @@ export default {
variables
:
{
projectPath
:
fullProjectPath
,
boardId
:
fullBoardId
(
boardId
),
iid
:
i
ssue
Iid
,
iid
:
i
tem
Iid
,
fromListId
:
getIdFromGraphQLId
(
fromListId
),
toListId
:
getIdFromGraphQLId
(
toListId
),
moveBeforeId
,
...
...
@@ -530,6 +539,40 @@ export default {
);
},
moveEpic
:
({
state
,
commit
},
{
itemId
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
})
=>
{
const
originalEpic
=
state
.
boardItems
[
itemId
];
const
fromList
=
state
.
boardItemsByListId
[
fromListId
];
const
originalIndex
=
fromList
.
indexOf
(
Number
(
itemId
));
commit
(
types
.
MOVE_EPIC
,
{
originalEpic
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
,
});
const
{
boardId
}
=
state
;
gqlClient
.
mutate
({
mutation
:
epicMoveListMutation
,
variables
:
{
epicId
:
fullEpicId
(
itemId
),
boardId
:
fullEpicBoardId
(
boardId
),
fromListId
,
toListId
,
},
})
.
then
(({
data
})
=>
{
if
(
data
?.
epicMoveList
?.
errors
.
length
)
{
commit
(
types
.
MOVE_EPIC_FAILURE
,
{
originalEpic
,
fromListId
,
toListId
,
originalIndex
});
}
})
.
catch
(()
=>
commit
(
types
.
MOVE_EPIC_FAILURE
,
{
originalEpic
,
fromListId
,
toListId
,
originalIndex
}),
);
},
fetchLists
:
({
getters
,
dispatch
})
=>
{
if
(
!
getters
.
isEpicBoard
)
{
dispatch
(
'
fetchIssueLists
'
);
...
...
ee/app/assets/javascripts/boards/stores/mutation_types.js
View file @
798e516f
...
...
@@ -31,6 +31,8 @@ export const SET_FILTERS = 'SET_FILTERS';
export
const
MOVE_ISSUE
=
'
MOVE_ISSUE
'
;
export
const
MOVE_ISSUE_SUCCESS
=
'
MOVE_ISSUE_SUCCESS
'
;
export
const
MOVE_ISSUE_FAILURE
=
'
MOVE_ISSUE_FAILURE
'
;
export
const
MOVE_EPIC
=
'
MOVE_EPIC
'
;
export
const
MOVE_EPIC_FAILURE
=
'
MOVE_EPIC_FAILURE
'
;
export
const
CREATE_LIST_FAILURE
=
'
CREATE_LIST_FAILURE
'
;
export
const
SET_BOARD_EPIC_USER_PREFERENCES
=
'
SET_BOARD_EPIC_USER_PREFERENCES
'
;
export
const
RECEIVE_MILESTONES_REQUEST
=
'
RECEIVE_MILESTONES_REQUEST
'
;
...
...
ee/app/assets/javascripts/boards/stores/mutations.js
View file @
798e516f
import
{
union
,
unionBy
}
from
'
lodash
'
;
import
Vue
from
'
vue
'
;
import
{
moveI
ssue
ListHelper
}
from
'
~/boards/boards_util
'
;
import
{
moveI
tem
ListHelper
}
from
'
~/boards/boards_util
'
;
import
{
issuableTypes
}
from
'
~/boards/constants
'
;
import
mutationsCE
,
{
addI
ssueToList
,
removeIssue
FromList
}
from
'
~/boards/stores/mutations
'
;
import
mutationsCE
,
{
addI
temToList
,
removeItem
FromList
}
from
'
~/boards/stores/mutations
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
ErrorMessages
}
from
'
../constants
'
;
import
*
as
mutationTypes
from
'
./mutation_types
'
;
...
...
@@ -168,7 +168,7 @@ export default {
const
fromList
=
state
.
boardLists
[
fromListId
];
const
toList
=
state
.
boardLists
[
toListId
];
const
issue
=
moveI
ssue
ListHelper
(
originalIssue
,
fromList
,
toList
);
const
issue
=
moveI
tem
ListHelper
(
originalIssue
,
fromList
,
toList
);
if
(
epicId
===
null
)
{
Vue
.
set
(
state
.
boardItems
,
issue
.
id
,
{
...
issue
,
epic
:
null
});
...
...
@@ -176,8 +176,37 @@ export default {
Vue
.
set
(
state
.
boardItems
,
issue
.
id
,
{
...
issue
,
epic
:
{
id
:
epicId
}
});
}
removeIssueFromList
({
state
,
listId
:
fromListId
,
issueId
:
issue
.
id
});
addIssueToList
({
state
,
listId
:
toListId
,
issueId
:
issue
.
id
,
moveBeforeId
,
moveAfterId
});
removeItemFromList
({
state
,
listId
:
fromListId
,
itemId
:
issue
.
id
});
addItemToList
({
state
,
listId
:
toListId
,
itemId
:
issue
.
id
,
moveBeforeId
,
moveAfterId
});
},
[
mutationTypes
.
MOVE_EPIC
]:
(
state
,
{
originalEpic
,
fromListId
,
toListId
,
moveBeforeId
,
moveAfterId
},
)
=>
{
const
fromList
=
state
.
boardLists
[
fromListId
];
const
toList
=
state
.
boardLists
[
toListId
];
const
epic
=
moveItemListHelper
(
originalEpic
,
fromList
,
toList
);
Vue
.
set
(
state
.
boardItems
,
epic
.
id
,
epic
);
removeItemFromList
({
state
,
listId
:
fromListId
,
itemId
:
epic
.
id
});
addItemToList
({
state
,
listId
:
toListId
,
itemId
:
epic
.
id
,
moveBeforeId
,
moveAfterId
});
},
[
mutationTypes
.
MOVE_EPIC_FAILURE
]:
(
state
,
{
originalEpic
,
fromListId
,
toListId
,
originalIndex
},
)
=>
{
state
.
error
=
s__
(
'
Boards|An error occurred while moving the epic. Please try again.
'
);
Vue
.
set
(
state
.
boardItems
,
originalEpic
.
id
,
originalEpic
);
removeItemFromList
({
state
,
listId
:
toListId
,
itemId
:
originalEpic
.
id
});
addItemToList
({
state
,
listId
:
fromListId
,
itemId
:
originalEpic
.
id
,
atIndex
:
originalIndex
,
});
},
[
mutationTypes
.
SET_BOARD_EPIC_USER_PREFERENCES
]:
(
state
,
val
)
=>
{
...
...
ee/spec/features/epic_boards/epic_boards_spec.rb
View file @
798e516f
...
...
@@ -3,6 +3,9 @@
require
'spec_helper'
RSpec
.
describe
'epic boards'
,
:js
do
include
DragTo
include
MobileHelpers
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
,
:public
)
}
...
...
@@ -63,7 +66,7 @@ RSpec.describe 'epic boards', :js do
end
end
it
'creates new column for label containing labeled
issue
'
do
it
'creates new column for label containing labeled
epic
'
do
click_button
'Create list'
wait_for_all_requests
...
...
@@ -77,6 +80,16 @@ RSpec.describe 'epic boards', :js do
expect
(
page
).
to
have_selector
(
'.board'
,
text:
label2
.
title
)
expect
(
find
(
'.board:nth-child(3) .board-card'
)).
to
have_content
(
epic3
.
title
)
end
it
'moves epic between lists'
do
expect
(
find
(
'.board:nth-child(1)'
)).
to
have_content
(
epic3
.
title
)
drag
(
list_from_index:
0
,
list_to_index:
1
)
wait_for_all_requests
expect
(
find
(
'.board:nth-child(1)'
)).
not_to
have_content
(
epic3
.
title
)
expect
(
find
(
'.board:nth-child(2)'
)).
to
have_content
(
epic3
.
title
)
end
end
context
'when user can admin epic boards'
do
...
...
@@ -113,4 +126,17 @@ RSpec.describe 'epic boards', :js do
def
list_header
(
list
)
find
(
".board[data-id='gid://gitlab/Boards::EpicList/
#{
list
.
id
}
'] .board-header"
)
end
def
drag
(
selector:
'.board-list'
,
list_from_index:
0
,
from_index:
0
,
to_index:
0
,
list_to_index:
0
,
perform_drop:
true
)
# ensure there is enough horizontal space for four lists
resize_window
(
2000
,
800
)
drag_to
(
selector:
selector
,
scrollable:
'#board-app'
,
list_from_index:
list_from_index
,
from_index:
from_index
,
to_index:
to_index
,
list_to_index:
list_to_index
,
perform_drop:
perform_drop
)
end
end
ee/spec/frontend/boards/mock_data.js
View file @
798e516f
...
...
@@ -216,6 +216,7 @@ export const mockEpic = {
closedIssues
:
2
,
},
issues
:
[
mockIssue
],
labels
:
[],
};
export
const
mockIssueWithEpic
=
{
...
mockIssue3
,
epic
:
{
id
:
mockEpic
.
id
,
iid
:
mockEpic
.
iid
}
};
...
...
@@ -238,6 +239,7 @@ export const mockEpics = [
parent
:
{
id
:
'
40
'
,
},
labels
:
[],
},
{
id
:
'
gid://gitlab/Epic/40
'
,
...
...
@@ -252,6 +254,7 @@ export const mockEpics = [
web_url
:
'
/groups/gitlab-org/marketing/-/epics/1
'
,
descendantCounts
:
defaultDescendantCounts
,
hasParent
:
false
,
labels
:
[],
},
{
id
:
'
gid://gitlab/Epic/39
'
,
...
...
@@ -266,6 +269,7 @@ export const mockEpics = [
web_url
:
'
/groups/gitlab-org/-/epics/12
'
,
descendantCounts
:
defaultDescendantCounts
,
hasParent
:
false
,
labels
:
[],
},
{
id
:
'
gid://gitlab/Epic/38
'
,
...
...
@@ -280,6 +284,7 @@ export const mockEpics = [
web_url
:
'
/groups/gitlab-org/-/epics/11
'
,
descendantCounts
:
defaultDescendantCounts
,
hasParent
:
false
,
labels
:
[],
},
{
id
:
'
gid://gitlab/Epic/37
'
,
...
...
@@ -294,6 +299,7 @@ export const mockEpics = [
web_url
:
'
/groups/gitlab-org/-/epics/10
'
,
descendantCounts
:
defaultDescendantCounts
,
hasParent
:
false
,
labels
:
[],
},
];
...
...
ee/spec/frontend/boards/stores/actions_spec.js
View file @
798e516f
...
...
@@ -14,7 +14,15 @@ import { issuableTypes } from '~/boards/constants';
import
*
as
typesCE
from
'
~/boards/stores/mutation_types
'
;
import
*
as
commonUtils
from
'
~/lib/utils/common_utils
'
;
import
{
mergeUrlParams
,
removeParams
}
from
'
~/lib/utils/url_utility
'
;
import
{
mockLists
,
mockIssue
,
mockIssue2
,
mockEpic
,
rawIssue
,
mockMilestones
}
from
'
../mock_data
'
;
import
{
mockLists
,
mockIssue
,
mockIssue2
,
mockEpic
,
mockEpics
,
rawIssue
,
mockMilestones
,
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
...
...
@@ -846,6 +854,21 @@ describe('setActiveIssueWeight', () => {
});
});
describe
.
each
`
isEpicBoard | issuableType | dispatchedAction
${
false
}
|
${
'
issuableTypes.issue
'
}
|
${
'
moveIssue
'
}
${
true
}
|
${
'
issuableTypes.epic
'
}
|
${
'
moveEpic
'
}
`
(
'
moveItem
'
,
({
isEpicBoard
,
issuableType
,
dispatchedAction
})
=>
{
it
(
`should dispatch
${
dispatchedAction
}
action when isEpicBoard is
${
isEpicBoard
}
`
,
async
()
=>
{
await
testAction
({
action
:
actions
.
moveItem
,
payload
:
{
itemId
:
1
},
state
:
{
isEpicBoard
,
issuableType
},
expectedActions
:
[{
type
:
dispatchedAction
,
payload
:
{
itemId
:
1
}
}],
});
});
});
describe
(
'
moveIssue
'
,
()
=>
{
const
epicId
=
'
gid://gitlab/Epic/1
'
;
...
...
@@ -882,9 +905,9 @@ describe('moveIssue', () => {
testAction
(
actions
.
moveIssue
,
{
i
ssue
Id
:
'
436
'
,
i
ssue
Iid
:
mockIssue
.
iid
,
i
ssue
Path
:
mockIssue
.
referencePath
,
i
tem
Id
:
'
436
'
,
i
tem
Iid
:
mockIssue
.
iid
,
i
tem
Path
:
mockIssue
.
referencePath
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
epicId
,
...
...
@@ -923,9 +946,9 @@ describe('moveIssue', () => {
testAction
(
actions
.
moveIssue
,
{
i
ssue
Id
:
'
436
'
,
i
ssue
Iid
:
mockIssue
.
iid
,
i
ssue
Path
:
mockIssue
.
referencePath
,
i
tem
Id
:
'
436
'
,
i
tem
Iid
:
mockIssue
.
iid
,
i
tem
Path
:
mockIssue
.
referencePath
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
epicId
,
...
...
@@ -955,113 +978,217 @@ describe('moveIssue', () => {
done
,
);
});
});
describe
.
each
`
isEpicBoard | issuableType | dispatchedAction
${
false
}
|
${
'
issuableTypes.issue
'
}
|
${
'
createIssueList
'
}
${
true
}
|
${
'
issuableTypes.epic
'
}
|
${
'
createEpicList
'
}
`
(
'
createList
'
,
({
isEpicBoard
,
issuableType
,
dispatchedAction
})
=>
{
it
(
`should dispatch
${
dispatchedAction
}
action when isEpicBoard is
${
isEpicBoard
}
`
,
async
()
=>
{
await
testAction
({
action
:
actions
.
createList
,
payload
:
{
backlog
:
true
},
state
:
{
isEpicBoard
,
issuableType
},
expectedActions
:
[{
type
:
dispatchedAction
,
payload
:
{
backlog
:
true
}
}],
});
});
});
describe
(
'
moveEpic
'
,
()
=>
{
const
listEpics
=
{
'
gid://gitlab/List/1
'
:
[
41
,
40
],
'
gid://gitlab/List/2
'
:
[],
};
const
epics
=
{
41
:
mockEpic
,
40
:
mockEpics
[
1
],
};
describe
(
'
createEpicList
'
,
()
=>
{
let
commit
;
let
dispatch
;
let
getters
;
beforeEach
(()
=>
{
commit
=
jest
.
fn
();
dispatch
=
jest
.
fn
();
getters
=
{
getListByLabelId
:
jest
.
fn
(),
};
const
state
=
{
fullPath
:
'
gitlab-org
'
,
boardId
:
1
,
boardType
:
'
group
'
,
disabled
:
false
,
boardLists
:
mockLists
,
boardItemsByListId
:
listEpics
,
boardItems
:
epics
,
issuableType
:
'
epic
'
,
};
it
(
'
should commit MOVE_EPIC mutation mutation when successful
'
,
(
done
)
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicMoveList
:
{
errors
:
[],
},
},
});
it
(
'
should dispatch addList action when creating backlog list
'
,
async
()
=>
{
const
backlogList
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
backlog
'
,
title
:
'
Open
'
,
position
:
0
,
};
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
:
backlogList
,
errors
:
[],
testAction
(
actions
.
moveEpic
,
{
itemId
:
'
41
'
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
state
,
[
{
type
:
types
.
MOVE_EPIC
,
payload
:
{
originalEpic
:
mockEpic
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
},
});
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
],
[],
done
,
);
});
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
addList
'
,
backlogList
);
it
(
'
should commit MOVE_EPIC mutation and MOVE_EPIC_FAILURE mutation when unsuccessful
'
,
(
done
)
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicMoveList
:
{
errors
:
[{
foo
:
'
bar
'
}],
},
},
});
it
(
'
dispatches highlightList after addList has succeeded
'
,
async
()
=>
{
const
list
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
label
'
,
title
:
'
Open
'
,
labelId
:
'
4
'
,
};
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
,
errors
:
[],
testAction
(
actions
.
moveEpic
,
{
itemId
:
'
41
'
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
state
,
[
{
type
:
types
.
MOVE_EPIC
,
payload
:
{
originalEpic
:
mockEpic
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
},
});
{
type
:
types
.
MOVE_EPIC_FAILURE
,
payload
:
{
originalEpic
:
mockEpic
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
originalIndex
:
0
,
},
},
],
[],
done
,
);
});
});
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
labelId
:
'
4
'
});
describe
.
each
`
isEpicBoard | issuableType | dispatchedAction
${
false
}
|
${
'
issuableTypes.issue
'
}
|
${
'
createIssueList
'
}
${
true
}
|
${
'
issuableTypes.epic
'
}
|
${
'
createEpicList
'
}
`
(
'
createList
'
,
({
isEpicBoard
,
issuableType
,
dispatchedAction
})
=>
{
it
(
`should dispatch
${
dispatchedAction
}
action when isEpicBoard is
${
isEpicBoard
}
`
,
async
()
=>
{
await
testAction
({
action
:
actions
.
createList
,
payload
:
{
backlog
:
true
},
state
:
{
isEpicBoard
,
issuableType
},
expectedActions
:
[{
type
:
dispatchedAction
,
payload
:
{
backlog
:
true
}
}],
});
});
});
describe
(
'
createEpicList
'
,
()
=>
{
let
commit
;
let
dispatch
;
let
getters
;
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
addList
'
,
list
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
highlightList
'
,
list
.
id
);
const
state
=
{
fullPath
:
'
gitlab-org
'
,
boardId
:
1
,
boardType
:
'
group
'
,
disabled
:
false
,
boardLists
:
mockLists
,
};
beforeEach
(()
=>
{
commit
=
jest
.
fn
();
dispatch
=
jest
.
fn
();
getters
=
{
getListByLabelId
:
jest
.
fn
(),
};
});
it
(
'
should dispatch addList action when creating backlog list
'
,
async
()
=>
{
const
backlogList
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
backlog
'
,
title
:
'
Open
'
,
position
:
0
,
};
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
:
backlogList
,
errors
:
[],
},
},
});
it
(
'
should commit CREATE_LIST_FAILURE mutation when API returns an error
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
:
{},
errors
:
[
'
foo
'
],
},
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
addList
'
,
backlogList
);
});
it
(
'
dispatches highlightList after addList has succeeded
'
,
async
()
=>
{
const
list
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
label
'
,
title
:
'
Open
'
,
labelId
:
'
4
'
,
};
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
,
errors
:
[],
},
});
},
});
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
labelId
:
'
4
'
});
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
addList
'
,
list
);
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
highlightList
'
,
list
.
id
);
});
expect
(
commit
).
toHaveBeenCalledWith
(
types
.
CREATE_LIST_FAILURE
,
'
foo
'
);
it
(
'
should commit CREATE_LIST_FAILURE mutation when API returns an error
'
,
async
()
=>
{
jest
.
spyOn
(
gqlClient
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
epicBoardListCreate
:
{
list
:
{},
errors
:
[
'
foo
'
],
},
},
});
it
(
'
highlights list and does not re-query if it already exists
'
,
async
()
=>
{
const
existingList
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
label
'
,
title
:
'
Some label
'
,
position
:
1
,
};
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
getters
=
{
getListByLabelId
:
jest
.
fn
().
mockReturnValue
(
existingList
),
};
expect
(
commit
).
toHaveBeenCalledWith
(
types
.
CREATE_LIST_FAILURE
,
'
foo
'
);
});
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
it
(
'
highlights list and does not re-query if it already exists
'
,
async
()
=>
{
const
existingList
=
{
id
:
'
gid://gitlab/List/1
'
,
listType
:
'
label
'
,
title
:
'
Some label
'
,
position
:
1
,
};
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
highlightList
'
,
existingList
.
id
);
expect
(
dispatch
).
toHaveBeenCalledTimes
(
1
);
expect
(
commit
).
not
.
toHaveBeenCalled
();
});
getters
=
{
getListByLabelId
:
jest
.
fn
().
mockReturnValue
(
existingList
),
};
await
actions
.
createEpicList
({
getters
,
state
,
commit
,
dispatch
},
{
backlog
:
true
});
expect
(
dispatch
).
toHaveBeenCalledWith
(
'
highlightList
'
,
existingList
.
id
);
expect
(
dispatch
).
toHaveBeenCalledTimes
(
1
);
expect
(
commit
).
not
.
toHaveBeenCalled
();
});
});
...
...
ee/spec/frontend/boards/stores/mutations_spec.js
View file @
798e516f
...
...
@@ -327,6 +327,40 @@ describe('MOVE_ISSUE', () => {
});
});
describe
(
'
MOVE_EPIC
'
,
()
=>
{
it
(
'
updates boardItemsByListId, moving epic between lists
'
,
()
=>
{
const
listIssues
=
{
'
gid://gitlab/List/1
'
:
[
mockEpic
.
id
,
mockEpics
[
1
].
id
],
'
gid://gitlab/List/2
'
:
[],
};
const
epics
=
{
1
:
mockEpic
,
2
:
mockEpics
[
1
],
};
state
=
{
...
state
,
boardItemsByListId
:
listIssues
,
boardLists
:
initialBoardListsState
,
boardItems
:
epics
,
};
mutations
.
MOVE_EPIC
(
state
,
{
originalEpic
:
mockEpics
[
1
],
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
});
const
updatedListEpics
=
{
'
gid://gitlab/List/1
'
:
[
mockEpic
.
id
],
'
gid://gitlab/List/2
'
:
[
mockEpics
[
1
].
id
],
};
expect
(
state
.
boardItemsByListId
).
toEqual
(
updatedListEpics
);
});
});
describe
(
'
SET_BOARD_EPIC_USER_PREFERENCES
'
,
()
=>
{
it
(
'
should replace userPreferences on the given epic
'
,
()
=>
{
state
=
{
...
...
locale/gitlab.pot
View file @
798e516f
...
...
@@ -4850,6 +4850,9 @@ msgstr ""
msgid "Boards|An error occurred while generating lists. Please reload the page."
msgstr ""
msgid "Boards|An error occurred while moving the epic. Please try again."
msgstr ""
msgid "Boards|An error occurred while moving the issue. Please try again."
msgstr ""
...
...
spec/frontend/boards/board_list_spec.js
View file @
798e516f
...
...
@@ -125,7 +125,7 @@ describe('Board list component', () => {
});
it
(
'
sets data attribute with issue id
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.board-card
'
).
attributes
(
'
data-i
ssue
-id
'
)).
toBe
(
'
1
'
);
expect
(
wrapper
.
find
(
'
.board-card
'
).
attributes
(
'
data-i
tem
-id
'
)).
toBe
(
'
1
'
);
});
it
(
'
shows new issue form
'
,
async
()
=>
{
...
...
@@ -258,7 +258,7 @@ describe('Board list component', () => {
describe
(
'
handleDragOnEnd
'
,
()
=>
{
it
(
'
removes class `is-dragging` from document body
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
moveI
ssue
'
).
mockImplementation
(()
=>
{});
jest
.
spyOn
(
wrapper
.
vm
,
'
moveI
tem
'
).
mockImplementation
(()
=>
{});
document
.
body
.
classList
.
add
(
'
is-dragging
'
);
findByTestId
(
'
tree-root-wrapper
'
).
vm
.
$emit
(
'
end
'
,
{
...
...
@@ -266,9 +266,9 @@ describe('Board list component', () => {
newIndex
:
0
,
item
:
{
dataset
:
{
i
ssue
Id
:
mockIssues
[
0
].
id
,
i
ssue
Iid
:
mockIssues
[
0
].
iid
,
i
ssue
Path
:
mockIssues
[
0
].
referencePath
,
i
tem
Id
:
mockIssues
[
0
].
id
,
i
tem
Iid
:
mockIssues
[
0
].
iid
,
i
tem
Path
:
mockIssues
[
0
].
referencePath
,
},
},
to
:
{
children
:
[],
dataset
:
{
listId
:
'
gid://gitlab/List/1
'
}
},
...
...
spec/frontend/boards/components/board_card_spec.js
View file @
798e516f
...
...
@@ -6,7 +6,7 @@ import BoardCardInner from '~/boards/components/board_card_inner.vue';
import
{
inactiveId
}
from
'
~/boards/constants
'
;
import
{
mockLabelList
,
mockIssue
}
from
'
../mock_data
'
;
describe
(
'
Board card
layout
'
,
()
=>
{
describe
(
'
Board card
'
,
()
=>
{
let
wrapper
;
let
store
;
let
mockActions
;
...
...
@@ -44,7 +44,7 @@ describe('Board card layout', () => {
store
,
propsData
:
{
list
:
mockLabelList
,
i
ssue
:
mockIssue
,
i
tem
:
mockIssue
,
disabled
:
false
,
index
:
0
,
...
propsData
,
...
...
@@ -113,7 +113,7 @@ describe('Board card layout', () => {
expect
(
wrapper
.
classes
()).
not
.
toContain
(
'
is-active
'
);
});
describe
(
'
when mouseup event is called on the
issue
card
'
,
()
=>
{
describe
(
'
when mouseup event is called on the card
'
,
()
=>
{
beforeEach
(()
=>
{
createStore
({
isSwimlanesOn
});
mountComponent
();
...
...
spec/frontend/boards/stores/actions_spec.js
View file @
798e516f
...
...
@@ -637,6 +637,15 @@ describe('resetIssues', () => {
});
});
describe
(
'
moveItem
'
,
()
=>
{
it
(
'
should dispatch moveIssue action
'
,
()
=>
{
testAction
({
action
:
actions
.
moveItem
,
expectedActions
:
[{
type
:
'
moveIssue
'
}],
});
});
});
describe
(
'
moveIssue
'
,
()
=>
{
const
listIssues
=
{
'
gid://gitlab/List/1
'
:
[
436
,
437
],
...
...
@@ -671,9 +680,9 @@ describe('moveIssue', () => {
testAction
(
actions
.
moveIssue
,
{
i
ssue
Id
:
'
436
'
,
i
ssue
Iid
:
mockIssue
.
iid
,
i
ssue
Path
:
mockIssue
.
referencePath
,
i
tem
Id
:
'
436
'
,
i
tem
Iid
:
mockIssue
.
iid
,
i
tem
Path
:
mockIssue
.
referencePath
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
...
...
@@ -722,9 +731,9 @@ describe('moveIssue', () => {
actions
.
moveIssue
(
{
state
,
commit
:
()
=>
{}
},
{
i
ssue
Id
:
mockIssue
.
id
,
i
ssue
Iid
:
mockIssue
.
iid
,
i
ssue
Path
:
mockIssue
.
referencePath
,
i
tem
Id
:
mockIssue
.
id
,
i
tem
Iid
:
mockIssue
.
iid
,
i
tem
Path
:
mockIssue
.
referencePath
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
...
...
@@ -746,9 +755,9 @@ describe('moveIssue', () => {
testAction
(
actions
.
moveIssue
,
{
i
ssue
Id
:
'
436
'
,
i
ssue
Iid
:
mockIssue
.
iid
,
i
ssue
Path
:
mockIssue
.
referencePath
,
i
tem
Id
:
'
436
'
,
i
tem
Iid
:
mockIssue
.
iid
,
i
tem
Path
:
mockIssue
.
referencePath
,
fromListId
:
'
gid://gitlab/List/1
'
,
toListId
:
'
gid://gitlab/List/2
'
,
},
...
...
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