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 { ...@@ -16,24 +16,24 @@ import {
ListTypeTitles, ListTypeTitles,
DraggableItemTypes, DraggableItemTypes,
} from 'ee_else_ce/boards/constants'; } 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 { import {
formatIssueInput,
formatBoardLists, formatBoardLists,
formatListIssues, formatListIssues,
formatListsPageInfo, formatListsPageInfo,
formatIssue, formatIssue,
formatIssueInput,
updateListPosition, updateListPosition,
moveItemListHelper, moveItemListHelper,
getMoveData, getMoveData,
FiltersInfo, FiltersInfo,
filterVariables, 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 { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.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 { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import { import {
EPIC_LANE_BASE_HEIGHT, EPIC_LANE_BASE_HEIGHT,
...@@ -11,6 +15,17 @@ import { ...@@ -11,6 +15,17 @@ import {
EpicFilterType, EpicFilterType,
} from './constants'; } from './constants';
export {
formatBoardLists,
formatListIssues,
formatListsPageInfo,
formatIssue,
updateListPosition,
moveItemListHelper,
getMoveData,
filterVariables,
} from '~/boards/boards_util';
export function getMilestone({ milestone }) { export function getMilestone({ milestone }) {
return milestone || null; return milestone || null;
} }
...@@ -23,6 +38,30 @@ export function fullMilestoneId(milestoneId) { ...@@ -23,6 +38,30 @@ export function fullMilestoneId(milestoneId) {
return `gid://gitlab/Milestone/${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) { export function fullUserId(userId) {
return `gid://gitlab/User/${userId}`; return `gid://gitlab/User/${userId}`;
} }
...@@ -96,6 +135,34 @@ export function formatEpicInput(epicInput, boardConfig) { ...@@ -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) { export function transformBoardConfig(boardConfig) {
const updatedBoardConfig = {}; const updatedBoardConfig = {};
const passedFilterParams = queryToObject(window.location.search, { gatherArrays: true }); 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 { ...@@ -29,6 +29,14 @@ fragment IssueNode on Issue {
milestone { milestone {
...MilestoneFragment ...MilestoneFragment
} }
iteration {
id
title
iterationCadence {
id
title
}
}
labels { labels {
nodes { nodes {
id id
......
...@@ -27,7 +27,7 @@ import { ...@@ -27,7 +27,7 @@ import {
FiltersInfo, FiltersInfo,
} from '../boards_util'; } 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 createEpicBoardListMutation from '../graphql/epic_board_list_create.mutation.graphql';
import epicCreateMutation from '../graphql/epic_create.mutation.graphql'; import epicCreateMutation from '../graphql/epic_create.mutation.graphql';
import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql'; import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
...@@ -35,6 +35,7 @@ import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql'; ...@@ -35,6 +35,7 @@ import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql'; import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql'; import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import subGroupsQuery from '../graphql/sub_groups.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 updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -94,6 +95,46 @@ export { gqlClient }; ...@@ -94,6 +95,46 @@ export { gqlClient };
export default { export default {
...actionsCE, ...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) => { setFilters: ({ commit, dispatch, state: { issuableType } }, filters) => {
if (filters.groupBy === GroupByParamType.epic) { if (filters.groupBy === GroupByParamType.epic) {
dispatch('setEpicSwimlanes'); dispatch('setEpicSwimlanes');
......
...@@ -278,7 +278,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -278,7 +278,7 @@ RSpec.describe 'Scoped issue boards', :js do
context 'iteration' do context 'iteration' do
context 'group with iterations' do context 'group with iterations' do
let_it_be(:cadence) { create(:iterations_cadence, group: group) } 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 context 'board not scoped to iteration' do
it 'sets board to current iteration' do it 'sets board to current iteration' do
...@@ -293,6 +293,29 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -293,6 +293,29 @@ RSpec.describe 'Scoped issue boards', :js do
end end
context 'board scoped to current iteration' do 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 it 'removes current iteration from board' do
create_board_scope('current_iteration', true) create_board_scope('current_iteration', true)
......
...@@ -3,7 +3,9 @@ import { ...@@ -3,7 +3,9 @@ import {
formatListEpics, formatListEpics,
formatEpicListsPageInfo, formatEpicListsPageInfo,
transformBoardConfig, transformBoardConfig,
formatIssueInput,
} from 'ee/boards/boards_util'; } from 'ee/boards/boards_util';
import { IterationIDs } from 'ee/boards/constants';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import { mockLabel } from './mock_data'; import { mockLabel } from './mock_data';
...@@ -105,6 +107,71 @@ describe('formatEpicListsPageInfo', () => { ...@@ -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', () => { describe('transformBoardConfig', () => {
const boardConfig = { const boardConfig = {
milestoneTitle: 'milestone', milestoneTitle: 'milestone',
......
...@@ -2,7 +2,13 @@ import axios from 'axios'; ...@@ -2,7 +2,13 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; 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 epicCreateMutation from 'ee/boards/graphql/epic_create.mutation.graphql';
import actions, { gqlClient } from 'ee/boards/stores/actions'; import actions, { gqlClient } from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types'; import * as types from 'ee/boards/stores/mutation_types';
...@@ -12,7 +18,9 @@ import { TEST_HOST } from 'helpers/test_constants'; ...@@ -12,7 +18,9 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mock_data'; import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mock_data';
import { formatListIssues } from '~/boards/boards_util'; 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 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 * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
...@@ -1390,3 +1398,145 @@ describe('setActiveEpicLabels', () => { ...@@ -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 { ...@@ -20,7 +20,7 @@ import {
formatIssue, formatIssue,
getMoveData, getMoveData,
updateListPosition, updateListPosition,
} from '~/boards/boards_util'; } from 'ee_else_ce/boards/boards_util';
import { gqlClient } from '~/boards/graphql'; import { gqlClient } from '~/boards/graphql';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.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