Commit a36c4a49 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '215920-issues-with-editing-epic-on-boards-sidebar' into 'master'

Resolve "Issues with editing epic on Boards sidebar"

See merge request gitlab-org/gitlab!32503
parents 36306fa2 1de805ca
import ListIssue from '~/boards/models/issue'; import ListIssue from '~/boards/models/issue';
import IssueProject from '~/boards/models/project'; import IssueProject from '~/boards/models/project';
import boardsStore from '~/boards/stores/boards_store';
class ListIssueEE extends ListIssue { class ListIssueEE extends ListIssue {
constructor(obj) { constructor(obj) {
...@@ -13,6 +14,10 @@ class ListIssueEE extends ListIssue { ...@@ -13,6 +14,10 @@ class ListIssueEE extends ListIssue {
this.project = new IssueProject(obj.project); this.project = new IssueProject(obj.project);
} }
} }
updateEpic(newEpic) {
boardsStore.updateIssueEpic(this, newEpic);
}
} }
window.ListIssue = ListIssueEE; window.ListIssue = ListIssueEE;
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this, no-param-reassign */
/*
no-param-reassign is disabled because one method of BoardsStoreEE
modify the passed parameter in conformity with non-ee BoardsStore.
*/
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
...@@ -68,6 +73,8 @@ class BoardsStoreEE { ...@@ -68,6 +73,8 @@ class BoardsStoreEE {
} }
}; };
this.store.updateIssueEpic = this.updateIssueEpic;
sidebarEventHub.$on('updateWeight', this.updateWeight.bind(this)); sidebarEventHub.$on('updateWeight', this.updateWeight.bind(this));
Object.assign(this.store, { Object.assign(this.store, {
...@@ -179,6 +186,10 @@ class BoardsStoreEE { ...@@ -179,6 +186,10 @@ class BoardsStoreEE {
this.store.findList('id', id).maxIssueCount = maxIssueCount; this.store.findList('id', id).maxIssueCount = maxIssueCount;
} }
updateIssueEpic(issue, newEpic) {
issue.epic = newEpic;
}
updateWeight(newWeight, id) { updateWeight(newWeight, id) {
const { issue } = this.store.detail; const { issue } = this.store.detail;
if (issue.id === id && issue.sidebarInfoEndpoint) { if (issue.id === id && issue.sidebarInfoEndpoint) {
......
...@@ -74,7 +74,13 @@ export default { ...@@ -74,7 +74,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']), ...mapState([
'epicSelectInProgress',
'epicsFetchInProgress',
'selectedEpic',
'searchQuery',
'selectedEpicIssueId',
]),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']), ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']),
dropdownSelectInProgress() { dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress; return this.initialEpicLoading || this.epicSelectInProgress;
...@@ -99,6 +105,7 @@ export default { ...@@ -99,6 +105,7 @@ export default {
*/ */
initialEpic() { initialEpic() {
this.setSelectedEpic(this.initialEpic); this.setSelectedEpic(this.initialEpic);
this.setSelectedEpicIssueId(this.epicIssueId);
}, },
/** /**
* Initial Epic is loaded via separate Sidebar store * Initial Epic is loaded via separate Sidebar store
...@@ -106,6 +113,7 @@ export default { ...@@ -106,6 +113,7 @@ export default {
*/ */
initialEpicLoading() { initialEpicLoading() {
this.setSelectedEpic(this.initialEpic); this.setSelectedEpic(this.initialEpic);
this.setSelectedEpicIssueId(this.epicIssueId);
}, },
/** /**
* Check if `searchQuery` presence has yielded any matching * Check if `searchQuery` presence has yielded any matching
...@@ -136,6 +144,7 @@ export default { ...@@ -136,6 +144,7 @@ export default {
'setIssueId', 'setIssueId',
'setSearchQuery', 'setSearchQuery',
'setSelectedEpic', 'setSelectedEpic',
'setSelectedEpicIssueId',
'fetchEpics', 'fetchEpics',
'assignIssueToEpic', 'assignIssueToEpic',
'removeIssueFromEpic', 'removeIssueFromEpic',
...@@ -159,7 +168,7 @@ export default { ...@@ -159,7 +168,7 @@ export default {
this.showDropdown = this.isDropdownVariantStandalone; this.showDropdown = this.isDropdownVariantStandalone;
}, },
handleItemSelect(epic) { handleItemSelect(epic) {
if (this.epicIssueId && epic.id === noneEpic.id && epic.title === noneEpic.title) { if (this.selectedEpicIssueId && epic.id === noneEpic.id && epic.title === noneEpic.title) {
this.removeIssueFromEpic(this.selectedEpic); this.removeIssueFromEpic(this.selectedEpic);
} else if (this.issueId) { } else if (this.issueId) {
this.assignIssueToEpic(epic); this.assignIssueToEpic(epic);
......
import Api from 'ee/api'; import Api from 'ee/api';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import flash from '~/flash'; import flash from '~/flash';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatDate, timeFor } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import boardsStore from '~/boards/stores/boards_store';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const setIssueId = ({ commit }, issueId) => commit(types.SET_ISSUE_ID, issueId); export const setIssueId = ({ commit }, issueId) => commit(types.SET_ISSUE_ID, issueId);
...@@ -16,6 +19,9 @@ export const setSearchQuery = ({ commit }, searchQuery) => ...@@ -16,6 +19,9 @@ export const setSearchQuery = ({ commit }, searchQuery) =>
export const setSelectedEpic = ({ commit }, selectedEpic) => export const setSelectedEpic = ({ commit }, selectedEpic) =>
commit(types.SET_SELECTED_EPIC, selectedEpic); commit(types.SET_SELECTED_EPIC, selectedEpic);
export const setSelectedEpicIssueId = ({ commit }, selectedEpicIssueId) =>
commit(types.SET_SELECTED_EPIC_ISSUE_ID, selectedEpicIssueId);
export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS); export const requestEpics = ({ commit }) => commit(types.REQUEST_EPICS);
export const receiveEpicsSuccess = ({ commit }, data) => { export const receiveEpicsSuccess = ({ commit }, data) => {
const epics = data.map(rawEpic => const epics = data.map(rawEpic =>
...@@ -52,14 +58,50 @@ export const fetchEpics = ({ state, dispatch }, search = '') => { ...@@ -52,14 +58,50 @@ export const fetchEpics = ({ state, dispatch }, search = '') => {
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE); export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => { export const receiveIssueUpdateSuccess = ({ state, commit }, { data, epic, isRemoval = false }) => {
/*
If EpicsSelect is loaded within Boards, -
we need to update "boardsStore.issue.detail.epic" which has -
a differently formatted timestamp that includes '<strong>' tag.
However, "data.epic" in the response of the API POST doesn't have '<strong>' tag.
("epic" param is also in a different format).
*/
function insertStrongTag(humanReadableTimestamp) {
if (humanReadableTimestamp === __('Past due')) {
return `<strong>${humanReadableTimestamp}</strong>`;
}
// Insert strong tag for for any number in the string.
// I.e., "3 days remaining" or "Осталось 3 дней"
// A similar transformation is done in the backend:
// app/serializers/entity_date_helper.rb
return humanReadableTimestamp.replace(/\d+/, '<strong>$&</strong>');
}
// Verify if update was successful // Verify if update was successful
if (data.epic.id === epic.id && data.issue.id === state.issueId) { if (data.epic.id === epic.id && data.issue.id === state.issueId) {
if (boardsStore.detail.issue.updateEpic) {
const formattedEpic = isRemoval
? { epic_issue_id: noneEpic.id }
: {
epic_issue_id: data.id,
group_id: data.epic.group_id,
human_readable_end_date: formatDate(data.epic.end_date, 'mmm d, yyyy'),
human_readable_timestamp: insertStrongTag(timeFor(data.epic.end_date)),
id: data.epic.id,
iid: data.epic.iid,
title: data.epic.title,
url: `/groups/${data.epic.web_url.replace(/.+groups\//, '')}`,
};
boardsStore.detail.issue.updateEpic(formattedEpic);
}
commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, { commit(types.RECEIVE_ISSUE_UPDATE_SUCCESS, {
selectedEpic: isRemoval ? noneEpic : epic, selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id, selectedEpicIssueId: data.id,
}); });
} }
}; };
/** /**
* Shows provided errorMessage in flash banner and * Shows provided errorMessage in flash banner and
* fires `RECEIVE_ISSUE_UPDATE_FAILURE` mutation * fires `RECEIVE_ISSUE_UPDATE_FAILURE` mutation
......
...@@ -4,6 +4,7 @@ export const SET_ISSUE_ID = 'SET_ISSUE_ID'; ...@@ -4,6 +4,7 @@ export const SET_ISSUE_ID = 'SET_ISSUE_ID';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_SELECTED_EPIC = 'SET_SELECTED_EPIC'; export const SET_SELECTED_EPIC = 'SET_SELECTED_EPIC';
export const SET_SELECTED_EPIC_ISSUE_ID = 'SET_SELECTED_EPIC_ISSUE_ID';
export const REQUEST_EPICS = 'REQUEST_EPICS'; export const REQUEST_EPICS = 'REQUEST_EPICS';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS'; export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
......
...@@ -24,6 +24,10 @@ export default { ...@@ -24,6 +24,10 @@ export default {
state.selectedEpic = selectedEpic; state.selectedEpic = selectedEpic;
}, },
[types.SET_SELECTED_EPIC_ISSUE_ID](state, selectedEpicIssueId) {
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.REQUEST_EPICS](state) { [types.REQUEST_EPICS](state) {
state.epicsFetchInProgress = true; state.epicsFetchInProgress = true;
}, },
......
---
title: Fix issues with editing epic on Boards sidebar
merge_request: 32503
author: Eulyeon Ko
type: fixed
...@@ -151,7 +151,7 @@ describe 'Issue Boards', :js do ...@@ -151,7 +151,7 @@ describe 'Issue Boards', :js do
context 'epic' do context 'epic' do
before do before do
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
group.add_owner(user)
visit project_board_path(project, board) visit project_board_path(project, board)
wait_for_requests wait_for_requests
end end
...@@ -166,14 +166,39 @@ describe 'Issue Boards', :js do ...@@ -166,14 +166,39 @@ describe 'Issue Boards', :js do
end end
context 'when the issue is associated with an epic' do context 'when the issue is associated with an epic' do
let(:epic) { create(:epic, group: group) } let(:epic1) { create(:epic, group: group, title: 'Foo') }
let!(:epic_issue) { create(:epic_issue, issue: issue1, epic: epic) } let!(:epic2) { create(:epic, group: group, title: 'Bar') }
let!(:epic_issue) { create(:epic_issue, issue: issue1, epic: epic1) }
it 'displays name of epic and links to it' do it 'displays name of epic and links to it' do
click_card(card1) click_card(card1)
wait_for_requests wait_for_requests
expect(find('.js-epic-label')).to have_link(epic.title, href: epic_path(epic)) expect(find('.js-epic-label')).to have_link(epic1.title, href: epic_path(epic1))
end
it 'updates the epic associated with the issue' do
click_card(card1)
wait_for_requests
page.within(find('.js-epic-block')) do
page.find('.sidebar-dropdown-toggle').click
wait_for_requests
click_link epic2.title
wait_for_requests
expect(page.find('.value')).to have_content(epic2.title)
end
# Ensure that boards_store is also updated the epic associated with the issue.
click_card(card1)
wait_for_requests
click_card(card1)
wait_for_requests
expect(find('.js-epic-label')).to have_content(epic2.title)
end end
end end
end end
......
...@@ -5,6 +5,7 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -5,6 +5,7 @@ import testAction from 'helpers/vuex_action_helper';
import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state'; import createDefaultState from 'ee/vue_shared/components/sidebar/epics_select/store/state';
import * as actions from 'ee/vue_shared/components/sidebar/epics_select/store/actions'; import * as actions from 'ee/vue_shared/components/sidebar/epics_select/store/actions';
import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types'; import * as types from 'ee/vue_shared/components/sidebar/epics_select/store/mutation_types';
import boardsStore from '~/boards/stores/boards_store';
import { noneEpic } from 'ee/vue_shared/constants'; import { noneEpic } from 'ee/vue_shared/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -88,6 +89,19 @@ describe('EpicsSelect', () => { ...@@ -88,6 +89,19 @@ describe('EpicsSelect', () => {
}); });
}); });
describe('setSelectedEpicIssueId', () => {
it('should set `selectedEpicIssueId` param on state', done => {
testAction(
actions.setSelectedEpicIssueId,
mockIssue.epic_issue_id,
state,
[{ type: types.SET_SELECTED_EPIC_ISSUE_ID, payload: mockIssue.epic_issue_id }],
[],
done,
);
});
});
describe('requestEpics', () => { describe('requestEpics', () => {
it('should set `state.epicsFetchInProgress` to true', done => { it('should set `state.epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: types.REQUEST_EPICS }], [], done); testAction(actions.requestEpics, {}, state, [{ type: types.REQUEST_EPICS }], [], done);
...@@ -248,6 +262,35 @@ describe('EpicsSelect', () => { ...@@ -248,6 +262,35 @@ describe('EpicsSelect', () => {
); );
}); });
it('should update the epic associated with the issue in BoardsStore if the update happened in Boards', done => {
boardsStore.detail.issue.updateEpic = jest.fn(() => {});
state.issueId = mockIssue.id;
const mockApiData = { ...mockAssignRemoveRes };
mockApiData.epic.web_url = '';
testAction(
actions.receiveIssueUpdateSuccess,
{
data: mockApiData,
epic: normalizedEpics[0],
},
state,
[
{
type: types.RECEIVE_ISSUE_UPDATE_SUCCESS,
payload: {
selectedEpic: normalizedEpics[0],
selectedEpicIssueId: mockApiData.id,
},
},
],
[],
done,
);
expect(boardsStore.detail.issue.updateEpic).toHaveBeenCalled();
});
it('should set updated selectedEpic with noneEpic to state when payload has matching Epic and Issue IDs and isRemoval param is true', done => { it('should set updated selectedEpic with noneEpic to state when payload has matching Epic and Issue IDs and isRemoval param is true', done => {
state.issueId = mockIssue.id; state.issueId = mockIssue.id;
......
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