Commit 67ac24fb authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '218164-drag-and-drop-between-lanes' into 'master'

Swimlanes - Move issue between epics

See merge request gitlab-org/gitlab!43803
parents 68ae71ff 274083a5
......@@ -10,11 +10,19 @@ const notImplemented = () => {
throw new Error('Not implemented!');
};
const removeIssueFromList = (state, listId, issueId) => {
const getListById = ({ state, listId }) => {
const listIndex = state.boardLists.findIndex(l => l.id === listId);
const list = state.boardLists[listIndex];
return { listIndex, list };
};
export const removeIssueFromList = ({ state, listId, issueId }) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
const { listIndex, list } = getListById({ state, listId });
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize - 1 });
};
const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
const listIssues = state.issuesByListId[listId];
let newIndex = atIndex || 0;
if (moveBeforeId) {
......@@ -24,6 +32,8 @@ const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atI
}
listIssues.splice(newIndex, 0, issueId);
Vue.set(state.issuesByListId, listId, listIssues);
const { listIndex, list } = getListById({ state, listId });
Vue.set(state.boardLists, listIndex, { ...list, issuesSize: list.issuesSize + 1 });
};
export default {
......@@ -142,7 +152,7 @@ export default {
const issue = moveIssueListHelper(originalIssue, fromList, toList);
Vue.set(state.issues, issue.id, issue);
removeIssueFromList(state, fromListId, issue.id);
removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
},
......@@ -157,7 +167,7 @@ export default {
) => {
state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
Vue.set(state.issues, originalIssue.id, originalIssue);
removeIssueFromList(state, toListId, originalIssue.id);
removeIssueFromList({ state, listId: toListId, issueId: originalIssue.id });
addIssueToList({
state,
listId: fromListId,
......@@ -187,7 +197,7 @@ export default {
[mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
removeIssueFromList(state, list.id, issue.id);
removeIssueFromList({ state, listId: list.id, issueId: issue.id });
},
[mutationTypes.SET_CURRENT_PAGE]: () => {
......
......@@ -143,7 +143,7 @@ export default function simulateDrag(options) {
const dragInterval = setInterval(() => {
const progress = (new Date().getTime() - startTime) / duration;
const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy + options.extraHeight) * progress;
const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
simulateEvent(overEl, 'pointermove', {
......
......@@ -146,7 +146,7 @@ export default {
<gl-loading-icon v-if="isLoading" class="gl-p-2" />
</div>
</div>
<div v-if="isExpanded" class="gl-display-flex">
<div v-if="isExpanded" class="gl-display-flex" data-testid="board-epic-lane-issues">
<issues-lane-list
v-for="list in lists"
:key="`${list.id}-issues`"
......
......@@ -38,6 +38,11 @@ export default {
required: false,
default: false,
},
epicId: {
type: String,
required: false,
default: null,
},
},
data() {
return {
......@@ -45,9 +50,9 @@ export default {
};
},
computed: {
...mapState(['activeId', 'filterParams']),
...mapState(['activeId', 'filterParams', 'canAdminEpic']),
treeRootWrapper() {
return this.canAdminList ? Draggable : 'ul';
return this.canAdminList && this.canAdminEpic ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
......@@ -56,6 +61,7 @@ export default {
group: 'board-epics-swimlanes',
tag: 'ul',
'ghost-class': 'board-card-drag-active',
'data-epic-id': this.epicId,
'data-list-id': this.list.id,
value: this.issues,
};
......@@ -81,7 +87,7 @@ export default {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
},
methods: {
...mapActions(['setActiveId', 'moveIssue', 'fetchIssuesForList']),
...mapActions(['setActiveId', 'moveIssue', 'moveIssueEpic', 'fetchIssuesForList']),
toggleForm() {
this.showIssueForm = !this.showIssueForm;
if (this.showIssueForm && this.isUnassignedIssuesLane) {
......@@ -103,10 +109,10 @@ export default {
// If issue is being moved within the same list
if (from === to) {
if (newIndex > oldIndex) {
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.issueId);
} else if (newIndex < oldIndex) {
} 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.issueId);
} else {
......@@ -132,6 +138,7 @@ export default {
toListId: to.dataset.listId,
moveBeforeId,
moveAfterId,
epicId: from.dataset.epicId !== to.dataset.epicId ? to.dataset.epicId || null : undefined,
});
},
},
......@@ -152,7 +159,7 @@ export default {
:is="treeRootWrapper"
v-if="list.isExpanded"
v-bind="treeRootOptions"
class="gl-p-2 gl-m-0"
class="board-cell gl-p-2 gl-m-0 gl-h-full"
@end="handleDragOnEnd"
>
<board-card-layout
......@@ -162,6 +169,7 @@ export default {
:index="index"
:list="list"
:issue="issue"
:disabled="disabled || !canAdminEpic"
:is-active="isActiveIssue(issue)"
@show="showIssue(issue)"
/>
......
......@@ -58,7 +58,7 @@ export default {
const epic = await this.setActiveIssueEpic(input);
if (epic && !this.getEpicById(epic.id)) {
this.receiveEpicsSuccess([epic, ...this.epics]);
this.receiveEpicsSuccess({ epics: [epic, ...this.epics] });
}
debounceByAnimationFrame(() => {
......
......@@ -7,4 +7,7 @@ fragment BoardEpicNode on BoardEpic {
webUrl
createdAt
closedAt
userPermissions {
adminEpic
}
}
#import "ee_else_ce/boards/queries/issue.fragment.graphql"
mutation IssueMoveList(
$projectPath: ID!
$iid: String!
$boardId: ID!
$fromListId: ID
$toListId: ID
$moveBeforeId: ID
$moveAfterId: ID
$epicId: EpicID
) {
issueMoveList(
input: {
projectPath: $projectPath
iid: $iid
boardId: $boardId
fromListId: $fromListId
toListId: $toListId
moveBeforeId: $moveBeforeId
moveAfterId: $moveAfterId
epicId: $epicId
}
) {
issue {
...IssueNode
}
errors
}
}
......@@ -11,12 +11,14 @@ import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types';
import { fullEpicId } from '../boards_util';
import { formatListIssues, fullBoardId } from '~/boards/boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/boards/eventhub';
import createDefaultClient from '~/lib/graphql';
import epicsSwimlanesQuery from '../queries/epics_swimlanes.query.graphql';
import issueSetEpic from '../queries/issue_set_epic.mutation.graphql';
import listsIssuesQuery from '~/boards/queries/lists_issues.query.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
......@@ -102,7 +104,7 @@ export default {
}));
if (!withLists) {
commit(types.RECEIVE_EPICS_SUCCESS, epicsFormatted);
commit(types.RECEIVE_EPICS_SUCCESS, { epics: epicsFormatted });
}
if (epics.pageInfo?.hasNextPage) {
......@@ -115,6 +117,7 @@ export default {
return {
epics: epicsFormatted,
lists: lists?.nodes,
canAdminEpic: epics.edges[0]?.node?.userPermissions?.adminEpic,
};
})
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
......@@ -212,7 +215,7 @@ export default {
if (state.isShowingEpicsSwimlanes) {
dispatch('fetchEpicsSwimlanes', {})
.then(({ lists, epics }) => {
.then(({ lists, epics, canAdminEpic }) => {
if (lists) {
let boardLists = lists.map(list =>
boardsStore.updateListPosition({ ...list, doNotFetchIssues: true }),
......@@ -222,7 +225,7 @@ export default {
}
if (epics) {
commit(types.RECEIVE_EPICS_SUCCESS, epics);
commit(types.RECEIVE_EPICS_SUCCESS, { epics, canAdminEpic });
}
})
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
......@@ -254,4 +257,50 @@ export default {
return data.issueSetEpic.issue.epic;
},
moveIssue: (
{ state, commit },
{ issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId, epicId },
) => {
const originalIssue = state.issues[issueId];
const fromList = state.issuesByListId[fromListId];
const originalIndex = fromList.indexOf(Number(issueId));
commit(types.MOVE_ISSUE, {
originalIssue,
fromListId,
toListId,
moveBeforeId,
moveAfterId,
epicId,
});
const { boardId } = state.endpoints;
const [fullProjectPath] = issuePath.split(/[#]/);
gqlClient
.mutate({
mutation: issueMoveListMutation,
variables: {
projectPath: fullProjectPath,
boardId: fullBoardId(boardId),
iid: issueIid,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
moveAfterId,
epicId,
},
})
.then(({ data }) => {
if (data?.issueMoveList?.errors.length) {
commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex });
} else {
const issue = data.issueMoveList?.issue;
commit(types.MOVE_ISSUE_SUCCESS, { issue });
}
})
.catch(() =>
commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
);
},
};
......@@ -23,3 +23,6 @@ export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const RESET_EPICS = 'RESET_EPICS';
export const SET_SHOW_LABELS = 'SET_SHOW_LABELS';
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';
import Vue from 'vue';
import { union } from 'lodash';
import mutationsCE from '~/boards/stores/mutations';
import mutationsCE, { addIssueToList, removeIssueFromList } from '~/boards/stores/mutations';
import { moveIssueListHelper } from '~/boards/boards_util';
import { s__ } from '~/locale';
import * as mutationTypes from './mutation_types';
......@@ -102,11 +103,31 @@ export default {
state.epicsSwimlanesFetchInProgress = false;
},
[mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, epics) => {
[mutationTypes.RECEIVE_EPICS_SUCCESS]: (state, { epics, canAdminEpic }) => {
Vue.set(state, 'epics', union(state.epics || [], epics));
state.canAdminEpic = canAdminEpic;
},
[mutationTypes.RESET_EPICS]: state => {
Vue.set(state, 'epics', []);
},
[mutationTypes.MOVE_ISSUE]: (
state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId, epicId },
) => {
const fromList = state.boardLists.find(l => l.id === fromListId);
const toList = state.boardLists.find(l => l.id === toListId);
const issue = moveIssueListHelper(originalIssue, fromList, toList);
if (epicId === null) {
Vue.set(state.issues, issue.id, { ...issue, epic: null });
} else if (epicId !== undefined) {
Vue.set(state.issues, issue.id, { ...issue, epic: { id: epicId } });
}
removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
},
};
......@@ -3,6 +3,7 @@ import createStateCE from '~/boards/stores/state';
export default () => ({
...createStateCE(),
canAdminEpic: false,
isShowingEpicsSwimlanes: false,
epicsSwimlanesFetchInProgress: false,
epics: [],
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'epics swimlanes', :js do
include DragTo
include MobileHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label]) }
let_it_be(:issue2) { create(:issue, project: project) }
let_it_be(:issue3) { create(:issue, project: project, state: :closed) }
let_it_be(:issue4) { create(:issue, project: project) }
let_it_be(:epic1) { create(:epic, group: group) }
let_it_be(:epic2) { create(:epic, group: group) }
let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) }
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3) }
before do
project.add_maintainer(user)
group.add_maintainer(user)
stub_licensed_features(epics: true)
sign_in(user)
visit_board_page
select_epics
end
context 'drag and drop issue' do
it 'between epics' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_first_epic(0, 1)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 1)
wait_for_board_cards_in_first_epic(1, 1)
wait_for_board_cards_in_second_epic(1, 0)
end
it 'from epic to unassigned issues lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 7)
wait_for_board_cards_in_second_epic(1, 0)
wait_for_board_cards_in_unassigned_lane(1, 1)
end
it 'from unassigned issues lane to epic' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_unassigned_lane(0, 1)
drag(list_from_index: 6, list_to_index: 3)
wait_for_board_cards_in_second_epic(0, 1)
wait_for_board_cards_in_unassigned_lane(0, 0)
end
it 'between lists within epic lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_first_epic(0, 1)
drag(list_from_index: 0, list_to_index: 1)
wait_for_board_cards(1, 1)
wait_for_board_cards(2, 2)
wait_for_board_cards_in_first_epic(0, 0)
wait_for_board_cards_in_first_epic(1, 1)
end
it 'between lists within unassigned lane' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_unassigned_lane(0, 1)
drag(list_from_index: 6, list_to_index: 7)
wait_for_board_cards(1, 1)
wait_for_board_cards(2, 2)
wait_for_board_cards_in_unassigned_lane(0, 0)
wait_for_board_cards_in_unassigned_lane(1, 1)
end
it 'between lists and epics' do
wait_for_board_cards(1, 2)
wait_for_board_cards_in_second_epic(1, 1)
drag(list_from_index: 4, list_to_index: 2)
wait_for_board_cards(2, 0)
wait_for_board_cards(3, 2)
wait_for_board_cards_in_first_epic(2, 2)
end
end
def visit_board_page
visit project_boards_path(project)
wait_for_requests
end
def select_epics
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
end
def drag(selector: '.board-cell', 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 boards
resize_window(2000, 1200)
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,
extra_height: 50)
end
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board-swimlanes-headers .board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
end
end
def wait_for_board_cards_in_first_epic(board_number, expected_cards)
page.within(all("[data-testid='board-epic-lane-issues']")[0]) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
def wait_for_board_cards_in_second_epic(board_number, expected_cards)
page.within(all("[data-testid='board-epic-lane-issues']")[1]) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
def wait_for_board_cards_in_unassigned_lane(board_number, expected_cards)
page.within(find("[data-testid='board-lane-unassigned-issues']")) do
page.within(all(".board")[board_number]) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
end
end
/* global ListIssue */
/* global List */
import Vue from 'vue';
import List from '~/boards/models/list';
import '~/boards/models/list';
import '~/boards/models/issue';
export const mockLists = [
{
......@@ -62,6 +66,34 @@ const labels = [
},
];
export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
iid: 27,
dueDate: null,
timeEstimate: 0,
weight: null,
confidential: false,
referencePath: 'gitlab-org/test-subgroup/gitlab-test#27',
path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27',
labels: {
nodes: [
{
id: 1,
title: 'test',
color: 'red',
description: 'testing',
},
],
},
assignees: {
nodes: assignees,
},
epic: {
id: 'gid://gitlab/Epic/41',
},
};
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: 27,
......@@ -79,6 +111,8 @@ export const mockIssue = {
},
};
export const mockIssueWithModel = new ListIssue({ ...mockIssue, id: '436' });
export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
......@@ -96,6 +130,8 @@ export const mockIssue2 = {
},
};
export const mockIssue2WithModel = new ListIssue({ ...mockIssue2, id: '437' });
export const mockIssue3 = {
id: 'gid://gitlab/Issue/438',
iid: 29,
......
......@@ -6,7 +6,15 @@ import * as types from 'ee/boards/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { ListType } from '~/boards/constants';
import { formatListIssues } from '~/boards/boards_util';
import { mockLists, mockIssue, mockEpic } from '../mock_data';
import {
mockLists,
mockIssue,
mockEpic,
rawIssue,
mockIssueWithModel,
mockIssue2WithModel,
mockListsWithModel,
} from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
......@@ -96,7 +104,7 @@ describe('fetchEpicsSwimlanes', () => {
[
{
type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic],
payload: { epics: [mockEpic] },
},
],
[],
......@@ -142,7 +150,7 @@ describe('fetchEpicsSwimlanes', () => {
[
{
type: types.RECEIVE_EPICS_SUCCESS,
payload: [mockEpic],
payload: { epics: [mockEpic] },
},
],
[
......@@ -381,3 +389,113 @@ describe('setActiveIssueEpic', () => {
await expect(actions.setActiveIssueEpic({ getters }, input)).rejects.toThrow(Error);
});
});
describe('moveIssue', () => {
const epicId = 'gid://gitlab/Epic/1';
const listIssues = {
'gid://gitlab/List/1': [436, 437],
'gid://gitlab/List/2': [],
};
const issues = {
'436': mockIssueWithModel,
'437': mockIssue2WithModel,
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
boardLists: mockListsWithModel,
issuesByListId: listIssues,
issues,
};
it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', done => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: rawIssue,
errors: [],
},
},
});
testAction(
actions.moveIssue,
{
issueId: '436',
issueIid: mockIssue.iid,
issuePath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
state,
[
{
type: types.MOVE_ISSUE,
payload: {
originalIssue: mockIssueWithModel,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
},
{
type: types.MOVE_ISSUE_SUCCESS,
payload: { issue: rawIssue },
},
],
[],
done,
);
});
it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', done => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: {},
errors: [{ foo: 'bar' }],
},
},
});
testAction(
actions.moveIssue,
{
issueId: '436',
issueIid: mockIssue.iid,
issuePath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
state,
[
{
type: types.MOVE_ISSUE,
payload: {
originalIssue: mockIssueWithModel,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
},
{
type: types.MOVE_ISSUE_FAILURE,
payload: {
originalIssue: mockIssueWithModel,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
},
},
],
[],
done,
);
});
});
......@@ -6,6 +6,8 @@ import {
mockEpics,
mockEpic,
mockListsWithModel,
mockIssueWithModel,
mockIssue2WithModel,
} from '../mock_data';
const expectNotImplemented = action => {
......@@ -211,7 +213,7 @@ describe('RECEIVE_EPICS_SUCCESS', () => {
epics: {},
};
mutations.RECEIVE_EPICS_SUCCESS(state, mockEpics);
mutations.RECEIVE_EPICS_SUCCESS(state, { epics: mockEpics });
expect(state.epics).toEqual(mockEpics);
});
......@@ -229,3 +231,62 @@ describe('RESET_EPICS', () => {
expect(state.epics).toEqual([]);
});
});
describe('MOVE_ISSUE', () => {
beforeEach(() => {
const listIssues = {
'gid://gitlab/List/1': [mockListsWithModel.id, mockIssue2WithModel.id],
'gid://gitlab/List/2': [],
};
const issues = {
'436': mockIssueWithModel,
'437': mockIssue2WithModel,
};
state = {
...state,
issuesByListId: listIssues,
boardLists: mockListsWithModel,
issues,
};
});
it('updates issuesByListId, moving issue between lists and updating epic id on issue', () => {
expect(state.issues['437'].epic.id).toEqual('gid://gitlab/Epic/40');
mutations.MOVE_ISSUE(state, {
originalIssue: mockIssue2WithModel,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockListsWithModel.id],
'gid://gitlab/List/2': [mockIssue2WithModel.id],
};
expect(state.issuesByListId).toEqual(updatedListIssues);
expect(state.issues['437'].epic.id).toEqual(epicId);
});
it('removes epic id from issue when epicId is null', () => {
expect(state.issues['437'].epic.id).toEqual('gid://gitlab/Epic/40');
mutations.MOVE_ISSUE(state, {
originalIssue: mockIssue2WithModel,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId: null,
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockListsWithModel.id],
'gid://gitlab/List/2': [mockIssue2WithModel.id],
};
expect(state.issuesByListId).toEqual(updatedListIssues);
expect(state.issues['437'].epic).toEqual(null);
});
});
......@@ -322,6 +322,7 @@ describe('Board Store Mutations', () => {
state = {
...state,
issuesByListId: listIssues,
boardLists: mockListsWithModel,
};
mutations.MOVE_ISSUE_FAILURE(state, {
......@@ -389,6 +390,7 @@ describe('Board Store Mutations', () => {
...state,
issuesByListId: listIssues,
issues,
boardLists: mockListsWithModel,
};
mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
......
# frozen_string_literal: true
module DragTo
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true)
# rubocop:disable Metrics/ParameterLists
def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true, extra_height: 0)
js = <<~JS
simulateDrag({
scrollable: document.querySelector('#{scrollable}'),
......@@ -14,7 +15,8 @@ module DragTo
el: document.querySelectorAll('#{selector}')[#{list_to_index}],
index: #{to_index}
},
performDrop: #{perform_drop}
performDrop: #{perform_drop},
extraHeight: #{extra_height}
});
JS
evaluate_script(js)
......@@ -23,6 +25,7 @@ module DragTo
loop while drag_active?
end
end
# rubocop:enable Metrics/ParameterLists
def drag_active?
page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?
......
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