Commit efe1beb2 authored by Florie Guibert's avatar Florie Guibert Committed by Kushal Pandya

Add label list to Epic board

Add new label list to epic board using add column
parent b420142b
......@@ -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