diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index c8cc94c4d00689b896993627850589179fb96369..901461ae603f846172cffaa20110d029081323b2 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -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'; diff --git a/ee/app/assets/javascripts/boards/boards_util.js b/ee/app/assets/javascripts/boards/boards_util.js index 0e218232735648eadc93b0444fba729dd0ccc0dc..d2a741c5bb7943da9ca08e58834d82691a3a3834 100644 --- a/ee/app/assets/javascripts/boards/boards_util.js +++ b/ee/app/assets/javascripts/boards/boards_util.js @@ -1,4 +1,8 @@ -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 }); diff --git a/ee/app/assets/javascripts/boards/graphql/board_current_iteration.query.graphql b/ee/app/assets/javascripts/boards/graphql/board_current_iteration.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..95bb70645f1a96bb7e8ff7cf35f2a2fac46b1233 --- /dev/null +++ b/ee/app/assets/javascripts/boards/graphql/board_current_iteration.query.graphql @@ -0,0 +1,25 @@ +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 + } + } + } + } +} diff --git a/ee/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/ee/app/assets/javascripts/boards/graphql/issue.fragment.graphql index 8f2249f0f86401ea02c3b3b84fd3ffe52b197020..bcbe322711f3990580df949b7029423436bf67f0 100644 --- a/ee/app/assets/javascripts/boards/graphql/issue.fragment.graphql +++ b/ee/app/assets/javascripts/boards/graphql/issue.fragment.graphql @@ -29,6 +29,14 @@ fragment IssueNode on Issue { milestone { ...MilestoneFragment } + iteration { + id + title + iterationCadence { + id + title + } + } labels { nodes { id diff --git a/ee/app/assets/javascripts/boards/stores/actions.js b/ee/app/assets/javascripts/boards/stores/actions.js index 44b9610d4215dc89db1a52e8c6549a7baeb69699..b59a8a898f1983de44db687ba4d3344ded650f2a 100644 --- a/ee/app/assets/javascripts/boards/stores/actions.js +++ b/ee/app/assets/javascripts/boards/stores/actions.js @@ -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'); diff --git a/ee/spec/features/boards/scoped_issue_board_spec.rb b/ee/spec/features/boards/scoped_issue_board_spec.rb index fa1ee9b19bdb370b30758ac7bb6f2e1e3f14023e..21cc0eb5b3fb67f55e0c4790722fc82e68d724ed 100644 --- a/ee/spec/features/boards/scoped_issue_board_spec.rb +++ b/ee/spec/features/boards/scoped_issue_board_spec.rb @@ -278,7 +278,7 @@ 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 @@ 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) diff --git a/ee/spec/frontend/boards/boards_util_spec.js b/ee/spec/frontend/boards/boards_util_spec.js index e143b2714ec9b4da3994b8744d5ed9e3ae4e9245..f9d4dde1826194101b8098b71a9d250960b61a49 100644 --- a/ee/spec/frontend/boards/boards_util_spec.js +++ b/ee/spec/frontend/boards/boards_util_spec.js @@ -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', diff --git a/ee/spec/frontend/boards/stores/actions_spec.js b/ee/spec/frontend/boards/stores/actions_spec.js index bddea29f5432692fac3aba4938431cdd0a8fc2a3..e23edfeb12b137630505424f5e85855df7ba6ec7 100644 --- a/ee/spec/frontend/boards/stores/actions_spec.js +++ b/ee/spec/frontend/boards/stores/actions_spec.js @@ -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), + }, + }); + }); + }); +}); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index f580469b072e54f9b8988e3f7f942ef2136e2a7a..08b4744ed6ec4531907a81b79464b5e76aa0ac19 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -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';