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 { ...@@ -45,7 +45,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['labels', 'labelsLoading']), ...mapState(['labels', 'labelsLoading', 'isEpicBoard']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']), ...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() { selectedLabel() {
return this.labels.find(({ id }) => id === this.selectedLabelId); return this.labels.find(({ id }) => id === this.selectedLabelId);
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
methods: { methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']), ...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
getListByLabel(label) { getListByLabel(label) {
if (this.shouldUseGraphQL) { if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.getListByLabelId(label); return this.getListByLabelId(label);
} }
return boardsStore.findListByLabelId(label.id); return boardsStore.findListByLabelId(label.id);
...@@ -66,7 +66,7 @@ export default { ...@@ -66,7 +66,7 @@ export default {
return Boolean(this.getListByLabel(label)); return Boolean(this.getListByLabel(label));
}, },
highlight(listId) { highlight(listId) {
if (this.shouldUseGraphQL) { if (this.shouldUseGraphQL || this.isEpicBoard) {
this.highlightList(listId); this.highlightList(listId);
} else { } else {
const list = boardsStore.state.lists.find(({ id }) => id === listId); const list = boardsStore.state.lists.find(({ id }) => id === listId);
...@@ -95,7 +95,7 @@ export default { ...@@ -95,7 +95,7 @@ export default {
return; return;
} }
if (this.shouldUseGraphQL) { if (this.shouldUseGraphQL || this.isEpicBoard) {
this.createList({ labelId: this.selectedLabelId }); this.createList({ labelId: this.selectedLabelId });
} else { } else {
boardsStore.new({ boardsStore.new({
...@@ -127,6 +127,7 @@ export default { ...@@ -127,6 +127,7 @@ export default {
<template> <template>
<div <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" 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" data-qa-selector="board_add_new_list"
> >
<div <div
......
...@@ -128,7 +128,11 @@ export default { ...@@ -128,7 +128,11 @@ export default {
}, flashAnimationDuration); }, flashAnimationDuration);
}, },
createList: ( createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
},
createIssueList: (
{ state, commit, dispatch, getters }, { state, commit, dispatch, getters },
{ backlog, labelId, milestoneId, assigneeId }, { backlog, labelId, milestoneId, assigneeId },
) => { ) => {
...@@ -172,7 +176,7 @@ export default { ...@@ -172,7 +176,7 @@ export default {
}, },
fetchLabels: ({ state, commit, getters }, searchTerm) => { fetchLabels: ({ state, commit, getters }, searchTerm) => {
const { fullPath, boardType } = state; const { fullPath, boardType, isEpicBoard } = state;
const variables = { const variables = {
fullPath, fullPath,
...@@ -191,7 +195,7 @@ export default { ...@@ -191,7 +195,7 @@ export default {
.then(({ data }) => { .then(({ data }) => {
let labels = data[boardType]?.labels.nodes; let labels = data[boardType]?.labels.nodes;
if (!getters.shouldUseGraphQL) { if (!getters.shouldUseGraphQL && !isEpicBoard) {
labels = labels.map((label) => ({ labels = labels.map((label) => ({
...label, ...label,
id: getIdFromGraphQLId(label.id), id: getIdFromGraphQLId(label.id),
......
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
color: var(--gray-500, $gray-500); color: var(--gray-500, $gray-500);
} }
[data-page$='epic_boards:show'],
.issue-boards-page { .issue-boards-page {
.content-wrapper { .content-wrapper {
padding-bottom: 0; padding-bottom: 0;
......
...@@ -197,7 +197,7 @@ ...@@ -197,7 +197,7 @@
#js-board-epics-swimlanes-toggle #js-board-epics-swimlanes-toggle
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } } .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 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 } .js-create-column-trigger{ data: board_list_data }
- else - else
= render 'shared/issuable/board_create_list_dropdown', board: board = 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!) { query ListEpics($fullPath: ID!, $boardId: BoardsEpicBoardID!) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
epicBoard(id: $boardId) { epicBoard(id: $boardId) {
lists { lists {
nodes { nodes {
id ...EpicBoardListFragment
title
position
listType
label {
...Label
}
} }
} }
} }
......
...@@ -30,6 +30,7 @@ import { ...@@ -30,6 +30,7 @@ import {
import { EpicFilterType, IterationFilterType, GroupByParamType } from '../constants'; import { EpicFilterType, IterationFilterType, GroupByParamType } from '../constants';
import epicQuery from '../graphql/epic.query.graphql'; 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 epicBoardListsQuery from '../graphql/epic_board_lists.query.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql'; import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql'; import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
...@@ -554,4 +555,48 @@ export default { ...@@ -554,4 +555,48 @@ export default {
}) })
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE)); .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'; ...@@ -31,4 +31,5 @@ export const SET_FILTERS = 'SET_FILTERS';
export const MOVE_ISSUE = 'MOVE_ISSUE'; export const MOVE_ISSUE = 'MOVE_ISSUE';
export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS'; export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE'; 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'; export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES';
...@@ -9,6 +9,7 @@ import { mapActions } from 'vuex'; ...@@ -9,6 +9,7 @@ import { mapActions } from 'vuex';
import BoardSidebar from 'ee_component/boards/components/board_sidebar'; import BoardSidebar from 'ee_component/boards/components/board_sidebar';
import toggleLabels from 'ee_component/boards/toggle_labels'; 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 BoardContent from '~/boards/components/board_content.vue';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue'; import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher'; import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
...@@ -110,6 +111,21 @@ export default () => { ...@@ -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(); toggleLabels();
mountMultipleBoardsSwitcher({ mountMultipleBoardsSwitcher({
......
...@@ -8,16 +8,19 @@ RSpec.describe 'epic boards', :js do ...@@ -8,16 +8,19 @@ RSpec.describe 'epic boards', :js do
let_it_be(:epic_board) { create(:epic_board, group: group) } let_it_be(:epic_board) { create(:epic_board, group: group) }
let_it_be(:label) { create(:group_label, group: group, name: 'Label1') } 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(: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(: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(: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(:epic1) { create(:epic, group: group, labels: [label], title: 'Epic1') }
let_it_be(:epic2) { create(:epic, group: group, title: 'Epic2') } 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 context 'display epics in board' do
before do before do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
group.add_maintainer(user)
sign_in(user) sign_in(user)
visit_epic_boards_page visit_epic_boards_page
end end
...@@ -34,10 +37,14 @@ RSpec.describe 'epic boards', :js do ...@@ -34,10 +37,14 @@ RSpec.describe 'epic boards', :js do
end end
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 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 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') expect(page).to have_content('Epic2')
end end
end end
...@@ -51,6 +58,21 @@ RSpec.describe 'epic boards', :js do ...@@ -51,6 +58,21 @@ RSpec.describe 'epic boards', :js do
end end
end 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 end
def visit_epic_boards_page def visit_epic_boards_page
......
...@@ -947,4 +947,112 @@ describe('moveIssue', () => { ...@@ -947,4 +947,112 @@ describe('moveIssue', () => {
done, 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', () => { ...@@ -210,6 +210,16 @@ describe('fetchIssueLists', () => {
}); });
describe('createList', () => { 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 commit;
let dispatch; let dispatch;
let getters; let getters;
...@@ -249,7 +259,7 @@ describe('createList', () => { ...@@ -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); expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
}); });
...@@ -271,7 +281,7 @@ describe('createList', () => { ...@@ -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('addList', list);
expect(dispatch).toHaveBeenCalledWith('highlightList', list.id); expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
...@@ -289,7 +299,7 @@ describe('createList', () => { ...@@ -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); expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
}); });
...@@ -306,7 +316,7 @@ describe('createList', () => { ...@@ -306,7 +316,7 @@ describe('createList', () => {
getListByLabelId: jest.fn().mockReturnValue(existingList), 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).toHaveBeenCalledWith('highlightList', existingList.id);
expect(dispatch).toHaveBeenCalledTimes(1); 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