Commit d15cc293 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch...

Merge branch '325778-moving-issue-to-board-list-of-different-type-should-not-remove-it-from-previous-list' into 'master'

Clone issue card on move when necessary [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58644
parents 1d76f120 96b703f5
import { sortBy } from 'lodash';
import { sortBy, cloneDeep } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ListType, NOT_FILTER } from './constants';
......@@ -113,6 +113,37 @@ export function formatIssueInput(issueInput, boardConfig) {
};
}
export function shouldCloneCard(fromListType, toListType) {
const involvesClosed = fromListType === ListType.closed || toListType === ListType.closed;
const involvesBacklog = fromListType === ListType.backlog || toListType === ListType.backlog;
if (involvesClosed || involvesBacklog) {
return false;
}
if (fromListType !== toListType) {
return true;
}
return false;
}
export function getMoveData(state, params) {
const { boardItems, boardItemsByListId, boardLists } = state;
const { itemId, fromListId, toListId } = params;
const fromListType = boardLists[fromListId].listType;
const toListType = boardLists[toListId].listType;
return {
reordering: fromListId === toListId,
shouldClone: shouldCloneCard(fromListType, toListType),
itemNotInToList: !boardItemsByListId[toListId].includes(itemId),
originalIssue: cloneDeep(boardItems[itemId]),
originalIndex: boardItemsByListId[fromListId].indexOf(itemId),
...params,
};
}
export function moveItemListHelper(item, fromList, toList) {
const updatedItem = item;
if (
......
......@@ -190,7 +190,7 @@ export default {
}
this.moveItem({
itemId,
itemId: Number(itemId),
itemIid,
itemPath,
fromListId: from.dataset.listId,
......
......@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/browser';
import { pick } from 'lodash';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import {
BoardType,
ListType,
......@@ -23,13 +24,14 @@ import {
formatIssueInput,
updateListPosition,
transformNotFilters,
moveItemListHelper,
getMoveData,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
......@@ -333,42 +335,134 @@ export default {
dispatch('moveIssue', payload);
},
moveIssue: (
{ state, commit },
{ itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId },
moveIssue: ({ dispatch, state }, params) => {
const moveData = getMoveData(state, params);
dispatch('moveIssueCard', moveData);
dispatch('updateMovedIssue', moveData);
dispatch('updateIssueOrder', { moveData });
},
moveIssueCard: ({ commit }, moveData) => {
const {
reordering,
shouldClone,
itemNotInToList,
originalIndex,
itemId,
fromListId,
toListId,
moveBeforeId,
moveAfterId,
} = moveData;
commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
if (reordering) {
commit(types.ADD_BOARD_ITEM_TO_LIST, {
itemId,
listId: toListId,
moveBeforeId,
moveAfterId,
});
return;
}
if (itemNotInToList) {
commit(types.ADD_BOARD_ITEM_TO_LIST, {
itemId,
listId: toListId,
moveBeforeId,
moveAfterId,
});
}
if (shouldClone) {
commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
}
},
updateMovedIssue: (
{ commit, state: { boardItems, boardLists } },
{ itemId, fromListId, toListId },
) => {
const originalIssue = state.boardItems[itemId];
const fromList = state.boardItemsByListId[fromListId];
const originalIndex = fromList.indexOf(Number(itemId));
commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
const updatedIssue = moveItemListHelper(
boardItems[itemId],
boardLists[fromListId],
boardLists[toListId],
);
const { boardId } = state;
const [fullProjectPath] = itemPath.split(/[#]/);
commit(types.UPDATE_BOARD_ITEM, updatedIssue);
},
gqlClient
.mutate({
undoMoveIssueCard: ({ commit }, moveData) => {
const {
reordering,
shouldClone,
itemNotInToList,
itemId,
fromListId,
toListId,
originalIssue,
originalIndex,
} = moveData;
commit(types.UPDATE_BOARD_ITEM, originalIssue);
if (reordering) {
commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
return;
}
if (shouldClone) {
commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
}
if (itemNotInToList) {
commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: toListId });
}
commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
},
updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
try {
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
const {
boardId,
boardItems: {
[itemId]: { iid, referencePath },
},
} = state;
const { data } = await gqlClient.mutate({
mutation: issueMoveListMutation,
variables: {
projectPath: fullProjectPath,
iid,
projectPath: referencePath.split(/[#]/)[0],
boardId: fullBoardId(boardId),
iid: itemIid,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
moveAfterId,
// 'mutationVariables' allows EE code to pass in extra parameters.
...mutationVariables,
},
})
.then(({ data }) => {
if (data?.issueMoveList?.errors.length) {
throw new Error();
} else {
const issue = data.issueMoveList?.issue;
commit(types.MOVE_ISSUE_SUCCESS, { issue });
});
if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
throw new Error('issueMoveList empty');
}
})
.catch(() =>
commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue });
} catch {
commit(
types.SET_ERROR,
s__('Boards|An error occurred while moving the issue. Please try again.'),
);
dispatch('undoMoveIssueCard', moveData);
}
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
......
......@@ -23,12 +23,10 @@ export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
export const MOVE_ISSUE = 'MOVE_ISSUE';
export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
......
......@@ -2,7 +2,7 @@ import { pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
import { formatIssue, moveItemListHelper } from '../boards_util';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
......@@ -183,40 +183,11 @@ export default {
notImplemented();
},
[mutationTypes.MOVE_ISSUE]: (
state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
) => {
const fromList = state.boardLists[fromListId];
const toList = state.boardLists[toListId];
const issue = moveItemListHelper(originalIssue, fromList, toList);
Vue.set(state.boardItems, issue.id, issue);
removeItemFromList({ state, listId: fromListId, itemId: issue.id });
addItemToList({ state, listId: toListId, itemId: issue.id, moveBeforeId, moveAfterId });
},
[mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
[mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => {
const issueId = getIdFromGraphQLId(issue.id);
Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId }));
},
[mutationTypes.MOVE_ISSUE_FAILURE]: (
state,
{ originalIssue, fromListId, toListId, originalIndex },
) => {
state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
Vue.set(state.boardItems, originalIssue.id, originalIssue);
removeItemFromList({ state, listId: toListId, itemId: originalIssue.id });
addItemToList({
state,
listId: fromListId,
itemId: originalIssue.id,
atIndex: originalIndex,
});
},
[mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
notImplemented();
},
......
......@@ -147,7 +147,7 @@ export default {
}
this.moveIssue({
itemId,
itemId: Number(itemId),
itemIid,
itemPath,
fromListId: from.dataset.listId,
......
......@@ -5,6 +5,7 @@ import {
formatListsPageInfo,
fullBoardId,
transformNotFilters,
getMoveData,
} from '~/boards/boards_util';
import { BoardType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
......@@ -12,7 +13,6 @@ import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
import actionsCE from '~/boards/stores/actions';
import boardsStore from '~/boards/stores/boards_store';
import * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import {
......@@ -37,7 +37,6 @@ import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import groupBoardAssigneesQuery from '../graphql/group_board_assignees.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetEpicMutation from '../graphql/issue_set_epic.mutation.graphql';
import issueSetWeightMutation from '../graphql/issue_set_weight.mutation.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
......@@ -482,50 +481,35 @@ export default {
}
},
moveIssue: (
{ state, commit },
{ itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId, epicId },
) => {
const originalIssue = state.boardItems[itemId];
const fromList = state.boardItemsByListId[fromListId];
const originalIndex = fromList.indexOf(Number(itemId));
commit(types.MOVE_ISSUE, {
originalIssue,
fromListId,
toListId,
moveBeforeId,
moveAfterId,
epicId,
moveIssue: ({ dispatch, state }, params) => {
const { itemId, epicId } = params;
const moveData = getMoveData(state, params);
dispatch('moveIssueCard', moveData);
dispatch('updateMovedIssue', moveData);
dispatch('updateEpicForIssue', { itemId, epicId });
dispatch('updateIssueOrder', {
moveData,
mutationVariables: { epicId },
});
},
const { boardId } = state;
const [fullProjectPath] = itemPath.split(/[#]/);
updateEpicForIssue: ({ commit, state: { boardItems } }, { itemId, epicId }) => {
const issue = boardItems[itemId];
gqlClient
.mutate({
mutation: issueMoveListMutation,
variables: {
projectPath: fullProjectPath,
boardId: fullBoardId(boardId),
iid: itemIid,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
moveAfterId,
epicId,
},
})
.then(({ data }) => {
if (data?.issueMoveList?.errors.length) {
throw new Error();
} else {
const issue = data.issueMoveList?.issue;
commit(types.MOVE_ISSUE_SUCCESS, { issue });
if (epicId === null) {
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: issue.id,
prop: 'epic',
value: null,
});
} else if (epicId !== undefined) {
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: issue.id,
prop: 'epic',
value: { id: epicId },
});
}
})
.catch(() =>
commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
);
},
moveEpic: ({ state, commit }, { itemId, fromListId, toListId, moveBeforeId, moveAfterId }) => {
......
......@@ -150,25 +150,6 @@ export default {
Vue.set(state, 'epics', []);
},
[mutationTypes.MOVE_ISSUE]: (
state,
{ originalIssue, fromListId, toListId, moveBeforeId, moveAfterId, epicId },
) => {
const fromList = state.boardLists[fromListId];
const toList = state.boardLists[toListId];
const issue = moveItemListHelper(originalIssue, fromList, toList);
if (epicId === null) {
Vue.set(state.boardItems, issue.id, { ...issue, epic: null });
} else if (epicId !== undefined) {
Vue.set(state.boardItems, issue.id, { ...issue, epic: { id: epicId } });
}
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 },
......
---
title: Clone issue card on move when necessary in epic swimlanes
merge_request: 58644
author:
type: fixed
......@@ -9,6 +9,7 @@ import * as types from 'ee/boards/stores/mutation_types';
import mutations from 'ee/boards/stores/mutations';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mock_data';
import { formatBoardLists, formatListIssues } from '~/boards/boards_util';
import { issuableTypes } from '~/boards/constants';
import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
......@@ -19,13 +20,10 @@ import {
labels,
mockLists,
mockIssue,
mockIssue2,
mockIssues,
mockEpic,
rawIssue,
mockMilestones,
mockAssignees,
mockEpics,
} from '../mock_data';
Vue.use(Vuex);
......@@ -903,204 +901,89 @@ describe.each`
});
describe('moveIssue', () => {
const epicId = 'gid://gitlab/Epic/1';
it('should dispatch a correct set of actions with epic id', () => {
const params = mockMoveIssueParams;
const listIssues = {
'gid://gitlab/List/1': [436, 437],
'gid://gitlab/List/2': [],
const moveData = {
...mockMoveData,
epicId: 'some-epic-id',
};
const issues = {
436: mockIssue,
437: mockIssue2,
};
const state = {
fullPath: 'gitlab-org',
boardId: 1,
boardType: 'group',
disabled: false,
boardLists: mockLists,
boardItemsByListId: listIssues,
boardItems: 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,
{
itemId: '436',
itemIid: mockIssue.iid,
itemPath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
state,
[
{
type: types.MOVE_ISSUE,
testAction({
action: actions.moveIssue,
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
...params,
epicId: 'some-epic-id',
},
state: mockMoveState,
expectedActions: [
{ type: 'moveIssueCard', payload: moveData },
{ type: 'updateMovedIssue', payload: moveData },
{ type: 'updateEpicForIssue', payload: { itemId: params.itemId, epicId: 'some-epic-id' } },
{
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,
{
itemId: '436',
itemIid: mockIssue.iid,
itemPath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
state,
[
{
type: types.MOVE_ISSUE,
type: 'updateIssueOrder',
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
},
moveData,
mutationVariables: {
epicId: 'some-epic-id',
},
{
type: types.MOVE_ISSUE_FAILURE,
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
},
},
],
[],
done,
);
});
});
});
describe('moveEpic', () => {
const listEpics = {
'gid://gitlab/List/1': [41, 40],
'gid://gitlab/List/2': [],
};
const epics = {
41: mockEpic,
40: mockEpics[1],
};
describe('updateEpicForIssue', () => {
let commonState;
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', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
epicMoveList: {
errors: [],
beforeEach(() => {
commonState = {
boardItems: {
itemId: {
id: 'issueId',
},
},
};
});
await testAction({
action: actions.moveEpic,
it.each([
[
'with epic id',
{
payload: {
itemId: '41',
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
itemId: 'itemId',
epicId: 'epicId',
},
state,
expectedMutations: [
{
type: types.MOVE_EPIC,
payload: {
originalEpic: mockEpic,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload: { itemId: 'issueId', prop: 'epic', value: { id: 'epicId' } },
},
],
});
});
it('should commit MOVE_EPIC mutation and MOVE_EPIC_FAILURE mutation when unsuccessful', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
epicMoveList: {
errors: [{ foo: 'bar' }],
},
},
});
await testAction({
action: actions.moveEpic,
payload: {
itemId: '41',
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
state,
expectedMutations: [
],
[
'with null as epic id',
{
type: types.MOVE_EPIC,
payload: {
originalEpic: mockEpic,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
itemId: 'itemId',
epicId: null,
},
expectedMutations: [
{
type: types.MOVE_EPIC_FAILURE,
payload: {
originalEpic: mockEpic,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload: { itemId: 'issueId', prop: 'epic', value: null },
},
],
},
],
])(`commits UPDATE_BOARD_ITEM_BY_ID mutation %s`, (_, { payload, expectedMutations }) => {
testAction({
action: actions.updateEpicForIssue,
payload,
state: commonState,
expectedMutations,
});
});
});
......
import mutations from 'ee/boards/stores/mutations';
import { mockIssue, mockIssue2, mockEpics, mockEpic, mockLists } from '../mock_data';
import { mockEpics, mockEpic, mockLists } from '../mock_data';
const expectNotImplemented = (action) => {
it('is not implemented', () => {
......@@ -7,8 +7,6 @@ const expectNotImplemented = (action) => {
});
};
const epicId = mockEpic.id;
const initialBoardListsState = {
'gid://gitlab/List/1': mockLists[0],
'gid://gitlab/List/2': mockLists[1],
......@@ -222,64 +220,6 @@ describe('RESET_EPICS', () => {
});
});
describe('MOVE_ISSUE', () => {
beforeEach(() => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
'gid://gitlab/List/2': [],
};
const issues = {
436: mockIssue,
437: mockIssue2,
};
state = {
...state,
boardItemsByListId: listIssues,
boardItems: issues,
};
});
it('updates boardItemsByListId, moving issue between lists and updating epic id on issue', () => {
expect(state.boardItems['437'].epic.id).toEqual('gid://gitlab/Epic/40');
mutations.MOVE_ISSUE(state, {
originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId,
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockIssue.id],
'gid://gitlab/List/2': [mockIssue2.id],
};
expect(state.boardItemsByListId).toEqual(updatedListIssues);
expect(state.boardItems['437'].epic.id).toEqual(epicId);
});
it('removes epic id from issue when epicId is null', () => {
expect(state.boardItems['437'].epic.id).toEqual('gid://gitlab/Epic/40');
mutations.MOVE_ISSUE(state, {
originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
epicId: null,
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockIssue.id],
'gid://gitlab/List/2': [mockIssue2.id],
};
expect(state.boardItemsByListId).toEqual(updatedListIssues);
expect(state.boardItems['437'].epic).toEqual(null);
});
});
describe('MOVE_EPIC', () => {
it('updates boardItemsByListId, moving epic between lists', () => {
const listIssues = {
......
......@@ -3,6 +3,7 @@
import { keyBy } from 'lodash';
import Vue from 'vue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
......@@ -488,3 +489,38 @@ export const mockBlockedIssue2 = {
blockedByCount: 4,
webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0',
};
export const mockMoveIssueParams = {
itemId: 1,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
moveBeforeId: undefined,
moveAfterId: undefined,
};
export const mockMoveState = {
boardLists: {
'gid://gitlab/List/1': {
listType: ListType.backlog,
},
'gid://gitlab/List/2': {
listType: ListType.closed,
},
},
boardItems: {
[mockMoveIssueParams.itemId]: { foo: 'bar' },
},
boardItemsByListId: {
[mockMoveIssueParams.fromListId]: [mockMoveIssueParams.itemId],
[mockMoveIssueParams.toListId]: [],
},
};
export const mockMoveData = {
reordering: false,
shouldClone: false,
itemNotInToList: true,
originalIndex: 0,
originalIssue: { foo: 'bar' },
...mockMoveIssueParams,
};
import * as Sentry from '@sentry/browser';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
import {
fullBoardId,
......@@ -6,14 +7,15 @@ import {
formatBoardLists,
formatIssueInput,
formatIssue,
getMoveData,
} from '~/boards/boards_util';
import { inactiveId, ISSUABLE } from '~/boards/constants';
import { inactiveId, ISSUABLE, ListType } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockLists,
mockListsById,
......@@ -25,6 +27,9 @@ import {
labels,
mockActiveIssue,
mockGroupProjects,
mockMoveIssueParams,
mockMoveState,
mockMoveData,
} from '../mock_data';
jest.mock('~/flash');
......@@ -653,64 +658,302 @@ describe('moveItem', () => {
});
describe('moveIssue', () => {
const listIssues = {
'gid://gitlab/List/1': [436, 437],
'gid://gitlab/List/2': [],
};
it('should dispatch a correct set of actions', () => {
testAction({
action: actions.moveIssue,
payload: mockMoveIssueParams,
state: mockMoveState,
expectedActions: [
{ type: 'moveIssueCard', payload: mockMoveData },
{ type: 'updateMovedIssue', payload: mockMoveData },
{ type: 'updateIssueOrder', payload: { moveData: mockMoveData } },
],
});
});
});
const issues = {
436: mockIssue,
437: mockIssue2,
describe('moveIssueCard and undoMoveIssueCard', () => {
describe('card should move without clonning', () => {
let state;
let params;
let moveMutations;
let undoMutations;
describe('when re-ordering card', () => {
beforeEach(
({
itemId = 123,
fromListId = 'gid://gitlab/List/1',
toListId = 'gid://gitlab/List/1',
originalIssue = { foo: 'bar' },
originalIndex = 0,
moveBeforeId = undefined,
moveAfterId = undefined,
} = {}) => {
state = {
boardLists: {
[toListId]: { listType: ListType.backlog },
[fromListId]: { listType: ListType.backlog },
},
boardItems: { [itemId]: originalIssue },
boardItemsByListId: { [fromListId]: [123] },
};
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
moveMutations = [
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
},
];
undoMutations = [
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: fromListId, atIndex: originalIndex },
},
];
},
);
const state = {
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: mockLists,
boardItemsByListId: listIssues,
boardItems: issues,
};
it('moveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.moveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: moveMutations,
});
});
it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: rawIssue,
errors: [],
it('undoMoveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.undoMoveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: undoMutations,
});
});
});
describe.each([
[
'issue moves out of backlog',
{
fromListType: ListType.backlog,
toListType: ListType.label,
},
],
[
'issue card moves to closed',
{
fromListType: ListType.label,
toListType: ListType.closed,
},
],
[
'issue card moves to non-closed, non-backlog list of the same type',
{
fromListType: ListType.label,
toListType: ListType.label,
},
],
])('when %s', (_, { toListType, fromListType }) => {
beforeEach(
({
itemId = 123,
fromListId = 'gid://gitlab/List/1',
toListId = 'gid://gitlab/List/2',
originalIssue = { foo: 'bar' },
originalIndex = 0,
moveBeforeId = undefined,
moveAfterId = undefined,
} = {}) => {
state = {
boardLists: {
[fromListId]: { listType: fromListType },
[toListId]: { listType: toListType },
},
boardItems: { [itemId]: originalIssue },
boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
};
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
moveMutations = [
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
},
];
undoMutations = [
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: fromListId, atIndex: originalIndex },
},
];
},
);
it('moveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.moveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: moveMutations,
});
});
testAction(
actions.moveIssue,
it('undoMoveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.undoMoveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: undoMutations,
});
});
});
});
describe('card should clone on move', () => {
let state;
let params;
let moveMutations;
let undoMutations;
describe.each([
[
'issue card moves to non-closed, non-backlog list of a different type',
{
itemId: '436',
itemIid: mockIssue.iid,
itemPath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
fromListType: ListType.label,
toListType: ListType.assignee,
},
],
])('when %s', (_, { toListType, fromListType }) => {
beforeEach(
({
itemId = 123,
fromListId = 'gid://gitlab/List/1',
toListId = 'gid://gitlab/List/2',
originalIssue = { foo: 'bar' },
originalIndex = 0,
moveBeforeId = undefined,
moveAfterId = undefined,
} = {}) => {
state = {
boardLists: {
[fromListId]: { listType: fromListType },
[toListId]: { listType: toListType },
},
boardItems: { [itemId]: originalIssue },
boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
};
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
moveMutations = [
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
},
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: fromListId, atIndex: originalIndex },
},
];
undoMutations = [
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
{
type: types.ADD_BOARD_ITEM_TO_LIST,
payload: { itemId, listId: fromListId, atIndex: originalIndex },
},
];
},
);
it('moveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.moveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: moveMutations,
});
});
it('undoMoveIssueCard commits a correct set of actions', () => {
testAction({
action: actions.undoMoveIssueCard,
state,
payload: getMoveData(state, params),
expectedMutations: undoMutations,
});
});
});
});
});
describe('updateMovedIssueCard', () => {
const label1 = {
id: 'label1',
};
it.each([
[
'issue without a label is moved to a label list',
{
type: types.MOVE_ISSUE,
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
state: {
boardLists: {
from: {},
to: {
listType: ListType.label,
label: label1,
},
},
{
type: types.MOVE_ISSUE_SUCCESS,
payload: { issue: rawIssue },
boardItems: {
1: {
labels: [],
},
},
},
moveData: {
itemId: 1,
fromListId: 'from',
toListId: 'to',
},
updatedIssue: { labels: [label1] },
},
],
[],
done,
);
])(
'should commit UPDATE_BOARD_ITEM with a correctly updated issue data when %s',
(_, { state, moveData, updatedIssue }) => {
testAction({
action: actions.updateMovedIssue,
payload: moveData,
state,
expectedMutations: [{ type: types.UPDATE_BOARD_ITEM, payload: updatedIssue }],
});
},
);
});
describe('updateIssueOrder', () => {
const issues = {
436: mockIssue,
437: mockIssue2,
};
const state = {
boardItems: issues,
boardId: 'gid://gitlab/Board/1',
};
const moveData = {
itemId: 436,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
};
it('calls mutate with the correct variables', () => {
const mutationVariables = {
......@@ -734,61 +977,56 @@ describe('moveIssue', () => {
},
});
actions.moveIssue(
{ state, commit: () => {} },
{
itemId: mockIssue.id,
itemIid: mockIssue.iid,
itemPath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
);
actions.updateIssueOrder({ state, commit: () => {}, dispatch: () => {} }, { moveData });
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
});
it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', (done) => {
it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: {},
errors: [{ foo: 'bar' }],
issue: rawIssue,
errors: [],
},
},
});
testAction(
actions.moveIssue,
{
itemId: '436',
itemIid: mockIssue.iid,
itemPath: mockIssue.referencePath,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
actions.updateIssueOrder,
{ moveData },
state,
[
{
type: types.MOVE_ISSUE,
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
type: types.MUTATE_ISSUE_SUCCESS,
payload: { issue: rawIssue },
},
],
[],
);
});
it('should commit SET_ERROR and dispatch undoMoveIssueCard', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: {},
errors: [{ foo: 'bar' }],
},
{
type: types.MOVE_ISSUE_FAILURE,
payload: {
originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
},
});
testAction(
actions.updateIssueOrder,
{ moveData },
state,
[
{
type: types.SET_ERROR,
payload: 'An error occurred while moving the issue. Please try again.',
},
],
[],
done,
[{ type: 'undoMoveIssueCard', payload: moveData }],
);
});
});
......
......@@ -394,41 +394,7 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR);
});
describe('MOVE_ISSUE', () => {
it('updates boardItemsByListId, moving issue between lists', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
'gid://gitlab/List/2': [],
};
const issues = {
1: mockIssue,
2: mockIssue2,
};
state = {
...state,
boardItemsByListId: listIssues,
boardLists: initialBoardListsState,
boardItems: issues,
};
mutations.MOVE_ISSUE(state, {
originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockIssue.id],
'gid://gitlab/List/2': [mockIssue2.id],
};
expect(state.boardItemsByListId).toEqual(updatedListIssues);
});
});
describe('MOVE_ISSUE_SUCCESS', () => {
describe('MUTATE_ISSUE_SUCCESS', () => {
it('updates issue in issues state', () => {
const issues = {
436: { id: rawIssue.id },
......@@ -439,7 +405,7 @@ describe('Board Store Mutations', () => {
boardItems: issues,
};
mutations.MOVE_ISSUE_SUCCESS(state, {
mutations.MUTATE_ISSUE_SUCCESS(state, {
issue: rawIssue,
});
......@@ -447,36 +413,6 @@ describe('Board Store Mutations', () => {
});
});
describe('MOVE_ISSUE_FAILURE', () => {
it('updates boardItemsByListId, reverting moving issue between lists, and sets error message', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id],
'gid://gitlab/List/2': [mockIssue2.id],
};
state = {
...state,
boardItemsByListId: listIssues,
boardLists: initialBoardListsState,
};
mutations.MOVE_ISSUE_FAILURE(state, {
originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 1,
});
const updatedListIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
'gid://gitlab/List/2': [],
};
expect(state.boardItemsByListId).toEqual(updatedListIssues);
expect(state.error).toEqual('An error occurred while moving the issue. Please try again.');
});
});
describe('UPDATE_BOARD_ITEM', () => {
it('updates the given issue in state.boardItems', () => {
const updatedIssue = { id: 'some_gid', foo: 'bar' };
......
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