Commit 35bccfc3 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'psi-board-iteration-on-issue' into 'master'

Add iteration to issues created on scoped boards

See merge request gitlab-org/gitlab!69731
parents 5bcd10c4 2d8bb4a1
......@@ -16,24 +16,24 @@ import {
ListTypeTitles,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
formatIssueInput,
formatBoardLists,
formatListIssues,
formatListsPageInfo,
formatIssue,
formatIssueInput,
updateListPosition,
moveItemListHelper,
getMoveData,
FiltersInfo,
filterVariables,
} from '../boards_util';
} from 'ee_else_ce/boards/boards_util';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
......
import { FiltersInfo as FiltersInfoCE } from '~/boards/boards_util';
import {
FiltersInfo as FiltersInfoCE,
formatIssueInput as formatIssueInputCe,
} from '~/boards/boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import {
EPIC_LANE_BASE_HEIGHT,
......@@ -11,6 +15,17 @@ import {
EpicFilterType,
} from './constants';
export {
formatBoardLists,
formatListIssues,
formatListsPageInfo,
formatIssue,
updateListPosition,
moveItemListHelper,
getMoveData,
filterVariables,
} from '~/boards/boards_util';
export function getMilestone({ milestone }) {
return milestone || null;
}
......@@ -23,6 +38,30 @@ export function fullMilestoneId(milestoneId) {
return `gid://gitlab/Milestone/${milestoneId}`;
}
function fullIterationId(id) {
if (!id) {
return null;
}
if (id === IterationIDs.CURRENT) {
return 'CURRENT';
}
if (id === IterationIDs.UPCOMING) {
return 'UPCOMING';
}
return `gid://gitlab/Iteration/${id}`;
}
function fullIterationCadenceId(id) {
if (!id) {
return null;
}
return `gid://gitlab/Iterations::Cadence/${getIdFromGraphQLId(id)}`;
}
export function fullUserId(userId) {
return `gid://gitlab/User/${userId}`;
}
......@@ -96,6 +135,34 @@ export function formatEpicInput(epicInput, boardConfig) {
};
}
function iterationObj(iterationId) {
const isWildcard = Object.values(IterationIDs).includes(iterationId);
const key = isWildcard ? 'iterationWildcardId' : 'iterationId';
return {
[key]: fullIterationId(iterationId),
};
}
export function formatIssueInput(issueInput, boardConfig) {
const { iterationId, iterationCadenceId } = boardConfig;
const iteration = gon.features?.iterationCadences
? {
iterationCadenceId: fullIterationCadenceId(iterationCadenceId),
...iterationObj(iterationId),
}
: {
iterationCadenceId,
...iterationObj(iterationId),
};
return {
...formatIssueInputCe(issueInput, boardConfig),
...iteration,
};
}
export function transformBoardConfig(boardConfig) {
const updatedBoardConfig = {};
const passedFilterParams = queryToObject(window.location.search, { gatherArrays: true });
......
query BoardCurrentIteration($fullPath: ID!, $isGroup: Boolean = true) {
group(fullPath: $fullPath) @include(if: $isGroup) {
id
iterations(state: current, first: 1, includeAncestors: true) {
nodes {
id
iterationCadence {
id
}
}
}
}
project(fullPath: $fullPath) @skip(if: $isGroup) {
id
iterations(state: current, first: 1, includeAncestors: true) {
nodes {
id
iterationCadence {
id
}
}
}
}
}
......@@ -29,6 +29,14 @@ fragment IssueNode on Issue {
milestone {
...MilestoneFragment
}
iteration {
id
title
iterationCadence {
id
title
}
}
labels {
nodes {
id
......
......@@ -27,7 +27,7 @@ import {
FiltersInfo,
} from '../boards_util';
import { EpicFilterType, GroupByParamType, FilterFields } from '../constants';
import { EpicFilterType, GroupByParamType, FilterFields, IterationIDs } from '../constants';
import createEpicBoardListMutation from '../graphql/epic_board_list_create.mutation.graphql';
import epicCreateMutation from '../graphql/epic_create.mutation.graphql';
import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
......@@ -35,6 +35,7 @@ import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import subGroupsQuery from '../graphql/sub_groups.query.graphql';
import currentIterationQuery from '../graphql/board_current_iteration.query.graphql';
import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
import * as types from './mutation_types';
......@@ -94,6 +95,46 @@ export { gqlClient };
export default {
...actionsCE,
addListNewIssue: async (
{ state: { boardConfig, boardType, fullPath }, dispatch, commit },
issueInputObj,
) => {
const { iterationId } = boardConfig;
let { iterationCadenceId } = boardConfig;
if (!iterationCadenceId && iterationId === IterationIDs.CURRENT) {
const iteration = await gqlClient
.query({
query: currentIterationQuery,
context: {
isSingleRequest: true,
},
variables: {
isGroup: boardType === BoardType.group,
fullPath,
},
})
.then(({ data }) => {
return data[boardType]?.iterations?.nodes?.[0];
});
iterationCadenceId = iteration.iterationCadence.id;
}
return actionsCE.addListNewIssue(
{
state: {
boardConfig: { ...boardConfig, iterationId, iterationCadenceId },
boardType,
fullPath,
},
dispatch,
commit,
},
issueInputObj,
);
},
setFilters: ({ commit, dispatch, state: { issuableType } }, filters) => {
if (filters.groupBy === GroupByParamType.epic) {
dispatch('setEpicSwimlanes');
......
......@@ -278,7 +278,7 @@ RSpec.describe 'Scoped issue boards', :js do
context 'iteration' do
context 'group with iterations' do
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, group: group, iterations_cadence: cadence) }
let_it_be(:iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: cadence, group: group, start_date: 1.day.ago, due_date: Date.today) }
context 'board not scoped to iteration' do
it 'sets board to current iteration' do
......@@ -293,6 +293,29 @@ RSpec.describe 'Scoped issue boards', :js do
end
context 'board scoped to current iteration' do
before do
stub_feature_flags(iteration_cadences: false)
end
it 'adds current iteration to new issues' do
update_board_scope('current_iteration', true)
wait_for_requests
page.within(first('.board')) do
click_button 'New issue'
end
page.within(first('.board-new-issue-form')) do
find('.form-control').set('issue in current iteration')
click_button 'Create issue'
end
wait_for_requests
expect(find('[data-testid="issue-boards-sidebar"]')).to have_text(iteration.title)
end
it 'removes current iteration from board' do
create_board_scope('current_iteration', true)
......
......@@ -3,7 +3,9 @@ import {
formatListEpics,
formatEpicListsPageInfo,
transformBoardConfig,
formatIssueInput,
} from 'ee/boards/boards_util';
import { IterationIDs } from 'ee/boards/constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import { mockLabel } from './mock_data';
......@@ -105,6 +107,71 @@ describe('formatEpicListsPageInfo', () => {
});
});
describe('formatIssueInput', () => {
const issueInput = {
labelIds: ['gid://gitlab/GroupLabel/5'],
projectPath: 'gitlab-org/gitlab-test',
id: 'gid://gitlab/Issue/11',
};
const expected = {
projectPath: 'gitlab-org/gitlab-test',
id: 'gid://gitlab/Issue/11',
labelIds: ['gid://gitlab/GroupLabel/5'],
assigneeIds: [],
milestoneId: undefined,
};
it('adds iterationIds to input', () => {
const boardConfig = {
iterationId: 66,
};
const result = formatIssueInput(issueInput, boardConfig);
expect(result).toEqual({
...expected,
iterationId: 'gid://gitlab/Iteration/66',
});
});
it('adds iterationWildcardId to when current iteration selected', () => {
const boardConfig = {
iterationId: IterationIDs.CURRENT,
};
const result = formatIssueInput(issueInput, boardConfig);
expect(result).toEqual({
...expected,
iterationWildcardId: 'CURRENT',
iterationCadenceId: undefined,
});
});
it('includes iterationCadenceId and iterationId', () => {
gon.features = {
...gon.features,
iterationCadences: true,
};
const boardConfig = {
iterationId: 66,
iterationCadenceId: 11,
};
const result = formatIssueInput(issueInput, boardConfig);
expect(result).toEqual({
...expected,
iterationCadenceId: 'gid://gitlab/Iterations::Cadence/11',
iterationId: 'gid://gitlab/Iteration/66',
});
delete gon.features.iterationCadences;
});
});
describe('transformBoardConfig', () => {
const boardConfig = {
milestoneTitle: 'milestone',
......
......@@ -2,7 +2,13 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vuex from 'vuex';
import { BoardType, GroupByParamType, listsQuery, issuableTypes } from 'ee/boards/constants';
import {
BoardType,
GroupByParamType,
listsQuery,
issuableTypes,
IterationIDs,
} from 'ee/boards/constants';
import epicCreateMutation from 'ee/boards/graphql/epic_create.mutation.graphql';
import actions, { gqlClient } from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types';
......@@ -12,7 +18,9 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mock_data';
import { formatListIssues } from '~/boards/boards_util';
import { formatIssueInput } from 'ee_else_ce/boards/boards_util';
import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import * as commonUtils from '~/lib/utils/common_utils';
......@@ -1390,3 +1398,145 @@ describe('setActiveEpicLabels', () => {
);
});
});
describe('addListNewIssue', () => {
let state;
const iterationCadenceId = 'gid://gitlab/Iterations::Cadence/1';
const baseState = {
boardType: 'group',
fullPath: 'gitlab-org/gitlab',
boardConfig: {
labelIds: [],
},
};
const queryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/1',
iterations: {
nodes: [
{
id: 'gid://gitlab/Iteration/1',
iterationCadence: {
id: iterationCadenceId,
},
},
],
},
},
},
};
const fakeList = {};
beforeEach(() => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
});
describe('without cadenceId', () => {
describe('currentIteration selected in board config', () => {
beforeEach(() => {
state = {
...baseState,
boardConfig: {
iterationId: IterationIDs.CURRENT,
},
};
});
it('adds iterationCadenceId from iteration', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
errors: [],
},
},
});
await actions.addListNewIssue(
{ dispatch: jest.fn(), commit: jest.fn(), state },
{ issueInput: mockIssue, list: fakeList },
);
expect(gqlClient.mutate).toHaveBeenCalledWith({
mutation: issueCreateMutation,
variables: {
input: formatIssueInput(mockIssue, {
...state.boardConfig,
iterationCadenceId,
}),
},
});
});
});
describe('currentIteration not in boardConfig', () => {
beforeEach(() => {
state = {
...baseState,
boardConfig: {
iterationId: null,
},
};
});
it('does not add iterationCadenceId', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
errors: [],
},
},
});
await actions.addListNewIssue(
{ dispatch: jest.fn(), commit: jest.fn(), state },
{ issueInput: mockIssue, list: fakeList },
);
expect(gqlClient.mutate).toHaveBeenCalledWith({
mutation: issueCreateMutation,
variables: {
input: formatIssueInput(mockIssue, state.boardConfig),
},
});
});
});
});
describe('with iterationCadenceId', () => {
beforeEach(() => {
state = {
...baseState,
boardConfig: {
iterationId: IterationIDs.CURRENT,
iterationCadenceId,
},
};
});
it('does not make query for cadence of current iteration', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
errors: [],
},
},
});
await actions.addListNewIssue(
{ dispatch: jest.fn(), commit: jest.fn(), state },
{ issueInput: mockIssue, list: fakeList },
);
expect(gqlClient.query).not.toHaveBeenCalled();
expect(gqlClient.mutate).toHaveBeenCalledWith({
mutation: issueCreateMutation,
variables: {
input: formatIssueInput(mockIssue, state.boardConfig),
},
});
});
});
});
......@@ -20,7 +20,7 @@ import {
formatIssue,
getMoveData,
updateListPosition,
} from '~/boards/boards_util';
} from 'ee_else_ce/boards/boards_util';
import { gqlClient } from '~/boards/graphql';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
......
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