Commit 1de805ca authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Kushal Pandya

Add methods for updating the epic associated with an issue

Add action to dispatch updateEpicIssue action in boards_store

Make boards_store update run only within the board component

Move epic-related methods to ee

Move all methods that update the epic-
associated with an issue to the ee counterparts per-
"Separation of EE specific content".

Add test for updating epic

Move methods to sub-components

Move the methods in issue and boards_store_ee.js -
to action.js for an easier reference later.

Add test for updateEpic

Add vuex methods for selectedEpicIssueId

This fixes the problem of removing an assigned epic.
Previously, selectedEpicIssueId wasn't updated after -
a render raising an error during Api.removeEpicIssue call.
A unit test is also added to test mutations.
parent 0a98de1f
import ListIssue from '~/boards/models/issue';
import IssueProject from '~/boards/models/project';
import boardsStore from '~/boards/stores/boards_store';
class ListIssueEE extends ListIssue {
constructor(obj) {
......@@ -13,6 +14,10 @@ class ListIssueEE extends ListIssue {
this.project = new IssueProject(obj.project);
}
}
updateEpic(newEpic) {
boardsStore.updateIssueEpic(this, newEpic);
}
}
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 Cookies from 'js-cookie';
import { __, sprintf } from '~/locale';
......@@ -68,6 +73,8 @@ class BoardsStoreEE {
}
};
this.store.updateIssueEpic = this.updateIssueEpic;
sidebarEventHub.$on('updateWeight', this.updateWeight.bind(this));
Object.assign(this.store, {
......@@ -179,6 +186,10 @@ class BoardsStoreEE {
this.store.findList('id', id).maxIssueCount = maxIssueCount;
}
updateIssueEpic(issue, newEpic) {
issue.epic = newEpic;
}
updateWeight(newWeight, id) {
const { issue } = this.store.detail;
if (issue.id === id && issue.sidebarInfoEndpoint) {
......
......@@ -74,7 +74,13 @@ export default {
};
},
computed: {
...mapState(['epicSelectInProgress', 'epicsFetchInProgress', 'selectedEpic', 'searchQuery']),
...mapState([
'epicSelectInProgress',
'epicsFetchInProgress',
'selectedEpic',
'searchQuery',
'selectedEpicIssueId',
]),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone', 'groupEpics']),
dropdownSelectInProgress() {
return this.initialEpicLoading || this.epicSelectInProgress;
......@@ -99,6 +105,7 @@ export default {
*/
initialEpic() {
this.setSelectedEpic(this.initialEpic);
this.setSelectedEpicIssueId(this.epicIssueId);
},
/**
* Initial Epic is loaded via separate Sidebar store
......@@ -106,6 +113,7 @@ export default {
*/
initialEpicLoading() {
this.setSelectedEpic(this.initialEpic);
this.setSelectedEpicIssueId(this.epicIssueId);
},
/**
* Check if `searchQuery` presence has yielded any matching
......@@ -136,6 +144,7 @@ export default {
'setIssueId',
'setSearchQuery',
'setSelectedEpic',
'setSelectedEpicIssueId',
'fetchEpics',
'assignIssueToEpic',
'removeIssueFromEpic',
......@@ -159,7 +168,7 @@ export default {
this.showDropdown = this.isDropdownVariantStandalone;
},
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);
} else if (this.issueId) {
this.assignIssueToEpic(epic);
......
import Api from 'ee/api';
import { noneEpic } from 'ee/vue_shared/constants';
import flash from '~/flash';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatDate, timeFor } from '~/lib/utils/datetime_utility';
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 setIssueId = ({ commit }, issueId) => commit(types.SET_ISSUE_ID, issueId);
......@@ -16,6 +19,9 @@ export const setSearchQuery = ({ commit }, searchQuery) =>
export const setSelectedEpic = ({ commit }, 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 receiveEpicsSuccess = ({ commit }, data) => {
const epics = data.map(rawEpic =>
......@@ -52,14 +58,50 @@ export const fetchEpics = ({ state, dispatch }, search = '') => {
export const requestIssueUpdate = ({ commit }) => commit(types.REQUEST_ISSUE_UPDATE);
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
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, {
selectedEpic: isRemoval ? noneEpic : epic,
selectedEpicIssueId: data.id,
});
}
};
/**
* Shows provided errorMessage in flash banner and
* fires `RECEIVE_ISSUE_UPDATE_FAILURE` mutation
......
......@@ -4,6 +4,7 @@ export const SET_ISSUE_ID = 'SET_ISSUE_ID';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
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 RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
......
......@@ -24,6 +24,10 @@ export default {
state.selectedEpic = selectedEpic;
},
[types.SET_SELECTED_EPIC_ISSUE_ID](state, selectedEpicIssueId) {
state.selectedEpicIssueId = selectedEpicIssueId;
},
[types.REQUEST_EPICS](state) {
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
context 'epic' do
before do
stub_licensed_features(epics: true)
group.add_owner(user)
visit project_board_path(project, board)
wait_for_requests
end
......@@ -166,14 +166,39 @@ describe 'Issue Boards', :js do
end
context 'when the issue is associated with an epic' do
let(:epic) { create(:epic, group: group) }
let!(:epic_issue) { create(:epic_issue, issue: issue1, epic: epic) }
let(:epic1) { create(:epic, group: group, title: 'Foo') }
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
click_card(card1)
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
......
......@@ -5,6 +5,7 @@ import testAction from 'helpers/vuex_action_helper';
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 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 { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -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', () => {
it('should set `state.epicsFetchInProgress` to true', done => {
testAction(actions.requestEpics, {}, state, [{ type: types.REQUEST_EPICS }], [], done);
......@@ -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 => {
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