Commit 58ab625f authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'psiterations' into 'master'

Add iteration to new board new list form

See merge request gitlab-org/gitlab!55500
parents 6a46a8f1 39130866
......@@ -104,6 +104,9 @@ export default () => {
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
},
store,
apolloProvider,
......
......@@ -134,7 +134,7 @@ export default {
createIssueList: (
{ state, commit, dispatch, getters },
{ backlog, labelId, milestoneId, assigneeId },
{ backlog, labelId, milestoneId, assigneeId, iterationId },
) => {
const { boardId } = state;
......@@ -154,6 +154,7 @@ export default {
labelId,
milestoneId,
assigneeId,
iterationId,
},
})
.then(({ data }) => {
......
......@@ -575,7 +575,7 @@ const boardsStore = {
},
saveList(list) {
const entity = list.label || list.assignee || list.milestone;
const entity = list.label || list.assignee || list.milestone || list.iteration;
let entityType = '';
if (list.label) {
entityType = 'label_id';
......@@ -583,6 +583,8 @@ const boardsStore = {
entityType = 'assignee_id';
} else if (IS_EE && list.milestone) {
entityType = 'milestone_id';
} else if (IS_EE && list.iteration) {
entityType = 'iteration_id';
}
return this.createList(entity.id, entityType)
......
......@@ -17,4 +17,10 @@ fragment BoardListFragment on BoardList {
webPath
description
}
iteration {
id
title
webPath
description
}
}
......@@ -5,6 +5,7 @@ mutation CreateBoardList(
$backlog: Boolean
$labelId: LabelID
$milestoneId: MilestoneID
$iterationId: IterationID
$assigneeId: UserID
) {
boardListCreate(
......@@ -13,6 +14,7 @@ mutation CreateBoardList(
backlog: $backlog
labelId: $labelId
milestoneId: $milestoneId
iterationId: $iterationId
assigneeId: $assigneeId
}
) {
......
query GroupBoardIterations($fullPath: ID!, $title: String) {
group(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}
query ProjectBoardIterations($fullPath: ID!, $title: String) {
project(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}
......@@ -35,6 +35,7 @@ import epicBoardListsQuery from '../graphql/epic_board_lists.query.graphql';
import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
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';
......@@ -42,6 +43,7 @@ import issueSetWeightMutation from '../graphql/issue_set_weight.mutation.graphql
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import projectBoardAssigneesQuery from '../graphql/project_board_assignees.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
......@@ -648,6 +650,50 @@ export default {
});
},
fetchIterations({ state, commit }, title) {
commit(types.RECEIVE_ITERATIONS_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
title,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardIterationsQuery;
}
if (boardType === BoardType.group) {
query = groupBoardIterationsQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const iterations = data[boardType]?.iterations.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
})
.catch((e) => {
commit(types.RECEIVE_ITERATIONS_FAILURE);
throw e;
});
},
fetchAssignees({ state, commit }, search) {
commit(types.RECEIVE_ASSIGNEES_REQUEST);
......@@ -694,9 +740,12 @@ export default {
});
},
createList: ({ getters, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
createList: (
{ getters, dispatch },
{ backlog, labelId, milestoneId, assigneeId, iterationId },
) => {
if (!getters.isEpicBoard) {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId, iterationId });
} else {
dispatch('createEpicList', { backlog, labelId });
}
......
......@@ -38,6 +38,9 @@ export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES'
export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
export const RECEIVE_ASSIGNEES_REQUEST = 'RECEIVE_ASSIGNEES_REQUEST';
export const RECEIVE_ASSIGNEES_SUCCESS = 'RECEIVE_ASSIGNEES_SUCCESS';
export const RECEIVE_ASSIGNEES_FAILURE = 'RECEIVE_ASSIGNEES_FAILURE';
......@@ -233,6 +233,20 @@ export default {
state.error = __('Failed to load milestones.');
},
[mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
state.iterationsLoading = true;
},
[mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
state.iterations = iterations;
state.iterationsLoading = false;
},
[mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
state.iterationsLoading = false;
state.error = __('Failed to load iterations.');
},
[mutationTypes.RECEIVE_ASSIGNEES_REQUEST](state) {
state.assigneesLoading = true;
},
......
......@@ -14,6 +14,8 @@ export default () => ({
epicsFlags: {},
milestones: [],
milestonesLoading: false,
iterations: [],
iterationsLoading: false,
assignees: [],
assigneesLoading: false,
});
......@@ -26,6 +26,9 @@ module EE
labels: board.labels.to_json(only: [:id, :title, :color, :text_color] ),
board_weight: board.weight,
weight_feature_available: current_board_parent.feature_available?(:issue_weights).to_s,
milestone_lists_available: current_board_parent.feature_available?(:board_milestone_lists).to_s,
assignee_lists_available: current_board_parent.feature_available?(:board_assignee_lists).to_s,
iteration_lists_available: current_board_parent.feature_available?(:board_iteration_lists).to_s,
show_promotion: show_feature_promotion,
scoped_labels: current_board_parent.feature_available?(:scoped_labels)&.to_s
}
......
......@@ -12,11 +12,13 @@ RSpec.describe 'User adds milestone lists', :js do
let_it_be(:user) { create(:user) }
let_it_be(:milestone) { create(:milestone, group: group) }
let_it_be(:iteration) { create(:iteration, group: group) }
let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
let_it_be(:issue_with_milestone) { create(:issue, project: project, milestone: milestone) }
let_it_be(:issue_with_assignee) { create(:issue, project: project, assignees: [user]) }
let_it_be(:issue_with_iteration) { create(:issue, project: project, iteration: iteration) }
before_all do
project.add_maintainer(user)
......@@ -34,7 +36,8 @@ RSpec.describe 'User adds milestone lists', :js do
before do
stub_licensed_features(
board_milestone_lists: true,
board_assignee_lists: true
board_assignee_lists: true,
board_iteration_lists: true
)
sign_in(user)
......@@ -67,6 +70,45 @@ RSpec.describe 'User adds milestone lists', :js do
expect(page).to have_selector('.board', text: user.name)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue_with_assignee.title)
end
it 'creates iteration column' do
add_list('Iteration', iteration.title)
expect(page).to have_selector('.board', text: iteration.title)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue_with_iteration.title)
end
end
describe 'without a license' do
before do
stub_licensed_features(
board_milestone_lists: false,
board_assignee_lists: false,
board_iteration_lists: false
)
sign_in(user)
stub_feature_flags(
board_new_list: true
)
visit project_board_path(project, project_board)
wait_for_all_requests
end
it 'does not show other list types' do
click_button 'Create list'
wait_for_all_requests
page.within(find("[data-testid='board-add-new-column']")) do
expect(page).not_to have_text('Iteration')
expect(page).not_to have_text('Assignee')
expect(page).not_to have_text('Milestone')
expect(page).not_to have_text('List type')
end
end
end
def add_list(list_type, title)
......
import { GlAvatarLabeled, GlSearchBoxByType, GlFormSelect } from '@gitlab/ui';
import { GlAvatarLabeled, GlSearchBoxByType, GlFormRadio, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardAddNewColumn from 'ee/boards/components/board_add_new_column.vue';
import BoardAddNewColumn, { listTypeInfo } from 'ee/boards/components/board_add_new_column.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants';
import defaultState from '~/boards/stores/state';
import { mockAssignees, mockLists } from '../mock_data';
import { mockAssignees, mockLists, mockIterations } from '../mock_data';
const mockLabelList = mockLists[1];
......@@ -32,6 +32,7 @@ describe('BoardAddNewColumn', () => {
selectedId,
labels = [],
assignees = [],
iterations = [],
getListByTypeId = jest.fn(),
actions = {},
} = {}) => {
......@@ -61,10 +62,15 @@ describe('BoardAddNewColumn', () => {
labelsLoading: false,
assignees,
assigneesLoading: false,
iterations,
iterationsLoading: false,
},
}),
provide: {
scopedLabelsAvailable: true,
milestoneListsAvailable: true,
assigneeListsAvailable: true,
iterationListsAvailable: true,
},
}),
);
......@@ -175,9 +181,9 @@ describe('BoardAddNewColumn', () => {
it('sets assignee placeholder text in form', async () => {
expect(findForm().props()).toMatchObject({
formDescription: BoardAddNewColumn.i18n.assigneeListDescription,
searchLabel: BoardAddNewColumn.i18n.selectAssignee,
searchPlaceholder: BoardAddNewColumn.i18n.searchAssignees,
formDescription: listTypeInfo.assignee.formDescription,
searchLabel: listTypeInfo.assignee.searchLabel,
searchPlaceholder: listTypeInfo.assignee.searchPlaceholder,
});
});
......@@ -193,4 +199,35 @@ describe('BoardAddNewColumn', () => {
});
});
});
describe('iteration list', () => {
beforeEach(async () => {
mountComponent({
iterations: mockIterations,
actions: {
fetchIterations: jest.fn(),
},
});
listTypeSelect().vm.$emit('change', ListType.iteration);
await nextTick();
});
it('sets iteration placeholder text in form', async () => {
expect(findForm().props()).toMatchObject({
formDescription: listTypeInfo.iteration.formDescription,
searchLabel: listTypeInfo.iteration.searchLabel,
searchPlaceholder: listTypeInfo.iteration.searchPlaceholder,
});
});
it('shows list of iterations', () => {
const itemList = wrapper.findAllComponents(GlFormRadio);
expect(itemList).toHaveLength(mockIterations.length);
expect(itemList.at(0).attributes('value')).toBe(mockIterations[0].id);
expect(itemList.at(1).attributes('value')).toBe(mockIterations[1].id);
});
});
});
......@@ -104,6 +104,17 @@ export const mockMilestones = [
},
];
export const mockIterations = [
{
id: 'gid://gitlab/Iteration/1',
title: 'Iteration 1',
},
{
id: 'gid://gitlab/Iteration/2',
title: 'Iteration 2',
},
];
const labels = [
{
id: 'gid://gitlab/GroupLabel/5',
......
......@@ -1270,6 +1270,77 @@ describe('fetchMilestones', () => {
});
});
describe('fetchIterations', () => {
const queryResponse = {
data: {
group: {
iterations: {
nodes: mockMilestones,
},
},
},
};
const queryErrors = {
data: {
group: {
errors: ['You cannot view these iterations'],
iterations: {},
},
},
};
function createStore({
state = {
boardType: 'group',
fullPath: 'gitlab-org/gitlab',
iterations: [],
iterationsLoading: false,
},
} = {}) {
return new Vuex.Store({
state,
mutations,
});
}
it('sets iterationsLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
actions.fetchIterations(store);
expect(store.state.iterationsLoading).toBe(true);
});
describe('success', () => {
it('sets state.iterations from query result', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
await actions.fetchIterations(store);
expect(store.state.iterationsLoading).toBe(false);
expect(store.state.iterations).toBe(mockMilestones);
});
});
describe('failure', () => {
it('throws an error and displays an error message', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors);
const store = createStore();
await expect(actions.fetchIterations(store)).rejects.toThrow();
expect(store.state.iterationsLoading).toBe(false);
expect(store.state.error).toBe('Failed to load iterations.');
});
});
});
describe('fetchAssignees', () => {
const queryResponse = {
data: {
......
......@@ -3629,6 +3629,9 @@ msgstr ""
msgid "An issue title is required"
msgstr ""
msgid "An iteration list displays issues in the selected iteration."
msgstr ""
msgid "An unauthenticated user"
msgstr ""
......@@ -12709,6 +12712,9 @@ msgstr ""
msgid "Failed to load groups, users and deploy keys."
msgstr ""
msgid "Failed to load iterations."
msgstr ""
msgid "Failed to load labels. Please try again."
msgstr ""
......@@ -26538,6 +26544,9 @@ msgstr ""
msgid "Search forks"
msgstr ""
msgid "Search iterations"
msgstr ""
msgid "Search labels"
msgstr ""
......
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