Commit 41b03ea7 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '230985-allow-users-to-create-a-new-label-list-within-an-epic-board' into 'master'

Add label list to Epic board [RUN-AS-IF-FOSS]

See merge request gitlab-org/gitlab!55011
parents 3aa2b656 efe1beb2
......@@ -45,7 +45,7 @@ export default {
};
},
computed: {
...mapState(['labels', 'labelsLoading']),
...mapState(['labels', 'labelsLoading', 'isEpicBoard']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() {
return this.labels.find(({ id }) => id === this.selectedLabelId);
......@@ -57,7 +57,7 @@ export default {
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
getListByLabel(label) {
if (this.shouldUseGraphQL) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.getListByLabelId(label);
}
return boardsStore.findListByLabelId(label.id);
......@@ -66,7 +66,7 @@ export default {
return Boolean(this.getListByLabel(label));
},
highlight(listId) {
if (this.shouldUseGraphQL) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.highlightList(listId);
} else {
const list = boardsStore.state.lists.find(({ id }) => id === listId);
......@@ -95,7 +95,7 @@ export default {
return;
}
if (this.shouldUseGraphQL) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.createList({ labelId: this.selectedLabelId });
} else {
boardsStore.new({
......@@ -127,6 +127,7 @@ export default {
<template>
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
data-testid="board-add-new-column"
data-qa-selector="board_add_new_list"
>
<div
......
......@@ -128,7 +128,11 @@ export default {
}, flashAnimationDuration);
},
createList: (
createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
},
createIssueList: (
{ state, commit, dispatch, getters },
{ backlog, labelId, milestoneId, assigneeId },
) => {
......@@ -172,7 +176,7 @@ export default {
},
fetchLabels: ({ state, commit, getters }, searchTerm) => {
const { fullPath, boardType } = state;
const { fullPath, boardType, isEpicBoard } = state;
const variables = {
fullPath,
......@@ -191,7 +195,7 @@ export default {
.then(({ data }) => {
let labels = data[boardType]?.labels.nodes;
if (!getters.shouldUseGraphQL) {
if (!getters.shouldUseGraphQL && !isEpicBoard) {
labels = labels.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
......
......@@ -30,6 +30,7 @@
color: var(--gray-500, $gray-500);
}
[data-page$='epic_boards:show'],
.issue-boards-page {
.content-wrapper {
padding-bottom: 0;
......
......@@ -197,7 +197,7 @@
#js-board-epics-swimlanes-toggle
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } }
- if user_can_admin_list
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml)
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml) || board.to_type == "EpicBoard"
.js-create-column-trigger{ data: board_list_data }
- else
= render 'shared/issuable/board_create_list_dropdown', board: board
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
fragment EpicBoardListFragment on EpicList {
id
title
position
listType
label {
...Label
}
}
#import "./epic_board_list.fragment.graphql"
mutation CreateEpicBoardList($boardId: BoardsEpicBoardID!, $backlog: Boolean, $labelId: LabelID) {
epicBoardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
list {
...EpicBoardListFragment
}
errors
}
}
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./epic_board_list.fragment.graphql"
query ListEpics($fullPath: ID!, $boardId: BoardsEpicBoardID!) {
group(fullPath: $fullPath) {
epicBoard(id: $boardId) {
lists {
nodes {
id
title
position
listType
label {
...Label
}
...EpicBoardListFragment
}
}
}
......
......@@ -30,6 +30,7 @@ import {
import { EpicFilterType, IterationFilterType, GroupByParamType } from '../constants';
import epicQuery from '../graphql/epic.query.graphql';
import createEpicBoardListMutation from '../graphql/epic_board_list_create.mutation.graphql';
import epicBoardListsQuery from '../graphql/epic_board_lists.query.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
......@@ -554,4 +555,48 @@ export default {
})
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
createList: ({ state, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
const { isEpicBoard } = state;
if (!isEpicBoard) {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
} else {
dispatch('createEpicList', { backlog, labelId });
}
},
createEpicList: ({ state, commit, dispatch, getters }, { backlog, labelId }) => {
const { boardId } = state;
const existingList = getters.getListByLabelId(labelId);
if (existingList) {
dispatch('highlightList', existingList.id);
return;
}
gqlClient
.mutate({
mutation: createEpicBoardListMutation,
variables: {
boardId: fullEpicBoardId(boardId),
backlog,
labelId,
},
})
.then(({ data }) => {
if (data?.epicBoardListCreate?.errors.length) {
commit(types.CREATE_LIST_FAILURE);
} else {
const list = data.epicBoardListCreate?.list;
dispatch('addList', list);
dispatch('highlightList', list.id);
}
})
.catch((e) => {
commit(types.CREATE_LIST_FAILURE);
throw e;
});
},
};
......@@ -31,4 +31,5 @@ export const SET_FILTERS = 'SET_FILTERS';
export const MOVE_ISSUE = 'MOVE_ISSUE';
export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES';
......@@ -9,6 +9,7 @@ import { mapActions } from 'vuex';
import BoardSidebar from 'ee_component/boards/components/board_sidebar';
import toggleLabels from 'ee_component/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
......@@ -110,6 +111,21 @@ export default () => {
},
});
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
// eslint-disable-next-line no-new
new Vue({
el: createColumnTriggerEl,
components: {
BoardAddNewColumnTrigger,
},
store,
render(createElement) {
return createElement(BoardAddNewColumnTrigger);
},
});
}
toggleLabels();
mountMultipleBoardsSwitcher({
......
......@@ -8,16 +8,19 @@ RSpec.describe 'epic boards', :js do
let_it_be(:epic_board) { create(:epic_board, group: group) }
let_it_be(:label) { create(:group_label, group: group, name: 'Label1') }
let_it_be(:label2) { create(:group_label, group: group, name: 'Label2') }
let_it_be(:label_list) { create(:epic_list, epic_board: epic_board, label: label, position: 0) }
let_it_be(:backlog_list) { create(:epic_list, epic_board: epic_board, list_type: :closed) }
let_it_be(:closed_list) { create(:epic_list, epic_board: epic_board, list_type: :backlog) }
let_it_be(:epic1) { create(:epic, group: group, labels: [label], title: 'Epic1') }
let_it_be(:epic2) { create(:epic, group: group, title: 'Epic2') }
let_it_be(:epic3) { create(:epic, group: group, labels: [label2], title: 'Epic3') }
context 'display epics in board' do
before do
stub_licensed_features(epics: true)
group.add_maintainer(user)
sign_in(user)
visit_epic_boards_page
end
......@@ -34,10 +37,14 @@ RSpec.describe 'epic boards', :js do
end
end
it 'displays one epic in Open list' do
it 'displays two epics in Open list' do
page.within("[data-board-type='backlog']") do
expect(page).to have_selector('.board-card', count: 1)
expect(page).to have_selector('.board-card', count: 2)
page.within(first('.board-card')) do
expect(page).to have_content('Epic3')
end
page.within('.board-card:nth-child(2)') do
expect(page).to have_content('Epic2')
end
end
......@@ -51,6 +58,21 @@ RSpec.describe 'epic boards', :js do
end
end
end
it 'creates new column for label containing labeled issue' do
click_button 'Create list'
wait_for_all_requests
page.within("[data-testid='board-add-new-column']") do
find('label', text: label2.title).click
click_button 'Add'
end
wait_for_all_requests
expect(page).to have_selector('.board', text: label2.title)
expect(find('.board:nth-child(3) .board-card')).to have_content(epic3.title)
end
end
def visit_epic_boards_page
......
......@@ -947,4 +947,112 @@ describe('moveIssue', () => {
done,
);
});
describe.each`
isEpicBoard | dispatchedAction
${false} | ${'createIssueList'}
${true} | ${'createEpicList'}
`('createList', ({ isEpicBoard, dispatchedAction }) => {
it(`should dispatch ${dispatchedAction} action when isEpicBoard is ${isEpicBoard} on state`, async () => {
await testAction({
action: actions.createList,
payload: { backlog: true },
state: { isEpicBoard },
expectedActions: [{ type: dispatchedAction, payload: { backlog: true } }],
});
});
});
describe('createEpicList', () => {
let commit;
let dispatch;
let getters;
beforeEach(() => {
commit = jest.fn();
dispatch = jest.fn();
getters = {
getListByLabelId: jest.fn(),
};
});
it('should dispatch addList action when creating backlog list', async () => {
const backlogList = {
id: 'gid://gitlab/List/1',
listType: 'backlog',
title: 'Open',
position: 0,
};
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
epicBoardListCreate: {
list: backlogList,
errors: [],
},
},
});
await actions.createEpicList({ getters, state, commit, dispatch }, { backlog: true });
expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
});
it('dispatches highlightList after addList has succeeded', async () => {
const list = {
id: 'gid://gitlab/List/1',
listType: 'label',
title: 'Open',
labelId: '4',
};
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
epicBoardListCreate: {
list,
errors: [],
},
},
});
await actions.createEpicList({ getters, state, commit, dispatch }, { labelId: '4' });
expect(dispatch).toHaveBeenCalledWith('addList', list);
expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
});
it('should commit CREATE_LIST_FAILURE mutation when API returns an error', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
epicBoardListCreate: {
list: {},
errors: [{ foo: 'bar' }],
},
},
});
await actions.createEpicList({ getters, state, commit, dispatch }, { backlog: true });
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
});
it('highlights list and does not re-query if it already exists', async () => {
const existingList = {
id: 'gid://gitlab/List/1',
listType: 'label',
title: 'Some label',
position: 1,
};
getters = {
getListByLabelId: jest.fn().mockReturnValue(existingList),
};
await actions.createEpicList({ getters, state, commit, dispatch }, { backlog: true });
expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(commit).not.toHaveBeenCalled();
});
});
});
......@@ -210,6 +210,16 @@ describe('fetchIssueLists', () => {
});
describe('createList', () => {
it('should dispatch createIssueList action', () => {
testAction({
action: actions.createList,
payload: { backlog: true },
expectedActions: [{ type: 'createIssueList', payload: { backlog: true } }],
});
});
});
describe('createIssueList', () => {
let commit;
let dispatch;
let getters;
......@@ -249,7 +259,7 @@ describe('createList', () => {
}),
);
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
});
......@@ -271,7 +281,7 @@ describe('createList', () => {
},
});
await actions.createList({ getters, state, commit, dispatch }, { labelId: '4' });
await actions.createIssueList({ getters, state, commit, dispatch }, { labelId: '4' });
expect(dispatch).toHaveBeenCalledWith('addList', list);
expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
......@@ -289,7 +299,7 @@ describe('createList', () => {
}),
);
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
});
......@@ -306,7 +316,7 @@ describe('createList', () => {
getListByLabelId: jest.fn().mockReturnValue(existingList),
};
await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
expect(dispatch).toHaveBeenCalledTimes(1);
......
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