From e84c943fa0609d05184cb95c845fa35cc1fcd432 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 12 Mar 2018 17:08:05 +0000 Subject: [PATCH 1/2] Fix loading icon being visible in the wrong button --- .../notes/components/comment_form.vue | 13 ++-- .../javascripts/notes/stores/actions.js | 15 ++++- .../javascripts/notes/stores/getters.js | 1 + app/assets/javascripts/notes/stores/index.js | 3 + .../notes/stores/mutation_types.js | 1 + .../javascripts/notes/stores/mutations.js | 4 ++ .../44149-issue-comment-buttons.yml | 5 ++ .../notes/components/comment_form_spec.js | 14 ++++ spec/javascripts/notes/stores/actions_spec.js | 16 +++++ .../javascripts/notes/stores/mutation_spec.js | 66 +++++++++++++++++++ 10 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/44149-issue-comment-buttons.yml diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 1785be01a0d..a649e180131 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -52,6 +52,7 @@ 'getNoteableData', 'getNotesData', 'openState', + 'isToggleStateButtonLoading', ]), noteableDisplayName() { return this.noteableType.replace(/_/g, ' '); @@ -143,6 +144,7 @@ 'closeIssue', 'reopenIssue', 'toggleIssueLocalState', + 'toggleStateButtonLoading', ]), setIsSubmitButtonDisabled(note, isSubmitting) { if (!_.isEmpty(note) && !isSubmitting) { @@ -170,13 +172,14 @@ if (this.noteType === constants.DISCUSSION) { noteData.data.note.type = constants.DISCUSSION_NOTE; } + this.note = ''; // Empty textarea while being requested. Repopulate in catch this.resizeTextarea(); this.stopPolling(); this.saveNote(noteData) .then((res) => { - this.isSubmitting = false; + this.enableButton(); this.restartPolling(); if (res.errors) { @@ -198,7 +201,7 @@ } }) .catch(() => { - this.isSubmitting = false; + this.enableButton(); this.discard(false); const msg = `Your comment could not be submitted! @@ -220,6 +223,7 @@ Please check your network connection and try again.`; .then(() => this.enableButton()) .catch(() => { this.enableButton(); + this.toggleStateButtonLoading(false); Flash( sprintf( __('Something went wrong while closing the %{issuable}. Please try again later'), @@ -232,6 +236,7 @@ Please check your network connection and try again.`; .then(() => this.enableButton()) .catch(() => { this.enableButton(); + this.toggleStateButtonLoading(false); Flash( sprintf( __('Something went wrong while reopening the %{issuable}. Please try again later'), @@ -419,13 +424,13 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 08ca01e542e..70fc368a471 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -71,21 +71,32 @@ export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion commit(mutationType, res); }); -export const closeIssue = ({ commit, dispatch, state }) => service +export const closeIssue = ({ commit, dispatch, state }) => { + dispatch('toggleStateButtonLoading', true); + return service .toggleIssueState(state.notesData.closePath) .then(res => res.json()) .then((data) => { commit(types.CLOSE_ISSUE); dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); }); +}; -export const reopenIssue = ({ commit, dispatch, state }) => service +export const reopenIssue = ({ commit, dispatch, state }) => { + dispatch('toggleStateButtonLoading', true); + return service .toggleIssueState(state.notesData.reopenPath) .then(res => res.json()) .then((data) => { commit(types.REOPEN_ISSUE); dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); }); +}; + +export const toggleStateButtonLoading = ({ commit }, value) => + commit(types.TOGGLE_STATE_BUTTON_LOADING, value); export const emitStateChangedEvent = ({ commit, getters }, data) => { const event = new CustomEvent('issuable_vue_app:change', { detail: { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e6180101c58..9ff5b0ec63f 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -9,6 +9,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 openState = state => state.noteableData.state; +export const isToggleStateButtonLoading = state => state.isToggleStateButtonLoading; export const getUserData = state => state.userData || {}; export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 488a9ca38d3..9ed19bf171e 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -12,6 +12,9 @@ export default new Vuex.Store({ targetNoteHash: null, lastFetchedAt: null, + // View layer + isToggleStateButtonLoading: false, + // holds endpoints and permissions provided through haml notesData: {}, userData: {}, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index da1b5a9e51a..b455e23ecde 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -17,3 +17,4 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; +export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 963b40be3fd..f07afbe4303 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -197,4 +197,8 @@ export default { [types.REOPEN_ISSUE](state) { Object.assign(state.noteableData, { state: constants.REOPENED }); }, + + [types.TOGGLE_STATE_BUTTON_LOADING](state, value) { + Object.assign(state, { isToggleStateButtonLoading: value }); + }, }; diff --git a/changelogs/unreleased/44149-issue-comment-buttons.yml b/changelogs/unreleased/44149-issue-comment-buttons.yml new file mode 100644 index 00000000000..c874c0d3d66 --- /dev/null +++ b/changelogs/unreleased/44149-issue-comment-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken loading state for close issue button +merge_request: +author: +type: fixed diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 90016436cb7..224debbeff6 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -200,6 +200,20 @@ describe('issue_comment_form component', () => { done(); }); }); + + describe('when clicking close/reopen button', () => { + it('should disable button and show a loading spinner', (done) => { + const toggleStateButton = vm.$el.querySelector('.js-action-button'); + + toggleStateButton.click(); + Vue.nextTick(() => { + expect(toggleStateButton.disabled).toEqual(true); + expect(toggleStateButton.querySelector('.js-loading-button-icon')).not.toBeNull(); + + done(); + }); + }); + }); }); describe('issue is confidential', () => { diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index ab80ed7bbfb..0f092810574 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -87,6 +87,7 @@ describe('Actions Notes Store', () => { store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } }) .then(() => { expect(store.state.noteableData.state).toEqual('closed'); + expect(store.state.isToggleStateButtonLoading).toEqual(false); done(); }) .catch(done.fail); @@ -98,6 +99,7 @@ describe('Actions Notes Store', () => { store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } }) .then(() => { expect(store.state.noteableData.state).toEqual('reopened'); + expect(store.state.isToggleStateButtonLoading).toEqual(false); done(); }) .catch(done.fail); @@ -116,6 +118,20 @@ describe('Actions Notes Store', () => { }); }); + describe('toggleStateButtonLoading', () => { + it('should set loading as true', (done) => { + testAction(actions.toggleStateButtonLoading, true, {}, [ + { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }, + ], done); + }); + + it('should set loading as false', (done) => { + testAction(actions.toggleStateButtonLoading, false, {}, [ + { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }, + ], done); + }); + }); + describe('toggleIssueLocalState', () => { it('sets issue state as closed', (done) => { testAction(actions.toggleIssueLocalState, 'closed', {}, [ diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index e4baefc5bfc..2627f721d9d 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -217,4 +217,70 @@ describe('Notes Store mutations', () => { expect(state.notes[0].notes[0].note).toEqual('Foo'); }); }); + + describe('CLOSE_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.CLOSE_ISSUE(state); + expect(state.noteableData.state).toEqual('closed'); + }); + }); + + describe('REOPEN_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.REOPEN_ISSUE(state); + expect(state.noteableData.state).toEqual('reopened'); + }); + }); + + describe('TOGGLE_STATE_BUTTON_LOADING', () => { + it('should set isToggleStateButtonLoading as true', () => { + const state = { + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, true); + expect(state.isToggleStateButtonLoading).toEqual(true); + }); + + it('should set isToggleStateButtonLoading as false', () => { + const state = { + notes: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: true, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, false); + expect(state.isToggleStateButtonLoading).toEqual(false); + }); + }); }); From 93c6842fbf74b0a680b3e730848b1d2b8bfceefa Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 13 Mar 2018 09:25:39 +0000 Subject: [PATCH 2/2] Use mapState instead of a getter --- app/assets/javascripts/notes/components/comment_form.vue | 4 +++- app/assets/javascripts/notes/stores/getters.js | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a649e180131..42bc383f4d2 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -1,6 +1,6 @@