diff options
-rw-r--r-- | app/assets/javascripts/notes/components/issue_comment_form.vue | 30 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/issue_discussion.vue | 15 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/issue_note_actions.vue | 10 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/issue_notes.vue | 5 | ||||
-rw-r--r-- | app/assets/javascripts/notes/index.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/actions.js | 205 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/getters.js | 15 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/index.js | 18 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/issue_notes_store.js | 342 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/issue_notes_utils.js | 35 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/mutation_types.js | 11 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/mutations.js | 127 | ||||
-rw-r--r-- | app/assets/javascripts/notes/stores/utils.js | 31 |
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(); + |