Commit 2114a94f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '33039-allow-changing-item-parent-epic-using-drag-and-drop-interaction' into 'master'

Epic tree move child with drag and drop

See merge request gitlab-org/gitlab!28629
parents 72788117 d3d903e3
......@@ -3361,7 +3361,7 @@ input EpicTreeNodeFieldsInputType {
"""
The id of the epic_issue or issue that the actual epic or issue is switched with
"""
adjacentReferenceId: ID!
adjacentReferenceId: ID
"""
The id of the epic_issue or epic that is being moved
......@@ -3376,7 +3376,7 @@ input EpicTreeNodeFieldsInputType {
"""
The type of the switch, after or before allowed
"""
relativePosition: MoveType!
relativePosition: MoveType
}
"""
......
......@@ -9668,13 +9668,9 @@
"name": "adjacentReferenceId",
"description": "The id of the epic_issue or issue that the actual epic or issue is switched with",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
......@@ -9682,13 +9678,9 @@
"name": "relativePosition",
"description": "The type of the switch, after or before allowed",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "MoveType",
"ofType": null
}
"kind": "ENUM",
"name": "MoveType",
"ofType": null
},
"defaultValue": null
},
......
......@@ -203,7 +203,9 @@ have a [start or due date](#start-date-and-due-date), a
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9367) in GitLab 12.5.
New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
New issues and child epics are added to the top of their respective lists in the **Epics and Issues**
tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot
be intermingled.
To reorder issues assigned to an epic:
......@@ -215,6 +217,24 @@ To reorder child epics assigned to an epic:
1. Go to the **Epics and Issues** tab.
1. Drag and drop epics into the desired order.
## Moving issues and child epics between epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in GitLab 13.0.
New issues and child epics are added to the top of their respective lists in the **Epics and Issues**
tab. You can move issues and child epics from one epic to another. Issues and child epics cannot
be intermingled.
To move an issue to another epic:
1. Go to the **Epics and Issues** tab.
1. Drag and drop issues into the desired parent epic.
To move child epics to another epic:
1. Go to the **Epics and Issues** tab.
1. Drag and drop epics into the desired parent epic.
## Updating epics
### Using bulk editing
......
......@@ -48,9 +48,7 @@ export default {
return this.childrenFlags[this.itemReference].itemExpanded ? __('Collapse') : __('Expand');
},
childrenFetchInProgress() {
return (
this.hasChildren && !this.childrenFlags[this.itemReference].itemChildrenFetchInProgress
);
return this.hasChildren && this.childrenFlags[this.itemReference].itemChildrenFetchInProgress;
},
itemExpanded() {
return this.hasChildren && this.childrenFlags[this.itemReference].itemExpanded;
......@@ -62,6 +60,9 @@ export default {
!this.childrenFlags[this.itemReference].itemChildrenFetchInProgress
);
},
showEpicDropzone() {
return !this.hasChildren && this.item.type === ChildType.Epic;
},
},
methods: {
...mapActions(['toggleItem']),
......@@ -87,7 +88,7 @@ export default {
>
<div class="list-item-body d-flex align-items-center">
<gl-deprecated-button
v-if="childrenFetchInProgress"
v-if="!childrenFetchInProgress && hasChildren"
v-gl-tooltip.hover
:title="chevronTooltip"
:class="chevronType"
......@@ -97,11 +98,7 @@ export default {
>
<icon :name="chevronType" />
</gl-deprecated-button>
<gl-loading-icon
v-if="childrenFlags[itemReference].itemChildrenFetchInProgress"
class="loading-icon"
size="sm"
/>
<gl-loading-icon v-if="childrenFetchInProgress" class="loading-icon" size="sm" />
<tree-item-body
class="tree-item-row"
:parent-item="parentItem"
......@@ -112,9 +109,9 @@ export default {
/>
</div>
<tree-root
v-if="itemExpanded"
v-if="itemExpanded || showEpicDropzone"
:parent-item="item"
:children="children[itemReference]"
:children="children[itemReference] || []"
class="sub-tree-root"
/>
</li>
......
......@@ -4,6 +4,8 @@ import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import TreeDragAndDropMixin from '../mixins/tree_dd_mixin';
import { ChildType } from '../constants';
export default {
components: {
GlDeprecatedButton,
......@@ -34,7 +36,7 @@ export default {
},
},
methods: {
...mapActions(['fetchNextPageItems', 'reorderItem']),
...mapActions(['fetchNextPageItems', 'reorderItem', 'moveItem', 'toggleItem']),
handleShowMoreClick() {
this.fetchInProgress = true;
this.fetchNextPageItems({
......@@ -47,6 +49,14 @@ export default {
this.fetchInProgress = false;
});
},
onMove(e) {
const item = e.relatedContext.element;
if (item?.type === ChildType.Epic)
this.toggleItem({
parentItem: item,
isDragging: true,
});
},
},
};
</script>
......@@ -56,6 +66,7 @@ export default {
:is="treeRootWrapper"
v-bind="treeRootOptions"
class="list-unstyled related-items-list tree-root"
:move="onMove"
@start="handleDragOnStart"
@end="handleDragOnEnd"
>
......
......@@ -12,10 +12,11 @@ export default {
const options = {
...defaultSortableConfig,
fallbackOnBody: false,
group: this.parentItem.reference,
group: 'sortable-container',
tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-reference': this.parentItem.reference,
'data-parent-id': this.parentItem.id,
value: this.children,
// This filters out/ignores all the chevron buttons (used for
// expanding and collapsing epic tree items) so the drag action
......@@ -104,22 +105,33 @@ export default {
*
* @param {object} event Object representing drag end event.
*/
handleDragOnEnd({ oldIndex, newIndex }) {
handleDragOnEnd(params) {
const { oldIndex, newIndex, from, to } = params;
document.body.classList.remove('is-dragging');
// If both old and new index of target are same,
// nothing was moved, we do an early return.
if (oldIndex === newIndex) return;
const targetItem = this.children[oldIndex];
this.reorderItem({
treeReorderMutation: this.getTreeReorderMutation({ oldIndex, newIndex, targetItem }),
parentItem: this.parentItem,
targetItem,
oldIndex,
newIndex,
});
if (from === to) {
// If both old and new index of target are same,
// nothing was moved, we do an early return.
if (oldIndex === newIndex) return;
this.reorderItem({
treeReorderMutation: this.getTreeReorderMutation({ oldIndex, newIndex, targetItem }),
parentItem: this.parentItem,
targetItem,
oldIndex,
newIndex,
});
} else {
this.moveItem({
oldParentItem: this.parentItem,
newParentItem: to.dataset,
targetItem,
oldIndex,
newIndex,
});
}
},
},
};
......@@ -12,7 +12,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { processQueryResponse, formatChildItem, gqClient } from '../utils/epic_utils';
import { ChildType, ChildState } from '../constants';
import { ChildType, ChildState, idProp, relativePositions } from '../constants';
import epicChildren from '../queries/epicChildren.query.graphql';
import epicChildReorder from '../queries/epicChildReorder.mutation.graphql';
......@@ -197,7 +197,7 @@ export const fetchNextPageItems = ({ dispatch, state }, { parentItem, isSubItem
});
};
export const toggleItem = ({ state, dispatch }, { parentItem }) => {
export const toggleItem = ({ state, dispatch }, { parentItem, isDragging = false }) => {
if (!state.childrenFlags[parentItem.reference].itemExpanded) {
if (!state.children[parentItem.reference]) {
dispatch('fetchItems', {
......@@ -209,7 +209,7 @@ export const toggleItem = ({ state, dispatch }, { parentItem }) => {
parentItem,
});
}
} else {
} else if (!isDragging) {
dispatch('collapseItem', {
parentItem,
});
......@@ -454,6 +454,84 @@ export const reorderItem = (
});
};
export const receiveMoveItemFailure = ({ commit }, data) => {
commit(types.MOVE_ITEM_FAILURE, data);
flash(s__('Epics|Something went wrong while moving item.'));
};
export const moveItem = (
{ dispatch, commit, state },
{ oldParentItem, newParentItem, targetItem, oldIndex, newIndex },
) => {
let adjacentItem;
let adjacentReferenceId;
let relativePosition = relativePositions.After;
let isFirstChild = false;
const newParentChildren = state.children[newParentItem.parentReference];
if (newParentChildren?.length > 0) {
adjacentItem = newParentChildren[newIndex];
if (!adjacentItem) {
adjacentItem = newParentChildren[newParentChildren.length - 1];
relativePosition = relativePositions.Before;
}
adjacentReferenceId = adjacentItem[idProp[adjacentItem.type]];
} else {
isFirstChild = true;
relativePosition = relativePositions.Before;
}
commit(types.MOVE_ITEM, {
oldParentItem,
newParentItem,
targetItem,
oldIndex,
newIndex,
isFirstChild,
});
return gqClient
.mutate({
mutation: epicChildReorder,
variables: {
epicTreeReorderInput: {
baseEpicId: oldParentItem.id,
moved: {
id: targetItem[idProp[targetItem.type]],
adjacentReferenceId,
relativePosition,
newParentId: newParentItem.parentId,
},
},
},
})
.then(({ data }) => {
// Mutation was unsuccessful;
// revert to original order and show flash error
if (data.epicTreeReorder.errors.length) {
dispatch('receiveMoveItemFailure', {
oldParentItem,
newParentItem,
targetItem,
newIndex,
oldIndex,
});
}
})
.catch(() => {
// Mutation was unsuccessful;
// revert to original order and show flash error
dispatch('receiveMoveItemFailure', {
oldParentItem,
newParentItem,
targetItem,
newIndex,
oldIndex,
});
});
};
export const receiveCreateIssueSuccess = ({ commit }) =>
commit(types.RECEIVE_CREATE_ITEM_SUCCESS, { insertAt: 0, items: [] });
export const receiveCreateIssueFailure = ({ commit }) => {
......
......@@ -41,6 +41,9 @@ export const RECEIVE_CREATE_ITEM_FAILURE = 'RECEIVE_CREATE_ITEM_FAILURE';
export const REORDER_ITEM = 'REORDER_ITEM';
export const MOVE_ITEM = 'MOVE_ITEM';
export const MOVE_ITEM_FAILURE = 'MOVE_ITEM_FAILURE';
export const SET_PROJECTS = 'SET_PROJECTS';
export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
export const RECIEVE_PROJECTS_SUCCESS = 'RECIEVE_PROJECTS_SUCCESS';
......
......@@ -214,6 +214,38 @@ export default {
state.children[parentItem.reference].splice(newIndex, 0, targetItem);
},
[types.MOVE_ITEM](
state,
{ oldParentItem, newParentItem, targetItem, oldIndex, newIndex, isFirstChild },
) {
// Remove from old position in previous parent
state.children[oldParentItem.reference].splice(oldIndex, 1);
if (state.children[oldParentItem.reference].length === 0) {
state.childrenFlags[oldParentItem.reference].itemHasChildren = false;
}
// Insert at new position in new parent
if (isFirstChild) {
Vue.set(state.children, newParentItem.parentReference, [targetItem]);
Vue.set(state.childrenFlags, newParentItem.parentReference, {
itemExpanded: true,
itemHasChildren: true,
});
} else {
state.children[newParentItem.parentReference].splice(newIndex, 0, targetItem);
}
},
[types.MOVE_ITEM_FAILURE](
state,
{ oldParentItem, newParentItem, targetItem, oldIndex, newIndex },
) {
// Remove from new position in new parent
state.children[newParentItem.parentReference].splice(newIndex, 1);
// Insert at old position in old parent
state.children[oldParentItem.reference].splice(oldIndex, 0, targetItem);
},
[types.REQUEST_PROJECTS](state) {
state.projectsFetchInProgress = true;
},
......
......@@ -14,12 +14,12 @@ module Types
argument :adjacent_reference_id,
GraphQL::ID_TYPE,
required: true,
required: false,
description: 'The id of the epic_issue or issue that the actual epic or issue is switched with'
argument :relative_position,
MoveTypeEnum,
required: true,
required: false,
description: 'The type of the switch, after or before allowed'
argument :new_parent_id,
......
......@@ -42,7 +42,12 @@ module Epics
end
def move!
moving_object.move_between(before_object, after_object)
if adjacent_reference
moving_object.move_between(before_object, after_object)
else
moving_object.move_to_start
end
moving_object.save!(touch: false)
end
......@@ -59,19 +64,26 @@ module Epics
end
def validate_objects
return 'Relative position is not valid.' unless valid_relative_position?
return 'Only epics and epic_issues are supported.' unless supported_types?
return 'You don\'t have permissions to move the objects.' unless authorized?
unless supported_type?(moving_object) && supported_type?(adjacent_reference)
return 'Only epics and epic_issues are supported.'
end
validate_adjacent_reference if adjacent_reference
end
return 'You don\'t have permissions to move the objects.' unless authorized?
def validate_adjacent_reference
return 'Relative position is not valid.' unless valid_relative_position?
if different_epic_parent?
return "The sibling object's parent must match the #{new_parent ? "new" : "current"} parent epic."
end
end
def supported_types?
return false if adjacent_reference && !supported_type?(adjacent_reference)
supported_type?(moving_object)
end
def valid_relative_position?
%w(before after).include?(params[:relative_position])
end
......@@ -90,7 +102,10 @@ module Epics
def authorized?
return false unless can?(current_user, :admin_epic, base_epic.group)
return false unless can?(current_user, :admin_epic, adjacent_reference_group)
if adjacent_reference
return false unless can?(current_user, :admin_epic, adjacent_reference_group)
end
if new_parent
return false unless can?(current_user, :admin_epic, new_parent.group)
......@@ -116,6 +131,8 @@ module Epics
end
def adjacent_reference
return unless params[:adjacent_reference_id]
@adjacent_reference ||= find_object(params[:adjacent_reference_id])&.sync
end
......
---
title: Epic tree move child with drag and drop
merge_request: 28629
author:
type: added
......@@ -114,10 +114,11 @@ describe('RelatedItemsTree', () => {
fallbackClass: 'is-dragging',
fallbackOnBody: false,
ghostClass: 'is-ghost',
group: mockParentItem.reference,
group: 'sortable-container',
tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-reference': mockParentItem.reference,
'data-parent-id': mockParentItem.id,
value: wrapper.vm.children,
filter: `.${treeItemChevronBtnClassName}`,
}),
......@@ -270,34 +271,63 @@ describe('RelatedItemsTree', () => {
expect(document.body.classList.contains('is-dragging')).toBe(false);
});
it('does not call `reorderItem` action when newIndex is same as oldIndex', () => {
jest.spyOn(wrapper.vm, 'reorderItem').mockImplementation(() => {});
describe('origin parent is destination parent', () => {
it('does not call `reorderItem` action when newIndex is same as oldIndex', () => {
jest.spyOn(wrapper.vm, 'reorderItem').mockImplementation(() => {});
wrapper.vm.handleDragOnEnd({
oldIndex: 0,
newIndex: 0,
});
wrapper.vm.handleDragOnEnd({
oldIndex: 0,
newIndex: 0,
from: wrapper.element,
to: wrapper.element,
});
expect(wrapper.vm.reorderItem).not.toHaveBeenCalled();
});
expect(wrapper.vm.reorderItem).not.toHaveBeenCalled();
});
it('calls `reorderItem` action when newIndex is different from oldIndex', () => {
jest.spyOn(wrapper.vm, 'reorderItem').mockImplementation(() => {});
it('calls `reorderItem` action when newIndex is different from oldIndex', () => {
jest.spyOn(wrapper.vm, 'reorderItem').mockImplementation(() => {});
wrapper.vm.handleDragOnEnd({
oldIndex: 1,
newIndex: 0,
wrapper.vm.handleDragOnEnd({
oldIndex: 1,
newIndex: 0,
from: wrapper.element,
to: wrapper.element,
});
expect(wrapper.vm.reorderItem).toHaveBeenCalledWith(
expect.objectContaining({
treeReorderMutation: expect.any(Object),
parentItem: wrapper.vm.parentItem,
targetItem: wrapper.vm.children[1],
oldIndex: 1,
newIndex: 0,
}),
);
});
});
expect(wrapper.vm.reorderItem).toHaveBeenCalledWith(
expect.objectContaining({
treeReorderMutation: expect.any(Object),
parentItem: wrapper.vm.parentItem,
targetItem: wrapper.vm.children[1],
describe('origin parent is different than destination parent', () => {
it('calls `moveItem`', () => {
jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {});
wrapper.vm.handleDragOnEnd({
oldIndex: 1,
newIndex: 0,
}),
);
from: wrapper.element,
to: wrapper.find('li:first-child .sub-tree-root'),
});
expect(wrapper.vm.moveItem).toHaveBeenCalledWith(
expect.objectContaining({
oldParentItem: wrapper.vm.parentItem,
newParentItem: wrapper.find('li:first-child .sub-tree-root').dataset,
targetItem: wrapper.vm.children[1],
oldIndex: 1,
newIndex: 0,
}),
);
});
});
});
});
......@@ -346,6 +376,32 @@ describe('RelatedItemsTree', () => {
);
});
});
describe('onMove', () => {
it('calls toggleItem action if move event finds epic element', () => {
jest.spyOn(wrapper.vm, 'toggleItem').mockImplementation(() => {});
const evt = {
relatedContext: {
element: mockParentItem,
},
};
wrapper.vm.onMove(evt);
expect(wrapper.vm.toggleItem).toHaveBeenCalled();
});
it(' does not call toggleItem action if move event does not find epic element', () => {
jest.spyOn(wrapper.vm, 'toggleItem').mockImplementation(() => {});
const evt = {
relatedContext: {
element: mockIssue2,
},
};
wrapper.vm.onMove(evt);
expect(wrapper.vm.toggleItem).not.toHaveBeenCalled();
});
});
});
describe('template', () => {
......
......@@ -16,6 +16,31 @@ export const mockParentItem = {
fullPath: 'gitlab-org',
title: 'Some sample epic',
reference: 'gitlab-org&1',
type: 'Epic',
userPermissions: {
adminEpic: true,
createEpic: true,
},
descendantCounts: {
openedEpics: 1,
closedEpics: 1,
openedIssues: 1,
closedIssues: 1,
},
healthStatus: {
issuesOnTrack: 1,
issuesAtRisk: 0,
issuesNeedingAttention: 1,
},
};
export const mockParentItem2 = {
id: 'gid://gitlab/Epic/43',
iid: 2,
fullPath: 'gitlab-org',
title: 'Some sample epic 2',
reference: 'gitlab-org&2',
parentReference: 'gitlab-org&2',
userPermissions: {
adminEpic: true,
createEpic: true,
......
......@@ -19,6 +19,7 @@ import { TEST_HOST } from 'spec/test_constants';
import {
mockInitialConfig,
mockParentItem,
mockParentItem2,
mockQueryResponse,
mockEpicTreeReorderInput,
mockReorderMutationResponse,
......@@ -1265,6 +1266,191 @@ describe('RelatedItemTree', () => {
});
});
describe('receiveMoveItemFailure', () => {
it('should revert moved item back to its original position on its original parent via MOVE_ITEM_FAILURE mutation', () => {
testAction(
actions.receiveMoveItemFailure,
{},
{},
[{ type: types.MOVE_ITEM_FAILURE, payload: {} }],
[],
);
});
it('should show flash error with message "Something went wrong while ordering item."', () => {
const message = 'Something went wrong while moving item.';
actions.receiveMoveItemFailure(
{
commit: () => {},
},
{
message,
},
);
expect(createFlash).toHaveBeenCalledWith(message);
});
});
describe('moveItem', () => {
beforeAll(() => {
state.children[mockParentItem2.parentReference] = [];
});
it('should perform MOVE_ITEM mutation with isFirstChild to true if parent has no children before request and do nothing on request success', () => {
jest.spyOn(epicUtils.gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: mockReorderMutationResponse,
}),
);
testAction(
actions.moveItem,
{
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
},
state,
[
{
type: types.MOVE_ITEM,
payload: {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
isFirstChild: true,
},
},
],
[],
);
});
it('should perform MOVE_ITEM mutation with isFirstChild to false if parent has children before request and do nothing on request success', () => {
jest.spyOn(epicUtils.gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: mockReorderMutationResponse,
}),
);
state.children[mockParentItem2.parentReference] = [{ id: '33' }];
testAction(
actions.moveItem,
{
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
},
state,
[
{
type: types.MOVE_ITEM,
payload: {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
isFirstChild: false,
},
},
],
[],
);
});
it('should perform MOVE_ITEM mutation before request and dispatch `receiveReorderItemFailure` when request response has errors on request success', () => {
jest.spyOn(epicUtils.gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
epicTreeReorder: {
...mockReorderMutationResponse.epicTreeReorder,
errors: [{ foo: 'bar' }],
},
},
}),
);
const payload = {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
};
testAction(
actions.moveItem,
payload,
state,
[
{
type: types.MOVE_ITEM,
payload: {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
isFirstChild: true,
},
},
],
[
{
type: 'receiveMoveItemFailure',
payload,
},
],
);
});
it('should perform MOVE_ITEM mutation before request and dispatch `receiveReorderItemFailure` on request failure', () => {
jest.spyOn(epicUtils.gqClient, 'mutate').mockReturnValue(Promise.reject());
const payload = {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
};
testAction(
actions.moveItem,
payload,
state,
[
{
type: types.MOVE_ITEM,
payload: {
oldParentItem: mockParentItem,
newParentItem: mockParentItem2,
targetItem: mockItems[1],
newIndex: 1,
oldIndex: 0,
isFirstChild: true,
},
},
],
[
{
type: 'receiveMoveItemFailure',
payload,
},
],
);
});
});
describe('receiveCreateIssueSuccess', () => {
it('should set `state.itemCreateInProgress` & `state.itemsFetchResultEmpty` to false', () => {
testAction(
......
......@@ -545,7 +545,7 @@ describe('RelatedItemsTree', () => {
});
describe(types.REORDER_ITEM, () => {
it('should reorder an item within children of provided parent based on provided indices', () => {
it('should reorder an item within children of provided parent based on provided indexes', () => {
state.parentItem = { reference: '&1' };
state.children[state.parentItem.reference] = ['foo', 'bar'];
......@@ -564,6 +564,112 @@ describe('RelatedItemsTree', () => {
});
});
describe(types.MOVE_ITEM, () => {
const defaultPayload = {
oldParentItem: {
reference: '&1',
},
targetItem: 'bar',
oldIndex: 1,
newIndex: 0,
isFirstChild: false,
};
it('should move an item from one parent to another with children based on provided indexes', () => {
const newParentItem = {
parentReference: '&2',
};
state.parentItem = { reference: '&1' };
state.children[state.parentItem.reference] = ['foo', 'bar'];
state.children[newParentItem.parentReference] = ['baz'];
mutations[types.MOVE_ITEM](state, {
...defaultPayload,
newParentItem,
});
expect(state.children[state.parentItem.reference]).toEqual(
expect.arrayContaining(['foo']),
);
expect(state.children[newParentItem.parentReference]).toEqual(
expect.arrayContaining(['bar', 'baz']),
);
});
it('should move an item from one parent to another without children based on provided indexes', () => {
const newParentItem = {
parentReference: '&2',
};
state.parentItem = { reference: '&1' };
state.children[state.parentItem.reference] = ['foo', 'bar'];
mutations[types.MOVE_ITEM](state, {
...defaultPayload,
newParentItem,
isFirstChild: true,
});
expect(state.children[state.parentItem.reference]).toEqual(
expect.arrayContaining(['foo']),
);
expect(state.children[newParentItem.parentReference]).toEqual(
expect.arrayContaining(['bar']),
);
});
it('should update itemHasChildren flags', () => {
const newParentItem = {
parentReference: '&2',
};
state.parentItem = { reference: '&1' };
state.children[state.parentItem.reference] = ['bar'];
state.childrenFlags[state.parentItem.reference] = { itemHasChildren: true };
mutations[types.MOVE_ITEM](state, {
...defaultPayload,
newParentItem,
oldIndex: 0,
isFirstChild: true,
});
expect(state.children[state.parentItem.reference].length).toEqual(0);
expect(state.childrenFlags[state.parentItem.reference].itemHasChildren).toEqual(false);
expect(state.children[newParentItem.parentReference]).toEqual(
expect.arrayContaining(['bar']),
);
expect(state.childrenFlags[newParentItem.parentReference].itemHasChildren).toEqual(true);
});
});
describe(types.MOVE_ITEM_FAILURE, () => {
it('should move an item from one parent to another with children based on provided indexes', () => {
const newParentItem = {
parentReference: '&2',
};
state.parentItem = { reference: '&1' };
state.children[state.parentItem.reference] = ['foo'];
state.children[newParentItem.parentReference] = ['bar', 'baz'];
mutations[types.MOVE_ITEM_FAILURE](state, {
oldParentItem: {
reference: '&1',
},
newParentItem,
targetItem: 'bar',
oldIndex: 1,
newIndex: 0,
isFirstChild: false,
});
expect(state.children[state.parentItem.reference]).toEqual(
expect.arrayContaining(['foo', 'bar']),
);
expect(state.children[newParentItem.parentReference]).toEqual(
expect.arrayContaining(['baz']),
);
});
});
describe(types.REQUEST_PROJECTS, () => {
it('should set `projectsFetchInProgress` to true within state', () => {
mutations[types.REQUEST_PROJECTS](state);
......
......@@ -93,9 +93,14 @@ describe Epics::TreeReorderService do
context 'when no object to switch is provided' do
let(:adjacent_reference_id) { nil }
let(:new_parent_id) { GitlabSchema.id_from_object(epic) }
before do
tree_object_2.update(epic: epic1)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
it 'updates the parent' do
expect { subject }.to change { tree_object_2.reload.epic }.from(epic1).to(epic)
end
end
......
......@@ -8341,6 +8341,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
msgid "Epics|Something went wrong while moving item."
msgstr ""
msgid "Epics|Something went wrong while ordering item."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment