Makes close/reopen issue request to inside the vue app
This commit is contained in:
parent
a4a47cfba8
commit
d637f87f88
|
|
@ -25,6 +25,51 @@ export default class Issue {
|
|||
if (Issue.createMrDropdownWrap) {
|
||||
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
|
||||
}
|
||||
|
||||
// Listen to state changes in the Vue app
|
||||
document.addEventListener('issuable_vue_app:change', (event) => {
|
||||
this.updateTopState(event.detail.isClosed, event.detail.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method updates the top area of the issue.
|
||||
*
|
||||
* Once the issue state changes, either through a click on the top area (jquery)
|
||||
* or a click on the bottom area (Vue) we need to update the top area.
|
||||
*
|
||||
* @param {Boolean} isClosed
|
||||
* @param {Array} data
|
||||
* @param {String} issueFailMessage
|
||||
*/
|
||||
updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
|
||||
if ('id' in data) {
|
||||
const isClosedBadge = $('div.status-box-issue-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
|
||||
isClosedBadge.toggleClass('hidden', !isClosed);
|
||||
isOpenBadge.toggleClass('hidden', isClosed);
|
||||
|
||||
$(document).trigger('issuable:change', isClosed);
|
||||
this.toggleCloseReopenButton(isClosed);
|
||||
|
||||
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
|
||||
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
|
||||
projectIssuesCounter.text(addDelimiter(numProjectIssues));
|
||||
|
||||
if (this.createMergeRequestDropdown) {
|
||||
if (isClosed) {
|
||||
this.createMergeRequestDropdown.unavailable();
|
||||
this.createMergeRequestDropdown.disable();
|
||||
} else {
|
||||
// We should check in case a branch was created in another tab
|
||||
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flash(issueFailMessage);
|
||||
}
|
||||
}
|
||||
|
||||
initIssueBtnEventListeners() {
|
||||
|
|
@ -45,34 +90,8 @@ export default class Issue {
|
|||
url = $button.attr('href');
|
||||
return axios.put(url)
|
||||
.then(({ data }) => {
|
||||
const isClosedBadge = $('div.status-box-issue-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
|
||||
if ('id' in data) {
|
||||
const isClosed = $button.hasClass('btn-close');
|
||||
isClosedBadge.toggleClass('hidden', !isClosed);
|
||||
isOpenBadge.toggleClass('hidden', isClosed);
|
||||
|
||||
$(document).trigger('issuable:change', isClosed);
|
||||
this.toggleCloseReopenButton(isClosed);
|
||||
|
||||
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
|
||||
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
|
||||
projectIssuesCounter.text(addDelimiter(numProjectIssues));
|
||||
|
||||
if (this.createMergeRequestDropdown) {
|
||||
if (isClosed) {
|
||||
this.createMergeRequestDropdown.unavailable();
|
||||
this.createMergeRequestDropdown.disable();
|
||||
} else {
|
||||
// We should check in case a branch was created in another tab
|
||||
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flash(issueFailMessage);
|
||||
}
|
||||
const isClosed = $button.hasClass('btn-close');
|
||||
this.updateTopState(isClosed, data);
|
||||
})
|
||||
.catch(() => flash(issueFailMessage))
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { mapActions, mapGetters } from 'vuex';
|
||||
import _ from 'underscore';
|
||||
import Autosize from 'autosize';
|
||||
import { __ } from '~/locale';
|
||||
import Flash from '../../flash';
|
||||
import Autosave from '../../autosave';
|
||||
import TaskList from '../../task_list';
|
||||
|
|
@ -30,9 +31,6 @@
|
|||
return {
|
||||
note: '',
|
||||
noteType: constants.COMMENT,
|
||||
// Can't use mapGetters,
|
||||
// this needs to be in the data object because it belongs to the state
|
||||
issueState: this.$store.getters.getNoteableData.state,
|
||||
isSubmitting: false,
|
||||
isSubmitButtonDisabled: true,
|
||||
};
|
||||
|
|
@ -43,7 +41,11 @@
|
|||
'getUserData',
|
||||
'getNoteableData',
|
||||
'getNotesData',
|
||||
'getIssueState',
|
||||
]),
|
||||
issueState() {
|
||||
return this.getIssueState;
|
||||
},
|
||||
isLoggedIn() {
|
||||
return this.getUserData.id;
|
||||
},
|
||||
|
|
@ -71,8 +73,6 @@
|
|||
return {
|
||||
'btn-reopen': !this.isIssueOpen,
|
||||
'btn-close': this.isIssueOpen,
|
||||
'js-note-target-close': this.isIssueOpen,
|
||||
'js-note-target-reopen': !this.isIssueOpen,
|
||||
};
|
||||
},
|
||||
markdownDocsPath() {
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
mounted() {
|
||||
// jQuery is needed here because it is a custom event being dispatched with jQuery.
|
||||
$(document).on('issuable:change', (e, isClosed) => {
|
||||
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
|
||||
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
|
||||
});
|
||||
|
||||
this.initAutoSave();
|
||||
|
|
@ -117,6 +117,9 @@
|
|||
'stopPolling',
|
||||
'restartPolling',
|
||||
'removePlaceholderNotes',
|
||||
'closeIssue',
|
||||
'reopenIssue',
|
||||
'toggleIssueLocalState',
|
||||
]),
|
||||
setIsSubmitButtonDisabled(note, isSubmitting) {
|
||||
if (!_.isEmpty(note) && !isSubmitting) {
|
||||
|
|
@ -185,12 +188,13 @@ Please check your network connection and try again.`;
|
|||
}
|
||||
},
|
||||
toggleIssueState() {
|
||||
this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
|
||||
|
||||
// This is out of scope for the Notes Vue component.
|
||||
// It was the shortest path to update the issue state and relevant places.
|
||||
const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
|
||||
$(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
|
||||
if (this.isIssueOpen) {
|
||||
this.closeIssue()
|
||||
.catch(() => Flash(__('Something went wrong while closing the issue. Please try again later')));
|
||||
} else {
|
||||
this.reopenIssue()
|
||||
.catch(() => Flash(__('Something went wrong while reopening the issue. Please try again later')));
|
||||
}
|
||||
},
|
||||
discard(shouldClear = true) {
|
||||
// `blur` is needed to clear slash commands autocomplete cache if event fired.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
|
|||
notesPath: notesDataset.notesPath,
|
||||
markdownDocsPath: notesDataset.markdownDocsPath,
|
||||
quickActionsDocsPath: notesDataset.quickActionsDocsPath,
|
||||
closeIssuePath: notesDataset.closeIssuePath,
|
||||
reopenIssuePath: notesDataset.reopenIssuePath,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,4 +32,7 @@ export default {
|
|||
toggleAward(endpoint, data) {
|
||||
return Vue.http.post(endpoint, data, { emulateJSON: true });
|
||||
},
|
||||
toggleIssueState(endpoint, data) {
|
||||
return Vue.http.put(endpoint, data);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
|
|||
export const removePlaceholderNotes = ({ commit }) =>
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||
|
||||
export const closeIssue = ({ commit, dispatch, state }) => service
|
||||
.toggleIssueState(state.notesData.closeIssuePath)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
commit(types.CLOSE_ISSUE);
|
||||
dispatch('emitStateChangedEvent', data);
|
||||
});
|
||||
|
||||
export const reopenIssue = ({ commit, dispatch, state }) => service
|
||||
.toggleIssueState(state.notesData.reopenIssuePath)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
commit(types.REOPEN_ISSUE);
|
||||
dispatch('emitStateChangedEvent', data);
|
||||
});
|
||||
|
||||
export const emitStateChangedEvent = ({ commit }, data) => {
|
||||
const event = new CustomEvent('issuable_vue_app:change', { detail: {
|
||||
data,
|
||||
isClosed: data.state === constants.CLOSED,
|
||||
} });
|
||||
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export const toggleIssueLocalState = ({ commit }, newState) => {
|
||||
if (newState === constants.CLOSED) {
|
||||
commit(types.CLOSE_ISSUE);
|
||||
} else if (newState === constants.REOPENED) {
|
||||
commit(types.REOPEN_ISSUE);
|
||||
}
|
||||
};
|
||||
|
||||
export const saveNote = ({ commit, dispatch }, noteData) => {
|
||||
const { note } = noteData.data.note;
|
||||
let placeholderText = note;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
|
|||
|
||||
export const getNoteableData = state => state.noteableData;
|
||||
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
|
||||
export const getIssueState = state => state.noteableData.state;
|
||||
|
||||
export const getUserData = state => state.userData || {};
|
||||
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
|
||||
|
|
|
|||
|
|
@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
|
|||
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
|
||||
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
|
||||
export const UPDATE_NOTE = 'UPDATE_NOTE';
|
||||
|
||||
// Issue
|
||||
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
|
||||
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
|
||||
|
|
|
|||
|
|
@ -152,4 +152,12 @@ export default {
|
|||
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
|
||||
}
|
||||
},
|
||||
|
||||
[types.CLOSE_ISSUE](state) {
|
||||
Object.assign(state.noteableData, { state: constants.CLOSED });
|
||||
},
|
||||
|
||||
[types.REOPEN_ISSUE](state) {
|
||||
Object.assign(state.noteableData, { state: constants.REOPENED });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
markdown_docs_path: help_page_path('user/markdown'),
|
||||
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
|
||||
notes_path: notes_url,
|
||||
close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
|
||||
reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
|
||||
last_fetched_at: Time.now.to_i,
|
||||
noteable_data: serialize_issuable(@issue),
|
||||
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix close button on issues not working on mobile
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const resetStore = (store) => {
|
||||
store.replaceState({
|
||||
notes: [],
|
||||
targetNoteHash: null,
|
||||
lastFetchedAt: null,
|
||||
|
||||
notesData: {},
|
||||
userData: {},
|
||||
noteableData: {},
|
||||
});
|
||||
};
|
||||
|
|
@ -7,6 +7,8 @@ export const notesDataMock = {
|
|||
notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
|
||||
quickActionsDocsPath: '/help/user/project/quick_actions',
|
||||
registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
|
||||
closeIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
|
||||
reopenIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
|
||||
};
|
||||
|
||||
export const userDataMock = {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import Vue from 'vue';
|
||||
import _ from 'underscore';
|
||||
import * as actions from '~/notes/stores/actions';
|
||||
import store from '~/notes/stores';
|
||||
import testAction from '../../helpers/vuex_action_helper';
|
||||
import { resetStore } from '../helpers';
|
||||
import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
|
||||
|
||||
describe('Actions Notes Store', () => {
|
||||
afterEach(() => {
|
||||
resetStore(store);
|
||||
});
|
||||
|
||||
describe('setNotesData', () => {
|
||||
it('should set received notes data', (done) => {
|
||||
testAction(actions.setNotesData, null, { notesData: {} }, [
|
||||
|
|
@ -58,4 +66,67 @@ describe('Actions Notes Store', () => {
|
|||
], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('async methods', () => {
|
||||
const interceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify({}), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(interceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
||||
});
|
||||
|
||||
describe('closeIssue', () => {
|
||||
it('sets state as closed', (done) => {
|
||||
store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
|
||||
.then(() => {
|
||||
expect(store.state.noteableData.state).toEqual('closed');
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reopenIssue', () => {
|
||||
it('sets state as reopened', (done) => {
|
||||
store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
|
||||
.then(() => {
|
||||
expect(store.state.noteableData.state).toEqual('reopened');
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitStateChangedEvent', () => {
|
||||
it('emits an event on the document', () => {
|
||||
document.addEventListener('issuable_vue_app:change', (event) => {
|
||||
expect(event.detail.data).toEqual({ id: '1', state: 'closed' });
|
||||
expect(event.detail.isClosed).toEqual(true);
|
||||
});
|
||||
|
||||
store.dispatch('emitStateChangedEvent', { id: '1', state: 'closed' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleIssueLocalState', () => {
|
||||
it('sets issue state as closed', (done) => {
|
||||
testAction(actions.toggleIssueLocalState, 'closed', {}, [
|
||||
{ type: 'CLOSE_ISSUE', payload: 'closed' },
|
||||
], done);
|
||||
});
|
||||
|
||||
it('sets issue state as reopened', (done) => {
|
||||
testAction(actions.toggleIssueLocalState, 'reopened', {}, [
|
||||
{ type: 'REOPEN_ISSUE', payload: 'reopened' },
|
||||
], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue