Commit d34c620f authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Add issue data and notes data provided through haml to the store to...

[ci skip] Add issue data and notes data provided through haml to the store to stop querying the DOM everywhere
parent 487ed06f
<script> <script>
/* global Flash */ /* global Flash */
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
...@@ -30,6 +30,10 @@ ...@@ -30,6 +30,10 @@
issueNoteSignedOutWidget, issueNoteSignedOutWidget,
}, },
computed: { computed: {
...mapGetters([
'getNotesDataByProp',
'getIssueDataByProp',
]),
isLoggedIn() { isLoggedIn() {
return window.gon.current_user_id; return window.gon.current_user_id;
}, },
...@@ -57,8 +61,7 @@ ...@@ -57,8 +61,7 @@
}; };
}, },
canUpdateIssue() { canUpdateIssue() {
const { issueData } = window.gl; return this.getIssueDataByProp(current_user).can_update;
return issueData && issueData.current_user.can_update;
}, },
}, },
methods: { methods: {
...@@ -146,11 +149,8 @@ ...@@ -146,11 +149,8 @@
}, },
}, },
mounted() { mounted() {
const issuableDataEl = document.getElementById('js-issuable-app-initial-data'); this.markdownDocsUrl = this.getIssueDataByProp(markdownDocs);
const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/&quot;/g, '"')); this.quickActionsDocsUrl = this.getIssueDataByProp(quickActionsDocs);
this.markdownDocsUrl = issueData.markdownDocs;
this.quickActionsDocsUrl = issueData.quickActionsDocs;
eventHub.$on('issueStateChanged', (isClosed) => { eventHub.$on('issueStateChanged', (isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import { mapGetters, mapActions, mapMutations } from 'vuex'; import { mapGetters, mapActions, mapMutations } from 'vuex';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants' import * as constants from '../constants'
import * as types from '../stores/mutation_types';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueNote from './issue_note.vue'; import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue'; import issueDiscussion from './issue_discussion.vue';
...@@ -17,6 +16,16 @@ ...@@ -17,6 +16,16 @@
export default { export default {
name: 'IssueNotes', name: 'IssueNotes',
props: {
issueData: {
type: Object,
required: true,
},
notesData: {
type: Object,
required: true,
},
},
store, store,
data() { data() {
return { return {
...@@ -36,20 +45,19 @@ ...@@ -36,20 +45,19 @@
...mapGetters([ ...mapGetters([
'notes', 'notes',
'notesById', 'notesById',
'getNotesData',
'getNotesDataByProp',
'setLastFetchedAt',
'setTargetNoteHash',
]), ]),
}, },
methods: { methods: {
...mapActions({ ...mapActions({
actionFetchNotes: 'fetchNotes', actionFetchNotes: 'fetchNotes',
}), poll: 'poll',
...mapActions([ toggleAward: 'toggleAward',
'poll', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
'toggleAward', setNotesData: 'setNotesData'
'scrollToNoteIfNeeded',
]),
...mapMutations({
setLastFetchedAt: types.SET_LAST_FETCHED_AT,
setTargetNoteHash: types.SET_TARGET_NOTE_HASH,
}), }),
getComponentName(note) { getComponentName(note) {
if (note.isPlaceholderNote) { if (note.isPlaceholderNote) {
...@@ -67,9 +75,7 @@ ...@@ -67,9 +75,7 @@
return note.individual_note ? note.notes[0] : note; return note.individual_note ? note.notes[0] : note;
}, },
fetchNotes() { fetchNotes() {
const { discussionsPath } = this.$el.parentNode.dataset; this.actionFetchNotes(his.getNotesDataByProp('discussionsPath'))
this.actionFetchNotes(discussionsPath)
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -78,23 +84,19 @@ ...@@ -78,23 +84,19 @@
this.checkLocationHash(); this.checkLocationHash();
}); });
}) })
.catch(() => { .catch(() => Flash('Something went wrong while fetching issue comments. Please try again.'));
Flash('Something went wrong while fetching issue comments. Please try again.');
});
}, },
initPolling() { initPolling() {
const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset; this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.setLastFetchedAt(lastFetchedAt);
// FIXME: @fatihacet Implement real polling mechanism // FIXME: @fatihacet Implement real polling mechanism
// TODO: FILIPA: DEAL WITH THIS
setInterval(() => { setInterval(() => {
this.poll() this.poll()
.then((res) => { .then((res) => {
this.setLastFetchedAt(res.lastFetchedAt); this.setLastFetchedAt(res.lastFetchedAt);
}) })
.catch(() => { .catch(() => Flash('Something went wrong while fetching latest comments.'));
Flash('Something went wrong while fetching latest comments.');
});
}, 15000); }, 15000);
}, },
bindEventHubListeners() { bindEventHubListeners() {
...@@ -106,6 +108,7 @@ ...@@ -106,6 +108,7 @@
.catch(() => Flash('Something went wrong on our end.')); .catch(() => Flash('Something went wrong on our end.'));
}); });
//TODO: FILIPA: REMOVE JQUERY
$(document).on('issuable:change', (e, isClosed) => { $(document).on('issuable:change', (e, isClosed) => {
eventHub.$emit('issueStateChanged', isClosed); eventHub.$emit('issueStateChanged', isClosed);
}); });
...@@ -120,6 +123,10 @@ ...@@ -120,6 +123,10 @@
} }
}, },
}, },
created() {
this.setNotesData(this.notesData);
this.setIssueData(this.issueData);
},
mounted() { mounted() {
this.fetchNotes(); this.fetchNotes();
this.initPolling(); this.initPolling();
...@@ -135,10 +142,12 @@ ...@@ -135,10 +142,12 @@
class="loading"> class="loading">
<loading-icon /> <loading-icon />
</div> </div>
<ul <ul
v-if="!isLoading" v-if="!isLoading"
id="notes-list" id="notes-list"
class="notes main-notes-list timeline"> class="notes main-notes-list timeline">
<component <component
v-for="note in notes" v-for="note in notes"
:is="getComponentName(note)" :is="getComponentName(note)"
...@@ -146,6 +155,7 @@ ...@@ -146,6 +155,7 @@
:key="note.id" :key="note.id"
/> />
</ul> </ul>
<issue-comment-form v-if="!isLoading" />
<issue-comment-form />
</div> </div>
</template> </template>
...@@ -38,8 +38,9 @@ ...@@ -38,8 +38,9 @@
:class="{ target: isTargetNote }" :class="{ target: isTargetNote }"
class="note system-note timeline-entry"> class="note system-note timeline-entry">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="timeline-icon"> <div
<span v-html="svg"></span> class="timeline-icon"
v-html="svg">
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="note-header"> <div class="note-header">
......
...@@ -6,3 +6,5 @@ export const COMMENT = 'comment'; ...@@ -6,3 +6,5 @@ export const COMMENT = 'comment';
export const OPENED = 'opened'; export const OPENED = 'opened';
export const REOPENED = 'reopened'; export const REOPENED = 'reopened';
export const CLOSED = 'closed'; export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
import Vue from 'vue'; import Vue from 'vue';
import issueNotes from './components/issue_notes.vue'; import issueNotesApp from './components/issue_notes_app.vue';
import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => new Vue({
const vm = new Vue({ el: '#js-vue-notes',
el: '#js-notes', components: {
components: { issueNotesApp,
issueNotes, },
}, data() {
render(createElement) { const notesDataset = document.getElementById('js-vue-notes').dataset;
return createElement('issue-notes', {
attrs: {
ref: 'notes',
},
});
},
});
window.issueNotes = { return {
refresh() { issueData: JSON.parse(notesDataset.issueData),
vm.$refs.notes.$store.dispatch('poll'); currentUserData: JSON.parse(notesDataset.currentUserData),
}, notesData: {
}; lastFetchedAt: notesDataset.lastFetchedAt,
}); discussionsPath: notesDataset.discussionsPath,
},
};
},
render(createElement) {
return createElement('issue-notes-app', {
attrs: {
ref: 'notes',
},
props: {
issueData: this.issueData,
notesData: this.notesData,
},
});
},
}));
// // TODO: FILIPA: FIX THIS
// window.issueNotes = {
// refresh() {
// vm.$refs.notes.$store.dispatch('poll');
// },
// };
...@@ -7,6 +7,13 @@ import service from '../services/issue_notes_service'; ...@@ -7,6 +7,13 @@ import service from '../services/issue_notes_service';
import loadAwardsHandler from '../../awards_handler'; import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
export const fetchNotes = ({ commit }, path) => service export const fetchNotes = ({ commit }, path) => service
.fetchNotes(path) .fetchNotes(path)
.then(res => res.json()) .then(res => res.json())
...@@ -20,43 +27,31 @@ export const deleteNote = ({ commit }, note) => service ...@@ -20,43 +27,31 @@ export const deleteNote = ({ commit }, note) => service
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
}); });
export const updateNote = ({ commit }, data) => { export const updateNote = ({ commit }, { endpoint, note }) => service
const { endpoint, note } = data; .updateNote(endpoint, note)
.then(res => res.json())
return service .then((res) => {
.updateNote(endpoint, note) commit(types.UPDATE_NOTE, res);
.then(res => res.json()) });
.then((res) => {
commit(types.UPDATE_NOTE, res);
});
};
export const replyToDiscussion = ({ commit }, note) => {
const { endpoint, data } = note;
return service
.replyToDiscussion(endpoint, data)
.then(res => res.json())
.then((res) => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
return res; export const replyToDiscussion = ({ commit }, { endpoint, data }) => service
}); .replyToDiscussion(endpoint, data)
}; .then(res => res.json())
.then((res) => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
export const createNewNote = ({ commit }, note) => { return res;
const { endpoint, data } = note; });
return service export const createNewNote = ({ commit }, { endpoint, data }) => service
.createNewNote(endpoint, data) .createNewNote(endpoint, data)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
if (!res.errors) { if (!res.errors) {
commit(types.ADD_NEW_NOTE, res); commit(types.ADD_NEW_NOTE, res);
} }
return res; return res;
}); });
};
export const saveNote = ({ commit, dispatch }, noteData) => { export const saveNote = ({ commit, dispatch }, noteData) => {
const { note } = noteData.data.note; const { note } = noteData.data.note;
...@@ -91,6 +86,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -91,6 +86,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
if (hasQuickActions && Object.keys(errors).length) { if (hasQuickActions && Object.keys(errors).length) {
dispatch('poll'); dispatch('poll');
$('.js-gfm-input').trigger('clear-commands-cache.atwho'); $('.js-gfm-input').trigger('clear-commands-cache.atwho');
Flash('Commands applied', 'notice', $(noteData.flashContainer)); Flash('Commands applied', 'notice', $(noteData.flashContainer));
} }
...@@ -136,9 +132,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -136,9 +132,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}; };
export const poll = ({ commit, state, getters }) => { export const poll = ({ commit, state, getters }) => {
const { notesPath } = $('.js-notes-wrapper')[0].dataset; return service.poll(state.notesData.notesPath, state.lastFetchedAt)
return service.poll(`${notesPath}?full_data=1`, state.lastFetchedAt)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
if (res.notes.length) { if (res.notes.length) {
...@@ -160,7 +154,6 @@ export const poll = ({ commit, state, getters }) => { ...@@ -160,7 +154,6 @@ export const poll = ({ commit, state, getters }) => {
} }
}); });
} }
return res; return res;
}); });
}; };
...@@ -175,20 +168,24 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { ...@@ -175,20 +168,24 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => {
.then(() => { .then(() => {
commit(types.TOGGLE_AWARD, { awardName, note }); commit(types.TOGGLE_AWARD, { awardName, note });
if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) { if (!skipMutalityCheck &&
const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; (awardName === constants.EMOJI_THUMBSUP || awardName === constants.EMOJI_THUMBSDOWN)) {
const counterAward = awardName === constants.EMOJI_THUMBSUP ?
constants.EMOJI_THUMBSDOWN :
constants.EMOJI_THUMBSUP;
const targetNote = getters.notesById[noteId]; const targetNote = getters.notesById[noteId];
let amIAwarded = false; let noteHasAward = false;
targetNote.award_emoji.forEach((a) => { targetNote.award_emoji.forEach((a) => {
if (a.name === counterAward && a.user.id === window.gon.current_user_id) { if (a.name === counterAward && a.user.id === window.gon.current_user_id) {
amIAwarded = true; noteHasAward = true;
} }
}); });
if (amIAwarded) { if (noteHasAward) {
data.awardName = counterAward; Object.assign(data, { awardName: counterAward });
data.skipMutalityCheck = true; Object.assign(data, { kipMutalityCheck: true });
dispatch(types.TOGGLE_AWARD, data); dispatch(types.TOGGLE_AWARD, data);
} }
...@@ -197,9 +194,7 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => { ...@@ -197,9 +194,7 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => {
}; };
export const scrollToNoteIfNeeded = (context, el) => { export const scrollToNoteIfNeeded = (context, el) => {
const isInViewport = gl.utils.isInViewport(el[0]); if (!gl.utils.isInViewport(el[0])) {
if (!isInViewport) {
gl.utils.scrollToElement(el); gl.utils.scrollToElement(el);
} }
}; };
export const notes = state => state.notes; export const notes = state => state.notes;
export const targetNoteHash = state => state.targetNoteHash; export const targetNoteHash = state => state.targetNoteHash;
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getIssueDataByProp = state => prop => state.notesData[prop];
export const getUserDataByProp = state => prop => state.notesData[prop];
export const notesById = (state) => { export const notesById = (state) => {
const notesByIdObject = {}; const notesByIdObject = {};
// TODO: FILIPA: TRANSFORM INTO A REDUCE
state.notes.forEach((note) => { state.notes.forEach((note) => {
note.notes.forEach((n) => { note.notes.forEach((n) => {
notesByIdObject[n.id] = n; notesByIdObject[n.id] = n;
......
...@@ -11,6 +11,11 @@ export default new Vuex.Store({ ...@@ -11,6 +11,11 @@ export default new Vuex.Store({
notes: [], notes: [],
targetNoteHash: null, targetNoteHash: null,
lastFetchedAt: null, lastFetchedAt: null,
// holds endpoints and permissions provided through haml
notesData: {},
userData: {},
issueData: {},
}, },
actions, actions,
getters, getters,
......
...@@ -2,6 +2,9 @@ export const ADD_NEW_NOTE = 'ADD_NEW_NOTE'; ...@@ -2,6 +2,9 @@ export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE'; export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_ISSUE_DATA = 'SET_ISSUE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
......
...@@ -58,6 +58,18 @@ export default { ...@@ -58,6 +58,18 @@ export default {
} }
}, },
[types.SET_NOTES_DATA](state, data) {
state.notesData = data;
},
[types.SET_ISSUE_DATA](state, data) {
state.issueData = data;
},
[types.SET_USER_DATA](state, data) {
state.userData = data;
},
[types.SET_INITAL_NOTES](state, notes) { [types.SET_INITAL_NOTES](state, notes) {
state.notes = notes; state.notes = notes;
}, },
......
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%section.js-notes-wrapper{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json), new_session_path: new_session_path(:user, redirect_to_referer: 'yes'), notes_path: notes_url, last_fetched_at: Time.now.to_i } } %section
#js-notes #js-vue-notes{ data: { discussions_path: discussions_namespace_project_issue_path(@project.namespace, @project, @issue, format: :json),
new_session_path: new_session_path(:user, redirect_to_referer: 'yes'),
notes_path: '#{notes_url}?full_data=1', last_fetched_at: Time.now.to_i,
issue_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json }}
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'notes' = webpack_bundle_tag 'notes'
/ #notes{style: "margin-top: 150px"}
/ = render 'shared/notes/notes_with_form', :autocomplete => true
= render "layouts/init_auto_complete" = render "layouts/init_auto_complete"
:javascript :javascript
......
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