Commit e52b9cf3 authored by Florie Guibert's avatar Florie Guibert

Swimlanes - Add issue to list

Add VueX action to add issue to list for Swimlanes
parent b814bc73
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util'; import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue'; import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue'; import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'BoardNewIssue', name: 'BoardNewIssue',
...@@ -13,6 +15,7 @@ export default { ...@@ -13,6 +15,7 @@ export default {
ProjectSelect, ProjectSelect,
GlButton, GlButton,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
groupId: { groupId: {
type: Number, type: Number,
...@@ -32,6 +35,7 @@ export default { ...@@ -32,6 +35,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isSwimlanesOn']),
disabled() { disabled() {
if (this.groupId) { if (this.groupId) {
return this.title === '' || !this.selectedProject.name; return this.title === '' || !this.selectedProject.name;
...@@ -44,6 +48,7 @@ export default { ...@@ -44,6 +48,7 @@ export default {
eventHub.$on('setSelectedProject', this.setSelectedProject); eventHub.$on('setSelectedProject', this.setSelectedProject);
}, },
methods: { methods: {
...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
if (this.title.trim() === '') return Promise.resolve(); if (this.title.trim() === '') return Promise.resolve();
...@@ -70,21 +75,31 @@ export default { ...@@ -70,21 +75,31 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
this.addListIssue({ list: this.list, issue, position: 0 });
}
return this.list return this.list
.newIssue(issue) .newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
boardsStore.setIssueDetail(issue); if (!this.glFeatures.boardsWithSwimlanes || !this.isSwimlanesOn) {
boardsStore.setListDetail(this.list); boardsStore.setIssueDetail(issue);
boardsStore.setListDetail(this.list);
}
}) })
.catch(() => { .catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
// Remove the issue // Remove the issue
this.list.removeIssue(issue); if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
this.addListIssueFailure({ list: this.list, issue });
} else {
this.list.removeIssue(issue);
}
// Show error message // Show error message
this.error = true; this.error = true;
......
...@@ -235,6 +235,14 @@ export default { ...@@ -235,6 +235,14 @@ export default {
notImplemented(); notImplemented();
}, },
addListIssue: ({ commit }, { list, issue, position }) => {
commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
},
addListIssueFailure: ({ commit }, { list, issue }) => {
commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
},
fetchBacklog: () => { fetchBacklog: () => {
notImplemented(); notImplemented();
}, },
......
...@@ -15,6 +15,7 @@ import { ...@@ -15,6 +15,7 @@ import {
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import { ListType } from '../constants'; import { ListType } from '../constants';
import IssueProject from '../models/project'; import IssueProject from '../models/project';
...@@ -303,7 +304,7 @@ const boardsStore = { ...@@ -303,7 +304,7 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) { onNewListIssueResponse(list, issue, data) {
issue.refreshData(data); issue.refreshData(data);
if (list.issuesSize > 1) { if (!gon.features.boardsWithSwimlanes && list.issuesSize > 1) {
const moveBeforeId = list.issues[1].id; const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId); this.moveIssue(issue.id, null, null, null, moveBeforeId);
} }
...@@ -710,6 +711,10 @@ const boardsStore = { ...@@ -710,6 +711,10 @@ const boardsStore = {
}, },
newIssue(id, issue) { newIssue(id, issue) {
if (typeof id === 'string') {
id = getIdFromGraphQLId(id);
}
return axios.post(this.generateIssuesPath(id), { return axios.post(this.generateIssuesPath(id), {
issue, issue,
}); });
......
...@@ -24,6 +24,8 @@ export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR'; ...@@ -24,6 +24,8 @@ export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
......
import Vue from 'vue'; import Vue from 'vue';
import { sortBy } from 'lodash'; import { sortBy, pull } from 'lodash';
import * as mutationTypes from './mutation_types'; import * as mutationTypes from './mutation_types';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -8,6 +8,10 @@ const notImplemented = () => { ...@@ -8,6 +8,10 @@ const notImplemented = () => {
throw new Error('Not implemented!'); throw new Error('Not implemented!');
}; };
const removeIssueFromList = (state, listId, issueId) => {
Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
};
export default { export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, showPromotion, ...endpoints } = data; const { boardType, disabled, showPromotion, ...endpoints } = data;
...@@ -131,6 +135,18 @@ export default { ...@@ -131,6 +135,18 @@ export default {
notImplemented(); notImplemented();
}, },
[mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
const listIssues = state.issuesByListId[list.id];
listIssues.splice(position, 0, issue.id);
Vue.set(state.issuesByListId, list.id, listIssues);
Vue.set(state.issues, issue.id, issue);
},
[mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
state.error = __('An error occurred while creating the issue. Please try again.');
removeIssueFromList(state, list.id, issue.id);
},
[mutationTypes.SET_CURRENT_PAGE]: () => { [mutationTypes.SET_CURRENT_PAGE]: () => {
notImplemented(); notImplemented();
}, },
......
...@@ -49,9 +49,15 @@ export default { ...@@ -49,9 +49,15 @@ export default {
}, },
computed: { computed: {
...mapState(['epics', 'isLoadingIssues']), ...mapState(['epics', 'isLoadingIssues']),
...mapGetters(['unassignedIssues']), ...mapGetters(['getUnassignedIssues']),
unassignedIssues() {
return listId => this.getUnassignedIssues(listId);
},
unassignedIssuesCount() { unassignedIssuesCount() {
return this.lists.reduce((total, list) => total + this.unassignedIssues(list.id).length, 0); return this.lists.reduce(
(total, list) => total + this.getUnassignedIssues(list.id).length,
0,
);
}, },
unassignedIssuesCountTooltipText() { unassignedIssuesCountTooltipText() {
return n__(`%d unassigned issue`, `%d unassigned issues`, this.unassignedIssuesCount); return n__(`%d unassigned issue`, `%d unassigned issues`, this.unassignedIssuesCount);
......
...@@ -12,7 +12,7 @@ export default { ...@@ -12,7 +12,7 @@ export default {
return getters.getIssues(listId).filter(issue => issue.epic && issue.epic.id === epicId); return getters.getIssues(listId).filter(issue => issue.epic && issue.epic.id === epicId);
}, },
unassignedIssues: (state, getters) => listId => { getUnassignedIssues: (state, getters) => listId => {
return getters.getIssues(listId).filter(i => i.epic === null); return getters.getIssues(listId).filter(i => Boolean(i.epic) === false);
}, },
}; };
...@@ -21,18 +21,14 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -21,18 +21,14 @@ RSpec.describe 'epics swimlanes', :js do
let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) } let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) }
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) } let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
context 'switch to swimlanes view' do before do
before do stub_licensed_features(epics: true)
stub_licensed_features(epics: true) sign_in(user)
sign_in(user) visit_board_page
visit_board_page select_epics
end
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
end
context 'switch to swimlanes view' do
it 'displays epics swimlanes when selecting Epic in Group by dropdown' do it 'displays epics swimlanes when selecting Epic in Group by dropdown' do
expect(page).to have_css('.board-swimlanes') expect(page).to have_css('.board-swimlanes')
...@@ -47,8 +43,70 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -47,8 +43,70 @@ RSpec.describe 'epics swimlanes', :js do
end end
end end
context 'add issue to swimlanes list' do
it 'displays new issue button' do
expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1)
end
it 'shows form in unassigned issues lane when clicking button' do
page.within(first('.board')) do
find('.issue-count-badge-add-button').click
end
page.within("[data-testid='board-lane-unassigned-issues']") do
expect(page).to have_selector('.board-new-issue-form')
end
end
it 'hides form when clicking cancel' do
page.within(first('.board')) do
find('.issue-count-badge-add-button').click
end
page.within("[data-testid='board-lane-unassigned-issues']") do
expect(page).to have_selector('.board-new-issue-form')
click_button 'Cancel'
expect(page).not_to have_selector('.board-new-issue-form')
end
end
it 'creates new issue in unassigned issues lane' do
page.within(first('.board')) do
find('.issue-count-badge-add-button').click
end
page.within(first('.board-new-issue-form')) do
find('.form-control').set('bug')
click_button 'Submit issue'
end
wait_for_requests
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('3')
end
page.within("[data-testid='board-lane-unassigned-issues']") do
page.within(first('.board-card')) do
issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference)
end
end
end
end
def visit_board_page def visit_board_page
visit project_boards_path(project) visit project_boards_path(project)
wait_for_requests wait_for_requests
end end
def select_epics
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
end
end end
...@@ -37,13 +37,12 @@ describe('EE Boards Store Getters', () => { ...@@ -37,13 +37,12 @@ describe('EE Boards Store Getters', () => {
}); });
}); });
describe('unassignedIssues', () => { describe('getUnassignedIssues', () => {
it('returns issues for a given listId and epicId', () => { it('returns issues not assigned to an epic for a given listId', () => {
const getIssues = () => [mockIssue, mockIssue3, mockIssue4]; const getIssues = () => [mockIssue, mockIssue3, mockIssue4];
expect(getters.unassignedIssues(boardsState, { getIssues })('gid://gitlab/List/1')).toEqual([ expect(
mockIssue3, getters.getUnassignedIssues(boardsState, { getIssues })('gid://gitlab/List/1'),
mockIssue4, ).toEqual([mockIssue3, mockIssue4]);
]);
}); });
}); });
}); });
...@@ -2618,6 +2618,9 @@ msgstr "" ...@@ -2618,6 +2618,9 @@ msgstr ""
msgid "An error occurred while committing your changes." msgid "An error occurred while committing your changes."
msgstr "" msgstr ""
msgid "An error occurred while creating the issue. Please try again."
msgstr ""
msgid "An error occurred while creating the list. Please try again." msgid "An error occurred while creating the list. Please try again."
msgstr "" msgstr ""
......
...@@ -312,7 +312,7 @@ describe('boardsStore', () => { ...@@ -312,7 +312,7 @@ describe('boardsStore', () => {
}); });
describe('newIssue', () => { describe('newIssue', () => {
const id = 'not-creative'; const id = 1;
const issue = { some: 'issue data' }; const issue = { some: 'issue data' };
const url = `${endpoints.listsEndpoint}/${id}/issues`; const url = `${endpoints.listsEndpoint}/${id}/issues`;
const expectedRequest = expect.objectContaining({ const expectedRequest = expect.objectContaining({
......
...@@ -184,6 +184,7 @@ describe('List model', () => { ...@@ -184,6 +184,7 @@ describe('List model', () => {
}), }),
); );
list.issues = []; list.issues = [];
global.gon.features = { boardsWithSwimlanes: false };
}); });
it('adds new issue to top of list', done => { it('adds new issue to top of list', done => {
......
...@@ -117,6 +117,29 @@ export const mockIssue = { ...@@ -117,6 +117,29 @@ export const mockIssue = {
], ],
}; };
export const mockIssue2 = {
title: 'Planning',
id: 2,
iid: 2,
confidential: false,
labels: [
{
id: 1,
title: 'plan',
color: 'blue',
description: 'planning',
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
};
export const BoardsMockData = { export const BoardsMockData = {
GET: { GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': { '/test/-/boards/1/lists/300/issues?id=300&page=1': {
......
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { mockListsWithModel } from '../mock_data'; import { mockListsWithModel, mockLists, mockIssue } from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions'; import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import { inactiveId, ListType } from '~/boards/constants'; import { inactiveId, ListType } from '~/boards/constants';
...@@ -236,6 +236,43 @@ describe('createNewIssue', () => { ...@@ -236,6 +236,43 @@ describe('createNewIssue', () => {
expectNotImplemented(actions.createNewIssue); expectNotImplemented(actions.createNewIssue);
}); });
describe('addListIssue', () => {
it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
const payload = {
list: mockLists[0],
issue: mockIssue,
position: 0,
};
testAction(
actions.addListIssue,
payload,
{},
[{ type: types.ADD_ISSUE_TO_LIST, payload }],
[],
done,
);
});
});
describe('addListIssueFailure', () => {
it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
const payload = {
list: mockLists[0],
issue: mockIssue,
};
testAction(
actions.addListIssueFailure,
payload,
{},
[{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }],
[],
done,
);
});
});
describe('fetchBacklog', () => { describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog); expectNotImplemented(actions.fetchBacklog);
}); });
......
import mutations from '~/boards/stores/mutations'; import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state'; import defaultState from '~/boards/stores/state';
import { listObj, listObjDuplicate, mockIssue, mockListsWithModel } from '../mock_data'; import {
listObj,
listObjDuplicate,
mockIssue,
mockIssue2,
mockListsWithModel,
mockLists,
} from '../mock_data';
const expectNotImplemented = action => { const expectNotImplemented = action => {
it('is not implemented', () => { it('is not implemented', () => {
...@@ -148,7 +155,7 @@ describe('Board Store Mutations', () => { ...@@ -148,7 +155,7 @@ describe('Board Store Mutations', () => {
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => { describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
it('sets isLoadingIssues to false and updates issuesByListId object', () => { it('sets isLoadingIssues to false and updates issuesByListId object', () => {
const listIssues = { const listIssues = {
'1': [mockIssue.id], '': [mockIssue.id],
}; };
const issues = { const issues = {
'1': mockIssue, '1': mockIssue,
...@@ -264,6 +271,50 @@ describe('Board Store Mutations', () => { ...@@ -264,6 +271,50 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
}); });
describe('ADD_ISSUE_TO_LIST', () => {
it('adds issue to issues state and issue id in list in issuesByListId', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id],
};
const issues = {
'1': mockIssue,
};
state = {
...state,
issuesByListId: listIssues,
issues,
};
mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 });
expect(state.issuesByListId['gid://gitlab/List/1']).toContain(mockIssue2.id);
expect(state.issues[mockIssue2.id]).toEqual(mockIssue2);
});
});
describe('ADD_ISSUE_TO_LIST_FAILURE', () => {
it('removes issue id from list in issuesByListId', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
};
const issues = {
'1': mockIssue,
'2': mockIssue2,
};
state = {
...state,
issuesByListId: listIssues,
issues,
};
mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
});
});
describe('SET_CURRENT_PAGE', () => { describe('SET_CURRENT_PAGE', () => {
expectNotImplemented(mutations.SET_CURRENT_PAGE); expectNotImplemented(mutations.SET_CURRENT_PAGE);
}); });
......
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