summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue30
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue15
-rw-r--r--app/assets/javascripts/notes/components/issue_note_actions.vue10
-rw-r--r--app/assets/javascripts/notes/components/issue_notes.vue5
-rw-r--r--app/assets/javascripts/notes/index.js10
-rw-r--r--app/assets/javascripts/notes/stores/actions.js205
-rw-r--r--app/assets/javascripts/notes/stores/getters.js15
-rw-r--r--app/assets/javascripts/notes/stores/index.js18
-rw-r--r--app/assets/javascripts/notes/stores/issue_notes_store.js342
-rw-r--r--app/assets/javascripts/notes/stores/issue_notes_utils.js35
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js11
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js127
-rw-r--r--app/assets/javascripts/notes/stores/utils.js31
13 files changed, 446 insertions, 408 deletions
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 7896f872a7d..6519624f7e0 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -209,12 +209,13 @@ export default {
aria-hidden="true"
class="fa fa-caret-down toggle-icon"></i>
</button>
- <ul
- class="note-type-dropdown dropdown-open-top dropdown-menu">
+ <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
<li
:class="{ 'droplab-item-selected': noteType === 'comment' }"
@click.prevent="setNoteType('comment')">
- <button class="btn btn-transparent">
+ <button
+ type="button"
+ class="btn btn-transparent">
<i
aria-hidden="true"
class="fa fa-check icon"></i>
@@ -230,10 +231,13 @@ export default {
<li
:class="{ 'droplab-item-selected': noteType === 'discussion' }"
@click.prevent="setNoteType('discussion')">
- <button class="btn btn-transparent">
+ <button
+ type="button"
+ class="btn btn-transparent">
<i
aria-hidden="true"
- class="fa fa-check icon"></i>
+ class="fa fa-check icon">
+ </i>
<div class="description">
<strong>Start discussion</strong>
<p>
@@ -244,21 +248,21 @@ export default {
</li>
</ul>
</div>
- <a
+ <button
+ type="button"
@click="handleSave(true)"
v-if="canUpdateIssue"
:class="actionButtonClassNames"
- class="btn btn-nr btn-comment btn-comment-and-close"
- role="button">
+ class="btn btn-nr btn-comment btn-comment-and-close">
{{issueActionButtonTitle}}
- </a>
- <a
+ </button>
+ <button
+ type="button"
v-if="note.length"
@click="discard"
- class="btn btn-cancel js-note-discard"
- role="button">
+ class="btn btn-cancel js-note-discard">
Discard draft
- </a>
+ </button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index 371ceffbf20..7fc4a41fa8a 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -71,7 +71,8 @@ export default {
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = 'Are you sure you want to cancel creating this comment?';
- const isConfirmed = confirm(msg); // eslint-disable-line
+ // eslint-disable-next-line no-alert
+ const isConfirmed = confirm(msg);
if (!isConfirmed) {
return;
}
@@ -112,7 +113,8 @@ export default {
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
- :img-size="40" />
+ :img-size="40"
+ />
</div>
<div class="timeline-content">
<div class="discussion">
@@ -123,13 +125,15 @@ export default {
:note-id="discussion.id"
:include-toggle="true"
:toggle-handler="toggleDiscussion"
- actionText="started a discussion" />
+ actionText="started a discussion"
+ />
<issue-note-edited-text
v-if="note.last_updated_by"
:edited-at="note.last_updated_at"
:edited-by="note.last_updated_by"
actionText="Last updated"
- className="discussion-headline-light js-discussion-headline" />
+ className="discussion-headline-light js-discussion-headline"
+ />
</div>
</div>
<div
@@ -142,7 +146,8 @@ export default {
v-for="note in note.notes"
:is="componentName(note)"
:note="componentData(note)"
- key="note.id" />
+ key="note.id"
+ />
</ul>
<div class="flash-container"></div>
<div class="discussion-reply-holder">
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue
index 48b1e1d513f..df5a060d894 100644
--- a/app/assets/javascripts/notes/components/issue_note_actions.vue
+++ b/app/assets/javascripts/notes/components/issue_note_actions.vue
@@ -2,6 +2,7 @@
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
+import loadingIcon from '../../vue_shared/components/loadingIcon.vue';
export default {
props: {
@@ -78,9 +79,7 @@ export default {
data-position="right"
href="#"
title="Add reaction">
- <i
- aria-hidden="true"
- class="fa fa-spinner fa-spin"></i>
+ <loading-icon />
<span
v-html="emojiSmiling"
class="link-highlight award-control-icon-neutral"></span>
@@ -122,15 +121,14 @@ export default {
</a>
</li>
<li v-if="canEdit">
- <a
+ <button
@click.prevent="deleteHandler"
class="btn btn-transparent js-note-delete js-note-delete"
- href="#"
type="button">
<span class="text-danger">
Delete comment
</span>
- </a>
+ </button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/notes/components/issue_notes.vue b/app/assets/javascripts/notes/components/issue_notes.vue
index 94372cd9a77..2fe071cc990 100644
--- a/app/assets/javascripts/notes/components/issue_notes.vue
+++ b/app/assets/javascripts/notes/components/issue_notes.vue
@@ -12,10 +12,7 @@ import issueSystemNote from './issue_system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
-
-Vue.use(Vuex);
-Vue.use(VueResource);
-const store = new Vuex.Store(storeOptions);
+import store from './store';
export default {
name: 'IssueNotes',
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index d48a9111ffe..914e08ff112 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -8,9 +8,13 @@ document.addEventListener('DOMContentLoaded', () => {
components: {
issueNotes,
},
- template: `
- <issue-notes ref="notes" />
- `,
+ render(createElement) {
+ return createElement('issue-notes', {
+ attrs: {
+ ref: 'notes',
+ },
+ });
+ },
});
window.issueNotes = {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
new file mode 100644
index 00000000000..3cfdcd68e0d
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -0,0 +1,205 @@
+/* global Flash */
+
+import * as types from './mutation_types';
+import * as utils from './issue_notes_utils';
+import service from '../services/issue_notes_service';
+import loadAwardsHandler from '../../awards_handler';
+import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
+
+export const fetchNotes = ({ commit }, path) => service
+ .fetchNotes(path)
+ .then(res => res.json())
+ .then((res) => {
+ commit(types.SET_INITAL_NOTES, res);
+ });
+
+export const deleteNote = ({ commit }, note) => service
+ .deleteNote(note.path)
+ .then(() => {
+ commit(types.DELETE_NOTE, note);
+ });
+
+export const updateNote = ({ commit }, data) => {
+ const { endpoint, note } = data;
+
+ return service
+ .updateNote(endpoint, note)
+ .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 createNewNote = ({ commit }, note) => {
+ const { endpoint, data } = note;
+
+ return service
+ .createNewNote(endpoint, data)
+ .then(res => res.json())
+ .then((res) => {
+ if (!res.errors) {
+ commit(types.ADD_NEW_NOTE, res);
+ }
+ return res;
+ });
+};
+
+export const saveNote = ({ commit, dispatch }, noteData) => {
+ const { note } = noteData.data.note;
+ let placeholderText = note;
+ const hasQuickActions = utils.hasQuickActions(placeholderText);
+ const replyId = noteData.data.in_reply_to_discussion_id;
+ const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
+
+ if (hasQuickActions) {
+ placeholderText = utils.stripQuickActions(placeholderText);
+ }
+
+ if (placeholderText.length) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ noteBody: placeholderText,
+ replyId,
+ });
+ }
+
+ if (hasQuickActions) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ isSystemNote: true,
+ noteBody: utils.getQuickActionText(note),
+ replyId,
+ });
+ }
+
+ return dispatch(methodToDispatch, noteData)
+ .then((res) => {
+ const { errors } = res;
+ const commandsChanges = res.commands_changes;
+
+ if (hasQuickActions && Object.keys(errors).length) {
+ dispatch('poll');
+ $('.js-gfm-input').trigger('clear-commands-cache.atwho');
+ Flash('Commands applied', 'notice', $(noteData.flashContainer));
+ }
+
+ if (commandsChanges) {
+ if (commandsChanges.emoji_award) {
+ const votesBlock = $('.js-awards-block').eq(0);
+
+ loadAwardsHandler()
+ .then((awardsHandler) => {
+ awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
+ awardsHandler.scrollToAwards();
+ })
+ .catch(() => {
+ Flash(
+ 'Something went wrong while adding your award. Please try again.',
+ null,
+ $(noteData.flashContainer),
+ );
+ });
+ }
+
+ if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
+ sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
+ }
+ }
+
+ if (errors && errors.commands_only) {
+ Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
+ }
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+
+ return res;
+ })
+ .catch(() => {
+ Flash(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ 'alert',
+ $(noteData.flashContainer),
+ );
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+ });
+};
+
+export const poll = ({ commit, state, getters }) => {
+ const { notesPath } = $('.js-notes-wrapper')[0].dataset;
+
+ return service
+ .poll(`${notesPath}?full_data=1`, state.lastFetchedAt)
+ .then(res => res.json())
+ .then((res) => {
+ if (res.notes.length) {
+ const { notesById } = getters;
+
+ res.notes.forEach((note) => {
+ if (notesById[note.id]) {
+ commit(types.UPDATE_NOTE, note);
+ } else if (note.type === 'DiscussionNote') {
+ const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (discussion) {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ });
+ }
+
+ return res;
+ });
+};
+
+export const toggleAward = ({ commit, getters, dispatch }, data) => {
+ const { endpoint, awardName, noteId, skipMutalityCheck } = data;
+ const note = getters.notesById[noteId];
+
+ return service
+ .toggleAward(endpoint, { name: awardName })
+ .then(res => res.json())
+ .then(() => {
+ commit(types.TOGGLE_AWARD, { awardName, note });
+
+ if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) {
+ const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
+ const targetNote = getters.notesById[noteId];
+ let amIAwarded = false;
+
+ targetNote.award_emoji.forEach((a) => {
+ if (a.name === counterAward && a.user.id === window.gon.current_user_id) {
+ amIAwarded = true;
+ }
+ });
+
+ if (amIAwarded) {
+ Object.assign(data, { awardName: counterAward });
+ Object.assign(data, { skipMutalityCheck: true });
+
+ dispatch(types.TOGGLE_AWARD, data);
+ }
+ }
+ });
+};
+
+export const scrollToNoteIfNeeded = (context, el) => {
+ const isInViewport = gl.utils.isInViewport(el[0]);
+
+ if (!isInViewport) {
+ gl.utils.scrollToElement(el);
+ }
+};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
new file mode 100644
index 00000000000..c3a9f0a5e89
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -0,0 +1,15 @@
+export const notes = state => state.notes;
+
+export const targetNoteHash = state => state.targetNoteHash;
+
+export const notesById = (state) => {
+ const notesByIdObject = {};
+
+ state.notes.forEach((note) => {
+ note.notes.forEach((n) => {
+ notesByIdObject[n.id] = n;
+ });
+ });
+
+ return notesByIdObject;
+};
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
new file mode 100644
index 00000000000..edca63fae67
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: {
+ notes: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+ },
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/notes/stores/issue_notes_store.js b/app/assets/javascripts/notes/stores/issue_notes_store.js
deleted file mode 100644
index abe5edee03b..00000000000
--- a/app/assets/javascripts/notes/stores/issue_notes_store.js
+++ /dev/null
@@ -1,342 +0,0 @@
-/* eslint-disable no-param-reassign */
-/* global Flash */
-
-import service from '../services/issue_notes_service';
-import utils from './issue_notes_utils';
-import loadAwardsHandler from '../../awards_handler';
-import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
-
-const state = {
- notes: [],
- targetNoteHash: null,
- lastFetchedAt: null,
-};
-
-const getters = {
- notes(storeState) {
- return storeState.notes;
- },
- targetNoteHash(storeState) {
- return storeState.targetNoteHash;
- },
- notesById(storeState) {
- const notesById = {};
-
- storeState.notes.forEach((note) => {
- note.notes.forEach((n) => {
- notesById[n.id] = n;
- });
- });
-
- return notesById;
- },
-};
-
-const mutations = {
- setInitialNotes(storeState, notes) {
- storeState.notes = notes;
- },
- setTargetNoteHash(storeState, hash) {
- storeState.targetNoteHash = hash;
- },
- toggleDiscussion(storeState, { discussionId }) {
- const discussion = utils.findNoteObjectById(storeState.notes, discussionId);
-
- discussion.expanded = !discussion.expanded;
- },
- deleteNote(storeState, note) {
- const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id);
-
- if (noteObj.individual_note) {
- storeState.notes.splice(storeState.notes.indexOf(noteObj), 1);
- } else {
- const comment = utils.findNoteObjectById(noteObj.notes, note.id);
- noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
-
- if (!noteObj.notes.length) {
- storeState.notes.splice(storeState.notes.indexOf(noteObj), 1);
- }
- }
- },
- addNewReplyToDiscussion(storeState, note) {
- const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id);
-
- if (noteObj) {
- noteObj.notes.push(note);
- }
- },
- updateNote(storeState, note) {
- const noteObj = utils.findNoteObjectById(storeState.notes, note.discussion_id);
-
- if (noteObj.individual_note) {
- noteObj.notes.splice(0, 1, note);
- } else {
- const comment = utils.findNoteObjectById(noteObj.notes, note.id);
- noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
- }
- },
- addNewNote(storeState, note) {
- const { discussion_id, type } = note;
- const noteData = {
- expanded: true,
- id: discussion_id,
- individual_note: !(type === 'DiscussionNote'),
- notes: [note],
- reply_id: discussion_id,
- };
-
- storeState.notes.push(noteData);
- },
- toggleAward(storeState, data) {
- const { awardName, note } = data;
- const { id, name, username } = window.gl.currentUserData;
- let index = -1;
-
- note.award_emoji.forEach((a, i) => {
- if (a.name === awardName && a.user.id === id) {
- index = i;
- }
- });
-
- if (index > -1) { // if I am awarded, remove my award
- note.award_emoji.splice(index, 1);
- } else {
- note.award_emoji.push({
- name: awardName,
- user: { id, name, username },
- });
- }
- },
- setLastFetchedAt(storeState, fetchedAt) {
- storeState.lastFetchedAt = fetchedAt;
- },
- showPlaceholderNote(storeState, data) {
- let notesArr = storeState.notes;
- if (data.replyId) {
- notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
- }
-
- notesArr.push({
- individual_note: true,
- isPlaceholderNote: true,
- placeholderType: data.isSystemNote ? 'systemNote' : 'note',
- notes: [
- {
- body: data.noteBody,
- },
- ],
- });
- },
- removePlaceholderNotes(storeState) {
- const { notes } = storeState;
-
- for (let i = notes.length - 1; i >= 0; i -= 1) {
- const note = notes[i];
- const children = note.notes;
-
- if (children.length && !note.individual_note) { // remove placeholder from discussions
- for (let j = children.length - 1; j >= 0; j -= 1) {
- if (children[j].isPlaceholderNote) {
- children.splice(j, 1);
- }
- }
- } else if (note.isPlaceholderNote) { // remove placeholders from state root
- notes.splice(i, 1);
- }
- }
- },
-};
-
-const actions = {
- fetchNotes(context, path) {
- return service
- .fetchNotes(path)
- .then(res => res.json())
- .then((res) => {
- context.commit('setInitialNotes', res);
- });
- },
- deleteNote(context, note) {
- return service
- .deleteNote(note.path)
- .then(() => {
- context.commit('deleteNote', note);
- });
- },
- updateNote(context, data) {
- const { endpoint, note } = data;
-
- return service
- .updateNote(endpoint, note)
- .then(res => res.json())
- .then((res) => {
- context.commit('updateNote', res);
- });
- },
- replyToDiscussion(context, noteData) {
- const { endpoint, data } = noteData;
-
- return service
- .replyToDiscussion(endpoint, data)
- .then(res => res.json())
- .then((res) => {
- context.commit('addNewReplyToDiscussion', res);
-
- return res;
- });
- },
- createNewNote(context, noteData) {
- const { endpoint, data } = noteData;
-
- return service
- .createNewNote(endpoint, data)
- .then(res => res.json())
- .then((res) => {
- if (!res.errors) {
- context.commit('addNewNote', res);
- }
- return res;
- });
- },
- saveNote(context, noteData) {
- const { note } = noteData.data.note;
- let placeholderText = note;
- const hasQuickActions = utils.hasQuickActions(placeholderText);
- const replyId = noteData.data.in_reply_to_discussion_id;
- const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
-
- if (hasQuickActions) {
- placeholderText = utils.stripQuickActions(placeholderText);
- }
-
- if (placeholderText.length) {
- context.commit('showPlaceholderNote', {
- noteBody: placeholderText,
- replyId,
- });
- }
-
- if (hasQuickActions) {
- context.commit('showPlaceholderNote', {
- isSystemNote: true,
- noteBody: utils.getQuickActionText(note),
- replyId,
- });
- }
-
- return context.dispatch(methodToDispatch, noteData)
- .then((res) => {
- const { errors } = res;
- const commandsChanges = res.commands_changes;
-
- if (hasQuickActions && Object.keys(errors).length) {
- context.dispatch('poll');
- $('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', $(noteData.flashContainer));
- }
-
- if (commandsChanges) {
- if (commandsChanges.emoji_award) {
- const votesBlock = $('.js-awards-block').eq(0);
-
- loadAwardsHandler().then((awardsHandler) => {
- awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
- awardsHandler.scrollToAwards();
- }).catch(() => {
- const msg = 'Something went wrong while adding your award. Please try again.';
- Flash(msg, $(noteData.flashContainer));
- });
- }
-
- if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
- sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
- }
- }
-
- if (errors && errors.commands_only) {
- Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
- }
- context.commit('removePlaceholderNotes');
-
- return res;
- })
- .catch(() => {
- const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
- Flash(msg, 'alert', $(noteData.flashContainer));
- context.commit('removePlaceholderNotes');
- });
- },
- poll(context) {
- const { notesPath } = $('.js-notes-wrapper')[0].dataset;
-
- return service
- .poll(`${notesPath}?full_data=1`, context.state.lastFetchedAt)
- .then(res => res.json())
- .then((res) => {
- if (res.notes.length) {
- const { notesById } = context.getters;
-
- res.notes.forEach((note) => {
- if (notesById[note.id]) {
- context.commit('updateNote', note);
- } else if (note.type === 'DiscussionNote') {
- const discussion = utils.findNoteObjectById(context.state.notes, note.discussion_id);
-
- if (discussion) {
- context.commit('addNewReplyToDiscussion', note);
- } else {
- context.commit('addNewNote', note);
- }
- } else {
- context.commit('addNewNote', note);
- }
- });
- }
-
- return res;
- });
- },
- toggleAward(context, data) {
- const { endpoint, awardName, noteId, skipMutalityCheck } = data;
- const note = context.getters.notesById[noteId];
-
- return service
- .toggleAward(endpoint, { name: awardName })
- .then(res => res.json())
- .then(() => {
- context.commit('toggleAward', { awardName, note });
-
- if (!skipMutalityCheck && (awardName === 'thumbsup' || awardName === 'thumbsdown')) {
- const counterAward = awardName === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
- const targetNote = context.getters.notesById[noteId];
- let amIAwarded = false;
-
- targetNote.award_emoji.forEach((a) => {
- if (a.name === counterAward && a.user.id === window.gon.current_user_id) {
- amIAwarded = true;
- }
- });
-
- if (amIAwarded) {
- data.awardName = counterAward;
- data.skipMutalityCheck = true;
- context.dispatch('toggleAward', data);
- }
- }
- });
- },
- scrollToNoteIfNeeded(context, el) {
- const isInViewport = gl.utils.isInViewport(el[0]);
-
- if (!isInViewport) {
- gl.utils.scrollToElement(el);
- }
- },
-};
-
-export default {
- state,
- getters,
- mutations,
- actions,
-};
diff --git a/app/assets/javascripts/notes/stores/issue_notes_utils.js b/app/assets/javascripts/notes/stores/issue_notes_utils.js
deleted file mode 100644
index eac80c9f9c2..00000000000
--- a/app/assets/javascripts/notes/stores/issue_notes_utils.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
-
-const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
-
-export default {
- findNoteObjectById(notes, id) {
- return notes.filter(n => n.id === id)[0];
- },
- getQuickActionText(note) {
- let text = 'Applying command';
- const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
-
- const executedCommands = quickActions.filter((command) => {
- const commandRegex = new RegExp(`/${command.name}`);
- return commandRegex.test(note);
- });
-
- if (executedCommands && executedCommands.length) {
- if (executedCommands.length > 1) {
- text = 'Applying multiple commands';
- } else {
- const commandDescription = executedCommands[0].description.toLowerCase();
- text = `Applying command to ${commandDescription}`;
- }
- }
-
- return text;
- },
- hasQuickActions(note) {
- return REGEX_QUICK_ACTIONS.test(note);
- },
- stripQuickActions(note) {
- return note.replace(REGEX_QUICK_ACTIONS, '').trim();
- },
-};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
new file mode 100644
index 00000000000..f84f26684ca
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -0,0 +1,11 @@
+export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
+export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
+export const DELETE_NOTE = 'DELETE_NOTE';
+export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
+export const SET_INITAL_NOTES = 'SET_INITIAL_NOTES';
+export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
+export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
+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';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
new file mode 100644
index 00000000000..61be4aa1864
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -0,0 +1,127 @@
+import * as utils from './utils';
+import * as types from './mutation_types';
+
+export default {
+ [types.ADD_NEW_NOTE](state, note) {
+ const { discussion_id, type } = note;
+ const noteData = {
+ expanded: true,
+ id: discussion_id,
+ individual_note: !(type === 'DiscussionNote'),
+ notes: [note],
+ reply_id: discussion_id,
+ };
+
+ state.notes.push(noteData);
+ },
+
+ [types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj) {
+ noteObj.notes.push(note);
+ }
+ },
+
+ [types.DELETE_NOTE](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj.individual_note) {
+ state.notes.splice(state.notes.indexOf(noteObj), 1);
+ } else {
+ const comment = utils.findNoteObjectById(noteObj.notes, note.id);
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
+
+ if (!noteObj.notes.length) {
+ state.notes.splice(state.notes.indexOf(noteObj), 1);
+ }
+ }
+ },
+
+ [types.REMOVE_PLACEHOLDER_NOTES](state) {
+ const { notes } = state;
+
+ for (let i = notes.length - 1; i >= 0; i -= 1) {
+ const note = notes[i];
+ const children = note.notes;
+
+ if (children.length && !note.individual_note) { // remove placeholder from discussions
+ for (let j = children.length - 1; j >= 0; j -= 1) {
+ if (children[j].isPlaceholderNote) {
+ children.splice(j, 1);
+ }
+ }
+ } else if (note.isPlaceholderNote) { // remove placeholders from state root
+ notes.splice(i, 1);
+ }
+ }
+ },
+
+ [types.SET_INITAL_NOTES](state, notes) {
+ state.notes = notes;
+ },
+
+ [types.SET_LAST_FETCHED_AT](state, fetchedAt) {
+ state.lastFetchedAt = fetchedAt;
+ },
+
+ [types.SET_TARGET_NOTE_HASH](state, hash) {
+ state.targetNoteHash = hash;
+ },
+
+ [types.SHOW_PLACEHOLDER_NOTE](state, data) {
+ let notesArr = state.notes;
+ if (data.replyId) {
+ notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
+ }
+
+ notesArr.push({
+ individual_note: true,
+ isPlaceholderNote: true,
+ placeholderType: data.isSystemNote ? 'systemNote' : 'note',
+ notes: [
+ {
+ body: data.noteBody,
+ },
+ ],
+ });
+ },
+
+ [types.TOGGLE_AWARD](state, data) {
+ const { awardName, note } = data;
+ const { id, name, username } = window.gl.currentUserData;
+ let index = -1;
+
+ note.award_emoji.forEach((a, i) => {
+ if (a.name === awardName && a.user.id === id) {
+ index = i;
+ }
+ });
+
+ if (index > -1) { // if I am awarded, remove my award
+ note.award_emoji.splice(index, 1);
+ } else {
+ note.award_emoji.push({
+ name: awardName,
+ user: { id, name, username },
+ });
+ }
+ },
+
+ [types.TOGGLE_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.notes, discussionId);
+
+ discussion.expanded = !discussion.expanded;
+ },
+
+ [types.UPDATE_NOTE](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj.individual_note) {
+ noteObj.notes.splice(0, 1, note);
+ } else {
+ const comment = utils.findNoteObjectById(noteObj.notes, note.id);
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+ }
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
new file mode 100644
index 00000000000..6074115e855
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -0,0 +1,31 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+
+const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
+
+export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
+
+export const getQuickActionText = (note) => {
+ let text = 'Applying command';
+ const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
+
+ const executedCommands = quickActions.filter((command) => {
+ const commandRegex = new RegExp(`/${command.name}`);
+ return commandRegex.test(note);
+ });
+
+ if (executedCommands && executedCommands.length) {
+ if (executedCommands.length > 1) {
+ text = 'Applying multiple commands';
+ } else {
+ const commandDescription = executedCommands[0].description.toLowerCase();
+ text = `Applying command to ${commandDescription}`;
+ }
+ }
+
+ return text;
+};
+
+export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
+
+export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
+