Commit 1e990c8c authored by Eulyeon Ko's avatar Eulyeon Ko

Clone issue card on move when needed

Also resolves duplicate key error by
checking if issue card already exists
in the destination list
parent f028aff4
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,123 @@ 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('requestIssueMoveListMutation', { moveData });
},
moveIssueCard: ({ commit }, moveData) => {
const { reordering, shouldClone, itemNotInToList } = moveData;
const { 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) {
const { originalIndex } = moveData;
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 } = moveData;
const { itemId, fromListId, toListId } = moveData;
const { 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 });
},
requestIssueMoveListMutation: 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('requestIssueMoveListMutation', {
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: 'requestIssueMoveListMutation',
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: 'requestIssueMoveListMutation', 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('requestIssueMoveListMutation', () => {
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,59 @@ 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.requestIssueMoveListMutation(
{ 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.requestIssueMoveListMutation,
{ 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.requestIssueMoveListMutation,
{ 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