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 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