From 37a31c200bedf44b3ee8297d3f119f9536ba50b9 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Thu, 30 Nov 2017 16:48:26 +1100 Subject: move notes app to vue_shared --- .../javascripts/notes/components/comment_form.vue | 381 ----------------- .../notes/components/discussion_locked_widget.vue | 26 -- .../javascripts/notes/components/note_actions.vue | 167 -------- .../notes/components/note_attachment.vue | 37 -- .../notes/components/note_awards_list.vue | 227 ----------- .../javascripts/notes/components/note_body.vue | 122 ------ .../notes/components/note_edited_text.vue | 47 --- .../javascripts/notes/components/note_form.vue | 174 -------- .../javascripts/notes/components/note_header.vue | 118 ------ .../notes/components/note_signed_out_widget.vue | 27 -- .../notes/components/noteable_discussion.vue | 229 ----------- .../javascripts/notes/components/noteable_note.vue | 187 --------- .../javascripts/notes/components/notes_app.vue | 151 ------- app/assets/javascripts/notes/constants.js | 11 - app/assets/javascripts/notes/event_hub.js | 3 - app/assets/javascripts/notes/index.js | 35 -- app/assets/javascripts/notes/mixins/autosave.js | 15 - .../javascripts/notes/mixins/issuable_state.js | 15 - .../javascripts/notes/services/notes_service.js | 35 -- app/assets/javascripts/notes/stores/actions.js | 226 ----------- app/assets/javascripts/notes/stores/getters.js | 31 -- app/assets/javascripts/notes/stores/index.js | 23 -- .../javascripts/notes/stores/mutation_types.js | 14 - app/assets/javascripts/notes/stores/mutations.js | 155 ------- app/assets/javascripts/notes/stores/utils.js | 31 -- .../components/notes/placeholder_note.vue | 70 ---- .../components/notes/placeholder_system_note.vue | 29 -- .../vue_shared/components/notes/system_note.vue | 73 ---- .../vue_shared/notes/components/comment_form.vue | 381 +++++++++++++++++ .../notes/components/discussion_locked_widget.vue | 26 ++ .../vue_shared/notes/components/note_actions.vue | 167 ++++++++ .../notes/components/note_attachment.vue | 37 ++ .../notes/components/note_awards_list.vue | 227 +++++++++++ .../vue_shared/notes/components/note_body.vue | 122 ++++++ .../notes/components/note_edited_text.vue | 47 +++ .../vue_shared/notes/components/note_form.vue | 174 ++++++++ .../vue_shared/notes/components/note_header.vue | 118 ++++++ .../notes/components/note_signed_out_widget.vue | 27 ++ .../notes/components/noteable_discussion.vue | 230 +++++++++++ .../vue_shared/notes/components/noteable_note.vue | 187 +++++++++ .../vue_shared/notes/components/notes_app.vue | 151 +++++++ .../notes/components/placeholder_note.vue | 70 ++++ .../notes/components/placeholder_system_note.vue | 29 ++ .../vue_shared/notes/components/system_note.vue | 73 ++++ .../javascripts/vue_shared/notes/constants.js | 11 + .../javascripts/vue_shared/notes/event_hub.js | 3 + app/assets/javascripts/vue_shared/notes/index.js | 37 ++ .../vue_shared/notes/mixins/autosave.js | 15 + .../vue_shared/notes/mixins/issuable_state.js | 15 + .../vue_shared/notes/services/notes_service.js | 35 ++ .../javascripts/vue_shared/notes/stores/actions.js | 226 +++++++++++ .../javascripts/vue_shared/notes/stores/getters.js | 31 ++ .../javascripts/vue_shared/notes/stores/index.js | 23 ++ .../vue_shared/notes/stores/mutation_types.js | 14 + .../vue_shared/notes/stores/mutations.js | 155 +++++++ .../javascripts/vue_shared/notes/stores/utils.js | 31 ++ .../notes/components/comment_form_spec.js | 207 ---------- .../notes/components/note_actions_spec.js | 91 ----- spec/javascripts/notes/components/note_app_spec.js | 255 ------------ .../notes/components/note_attachment_spec.js | 23 -- .../notes/components/note_awards_list_spec.js | 56 --- .../javascripts/notes/components/note_body_spec.js | 46 --- .../notes/components/note_edited_text_spec.js | 47 --- .../javascripts/notes/components/note_form_spec.js | 112 ----- .../notes/components/note_header_spec.js | 94 ----- .../components/note_signed_out_widget_spec.js | 37 -- .../notes/components/noteable_discussion_spec.js | 50 --- .../notes/components/noteable_note_spec.js | 44 -- spec/javascripts/notes/mock_data.js | 449 --------------------- spec/javascripts/notes/stores/actions_spec.js | 61 --- spec/javascripts/notes/stores/getters_spec.js | 58 --- spec/javascripts/notes/stores/mutation_spec.js | 219 ---------- .../components/notes/placeholder_note_spec.js | 39 -- .../notes/placeholder_system_note_spec.js | 25 -- .../components/notes/system_note_spec.js | 57 --- .../notes/components/comment_form_spec.js | 207 ++++++++++ .../notes/components/note_actions_spec.js | 91 +++++ .../vue_shared/notes/components/note_app_spec.js | 255 ++++++++++++ .../notes/components/note_attachment_spec.js | 23 ++ .../notes/components/note_awards_list_spec.js | 56 +++ .../vue_shared/notes/components/note_body_spec.js | 46 +++ .../notes/components/note_edited_text_spec.js | 47 +++ .../vue_shared/notes/components/note_form_spec.js | 112 +++++ .../notes/components/note_header_spec.js | 94 +++++ .../components/note_signed_out_widget_spec.js | 37 ++ .../notes/components/noteable_discussion_spec.js | 50 +++ .../notes/components/noteable_note_spec.js | 44 ++ .../components/notes/placeholder_note_spec.js | 39 ++ .../notes/placeholder_system_note_spec.js | 25 ++ .../notes/components/notes/system_note_spec.js | 57 +++ spec/javascripts/vue_shared/notes/mock_data.js | 449 +++++++++++++++++++++ .../vue_shared/notes/stores/actions_spec.js | 61 +++ .../vue_shared/notes/stores/getters_spec.js | 58 +++ .../vue_shared/notes/stores/mutation_spec.js | 219 ++++++++++ 94 files changed, 4632 insertions(+), 4629 deletions(-) delete mode 100644 app/assets/javascripts/notes/components/comment_form.vue delete mode 100644 app/assets/javascripts/notes/components/discussion_locked_widget.vue delete mode 100644 app/assets/javascripts/notes/components/note_actions.vue delete mode 100644 app/assets/javascripts/notes/components/note_attachment.vue delete mode 100644 app/assets/javascripts/notes/components/note_awards_list.vue delete mode 100644 app/assets/javascripts/notes/components/note_body.vue delete mode 100644 app/assets/javascripts/notes/components/note_edited_text.vue delete mode 100644 app/assets/javascripts/notes/components/note_form.vue delete mode 100644 app/assets/javascripts/notes/components/note_header.vue delete mode 100644 app/assets/javascripts/notes/components/note_signed_out_widget.vue delete mode 100644 app/assets/javascripts/notes/components/noteable_discussion.vue delete mode 100644 app/assets/javascripts/notes/components/noteable_note.vue delete mode 100644 app/assets/javascripts/notes/components/notes_app.vue delete mode 100644 app/assets/javascripts/notes/constants.js delete mode 100644 app/assets/javascripts/notes/event_hub.js delete mode 100644 app/assets/javascripts/notes/index.js delete mode 100644 app/assets/javascripts/notes/mixins/autosave.js delete mode 100644 app/assets/javascripts/notes/mixins/issuable_state.js delete mode 100644 app/assets/javascripts/notes/services/notes_service.js delete mode 100644 app/assets/javascripts/notes/stores/actions.js delete mode 100644 app/assets/javascripts/notes/stores/getters.js delete mode 100644 app/assets/javascripts/notes/stores/index.js delete mode 100644 app/assets/javascripts/notes/stores/mutation_types.js delete mode 100644 app/assets/javascripts/notes/stores/mutations.js delete mode 100644 app/assets/javascripts/notes/stores/utils.js delete mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue delete mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue delete mode 100644 app/assets/javascripts/vue_shared/components/notes/system_note.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/comment_form.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/discussion_locked_widget.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_actions.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_attachment.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_awards_list.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_body.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_edited_text.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_form.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_header.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/note_signed_out_widget.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/noteable_discussion.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/noteable_note.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/notes_app.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/placeholder_note.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/placeholder_system_note.vue create mode 100644 app/assets/javascripts/vue_shared/notes/components/system_note.vue create mode 100644 app/assets/javascripts/vue_shared/notes/constants.js create mode 100644 app/assets/javascripts/vue_shared/notes/event_hub.js create mode 100644 app/assets/javascripts/vue_shared/notes/index.js create mode 100644 app/assets/javascripts/vue_shared/notes/mixins/autosave.js create mode 100644 app/assets/javascripts/vue_shared/notes/mixins/issuable_state.js create mode 100644 app/assets/javascripts/vue_shared/notes/services/notes_service.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/actions.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/getters.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/index.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/mutation_types.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/mutations.js create mode 100644 app/assets/javascripts/vue_shared/notes/stores/utils.js delete mode 100644 spec/javascripts/notes/components/comment_form_spec.js delete mode 100644 spec/javascripts/notes/components/note_actions_spec.js delete mode 100644 spec/javascripts/notes/components/note_app_spec.js delete mode 100644 spec/javascripts/notes/components/note_attachment_spec.js delete mode 100644 spec/javascripts/notes/components/note_awards_list_spec.js delete mode 100644 spec/javascripts/notes/components/note_body_spec.js delete mode 100644 spec/javascripts/notes/components/note_edited_text_spec.js delete mode 100644 spec/javascripts/notes/components/note_form_spec.js delete mode 100644 spec/javascripts/notes/components/note_header_spec.js delete mode 100644 spec/javascripts/notes/components/note_signed_out_widget_spec.js delete mode 100644 spec/javascripts/notes/components/noteable_discussion_spec.js delete mode 100644 spec/javascripts/notes/components/noteable_note_spec.js delete mode 100644 spec/javascripts/notes/mock_data.js delete mode 100644 spec/javascripts/notes/stores/actions_spec.js delete mode 100644 spec/javascripts/notes/stores/getters_spec.js delete mode 100644 spec/javascripts/notes/stores/mutation_spec.js delete mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js delete mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js delete mode 100644 spec/javascripts/vue_shared/components/notes/system_note_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/comment_form_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_actions_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_app_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_attachment_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_awards_list_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_body_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_edited_text_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_form_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_header_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/note_signed_out_widget_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/noteable_discussion_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/noteable_note_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/notes/placeholder_note_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/notes/placeholder_system_note_spec.js create mode 100644 spec/javascripts/vue_shared/notes/components/notes/system_note_spec.js create mode 100644 spec/javascripts/vue_shared/notes/mock_data.js create mode 100644 spec/javascripts/vue_shared/notes/stores/actions_spec.js create mode 100644 spec/javascripts/vue_shared/notes/stores/getters_spec.js create mode 100644 spec/javascripts/vue_shared/notes/stores/mutation_spec.js diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue deleted file mode 100644 index e594377bc40..00000000000 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ /dev/null @@ -1,381 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue deleted file mode 100644 index e6f7ee56ff3..00000000000 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue deleted file mode 100644 index 45fc6196be4..00000000000 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue deleted file mode 100644 index cd9571a4002..00000000000 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue deleted file mode 100644 index c3a340139e7..00000000000 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue deleted file mode 100644 index ac4e1ffe53a..00000000000 --- a/app/assets/javascripts/notes/components/note_body.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue deleted file mode 100644 index 49e09f0ecc5..00000000000 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue deleted file mode 100644 index 4d527cb6643..00000000000 --- a/app/assets/javascripts/notes/components/note_form.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue deleted file mode 100644 index 63aa3d777d0..00000000000 --- a/app/assets/javascripts/notes/components/note_header.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue deleted file mode 100644 index 45d3c2de355..00000000000 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue deleted file mode 100644 index d81b5004055..00000000000 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ /dev/null @@ -1,229 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue deleted file mode 100644 index 32e0332b711..00000000000 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ /dev/null @@ -1,187 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue deleted file mode 100644 index 8315b3e5ba6..00000000000 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js deleted file mode 100644 index a6961063c01..00000000000 --- a/app/assets/javascripts/notes/constants.js +++ /dev/null @@ -1,11 +0,0 @@ -export const DISCUSSION_NOTE = 'DiscussionNote'; -export const DISCUSSION = 'discussion'; -export const NOTE = 'note'; -export const SYSTEM_NOTE = 'systemNote'; -export const COMMENT = 'comment'; -export const OPENED = 'opened'; -export const REOPENED = 'reopened'; -export const CLOSED = 'closed'; -export const EMOJI_THUMBSUP = 'thumbsup'; -export const EMOJI_THUMBSDOWN = 'thumbsdown'; -export const NOTEABLE_TYPE = 'Issue'; diff --git a/app/assets/javascripts/notes/event_hub.js b/app/assets/javascripts/notes/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/notes/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js deleted file mode 100644 index ec77246e0d2..00000000000 --- a/app/assets/javascripts/notes/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; -import notesApp from './components/notes_app.vue'; - -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-vue-notes', - components: { - notesApp, - }, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; - - return { - noteableData: JSON.parse(notesDataset.noteableData), - currentUserData: JSON.parse(notesDataset.currentUserData), - notesData: { - lastFetchedAt: notesDataset.lastFetchedAt, - discussionsPath: notesDataset.discussionsPath, - newSessionPath: notesDataset.newSessionPath, - registerPath: notesDataset.registerPath, - notesPath: notesDataset.notesPath, - markdownDocsPath: notesDataset.markdownDocsPath, - quickActionsDocsPath: notesDataset.quickActionsDocsPath, - }, - }; - }, - render(createElement) { - return createElement('notes-app', { - props: { - noteableData: this.noteableData, - notesData: this.notesData, - userData: this.currentUserData, - }, - }); - }, -})); diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js deleted file mode 100644 index a008171beda..00000000000 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ /dev/null @@ -1,15 +0,0 @@ -import Autosave from '../../autosave'; - -export default { - methods: { - initAutoSave() { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue'); - }, - resetAutoSave() { - this.autosave.reset(); - }, - setAutoSave() { - this.autosave.save(); - }, - }, -}; diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js deleted file mode 100644 index 97f3ea0d5de..00000000000 --- a/app/assets/javascripts/notes/mixins/issuable_state.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - methods: { - isConfidential(issue) { - return !!issue.confidential; - }, - - isLocked(issue) { - return !!issue.discussion_locked; - }, - - hasWarning(issue) { - return this.isConfidential(issue) || this.isLocked(issue); - }, - }, -}; diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js deleted file mode 100644 index b51b0cb2013..00000000000 --- a/app/assets/javascripts/notes/services/notes_service.js +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); - -export default { - fetchNotes(endpoint) { - return Vue.http.get(endpoint); - }, - deleteNote(endpoint) { - return Vue.http.delete(endpoint); - }, - replyToDiscussion(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); - }, - updateNote(endpoint, data) { - return Vue.http.put(endpoint, data, { emulateJSON: true }); - }, - createNewNote(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); - }, - poll(data = {}) { - const { endpoint, lastFetchedAt } = data; - const options = { - headers: { - 'X-Last-Fetched-At': lastFetchedAt, - }, - }; - - return Vue.http.get(endpoint, options); - }, - toggleAward(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); - }, -}; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js deleted file mode 100644 index 085b18642ba..00000000000 --- a/app/assets/javascripts/notes/stores/actions.js +++ /dev/null @@ -1,226 +0,0 @@ -import Visibility from 'visibilityjs'; -import Flash from '../../flash'; -import Poll from '../../lib/utils/poll'; -import * as types from './mutation_types'; -import * as utils from './utils'; -import * as constants from '../constants'; -import service from '../services/notes_service'; -import loadAwardsHandler from '../../awards_handler'; -import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; -import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; - -let eTagPoll; - -export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); -export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_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_INITIAL_NOTES, data); -export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); -export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); - -export const fetchNotes = ({ commit }, path) => service - .fetchNotes(path) - .then(res => res.json()) - .then((res) => { - commit(types.SET_INITIAL_NOTES, res); - }); - -export const deleteNote = ({ commit }, note) => service - .deleteNote(note.path) - .then(() => { - commit(types.DELETE_NOTE, note); - }); - -export const updateNote = ({ commit }, { endpoint, note }) => service - .updateNote(endpoint, note) - .then(res => res.json()) - .then((res) => { - commit(types.UPDATE_NOTE, 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); - - return res; - }); - -export const createNewNote = ({ commit }, { endpoint, data }) => service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then((res) => { - if (!res.errors) { - commit(types.ADD_NEW_NOTE, res); - } - return res; - }); - -export const removePlaceholderNotes = ({ commit }) => - commit(types.REMOVE_PLACEHOLDER_NOTES); - -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'; - - commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders - $('.notes-form .flash-container').hide(); // hide previous flash notification - - 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 && errors && Object.keys(errors).length) { - eTagPoll.makeRequest(); - - $('.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.', - 'alert', - 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; - }); -}; - -const pollSuccessCallBack = (resp, commit, state, getters) => { - if (resp.notes && resp.notes.length) { - const { notesById } = getters; - - resp.notes.forEach((note) => { - if (notesById[note.id]) { - commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE) { - 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); - } - }); - } - - commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt); - - return resp; -}; - -export const poll = ({ commit, state, getters }) => { - const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; - - eTagPoll = new Poll({ - resource: service, - method: 'poll', - data: requestData, - successCallback: resp => resp.json() - .then(data => pollSuccessCallBack(data, commit, state, getters)), - errorCallback: () => Flash('Something went wrong while fetching latest comments.'), - }); - - if (!Visibility.hidden()) { - eTagPoll.makeRequest(); - } else { - service.poll(requestData); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - eTagPoll.restart(); - } else { - eTagPoll.stop(); - } - }); -}; - -export const stopPolling = () => { - eTagPoll.stop(); -}; - -export const restartPolling = () => { - eTagPoll.restart(); -}; - -export const fetchData = ({ commit, state, getters }) => { - const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; - - service.poll(requestData) - .then(resp => resp.json) - .then(data => pollSuccessCallBack(data, commit, state, getters)) - .catch(() => Flash('Something went wrong while fetching latest comments.')); -}; - -export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { - commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); -}; - -export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { - const { endpoint, awardName } = data; - - return service - .toggleAward(endpoint, { name: awardName }) - .then(res => res.json()) - .then(() => { - dispatch('toggleAward', data); - }); -}; - -export const scrollToNoteIfNeeded = (context, el) => { - if (!isInViewport(el[0])) { - scrollToElement(el); - } -}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js deleted file mode 100644 index e18b277119e..00000000000 --- a/app/assets/javascripts/notes/stores/getters.js +++ /dev/null @@ -1,31 +0,0 @@ -import _ from 'underscore'; - -export const notes = state => state.notes; -export const targetNoteHash = state => state.targetNoteHash; - -export const getNotesData = state => state.notesData; -export const getNotesDataByProp = state => prop => state.notesData[prop]; - -export const getNoteableData = state => state.noteableData; -export const getNoteableDataByProp = state => prop => state.noteableData[prop]; - -export const getUserData = state => state.userData || {}; -export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; - -export const notesById = state => state.notes.reduce((acc, note) => { - note.notes.every(n => Object.assign(acc, { [n.id]: n })); - return acc; -}, {}); - -const reverseNotes = array => array.slice(0).reverse(); -const isLastNote = (note, state) => !note.system && - state.userData && note.author && - note.author.id === state.userData.id; - -export const getCurrentUserLastNote = state => _.flatten( - reverseNotes(state.notes) - .map(note => reverseNotes(note.notes)), - ).find(el => isLastNote(el, state)); - -export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) - .find(el => isLastNote(el, state)); diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js deleted file mode 100644 index 488a9ca38d3..00000000000 --- a/app/assets/javascripts/notes/stores/index.js +++ /dev/null @@ -1,23 +0,0 @@ -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, - - // holds endpoints and permissions provided through haml - notesData: {}, - userData: {}, - noteableData: {}, - }, - actions, - getters, - mutations, -}); diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js deleted file mode 100644 index d520c197407..00000000000 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ /dev/null @@ -1,14 +0,0 @@ -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_NOTES_DATA = 'SET_NOTES_DATA'; -export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA'; -export const SET_USER_DATA = 'SET_USER_DATA'; -export const SET_INITIAL_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 deleted file mode 100644 index 20f81a430c2..00000000000 --- a/app/assets/javascripts/notes/stores/mutations.js +++ /dev/null @@ -1,155 +0,0 @@ -import * as utils from './utils'; -import * as types from './mutation_types'; -import * as constants from '../constants'; - -export default { - [types.ADD_NEW_NOTE](state, note) { - const { discussion_id, type } = note; - const [exists] = state.notes.filter(n => n.id === note.discussion_id); - - if (!exists) { - const noteData = { - expanded: true, - id: discussion_id, - individual_note: !(type === constants.DISCUSSION_NOTE), - 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_NOTES_DATA](state, data) { - Object.assign(state, { notesData: data }); - }, - - [types.SET_NOTEABLE_DATA](state, data) { - Object.assign(state, { noteableData: data }); - }, - - [types.SET_USER_DATA](state, data) { - Object.assign(state, { userData: data }); - }, - [types.SET_INITIAL_NOTES](state, notesData) { - const notes = []; - - notesData.forEach((note) => { - // To support legacy notes, should be very rare case. - if (note.individual_note && note.notes.length > 1) { - note.notes.forEach((n) => { - const nn = Object.assign({}, note); - nn.notes = [n]; // override notes array to only have one item to mimick individual_note - notes.push(nn); - }); - } else { - notes.push(note); - } - }); - - Object.assign(state, { notes }); - }, - - [types.SET_LAST_FETCHED_AT](state, fetchedAt) { - Object.assign(state, { lastFetchedAt: fetchedAt }); - }, - - [types.SET_TARGET_NOTE_HASH](state, hash) { - Object.assign(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 ? constants.SYSTEM_NOTE : constants.NOTE, - notes: [ - { - body: data.noteBody, - }, - ], - }); - }, - - [types.TOGGLE_AWARD](state, data) { - const { awardName, note } = data; - const { id, name, username } = state.userData; - - const hasEmojiAwardedByCurrentUser = note.award_emoji - .filter(emoji => emoji.name === data.awardName && emoji.user.id === id); - - if (hasEmojiAwardedByCurrentUser.length) { - // If current user has awarded this emoji, remove it. - note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 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 deleted file mode 100644 index 6074115e855..00000000000 --- a/app/assets/javascripts/notes/stores/utils.js +++ /dev/null @@ -1,31 +0,0 @@ -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(); - diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue deleted file mode 100644 index e467ca56704..00000000000 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue deleted file mode 100644 index d805fea8006..00000000000 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue deleted file mode 100644 index 2248699c399..00000000000 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/notes/components/comment_form.vue b/app/assets/javascripts/vue_shared/notes/components/comment_form.vue new file mode 100644 index 00000000000..413e143d949 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/comment_form.vue @@ -0,0 +1,381 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/vue_shared/notes/components/discussion_locked_widget.vue new file mode 100644 index 00000000000..e6f7ee56ff3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/discussion_locked_widget.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_actions.vue b/app/assets/javascripts/vue_shared/notes/components/note_actions.vue new file mode 100644 index 00000000000..45fc6196be4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_actions.vue @@ -0,0 +1,167 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_attachment.vue b/app/assets/javascripts/vue_shared/notes/components/note_attachment.vue new file mode 100644 index 00000000000..cd9571a4002 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_attachment.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_awards_list.vue b/app/assets/javascripts/vue_shared/notes/components/note_awards_list.vue new file mode 100644 index 00000000000..169fac439d5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_awards_list.vue @@ -0,0 +1,227 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_body.vue b/app/assets/javascripts/vue_shared/notes/components/note_body.vue new file mode 100644 index 00000000000..33b1eb945f2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_body.vue @@ -0,0 +1,122 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_edited_text.vue b/app/assets/javascripts/vue_shared/notes/components/note_edited_text.vue new file mode 100644 index 00000000000..45b8dddb907 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_edited_text.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_form.vue b/app/assets/javascripts/vue_shared/notes/components/note_form.vue new file mode 100644 index 00000000000..48bf08ba554 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_form.vue @@ -0,0 +1,174 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_header.vue b/app/assets/javascripts/vue_shared/notes/components/note_header.vue new file mode 100644 index 00000000000..e9e26fe207b --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_header.vue @@ -0,0 +1,118 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/vue_shared/notes/components/note_signed_out_widget.vue new file mode 100644 index 00000000000..45d3c2de355 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/note_signed_out_widget.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/noteable_discussion.vue b/app/assets/javascripts/vue_shared/notes/components/noteable_discussion.vue new file mode 100644 index 00000000000..5b37b74b604 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/noteable_discussion.vue @@ -0,0 +1,230 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/noteable_note.vue b/app/assets/javascripts/vue_shared/notes/components/noteable_note.vue new file mode 100644 index 00000000000..805bac0bf23 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/noteable_note.vue @@ -0,0 +1,187 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/notes_app.vue b/app/assets/javascripts/vue_shared/notes/components/notes_app.vue new file mode 100644 index 00000000000..12f25ef9f44 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/notes_app.vue @@ -0,0 +1,151 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/placeholder_note.vue b/app/assets/javascripts/vue_shared/notes/components/placeholder_note.vue new file mode 100644 index 00000000000..6f12b11e096 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/placeholder_note.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/notes/components/placeholder_system_note.vue new file mode 100644 index 00000000000..d805fea8006 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/placeholder_system_note.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/components/system_note.vue b/app/assets/javascripts/vue_shared/notes/components/system_note.vue new file mode 100644 index 00000000000..e593ec67873 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/components/system_note.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/assets/javascripts/vue_shared/notes/constants.js b/app/assets/javascripts/vue_shared/notes/constants.js new file mode 100644 index 00000000000..a6961063c01 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/constants.js @@ -0,0 +1,11 @@ +export const DISCUSSION_NOTE = 'DiscussionNote'; +export const DISCUSSION = 'discussion'; +export const NOTE = 'note'; +export const SYSTEM_NOTE = 'systemNote'; +export const COMMENT = 'comment'; +export const OPENED = 'opened'; +export const REOPENED = 'reopened'; +export const CLOSED = 'closed'; +export const EMOJI_THUMBSUP = 'thumbsup'; +export const EMOJI_THUMBSDOWN = 'thumbsdown'; +export const NOTEABLE_TYPE = 'Issue'; diff --git a/app/assets/javascripts/vue_shared/notes/event_hub.js b/app/assets/javascripts/vue_shared/notes/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/vue_shared/notes/index.js b/app/assets/javascripts/vue_shared/notes/index.js new file mode 100644 index 00000000000..72a535d1589 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import notesApp from './components/notes_app.vue'; + +export default function () { + return new Vue({ + el: '#js-vue-notes', + components: { + notesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + + return { + noteableData: JSON.parse(notesDataset.noteableData), + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: { + lastFetchedAt: notesDataset.lastFetchedAt, + discussionsPath: notesDataset.discussionsPath, + newSessionPath: notesDataset.newSessionPath, + registerPath: notesDataset.registerPath, + notesPath: notesDataset.notesPath, + markdownDocsPath: notesDataset.markdownDocsPath, + quickActionsDocsPath: notesDataset.quickActionsDocsPath, + }, + }; + }, + render(createElement) { + return createElement('notes-app', { + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/vue_shared/notes/mixins/autosave.js b/app/assets/javascripts/vue_shared/notes/mixins/autosave.js new file mode 100644 index 00000000000..0db88cdeba5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/mixins/autosave.js @@ -0,0 +1,15 @@ +import Autosave from '~/autosave'; + +export default { + methods: { + initAutoSave() { + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue'); + }, + resetAutoSave() { + this.autosave.reset(); + }, + setAutoSave() { + this.autosave.save(); + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/notes/mixins/issuable_state.js b/app/assets/javascripts/vue_shared/notes/mixins/issuable_state.js new file mode 100644 index 00000000000..97f3ea0d5de --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/mixins/issuable_state.js @@ -0,0 +1,15 @@ +export default { + methods: { + isConfidential(issue) { + return !!issue.confidential; + }, + + isLocked(issue) { + return !!issue.discussion_locked; + }, + + hasWarning(issue) { + return this.isConfidential(issue) || this.isLocked(issue); + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/notes/services/notes_service.js b/app/assets/javascripts/vue_shared/notes/services/notes_service.js new file mode 100644 index 00000000000..b51b0cb2013 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/services/notes_service.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default { + fetchNotes(endpoint) { + return Vue.http.get(endpoint); + }, + deleteNote(endpoint) { + return Vue.http.delete(endpoint); + }, + replyToDiscussion(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + }, + updateNote(endpoint, data) { + return Vue.http.put(endpoint, data, { emulateJSON: true }); + }, + createNewNote(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + }, + poll(data = {}) { + const { endpoint, lastFetchedAt } = data; + const options = { + headers: { + 'X-Last-Fetched-At': lastFetchedAt, + }, + }; + + return Vue.http.get(endpoint, options); + }, + toggleAward(endpoint, data) { + return Vue.http.post(endpoint, data, { emulateJSON: true }); + }, +}; diff --git a/app/assets/javascripts/vue_shared/notes/stores/actions.js b/app/assets/javascripts/vue_shared/notes/stores/actions.js new file mode 100644 index 00000000000..957f642bce7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/stores/actions.js @@ -0,0 +1,226 @@ +import Visibility from 'visibilityjs'; +import Flash from '~/flash'; +import Poll from '~/lib/utils/poll'; +import * as types from './mutation_types'; +import * as utils from './utils'; +import * as constants from '../constants'; +import service from '../services/notes_service'; +import loadAwardsHandler from '~/awards_handler'; +import sidebarTimeTrackingEventHub from '~/sidebar/event_hub'; +import { isInViewport, scrollToElement } from '~/lib/utils/common_utils'; + +let eTagPoll; + +export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); +export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_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_INITIAL_NOTES, data); +export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); +export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); + +export const fetchNotes = ({ commit }, path) => service + .fetchNotes(path) + .then(res => res.json()) + .then((res) => { + commit(types.SET_INITIAL_NOTES, res); + }); + +export const deleteNote = ({ commit }, note) => service + .deleteNote(note.path) + .then(() => { + commit(types.DELETE_NOTE, note); + }); + +export const updateNote = ({ commit }, { endpoint, note }) => service + .updateNote(endpoint, note) + .then(res => res.json()) + .then((res) => { + commit(types.UPDATE_NOTE, 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); + + return res; + }); + +export const createNewNote = ({ commit }, { endpoint, data }) => service + .createNewNote(endpoint, data) + .then(res => res.json()) + .then((res) => { + if (!res.errors) { + commit(types.ADD_NEW_NOTE, res); + } + return res; + }); + +export const removePlaceholderNotes = ({ commit }) => + commit(types.REMOVE_PLACEHOLDER_NOTES); + +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'; + + commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders + $('.notes-form .flash-container').hide(); // hide previous flash notification + + 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 && errors && Object.keys(errors).length) { + eTagPoll.makeRequest(); + + $('.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.', + 'alert', + 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; + }); +}; + +const pollSuccessCallBack = (resp, commit, state, getters) => { + if (resp.notes && resp.notes.length) { + const { notesById } = getters; + + resp.notes.forEach((note) => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === constants.DISCUSSION_NOTE) { + 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); + } + }); + } + + commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt); + + return resp; +}; + +export const poll = ({ commit, state, getters }) => { + const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; + + eTagPoll = new Poll({ + resource: service, + method: 'poll', + data: requestData, + successCallback: resp => resp.json() + .then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => Flash('Something went wrong while fetching latest comments.'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + service.poll(requestData); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + eTagPoll.restart(); + } else { + eTagPoll.stop(); + } + }); +}; + +export const stopPolling = () => { + eTagPoll.stop(); +}; + +export const restartPolling = () => { + eTagPoll.restart(); +}; + +export const fetchData = ({ commit, state, getters }) => { + const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; + + service.poll(requestData) + .then(resp => resp.json) + .then(data => pollSuccessCallBack(data, commit, state, getters)) + .catch(() => Flash('Something went wrong while fetching latest comments.')); +}; + +export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { + commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); +}; + +export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { + const { endpoint, awardName } = data; + + return service + .toggleAward(endpoint, { name: awardName }) + .then(res => res.json()) + .then(() => { + dispatch('toggleAward', data); + }); +}; + +export const scrollToNoteIfNeeded = (context, el) => { + if (!isInViewport(el[0])) { + scrollToElement(el); + } +}; diff --git a/app/assets/javascripts/vue_shared/notes/stores/getters.js b/app/assets/javascripts/vue_shared/notes/stores/getters.js new file mode 100644 index 00000000000..e18b277119e --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/stores/getters.js @@ -0,0 +1,31 @@ +import _ from 'underscore'; + +export const notes = state => state.notes; +export const targetNoteHash = state => state.targetNoteHash; + +export const getNotesData = state => state.notesData; +export const getNotesDataByProp = state => prop => state.notesData[prop]; + +export const getNoteableData = state => state.noteableData; +export const getNoteableDataByProp = state => prop => state.noteableData[prop]; + +export const getUserData = state => state.userData || {}; +export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; + +export const notesById = state => state.notes.reduce((acc, note) => { + note.notes.every(n => Object.assign(acc, { [n.id]: n })); + return acc; +}, {}); + +const reverseNotes = array => array.slice(0).reverse(); +const isLastNote = (note, state) => !note.system && + state.userData && note.author && + note.author.id === state.userData.id; + +export const getCurrentUserLastNote = state => _.flatten( + reverseNotes(state.notes) + .map(note => reverseNotes(note.notes)), + ).find(el => isLastNote(el, state)); + +export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) + .find(el => isLastNote(el, state)); diff --git a/app/assets/javascripts/vue_shared/notes/stores/index.js b/app/assets/javascripts/vue_shared/notes/stores/index.js new file mode 100644 index 00000000000..488a9ca38d3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/stores/index.js @@ -0,0 +1,23 @@ +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, + + // holds endpoints and permissions provided through haml + notesData: {}, + userData: {}, + noteableData: {}, + }, + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/vue_shared/notes/stores/mutation_types.js b/app/assets/javascripts/vue_shared/notes/stores/mutation_types.js new file mode 100644 index 00000000000..d520c197407 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/stores/mutation_types.js @@ -0,0 +1,14 @@ +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_NOTES_DATA = 'SET_NOTES_DATA'; +export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA'; +export const SET_USER_DATA = 'SET_USER_DATA'; +export const SET_INITIAL_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/vue_shared/notes/stores/mutations.js b/app/assets/javascripts/vue_shared/notes/stores/mutations.js new file mode 100644 index 00000000000..20f81a430c2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/notes/stores/mutations.js @@ -0,0 +1,155 @@ +import * as utils from './utils'; +import * as types from './mutation_types'; +import * as constants from '../constants'; + +export default { + [types.ADD_NEW_NOTE](state, note) { + const { discussion_id, type } = note; + const [exists] = state.notes.filter(n => n.id === note.discussion_id); + + if (!exists) { + const noteData = { + expanded: true, + id: discussion_id, + individual_note: !(type === constants.DISCUSSION_NOTE), + 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_NOTES_DATA](state, data) { + Object.assign(state, { notesData: data }); + }, + + [types.SET_NOTEABLE_DATA](state, data) { + Object.assign(state, { noteableData: data }); + }, + + [types.SET_USER_DATA](state, data) { + Object.assign(state, { userData: data }); + }, + [types.SET_INITIAL_NOTES](state, notesData) { + const notes = []; + + notesData.forEach((note) => { + // To support legacy notes, should be very rare case. + if (note.individual_note && note.notes.length > 1) { + note.notes.forEach((n) => { + const nn = Object.assign({}, note); + nn.notes = [n]; // override notes array to only have one item to mimick individual_note + notes.push(nn); + }); + } else { + notes.push(note); + } + }); + + Object.assign(state, { notes }); + }, + + [types.SET_LAST_FETCHED_AT](state, fetchedAt) { + Object.assign(state, { lastFetchedAt: fetchedAt }); + }, + + [types.SET_TARGET_NOTE_HASH](state, hash) { + Object.assign(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 ? constants.SYSTEM_NOTE : constants.NOTE, + notes: [ + { + body: data.noteBody, + }, + ], + }); + }, + + [types.TOGGLE_AWARD](state, data) { + const { awardName, note } = data; + const { id, name, username } = state.userData; + + const hasEmojiAwardedByCurrentUser = note.award_emoji + .filter(emoji => emoji.name === data.awardName && emoji.user.id === id); + + if (hasEmojiAwardedByCurrentUser.length) { + // If current user has awarded this emoji, remove it. + note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 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/vue_shared/notes/stores/utils.js b/app/assets/javascripts/vue_shared/notes/stores/utils.js new file mode 100644 index 00000000000..6074115e855 --- /dev/null +++ b/app/assets/javascripts/vue_shared/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(); + diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js deleted file mode 100644 index 20e352dd8bd..00000000000 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ /dev/null @@ -1,207 +0,0 @@ -import Vue from 'vue'; -import Autosize from 'autosize'; -import store from '~/notes/stores'; -import issueCommentForm from '~/notes/components/comment_form.vue'; -import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; -import { keyboardDownEvent } from '../../issue_show/helpers'; - -describe('issue_comment_form component', () => { - let vm; - const Component = Vue.extend(issueCommentForm); - let mountComponent; - - beforeEach(() => { - mountComponent = () => new Component({ - store, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('user is logged in', () => { - beforeEach(() => { - store.dispatch('setUserData', userDataMock); - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - - vm = mountComponent(); - }); - - it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - }); - - describe('handleSave', () => { - it('should request to save note when note is entered', () => { - vm.note = 'hello world'; - spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); - spyOn(vm, 'resizeTextarea'); - spyOn(vm, 'stopPolling'); - - vm.handleSave(); - expect(vm.isSubmitting).toEqual(true); - expect(vm.note).toEqual(''); - expect(vm.saveNote).toHaveBeenCalled(); - expect(vm.stopPolling).toHaveBeenCalled(); - expect(vm.resizeTextarea).toHaveBeenCalled(); - }); - - it('should toggle issue state when no note', () => { - spyOn(vm, 'toggleIssueState'); - - vm.handleSave(); - - expect(vm.toggleIssueState).toHaveBeenCalled(); - }); - - it('should disable action button whilst submitting', (done) => { - const saveNotePromise = Promise.resolve(); - vm.note = 'hello world'; - spyOn(vm, 'saveNote').and.returnValue(saveNotePromise); - spyOn(vm, 'stopPolling'); - - const actionButton = vm.$el.querySelector('.js-action-button'); - - vm.handleSave(); - - Vue.nextTick() - .then(() => expect(actionButton.disabled).toBeTruthy()) - .then(saveNotePromise) - .then(Vue.nextTick) - .then(() => expect(actionButton.disabled).toBeFalsy()) - .then(done) - .catch(done.fail); - }); - }); - - describe('textarea', () => { - it('should render textarea with placeholder', () => { - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); - }); - - it('should make textarea disabled while requesting', (done) => { - const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); - vm.note = 'hello world'; - spyOn(vm, 'stopPolling'); - spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); - - vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. - $submitButton.trigger('click'); - - vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. - expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); - done(); - }); - }); - }); - - it('should support quick actions', () => { - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), - ).toEqual('true'); - }); - - it('should link to markdown docs', () => { - const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); - }); - - it('should link to quick actions docs', () => { - const { quickActionsDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); - }); - - it('should resize textarea after note discarded', (done) => { - spyOn(Autosize, 'update'); - spyOn(vm, 'discard').and.callThrough(); - - vm.note = 'foo'; - vm.discard(); - - Vue.nextTick(() => { - expect(Autosize.update).toHaveBeenCalled(); - done(); - }); - }); - - describe('edit mode', () => { - it('should enter edit mode when arrow up is pressed', () => { - spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); - vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true)); - - expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); - }); - }); - - describe('event enter', () => { - it('should save note when cmd/ctrl+enter is pressed', () => { - spyOn(vm, 'handleSave').and.callThrough(); - vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; - vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); - - expect(vm.handleSave).toHaveBeenCalled(); - }); - }); - }); - - describe('actions', () => { - it('should be possible to close the issue', () => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue'); - }); - - it('should render comment button as disabled', () => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled'); - }); - - it('should enable comment button if it has note', (done) => { - vm.note = 'Foo'; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null); - done(); - }); - }); - - it('should update buttons texts when it has note', (done) => { - vm.note = 'Foo'; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue'); - expect(vm.$el.querySelector('.js-note-discard')).toBeDefined(); - done(); - }); - }); - }); - - describe('issue is confidential', () => { - it('shows information warning', (done) => { - store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); - done(); - }); - }); - }); - }); - - describe('user is not logged in', () => { - beforeEach(() => { - store.dispatch('setUserData', null); - store.dispatch('setNoteableData', loggedOutnoteableData); - store.dispatch('setNotesData', notesDataMock); - - vm = mountComponent(); - }); - - it('should render signed out widget', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); - }); - - it('should not render submission form', () => { - expect(vm.$el.querySelector('textarea')).toEqual(null); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js deleted file mode 100644 index ab81aabb992..00000000000 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import Vue from 'vue'; -import store from '~/notes/stores'; -import noteActions from '~/notes/components/note_actions.vue'; -import { userDataMock } from '../mock_data'; - -describe('issse_note_actions component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(noteActions); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('user is logged in', () => { - let props; - - beforeEach(() => { - props = { - accessLevel: 'Master', - authorId: 26, - canDelete: true, - canEdit: true, - canReportAsAbuse: true, - noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', - }; - - store.dispatch('setUserData', userDataMock); - - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - it('should render access level badge', () => { - expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel); - }); - - it('should render emoji link', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); - }); - - describe('actions dropdown', () => { - it('should be possible to edit the comment', () => { - expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); - }); - - it('should be possible to report as abuse', () => { - expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); - }); - - it('should be possible to delete comment', () => { - expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); - }); - }); - }); - - describe('user is not logged in', () => { - let props; - - beforeEach(() => { - store.dispatch('setUserData', {}); - props = { - accessLevel: 'Master', - authorId: 26, - canDelete: false, - canEdit: false, - canReportAsAbuse: false, - noteId: 539, - reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', - }; - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - it('should not render emoji link', () => { - expect(vm.$el.querySelector('.js-add-award')).toEqual(null); - }); - - it('should not render actions dropdown', () => { - expect(vm.$el.querySelector('.more-actions')).toEqual(null); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js deleted file mode 100644 index a0352b6395e..00000000000 --- a/spec/javascripts/notes/components/note_app_spec.js +++ /dev/null @@ -1,255 +0,0 @@ -import Vue from 'vue'; -import notesApp from '~/notes/components/notes_app.vue'; -import service from '~/notes/services/notes_service'; -import * as mockData from '../mock_data'; - -describe('note_app', () => { - let mountComponent; - let vm; - - const individualNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { - status: 200, - })); - }; - - const discussionNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { - status: 200, - })); - }; - - beforeEach(() => { - const IssueNotesApp = Vue.extend(notesApp); - - mountComponent = (data) => { - const props = data || { - noteableData: mockData.noteableDataMock, - notesData: mockData.notesDataMock, - userData: mockData.userDataMock, - }; - - return new IssueNotesApp({ - propsData: props, - }).$mount(); - }; - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('set data', () => { - const responseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; - - beforeEach(() => { - Vue.http.interceptors.push(responseInterceptor); - vm = mountComponent(); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); - }); - - it('should set notes data', () => { - expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock); - }); - - it('should set issue data', () => { - expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock); - }); - - it('should set user data', () => { - expect(vm.$store.state.userData).toEqual(mockData.userDataMock); - }); - - it('should fetch notes', () => { - expect(vm.$store.state.notes).toEqual([]); - }); - }); - - describe('render', () => { - beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); - vm = mountComponent(); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); - }); - - it('should render list of notes', (done) => { - const note = mockData.individualNoteServerResponse[0].notes[0]; - - setTimeout(() => { - expect( - vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), - ).toEqual(note.author.name); - - expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html); - done(); - }, 0); - }); - - it('should render form', () => { - expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); - }); - - it('should render form comment button as disabled', () => { - expect( - vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), - ).toEqual('disabled'); - }); - }); - - describe('while fetching data', () => { - beforeEach(() => { - vm = mountComponent(); - }); - - it('should render loading icon', () => { - expect(vm.$el.querySelector('.js-loading')).toBeDefined(); - }); - - it('should render form', () => { - expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); - expect( - vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); - }); - }); - - describe('update note', () => { - describe('individual note', () => { - beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); - vm = mountComponent(); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); - }); - - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); - }); - - it('calls the service to update the note', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); - }); - }); - - describe('dicussion note', () => { - beforeEach(() => { - Vue.http.interceptors.push(discussionNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); - vm = mountComponent(); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); - }); - - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); - }); - - it('updates the note and resets the edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); - }); - }); - }); - - describe('new note form', () => { - beforeEach(() => { - vm = mountComponent(); - }); - - it('should render markdown docs url', () => { - const { markdownDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); - }); - - it('should render quick action docs url', () => { - const { quickActionsDocsPath } = mockData.notesDataMock; - expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); - }); - }); - - describe('edit form', () => { - beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); - vm = mountComponent(); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); - }); - - it('should render markdown docs url', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - const { markdownDocsPath } = mockData.notesDataMock; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(), - ).toEqual('Markdown is supported'); - done(); - }); - }, 0); - }); - - it('should not render quick actions docs url', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - const { quickActionsDocsPath } = mockData.notesDataMock; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`), - ).toEqual(null); - done(); - }); - }, 0); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_attachment_spec.js b/spec/javascripts/notes/components/note_attachment_spec.js deleted file mode 100644 index b14a518b622..00000000000 --- a/spec/javascripts/notes/components/note_attachment_spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import Vue from 'vue'; -import noteAttachment from '~/notes/components/note_attachment.vue'; - -describe('issue note attachment', () => { - it('should render properly', () => { - const props = { - attachment: { - filename: 'dk.png', - image: true, - url: '/dk.png', - }, - }; - - const Component = Vue.extend(noteAttachment); - const vm = new Component({ - propsData: props, - }).$mount(); - - expect(vm.$el.classList.contains('note-attachment')).toBeTruthy(); - expect(vm.$el.querySelector('img').src).toContain(props.attachment.url); - expect(vm.$el.querySelector('a').href).toContain(props.attachment.url); - }); -}); diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js deleted file mode 100644 index 15995ec5a05..00000000000 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import Vue from 'vue'; -import store from '~/notes/stores'; -import awardsNote from '~/notes/components/note_awards_list.vue'; -import { noteableDataMock, notesDataMock } from '../mock_data'; - -describe('note_awards_list component', () => { - let vm; - let awardsMock; - - beforeEach(() => { - const Component = Vue.extend(awardsNote); - - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - awardsMock = [ - { - name: 'flag_tz', - user: { id: 1, name: 'Administrator', username: 'root' }, - }, - { - name: 'cartwheel_tone3', - user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, - }, - ]; - - vm = new Component({ - store, - propsData: { - awards: awardsMock, - noteAuthorId: 2, - noteId: 545, - toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', - }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render awarded emojis', () => { - expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); - expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); - }); - - it('should be possible to remove awareded emoji', () => { - spyOn(vm, 'handleAward').and.callThrough(); - vm.$el.querySelector('.js-awards-block button').click(); - - expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); - }); - - it('should be possible to add new emoji', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); - }); -}); diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js deleted file mode 100644 index b42e7943b98..00000000000 --- a/spec/javascripts/notes/components/note_body_spec.js +++ /dev/null @@ -1,46 +0,0 @@ - -import Vue from 'vue'; -import store from '~/notes/stores'; -import noteBody from '~/notes/components/note_body.vue'; -import { noteableDataMock, notesDataMock, note } from '../mock_data'; - -describe('issue_note_body component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(noteBody); - - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - - vm = new Component({ - store, - propsData: { - note, - canEdit: true, - }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render the note', () => { - expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); - }); - - it('should be render form if user is editing', (done) => { - vm.isEditing = true; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined(); - done(); - }); - }); - - it('should render awards list', () => { - expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined(); - expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined(); - }); -}); diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js deleted file mode 100644 index e0b991c32ec..00000000000 --- a/spec/javascripts/notes/components/note_edited_text_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; -import noteEditedText from '~/notes/components/note_edited_text.vue'; - -describe('note_edited_text', () => { - let vm; - let props; - - beforeEach(() => { - const Component = Vue.extend(noteEditedText); - props = { - actionText: 'Edited', - className: 'foo-bar', - editedAt: '2017-08-04T09:52:31.062Z', - editedBy: { - avatar_url: 'path', - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - }; - - vm = new Component({ - propsData: props, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render block with provided className', () => { - expect(vm.$el.className).toEqual(props.className); - }); - - it('should render provided actionText', () => { - expect(vm.$el.textContent).toContain(props.actionText); - }); - - it('should render provided user information', () => { - const authorLink = vm.$el.querySelector('.js-vue-author'); - - expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path); - expect(authorLink.textContent.trim()).toEqual(props.editedBy.name); - }); -}); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js deleted file mode 100644 index 86e9e2a32a9..00000000000 --- a/spec/javascripts/notes/components/note_form_spec.js +++ /dev/null @@ -1,112 +0,0 @@ -import Vue from 'vue'; -import store from '~/notes/stores'; -import issueNoteForm from '~/notes/components/note_form.vue'; -import { noteableDataMock, notesDataMock } from '../mock_data'; -import { keyboardDownEvent } from '../../issue_show/helpers'; - -describe('issue_note_form component', () => { - let vm; - let props; - - beforeEach(() => { - const Component = Vue.extend(issueNoteForm); - - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - - props = { - isEditing: false, - noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', - noteId: 545, - }; - - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('conflicts editing', () => { - it('should show conflict message if note changes outside the component', (done) => { - vm.isEditing = true; - vm.noteBody = 'Foo'; - const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), - ).toEqual(message); - done(); - }); - }); - }); - - describe('form', () => { - it('should render text area with placeholder', () => { - expect( - vm.$el.querySelector('textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); - }); - - it('should link to markdown docs', () => { - const { markdownDocsPath } = notesDataMock; - expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); - }); - - describe('keyboard events', () => { - describe('up', () => { - it('should ender edit mode', () => { - spyOn(vm, 'editMyLastNote').and.callThrough(); - vm.$el.querySelector('textarea').value = 'Foo'; - vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true)); - - expect(vm.editMyLastNote).toHaveBeenCalled(); - }); - }); - - describe('enter', () => { - it('should submit note', () => { - spyOn(vm, 'handleUpdate').and.callThrough(); - vm.$el.querySelector('textarea').value = 'Foo'; - vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); - - expect(vm.handleUpdate).toHaveBeenCalled(); - }); - }); - }); - - describe('actions', () => { - it('should be possible to cancel', (done) => { - spyOn(vm, 'cancelHandler').and.callThrough(); - vm.isEditing = true; - - Vue.nextTick(() => { - vm.$el.querySelector('.note-edit-cancel').click(); - - Vue.nextTick(() => { - expect(vm.cancelHandler).toHaveBeenCalled(); - done(); - }); - }); - }); - - it('should be possible to update the note', (done) => { - vm.isEditing = true; - - Vue.nextTick(() => { - vm.$el.querySelector('textarea').value = 'Foo'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - Vue.nextTick(() => { - expect(vm.isSubmitting).toEqual(true); - done(); - }); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js deleted file mode 100644 index 16a76b11321..00000000000 --- a/spec/javascripts/notes/components/note_header_spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import Vue from 'vue'; -import noteHeader from '~/notes/components/note_header.vue'; -import store from '~/notes/stores'; - -describe('note_header component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(noteHeader); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('individual note', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'commented', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: false, - noteId: 1394, - }, - }).$mount(); - }); - - it('should render user information', () => { - expect( - vm.$el.querySelector('.note-header-author-name').textContent.trim(), - ).toEqual('Root'); - expect( - vm.$el.querySelector('.note-header-info a').getAttribute('href'), - ).toEqual('/root'); - }); - - it('should render timestamp link', () => { - expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); - }); - }); - - describe('discussion', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'started a discussion', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: true, - noteId: 1395, - }, - }).$mount(); - }); - - it('should render toggle button', () => { - expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); - }); - - it('should toggle the disucssion icon', (done) => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'), - ).toEqual(true); - - vm.$el.querySelector('.js-vue-toggle-button').click(); - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'), - ).toEqual(true); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js deleted file mode 100644 index 6cba8053888..00000000000 --- a/spec/javascripts/notes/components/note_signed_out_widget_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from 'vue'; -import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; -import store from '~/notes/stores'; -import { notesDataMock } from '../mock_data'; - -describe('note_signed_out_widget component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(noteSignedOut); - store.dispatch('setNotesData', notesDataMock); - - vm = new Component({ - store, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render sign in link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent, - ).toEqual('sign in'); - }); - - it('should render register link provided in the store', () => { - expect( - vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent, - ).toEqual('register'); - }); - - it('should render information text', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); - }); -}); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js deleted file mode 100644 index 1928a91ee91..00000000000 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/noteable_discussion.vue'; -import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; - -describe('issue_discussion component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issueDiscussion); - - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - - vm = new Component({ - store, - propsData: { - note: discussionMock, - }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render user avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined(); - }); - - it('should render discussion header', () => { - expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); - expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); - }); - - describe('actions', () => { - it('should render reply button', () => { - expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...'); - }); - - it('should toggle reply form', (done) => { - vm.$el.querySelector('.js-vue-discussion-reply').click(); - Vue.nextTick(() => { - expect(vm.$refs.noteForm).toBeDefined(); - expect(vm.isReplying).toEqual(true); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js deleted file mode 100644 index 623a2d6d9d7..00000000000 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ /dev/null @@ -1,44 +0,0 @@ - -import Vue from 'vue'; -import store from '~/notes/stores'; -import issueNote from '~/notes/components/noteable_note.vue'; -import { noteableDataMock, notesDataMock, note } from '../mock_data'; - -describe('issue_note', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issueNote); - - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - - vm = new Component({ - store, - propsData: { - note, - }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render user information', () => { - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url); - }); - - it('should render note header content', () => { - expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name); - expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented'); - }); - - it('should render note actions', () => { - expect(vm.$el.querySelector('.note-actions')).toBeDefined(); - }); - - it('should render issue body', () => { - expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); - }); -}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js deleted file mode 100644 index 42497de3c55..00000000000 --- a/spec/javascripts/notes/mock_data.js +++ /dev/null @@ -1,449 +0,0 @@ -/* eslint-disable */ -export const notesDataMock = { - discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', - lastFetchedAt: '1501862675', - markdownDocsPath: '/help/user/markdown', - newSessionPath: '/users/sign_in?redirect_to_referer=yes', - 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', -}; - -export const userDataMock = { - avatar_url: 'mock_path', - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', -}; - -export const noteableDataMock = { - assignees: [], - author_id: 1, - branch_name: null, - confidential: false, - create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', - created_at: '2017-02-07T10:11:18.395Z', - current_user: { - can_create_note: true, - can_update: true, - }, - deleted_at: null, - description: '', - due_date: null, - human_time_estimate: null, - human_total_time_spent: null, - id: 98, - iid: 26, - labels: [], - lock_version: null, - milestone: null, - milestone_id: null, - moved_to_id: null, - preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', - project_id: 2, - state: 'opened', - time_estimate: 0, - title: '14', - total_time_spent: 0, - updated_at: '2017-08-04T09:53:01.226Z', - updated_by_id: 1, - web_url: '/gitlab-org/gitlab-ce/issues/26', -}; - -export const lastFetchedAt = '1501862675'; - -export const individualNote = { - expanded: true, - id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', - individual_note: true, - notes: [{ - id: 1390, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: 'test', - path: '/root', - }, - created_at: '2017-08-01T17: 09: 33.762Z', - updated_at: '2017-08-01T17: 09: 33.762Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: null, - human_access: 'Owner', - note: 'sdfdsaf', - note_html: '

sdfdsaf

', - current_user: { can_edit: true }, - discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', - emoji_awardable: true, - award_emoji: [ - { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, - { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, - ], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1390', - }], - reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', -}; - -export const note = { - "id": 546, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Administrator", - "username": "root", - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "path": "/root" - }, - "created_at": "2017-08-10T15:24:03.087Z", - "updated_at": "2017-08-10T15:24:03.087Z", - "system": false, - "noteable_id": 67, - "noteable_type": "Issue", - "noteable_iid": 7, - "type": null, - "human_access": "Owner", - "note": "Vel id placeat reprehenderit sit numquam.", - "note_html": "

Vel id placeat reprehenderit sit numquam.

", - "current_user": { - "can_edit": true - }, - "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { - "id": 1, - "name": "Administrator", - "username": "root" - } - }, { - "name": "bath_tone3", - "user": { - "id": 1, - "name": "Administrator", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/546" - } - -export const discussionMock = { - id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - expanded: true, - notes: [{ - id: 1395, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - created_at: '2017-08-02T10:51:58.559Z', - updated_at: '2017-08-02T10:51:58.559Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'THIS IS A DICUSSSION!', - note_html: '

THIS IS A DICUSSSION!

', - current_user: { - can_edit: true, - }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1395', - }, { - id: 1396, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - created_at: '2017-08-02T10:56:50.980Z', - updated_at: '2017-08-03T14:19:35.691Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'sadfasdsdgdsf', - note_html: '

sadfasdsdgdsf

', - last_edited_at: '2017-08-03T14:19:35.691Z', - last_edited_by: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - current_user: { - can_edit: true, - }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1396', - }, { - id: 1437, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - created_at: '2017-08-03T18:11:18.780Z', - updated_at: '2017-08-04T09:52:31.062Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'adsfasf Should disappear', - note_html: '

adsfasf Should disappear

', - last_edited_at: '2017-08-04T09:52:31.062Z', - last_edited_by: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - current_user: { - can_edit: true, - }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1437', - }], - individual_note: false, -}; - -export const loggedOutnoteableData = { - "id": 98, - "iid": 26, - "author_id": 1, - "description": "", - "lock_version": 1, - "milestone_id": null, - "state": "opened", - "title": "asdsa", - "updated_by_id": 1, - "created_at": "2017-02-07T10:11:18.395Z", - "updated_at": "2017-08-08T10:22:51.564Z", - "deleted_at": null, - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": null, - "human_total_time_spent": null, - "milestone": null, - "labels": [], - "branch_name": null, - "confidential": false, - "assignees": [{ - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "web_url": "http://localhost:3000/root" - }], - "due_date": null, - "moved_to_id": null, - "project_id": 2, - "web_url": "/gitlab-org/gitlab-ce/issues/26", - "current_user": { - "can_create_note": false, - "can_update": false - }, - "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue", - "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" -} - -export const individualNoteServerResponse = [{ - "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "expanded": true, - "notes": [{ - "id": 1390, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-01T17:09:33.762Z", - "updated_at": "2017-08-01T17:09:33.762Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "sdfdsaf", - "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }, { - "name": "art", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1390" - }], - "individual_note": true - }, { - "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "expanded": true, - "notes": [{ - "id": 1391, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-02T10:51:38.685Z", - "updated_at": "2017-08-02T10:51:38.685Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "New note!", - "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1391" - }], - "individual_note": true -}]; - -export const discussionNoteServerResponse = [{ - "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "expanded": true, - "notes": [{ - "id": 1471, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-08T16:53:00.666Z", - "updated_at": "2017-08-08T16:53:00.666Z", - "system": false, - "noteable_id": 124, - "noteable_type": "Issue", - "noteable_iid": 29, - "type": "DiscussionNote", - "human_access": "Owner", - "note": "Adding a comment", - "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1471" - }], - "individual_note": false -}]; diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js deleted file mode 100644 index e092320f9a3..00000000000 --- a/spec/javascripts/notes/stores/actions_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import * as actions from '~/notes/stores/actions'; -import testAction from '../../helpers/vuex_action_helper'; -import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; - -describe('Actions Notes Store', () => { - describe('setNotesData', () => { - it('should set received notes data', (done) => { - testAction(actions.setNotesData, null, { notesData: {} }, [ - { type: 'SET_NOTES_DATA', payload: notesDataMock }, - ], done); - }); - }); - - describe('setNoteableData', () => { - it('should set received issue data', (done) => { - testAction(actions.setNoteableData, null, { noteableData: {} }, [ - { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }, - ], done); - }); - }); - - describe('setUserData', () => { - it('should set received user data', (done) => { - testAction(actions.setUserData, null, { userData: {} }, [ - { type: 'SET_USER_DATA', payload: userDataMock }, - ], done); - }); - }); - - describe('setLastFetchedAt', () => { - it('should set received timestamp', (done) => { - testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [ - { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }, - ], done); - }); - }); - - describe('setInitialNotes', () => { - it('should set initial notes', (done) => { - testAction(actions.setInitialNotes, null, { notes: [] }, [ - { type: 'SET_INITIAL_NOTES', payload: [individualNote] }, - ], done); - }); - }); - - describe('setTargetNoteHash', () => { - it('should set target note hash', (done) => { - testAction(actions.setTargetNoteHash, null, { notes: [] }, [ - { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }, - ], done); - }); - }); - - describe('toggleDiscussion', () => { - it('should toggle discussion', (done) => { - testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [ - { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }, - ], done); - }); - }); -}); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js deleted file mode 100644 index c5a84b71788..00000000000 --- a/spec/javascripts/notes/stores/getters_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import * as getters from '~/notes/stores/getters'; -import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; - -describe('Getters Notes Store', () => { - let state; - beforeEach(() => { - state = { - notes: [individualNote], - targetNoteHash: 'hash', - lastFetchedAt: 'timestamp', - - notesData: notesDataMock, - userData: userDataMock, - noteableData: noteableDataMock, - }; - }); - describe('notes', () => { - it('should return all notes in the store', () => { - expect(getters.notes(state)).toEqual([individualNote]); - }); - }); - - describe('targetNoteHash', () => { - it('should return `targetNoteHash`', () => { - expect(getters.targetNoteHash(state)).toEqual('hash'); - }); - }); - - describe('getNotesData', () => { - it('should return all data in `notesData`', () => { - expect(getters.getNotesData(state)).toEqual(notesDataMock); - }); - }); - - describe('getNoteableData', () => { - it('should return all data in `noteableData`', () => { - expect(getters.getNoteableData(state)).toEqual(noteableDataMock); - }); - }); - - describe('getUserData', () => { - it('should return all data in `userData`', () => { - expect(getters.getUserData(state)).toEqual(userDataMock); - }); - }); - - describe('notesById', () => { - it('should return the note for the given id', () => { - expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); - }); - }); - - describe('getCurrentUserLastNote', () => { - it('should return the last note of the current user', () => { - expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); - }); - }); -}); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js deleted file mode 100644 index 22d99998a7d..00000000000 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import mutations from '~/notes/stores/mutations'; -import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; - -describe('Mutation Notes Store', () => { - describe('ADD_NEW_NOTE', () => { - let state; - let noteData; - - beforeEach(() => { - state = { notes: [] }; - noteData = { - expanded: true, - id: note.discussion_id, - individual_note: true, - notes: [note], - reply_id: note.discussion_id, - }; - mutations.ADD_NEW_NOTE(state, note); - }); - - it('should add a new note to an array of notes', () => { - expect(state).toEqual({ - notes: [noteData], - }); - expect(state.notes.length).toBe(1); - }); - - it('should not add the same note to the notes array', () => { - mutations.ADD_NEW_NOTE(state, note); - expect(state.notes.length).toBe(1); - }); - }); - - describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { - it('should add a reply to a specific discussion', () => { - const state = { notes: [discussionMock] }; - const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); - mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - - expect(state.notes[0].notes.length).toEqual(4); - }); - }); - - describe('DELETE_NOTE', () => { - it('should delete a note ', () => { - const state = { notes: [discussionMock] }; - const toDelete = discussionMock.notes[0]; - const lengthBefore = discussionMock.notes.length; - - mutations.DELETE_NOTE(state, toDelete); - - expect(state.notes[0].notes.length).toEqual(lengthBefore - 1); - }); - }); - - describe('REMOVE_PLACEHOLDER_NOTES', () => { - it('should remove all placeholder notes in indivudal notes and discussion', () => { - const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); - const state = { notes: [placeholderNote] }; - mutations.REMOVE_PLACEHOLDER_NOTES(state); - - expect(state.notes).toEqual([]); - }); - }); - - describe('SET_NOTES_DATA', () => { - it('should set an object with notesData', () => { - const state = { - notesData: {}, - }; - - mutations.SET_NOTES_DATA(state, notesDataMock); - expect(state.notesData).toEqual(notesDataMock); - }); - }); - - describe('SET_NOTEABLE_DATA', () => { - it('should set the issue data', () => { - const state = { - noteableData: {}, - }; - - mutations.SET_NOTEABLE_DATA(state, noteableDataMock); - expect(state.noteableData).toEqual(noteableDataMock); - }); - }); - - describe('SET_USER_DATA', () => { - it('should set the user data', () => { - const state = { - userData: {}, - }; - - mutations.SET_USER_DATA(state, userDataMock); - expect(state.userData).toEqual(userDataMock); - }); - }); - - describe('SET_INITIAL_NOTES', () => { - it('should set the initial notes received', () => { - const state = { - notes: [], - }; - - mutations.SET_INITIAL_NOTES(state, [note]); - expect(state.notes).toEqual([note]); - }); - }); - - describe('SET_LAST_FETCHED_AT', () => { - it('should set timestamp', () => { - const state = { - lastFetchedAt: [], - }; - - mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); - expect(state.lastFetchedAt).toEqual('timestamp'); - }); - }); - - describe('SET_TARGET_NOTE_HASH', () => { - it('should set the note hash', () => { - const state = { - targetNoteHash: [], - }; - - mutations.SET_TARGET_NOTE_HASH(state, 'hash'); - expect(state.targetNoteHash).toEqual('hash'); - }); - }); - - describe('SHOW_PLACEHOLDER_NOTE', () => { - it('should set a placeholder note', () => { - const state = { - notes: [], - }; - mutations.SHOW_PLACEHOLDER_NOTE(state, note); - expect(state.notes[0].isPlaceholderNote).toEqual(true); - }); - }); - - describe('TOGGLE_AWARD', () => { - it('should add award if user has not reacted yet', () => { - const state = { - notes: [note], - userData: userDataMock, - }; - - const data = { - note, - awardName: 'cartwheel', - }; - - mutations.TOGGLE_AWARD(state, data); - const lastIndex = state.notes[0].award_emoji.length - 1; - - expect(state.notes[0].award_emoji[lastIndex]).toEqual({ - name: 'cartwheel', - user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, - }); - }); - - it('should remove award if user already reacted', () => { - const state = { - notes: [note], - userData: { - id: 1, - name: 'Administrator', - username: 'root', - }, - }; - - const data = { - note, - awardName: 'bath_tone3', - }; - mutations.TOGGLE_AWARD(state, data); - expect(state.notes[0].award_emoji.length).toEqual(2); - }); - }); - - describe('TOGGLE_DISCUSSION', () => { - it('should open a closed discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: false }); - - const state = { - notes: [discussion], - }; - - mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); - - expect(state.notes[0].expanded).toEqual(true); - }); - - it('should close a opened discussion', () => { - const state = { - notes: [discussionMock], - }; - - mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); - - expect(state.notes[0].expanded).toEqual(false); - }); - }); - - describe('UPDATE_NOTE', () => { - it('should update a note', () => { - const state = { - notes: [individualNote], - }; - - const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); - - mutations.UPDATE_NOTE(state, updated); - - expect(state.notes[0].notes[0].note).toEqual('Foo'); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js deleted file mode 100644 index ba8ab0b2cd7..00000000000 --- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; -import store from '~/notes/stores'; -import { userDataMock } from '../../../notes/mock_data'; - -describe('issue placeholder system note component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issuePlaceholderNote); - store.dispatch('setUserData', userDataMock); - vm = new Component({ - store, - propsData: { note: { body: 'Foo' } }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('user information', () => { - it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); - }); - }); - - describe('note content', () => { - it('should render note header information', () => { - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); - }); - - it('should render note body', () => { - expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js deleted file mode 100644 index 7b8e6c330c2..00000000000 --- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import Vue from 'vue'; -import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; - -describe('placeholder system note component', () => { - let PlaceholderSystemNote; - let vm; - - beforeEach(() => { - PlaceholderSystemNote = Vue.extend(placeholderSystemNote); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render system note placeholder with plain text', () => { - vm = mountComponent(PlaceholderSystemNote, { - note: { body: 'This is a placeholder' }, - }); - - expect(vm.$el.tagName).toEqual('LI'); - expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); - }); -}); diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js deleted file mode 100644 index 36aaf0a6c2e..00000000000 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import Vue from 'vue'; -import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; -import store from '~/notes/stores'; - -describe('issue system note', () => { - let vm; - let props; - - beforeEach(() => { - props = { - note: { - id: 1424, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: 'path', - path: '/root', - }, - note_html: '

closed

', - system_note_icon_name: 'icon_status_closed', - created_at: '2017-08-02T10:51:58.559Z', - }, - }; - - store.dispatch('setTargetNoteHash', `note_${props.note.id}`); - - const Component = Vue.extend(issueSystemNote); - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render a list item with correct id', () => { - expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); - }); - - it('should render target class is note is target note', () => { - expect(vm.$el.classList).toContain('target'); - }); - - it('should render svg icon', () => { - expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); - }); - - it('should render note header component', () => { - expect( - vm.$el.querySelector('.system-note-message').innerHTML, - ).toEqual(props.note.note_html); - }); -}); diff --git a/spec/javascripts/vue_shared/notes/components/comment_form_spec.js b/spec/javascripts/vue_shared/notes/components/comment_form_spec.js new file mode 100644 index 00000000000..516e1b1b192 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/comment_form_spec.js @@ -0,0 +1,207 @@ +import Vue from 'vue'; +import Autosize from 'autosize'; +import store from '~/vue_shared/notes/stores'; +import issueCommentForm from '~/vue_shared/notes/components/comment_form.vue'; +import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; + +describe('issue_comment_form component', () => { + let vm; + const Component = Vue.extend(issueCommentForm); + let mountComponent; + + beforeEach(() => { + mountComponent = () => new Component({ + store, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user is logged in', () => { + beforeEach(() => { + store.dispatch('setUserData', userDataMock); + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = mountComponent(); + }); + + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + }); + + describe('handleSave', () => { + it('should request to save note when note is entered', () => { + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + spyOn(vm, 'resizeTextarea'); + spyOn(vm, 'stopPolling'); + + vm.handleSave(); + expect(vm.isSubmitting).toEqual(true); + expect(vm.note).toEqual(''); + expect(vm.saveNote).toHaveBeenCalled(); + expect(vm.stopPolling).toHaveBeenCalled(); + expect(vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + spyOn(vm, 'toggleIssueState'); + + vm.handleSave(); + + expect(vm.toggleIssueState).toHaveBeenCalled(); + }); + + it('should disable action button whilst submitting', (done) => { + const saveNotePromise = Promise.resolve(); + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(saveNotePromise); + spyOn(vm, 'stopPolling'); + + const actionButton = vm.$el.querySelector('.js-action-button'); + + vm.handleSave(); + + Vue.nextTick() + .then(() => expect(actionButton.disabled).toBeTruthy()) + .then(saveNotePromise) + .then(Vue.nextTick) + .then(() => expect(actionButton.disabled).toBeFalsy()) + .then(done) + .catch(done.fail); + }); + }); + + describe('textarea', () => { + it('should render textarea with placeholder', () => { + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + + it('should make textarea disabled while requesting', (done) => { + const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); + vm.note = 'hello world'; + spyOn(vm, 'stopPolling'); + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + + vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); + done(); + }); + }); + }); + + it('should support quick actions', () => { + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), + ).toEqual('true'); + }); + + it('should link to markdown docs', () => { + const { markdownDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + }); + + it('should link to quick actions docs', () => { + const { quickActionsDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + }); + + it('should resize textarea after note discarded', (done) => { + spyOn(Autosize, 'update'); + spyOn(vm, 'discard').and.callThrough(); + + vm.note = 'foo'; + vm.discard(); + + Vue.nextTick(() => { + expect(Autosize.update).toHaveBeenCalled(); + done(); + }); + }); + + describe('edit mode', () => { + it('should enter edit mode when arrow up is pressed', () => { + spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true)); + + expect(vm.editCurrentUserLastNote).toHaveBeenCalled(); + }); + }); + + describe('event enter', () => { + it('should save note when cmd/ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); + }); + }); + + describe('actions', () => { + it('should be possible to close the issue', () => { + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue'); + }); + + it('should render comment button as disabled', () => { + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('should enable comment button if it has note', (done) => { + vm.note = 'Foo'; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null); + done(); + }); + }); + + it('should update buttons texts when it has note', (done) => { + vm.note = 'Foo'; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue'); + expect(vm.$el.querySelector('.js-note-discard')).toBeDefined(); + done(); + }); + }); + }); + + describe('issue is confidential', () => { + it('shows information warning', (done) => { + store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined(); + done(); + }); + }); + }); + }); + + describe('user is not logged in', () => { + beforeEach(() => { + store.dispatch('setUserData', null); + store.dispatch('setNoteableData', loggedOutnoteableData); + store.dispatch('setNotesData', notesDataMock); + + vm = mountComponent(); + }); + + it('should render signed out widget', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + }); + + it('should not render submission form', () => { + expect(vm.$el.querySelector('textarea')).toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_actions_spec.js b/spec/javascripts/vue_shared/notes/components/note_actions_spec.js new file mode 100644 index 00000000000..e31e8587223 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_actions_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import noteActions from '~/vue_shared/notes/components/note_actions.vue'; +import { userDataMock } from '../mock_data'; + +describe('issse_note_actions component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(noteActions); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user is logged in', () => { + let props; + + beforeEach(() => { + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: true, + canEdit: true, + canReportAsAbuse: true, + noteId: 539, + reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + }; + + store.dispatch('setUserData', userDataMock); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should render access level badge', () => { + expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel); + }); + + it('should render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + }); + + describe('actions dropdown', () => { + it('should be possible to edit the comment', () => { + expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); + }); + + it('should be possible to report as abuse', () => { + expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); + }); + + it('should be possible to delete comment', () => { + expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); + }); + }); + }); + + describe('user is not logged in', () => { + let props; + + beforeEach(() => { + store.dispatch('setUserData', {}); + props = { + accessLevel: 'Master', + authorId: 26, + canDelete: false, + canEdit: false, + canReportAsAbuse: false, + noteId: 539, + reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', + }; + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + it('should not render emoji link', () => { + expect(vm.$el.querySelector('.js-add-award')).toEqual(null); + }); + + it('should not render actions dropdown', () => { + expect(vm.$el.querySelector('.more-actions')).toEqual(null); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_app_spec.js b/spec/javascripts/vue_shared/notes/components/note_app_spec.js new file mode 100644 index 00000000000..829f0b4d5eb --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_app_spec.js @@ -0,0 +1,255 @@ +import Vue from 'vue'; +import notesApp from '~/vue_shared/notes/components/notes_app.vue'; +import service from '~/vue_shared/notes/services/notes_service'; +import * as mockData from '../mock_data'; + +describe('note_app', () => { + let mountComponent; + let vm; + + const individualNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { + status: 200, + })); + }; + + const discussionNoteInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + const IssueNotesApp = Vue.extend(notesApp); + + mountComponent = (data) => { + const props = data || { + noteableData: mockData.noteableDataMock, + notesData: mockData.notesDataMock, + userData: mockData.userDataMock, + }; + + return new IssueNotesApp({ + propsData: props, + }).$mount(); + }; + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('set data', () => { + const responseInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(responseInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor); + }); + + it('should set notes data', () => { + expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock); + }); + + it('should set issue data', () => { + expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock); + }); + + it('should set user data', () => { + expect(vm.$store.state.userData).toEqual(mockData.userDataMock); + }); + + it('should fetch notes', () => { + expect(vm.$store.state.notes).toEqual([]); + }); + }); + + describe('render', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('should render list of notes', (done) => { + const note = mockData.individualNoteServerResponse[0].notes[0]; + + setTimeout(() => { + expect( + vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(), + ).toEqual(note.author.name); + + expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html); + done(); + }, 0); + }); + + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + + it('should render form comment button as disabled', () => { + expect( + vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'), + ).toEqual('disabled'); + }); + }); + + describe('while fetching data', () => { + beforeEach(() => { + vm = mountComponent(); + }); + + it('should render loading icon', () => { + expect(vm.$el.querySelector('.js-loading')).toBeDefined(); + }); + + it('should render form', () => { + expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); + expect( + vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + }); + + describe('update note', () => { + describe('individual note', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('renders edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); + }); + }, 0); + }); + + it('calls the service to update the note', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); + + describe('dicussion note', () => { + beforeEach(() => { + Vue.http.interceptors.push(discussionNoteInterceptor); + spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); + }); + + it('renders edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); + done(); + }); + }, 0); + }); + + it('updates the note and resets the edit form', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(() => { + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + done(); + }); + }, 0); + }); + }); + }); + + describe('new note form', () => { + beforeEach(() => { + vm = mountComponent(); + }); + + it('should render markdown docs url', () => { + const { markdownDocsPath } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + }); + + it('should render quick action docs url', () => { + const { quickActionsDocsPath } = mockData.notesDataMock; + expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); + }); + }); + + describe('edit form', () => { + beforeEach(() => { + Vue.http.interceptors.push(individualNoteInterceptor); + vm = mountComponent(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + }); + + it('should render markdown docs url', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + const { markdownDocsPath } = mockData.notesDataMock; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(), + ).toEqual('Markdown is supported'); + done(); + }); + }, 0); + }); + + it('should not render quick actions docs url', (done) => { + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + const { quickActionsDocsPath } = mockData.notesDataMock; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`), + ).toEqual(null); + done(); + }); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_attachment_spec.js b/spec/javascripts/vue_shared/notes/components/note_attachment_spec.js new file mode 100644 index 00000000000..7469ad1c0dd --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_attachment_spec.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import noteAttachment from '~/vue_shared/notes/components/note_attachment.vue'; + +describe('issue note attachment', () => { + it('should render properly', () => { + const props = { + attachment: { + filename: 'dk.png', + image: true, + url: '/dk.png', + }, + }; + + const Component = Vue.extend(noteAttachment); + const vm = new Component({ + propsData: props, + }).$mount(); + + expect(vm.$el.classList.contains('note-attachment')).toBeTruthy(); + expect(vm.$el.querySelector('img').src).toContain(props.attachment.url); + expect(vm.$el.querySelector('a').href).toContain(props.attachment.url); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_awards_list_spec.js b/spec/javascripts/vue_shared/notes/components/note_awards_list_spec.js new file mode 100644 index 00000000000..8091cc008dc --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_awards_list_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import awardsNote from '~/vue_shared/notes/components/note_awards_list.vue'; +import { noteableDataMock, notesDataMock } from '../mock_data'; + +describe('note_awards_list component', () => { + let vm; + let awardsMock; + + beforeEach(() => { + const Component = Vue.extend(awardsNote); + + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + awardsMock = [ + { + name: 'flag_tz', + user: { id: 1, name: 'Administrator', username: 'root' }, + }, + { + name: 'cartwheel_tone3', + user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, + }, + ]; + + vm = new Component({ + store, + propsData: { + awards: awardsMock, + noteAuthorId: 2, + noteId: 545, + toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render awarded emojis', () => { + expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); + expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); + }); + + it('should be possible to remove awareded emoji', () => { + spyOn(vm, 'handleAward').and.callThrough(); + vm.$el.querySelector('.js-awards-block button').click(); + + expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); + }); + + it('should be possible to add new emoji', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_body_spec.js b/spec/javascripts/vue_shared/notes/components/note_body_spec.js new file mode 100644 index 00000000000..3f5d85024b0 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_body_spec.js @@ -0,0 +1,46 @@ + +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import noteBody from '~/vue_shared/notes/components/note_body.vue'; +import { noteableDataMock, notesDataMock, note } from '../mock_data'; + +describe('issue_note_body component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(noteBody); + + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + canEdit: true, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render the note', () => { + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + }); + + it('should be render form if user is editing', (done) => { + vm.isEditing = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined(); + done(); + }); + }); + + it('should render awards list', () => { + expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined(); + expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined(); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_edited_text_spec.js b/spec/javascripts/vue_shared/notes/components/note_edited_text_spec.js new file mode 100644 index 00000000000..6c90ef369f6 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_edited_text_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import noteEditedText from '~/vue_shared/notes/components/note_edited_text.vue'; + +describe('note_edited_text', () => { + let vm; + let props; + + beforeEach(() => { + const Component = Vue.extend(noteEditedText); + props = { + actionText: 'Edited', + className: 'foo-bar', + editedAt: '2017-08-04T09:52:31.062Z', + editedBy: { + avatar_url: 'path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + }; + + vm = new Component({ + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render block with provided className', () => { + expect(vm.$el.className).toEqual(props.className); + }); + + it('should render provided actionText', () => { + expect(vm.$el.textContent).toContain(props.actionText); + }); + + it('should render provided user information', () => { + const authorLink = vm.$el.querySelector('.js-vue-author'); + + expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path); + expect(authorLink.textContent.trim()).toEqual(props.editedBy.name); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_form_spec.js b/spec/javascripts/vue_shared/notes/components/note_form_spec.js new file mode 100644 index 00000000000..4c56057b448 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_form_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import issueNoteForm from '~/vue_shared/notes/components/note_form.vue'; +import { noteableDataMock, notesDataMock } from '../mock_data'; +import { keyboardDownEvent } from '../../issue_show/helpers'; + +describe('issue_note_form component', () => { + let vm; + let props; + + beforeEach(() => { + const Component = Vue.extend(issueNoteForm); + + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + props = { + isEditing: false, + noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', + noteId: 545, + }; + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('conflicts editing', () => { + it('should show conflict message if note changes outside the component', (done) => { + vm.isEditing = true; + vm.noteBody = 'Foo'; + const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual(message); + done(); + }); + }); + }); + + describe('form', () => { + it('should render text area with placeholder', () => { + expect( + vm.$el.querySelector('textarea').getAttribute('placeholder'), + ).toEqual('Write a comment or drag your files here...'); + }); + + it('should link to markdown docs', () => { + const { markdownDocsPath } = notesDataMock; + expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown'); + }); + + describe('keyboard events', () => { + describe('up', () => { + it('should ender edit mode', () => { + spyOn(vm, 'editMyLastNote').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true)); + + expect(vm.editMyLastNote).toHaveBeenCalled(); + }); + }); + + describe('enter', () => { + it('should submit note', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); + }); + }); + + describe('actions', () => { + it('should be possible to cancel', (done) => { + spyOn(vm, 'cancelHandler').and.callThrough(); + vm.isEditing = true; + + Vue.nextTick(() => { + vm.$el.querySelector('.note-edit-cancel').click(); + + Vue.nextTick(() => { + expect(vm.cancelHandler).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should be possible to update the note', (done) => { + vm.isEditing = true; + + Vue.nextTick(() => { + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + Vue.nextTick(() => { + expect(vm.isSubmitting).toEqual(true); + done(); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_header_spec.js b/spec/javascripts/vue_shared/notes/components/note_header_spec.js new file mode 100644 index 00000000000..9d578c1e75c --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_header_spec.js @@ -0,0 +1,94 @@ +import Vue from 'vue'; +import noteHeader from '~/vue_shared/notes/components/note_header.vue'; +import store from '~/vue_shared/notes/stores'; + +describe('note_header component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(noteHeader); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('individual note', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'commented', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: false, + noteId: 1394, + }, + }).$mount(); + }); + + it('should render user information', () => { + expect( + vm.$el.querySelector('.note-header-author-name').textContent.trim(), + ).toEqual('Root'); + expect( + vm.$el.querySelector('.note-header-info a').getAttribute('href'), + ).toEqual('/root'); + }); + + it('should render timestamp link', () => { + expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + }); + }); + + describe('discussion', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'started a discussion', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: true, + noteId: 1395, + }, + }).$mount(); + }); + + it('should render toggle button', () => { + expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + }); + + it('should toggle the disucssion icon', (done) => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'), + ).toEqual(true); + + vm.$el.querySelector('.js-vue-toggle-button').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'), + ).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/note_signed_out_widget_spec.js b/spec/javascripts/vue_shared/notes/components/note_signed_out_widget_spec.js new file mode 100644 index 00000000000..185ab3ce02c --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/note_signed_out_widget_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import noteSignedOut from '~/vue_shared/notes/components/note_signed_out_widget.vue'; +import store from '~/vue_shared/notes/stores'; +import { notesDataMock } from '../mock_data'; + +describe('note_signed_out_widget component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(noteSignedOut); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render sign in link provided in the store', () => { + expect( + vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent, + ).toEqual('sign in'); + }); + + it('should render register link provided in the store', () => { + expect( + vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent, + ).toEqual('register'); + }); + + it('should render information text', () => { + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply'); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/noteable_discussion_spec.js b/spec/javascripts/vue_shared/notes/components/noteable_discussion_spec.js new file mode 100644 index 00000000000..28bf2083fad --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/noteable_discussion_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import issueDiscussion from '~/vue_shared/notes/components/noteable_discussion.vue'; +import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; + +describe('issue_discussion component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueDiscussion); + + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note: discussionMock, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render user avatar', () => { + expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined(); + }); + + it('should render discussion header', () => { + expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); + expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); + }); + + describe('actions', () => { + it('should render reply button', () => { + expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...'); + }); + + it('should toggle reply form', (done) => { + vm.$el.querySelector('.js-vue-discussion-reply').click(); + Vue.nextTick(() => { + expect(vm.$refs.noteForm).toBeDefined(); + expect(vm.isReplying).toEqual(true); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/noteable_note_spec.js b/spec/javascripts/vue_shared/notes/components/noteable_note_spec.js new file mode 100644 index 00000000000..679abde6c49 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/noteable_note_spec.js @@ -0,0 +1,44 @@ + +import Vue from 'vue'; +import store from '~/vue_shared/notes/stores'; +import issueNote from '~/vue_shared/notes/components/noteable_note.vue'; +import { noteableDataMock, notesDataMock, note } from '../mock_data'; + +describe('issue_note', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issueNote); + + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + + vm = new Component({ + store, + propsData: { + note, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render user information', () => { + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url); + }); + + it('should render note header content', () => { + expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name); + expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented'); + }); + + it('should render note actions', () => { + expect(vm.$el.querySelector('.note-actions')).toBeDefined(); + }); + + it('should render issue body', () => { + expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/notes/components/notes/placeholder_note_spec.js new file mode 100644 index 00000000000..7c4123e70ed --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/notes/placeholder_note_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import issuePlaceholderNote from '~/vue_shared/notes/components/placeholder_note.vue'; +import store from '~/vue_shared/notes/stores'; +import { userDataMock } from '../../../notes/mock_data'; + +describe('issue placeholder system note component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issuePlaceholderNote); + store.dispatch('setUserData', userDataMock); + vm = new Component({ + store, + propsData: { note: { body: 'Foo' } }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user information', () => { + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); + }); + }); + + describe('note content', () => { + it('should render note header information', () => { + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); + }); + + it('should render note body', () => { + expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/notes/components/notes/placeholder_system_note_spec.js new file mode 100644 index 00000000000..2b7398bcefe --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/notes/placeholder_system_note_spec.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/vue_shared/notes/components/placeholder_system_note.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('placeholder system note component', () => { + let PlaceholderSystemNote; + let vm; + + beforeEach(() => { + PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render system note placeholder with plain text', () => { + vm = mountComponent(PlaceholderSystemNote, { + note: { body: 'This is a placeholder' }, + }); + + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/notes/components/notes/system_note_spec.js new file mode 100644 index 00000000000..60e591d9d5f --- /dev/null +++ b/spec/javascripts/vue_shared/notes/components/notes/system_note_spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import issueSystemNote from '~/vue_shared/notes/components/system_note.vue'; +import store from '~/vue_shared/notes/stores'; + +describe('issue system note', () => { + let vm; + let props; + + beforeEach(() => { + props = { + note: { + id: 1424, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'path', + path: '/root', + }, + note_html: '

closed

', + system_note_icon_name: 'icon_status_closed', + created_at: '2017-08-02T10:51:58.559Z', + }, + }; + + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + const Component = Vue.extend(issueSystemNote); + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a list item with correct id', () => { + expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); + }); + + it('should render target class is note is target note', () => { + expect(vm.$el.classList).toContain('target'); + }); + + it('should render svg icon', () => { + expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); + }); + + it('should render note header component', () => { + expect( + vm.$el.querySelector('.system-note-message').innerHTML, + ).toEqual(props.note.note_html); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/mock_data.js b/spec/javascripts/vue_shared/notes/mock_data.js new file mode 100644 index 00000000000..42497de3c55 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/mock_data.js @@ -0,0 +1,449 @@ +/* eslint-disable */ +export const notesDataMock = { + discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', + lastFetchedAt: '1501862675', + markdownDocsPath: '/help/user/markdown', + newSessionPath: '/users/sign_in?redirect_to_referer=yes', + 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', +}; + +export const userDataMock = { + avatar_url: 'mock_path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', +}; + +export const noteableDataMock = { + assignees: [], + author_id: 1, + branch_name: null, + confidential: false, + create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', + created_at: '2017-02-07T10:11:18.395Z', + current_user: { + can_create_note: true, + can_update: true, + }, + deleted_at: null, + description: '', + due_date: null, + human_time_estimate: null, + human_total_time_spent: null, + id: 98, + iid: 26, + labels: [], + lock_version: null, + milestone: null, + milestone_id: null, + moved_to_id: null, + preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', + project_id: 2, + state: 'opened', + time_estimate: 0, + title: '14', + total_time_spent: 0, + updated_at: '2017-08-04T09:53:01.226Z', + updated_by_id: 1, + web_url: '/gitlab-org/gitlab-ce/issues/26', +}; + +export const lastFetchedAt = '1501862675'; + +export const individualNote = { + expanded: true, + id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + individual_note: true, + notes: [{ + id: 1390, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'test', + path: '/root', + }, + created_at: '2017-08-01T17: 09: 33.762Z', + updated_at: '2017-08-01T17: 09: 33.762Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'sdfdsaf', + note_html: '

sdfdsaf

', + current_user: { can_edit: true }, + discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + emoji_awardable: true, + award_emoji: [ + { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, + { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, + ], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1390', + }], + reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', +}; + +export const note = { + "id": 546, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "path": "/root" + }, + "created_at": "2017-08-10T15:24:03.087Z", + "updated_at": "2017-08-10T15:24:03.087Z", + "system": false, + "noteable_id": 67, + "noteable_type": "Issue", + "noteable_iid": 7, + "type": null, + "human_access": "Owner", + "note": "Vel id placeat reprehenderit sit numquam.", + "note_html": "

Vel id placeat reprehenderit sit numquam.

", + "current_user": { + "can_edit": true + }, + "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }, { + "name": "bath_tone3", + "user": { + "id": 1, + "name": "Administrator", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/546" + } + +export const discussionMock = { + id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + expanded: true, + notes: [{ + id: 1395, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:51:58.559Z', + updated_at: '2017-08-02T10:51:58.559Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'THIS IS A DICUSSSION!', + note_html: '

THIS IS A DICUSSSION!

', + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1395', + }, { + id: 1396, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:56:50.980Z', + updated_at: '2017-08-03T14:19:35.691Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'sadfasdsdgdsf', + note_html: '

sadfasdsdgdsf

', + last_edited_at: '2017-08-03T14:19:35.691Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1396', + }, { + id: 1437, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-03T18:11:18.780Z', + updated_at: '2017-08-04T09:52:31.062Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'adsfasf Should disappear', + note_html: '

adsfasf Should disappear

', + last_edited_at: '2017-08-04T09:52:31.062Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', + report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1437', + }], + individual_note: false, +}; + +export const loggedOutnoteableData = { + "id": 98, + "iid": 26, + "author_id": 1, + "description": "", + "lock_version": 1, + "milestone_id": null, + "state": "opened", + "title": "asdsa", + "updated_by_id": 1, + "created_at": "2017-02-07T10:11:18.395Z", + "updated_at": "2017-08-08T10:22:51.564Z", + "deleted_at": null, + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null, + "milestone": null, + "labels": [], + "branch_name": null, + "confidential": false, + "assignees": [{ + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "web_url": "http://localhost:3000/root" + }], + "due_date": null, + "moved_to_id": null, + "project_id": 2, + "web_url": "/gitlab-org/gitlab-ce/issues/26", + "current_user": { + "can_create_note": false, + "can_update": false + }, + "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue", + "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" +} + +export const individualNoteServerResponse = [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true + }, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true +}]; + +export const discussionNoteServerResponse = [{ + "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "expanded": true, + "notes": [{ + "id": 1471, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-08-08T16:53:00.666Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" + }], + "individual_note": false +}]; diff --git a/spec/javascripts/vue_shared/notes/stores/actions_spec.js b/spec/javascripts/vue_shared/notes/stores/actions_spec.js new file mode 100644 index 00000000000..46b4f5f2163 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/stores/actions_spec.js @@ -0,0 +1,61 @@ +import * as actions from '~/vue_shared/notes/stores/actions'; +import testAction from '../../helpers/vuex_action_helper'; +import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; + +describe('Actions Notes Store', () => { + describe('setNotesData', () => { + it('should set received notes data', (done) => { + testAction(actions.setNotesData, null, { notesData: {} }, [ + { type: 'SET_NOTES_DATA', payload: notesDataMock }, + ], done); + }); + }); + + describe('setNoteableData', () => { + it('should set received issue data', (done) => { + testAction(actions.setNoteableData, null, { noteableData: {} }, [ + { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }, + ], done); + }); + }); + + describe('setUserData', () => { + it('should set received user data', (done) => { + testAction(actions.setUserData, null, { userData: {} }, [ + { type: 'SET_USER_DATA', payload: userDataMock }, + ], done); + }); + }); + + describe('setLastFetchedAt', () => { + it('should set received timestamp', (done) => { + testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [ + { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }, + ], done); + }); + }); + + describe('setInitialNotes', () => { + it('should set initial notes', (done) => { + testAction(actions.setInitialNotes, null, { notes: [] }, [ + { type: 'SET_INITIAL_NOTES', payload: [individualNote] }, + ], done); + }); + }); + + describe('setTargetNoteHash', () => { + it('should set target note hash', (done) => { + testAction(actions.setTargetNoteHash, null, { notes: [] }, [ + { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }, + ], done); + }); + }); + + describe('toggleDiscussion', () => { + it('should toggle discussion', (done) => { + testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [ + { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }, + ], done); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/stores/getters_spec.js b/spec/javascripts/vue_shared/notes/stores/getters_spec.js new file mode 100644 index 00000000000..ee810f9ca05 --- /dev/null +++ b/spec/javascripts/vue_shared/notes/stores/getters_spec.js @@ -0,0 +1,58 @@ +import * as getters from '~/vue_shared/notes/stores/getters'; +import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; + +describe('Getters Notes Store', () => { + let state; + beforeEach(() => { + state = { + notes: [individualNote], + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + }); + describe('notes', () => { + it('should return all notes in the store', () => { + expect(getters.notes(state)).toEqual([individualNote]); + }); + }); + + describe('targetNoteHash', () => { + it('should return `targetNoteHash`', () => { + expect(getters.targetNoteHash(state)).toEqual('hash'); + }); + }); + + describe('getNotesData', () => { + it('should return all data in `notesData`', () => { + expect(getters.getNotesData(state)).toEqual(notesDataMock); + }); + }); + + describe('getNoteableData', () => { + it('should return all data in `noteableData`', () => { + expect(getters.getNoteableData(state)).toEqual(noteableDataMock); + }); + }); + + describe('getUserData', () => { + it('should return all data in `userData`', () => { + expect(getters.getUserData(state)).toEqual(userDataMock); + }); + }); + + describe('notesById', () => { + it('should return the note for the given id', () => { + expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); + }); + }); + + describe('getCurrentUserLastNote', () => { + it('should return the last note of the current user', () => { + expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/notes/stores/mutation_spec.js b/spec/javascripts/vue_shared/notes/stores/mutation_spec.js new file mode 100644 index 00000000000..851870079fb --- /dev/null +++ b/spec/javascripts/vue_shared/notes/stores/mutation_spec.js @@ -0,0 +1,219 @@ +import mutations from '~/vue_shared/notes/stores/mutations'; +import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; + +describe('Mutation Notes Store', () => { + describe('ADD_NEW_NOTE', () => { + let state; + let noteData; + + beforeEach(() => { + state = { notes: [] }; + noteData = { + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }; + mutations.ADD_NEW_NOTE(state, note); + }); + + it('should add a new note to an array of notes', () => { + expect(state).toEqual({ + notes: [noteData], + }); + expect(state.notes.length).toBe(1); + }); + + it('should not add the same note to the notes array', () => { + mutations.ADD_NEW_NOTE(state, note); + expect(state.notes.length).toBe(1); + }); + }); + + describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { + it('should add a reply to a specific discussion', () => { + const state = { notes: [discussionMock] }; + const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + + expect(state.notes[0].notes.length).toEqual(4); + }); + }); + + describe('DELETE_NOTE', () => { + it('should delete a note ', () => { + const state = { notes: [discussionMock] }; + const toDelete = discussionMock.notes[0]; + const lengthBefore = discussionMock.notes.length; + + mutations.DELETE_NOTE(state, toDelete); + + expect(state.notes[0].notes.length).toEqual(lengthBefore - 1); + }); + }); + + describe('REMOVE_PLACEHOLDER_NOTES', () => { + it('should remove all placeholder notes in indivudal notes and discussion', () => { + const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); + const state = { notes: [placeholderNote] }; + mutations.REMOVE_PLACEHOLDER_NOTES(state); + + expect(state.notes).toEqual([]); + }); + }); + + describe('SET_NOTES_DATA', () => { + it('should set an object with notesData', () => { + const state = { + notesData: {}, + }; + + mutations.SET_NOTES_DATA(state, notesDataMock); + expect(state.notesData).toEqual(notesDataMock); + }); + }); + + describe('SET_NOTEABLE_DATA', () => { + it('should set the issue data', () => { + const state = { + noteableData: {}, + }; + + mutations.SET_NOTEABLE_DATA(state, noteableDataMock); + expect(state.noteableData).toEqual(noteableDataMock); + }); + }); + + describe('SET_USER_DATA', () => { + it('should set the user data', () => { + const state = { + userData: {}, + }; + + mutations.SET_USER_DATA(state, userDataMock); + expect(state.userData).toEqual(userDataMock); + }); + }); + + describe('SET_INITIAL_NOTES', () => { + it('should set the initial notes received', () => { + const state = { + notes: [], + }; + + mutations.SET_INITIAL_NOTES(state, [note]); + expect(state.notes).toEqual([note]); + }); + }); + + describe('SET_LAST_FETCHED_AT', () => { + it('should set timestamp', () => { + const state = { + lastFetchedAt: [], + }; + + mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); + expect(state.lastFetchedAt).toEqual('timestamp'); + }); + }); + + describe('SET_TARGET_NOTE_HASH', () => { + it('should set the note hash', () => { + const state = { + targetNoteHash: [], + }; + + mutations.SET_TARGET_NOTE_HASH(state, 'hash'); + expect(state.targetNoteHash).toEqual('hash'); + }); + }); + + describe('SHOW_PLACEHOLDER_NOTE', () => { + it('should set a placeholder note', () => { + const state = { + notes: [], + }; + mutations.SHOW_PLACEHOLDER_NOTE(state, note); + expect(state.notes[0].isPlaceholderNote).toEqual(true); + }); + }); + + describe('TOGGLE_AWARD', () => { + it('should add award if user has not reacted yet', () => { + const state = { + notes: [note], + userData: userDataMock, + }; + + const data = { + note, + awardName: 'cartwheel', + }; + + mutations.TOGGLE_AWARD(state, data); + const lastIndex = state.notes[0].award_emoji.length - 1; + + expect(state.notes[0].award_emoji[lastIndex]).toEqual({ + name: 'cartwheel', + user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, + }); + }); + + it('should remove award if user already reacted', () => { + const state = { + notes: [note], + userData: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }; + + const data = { + note, + awardName: 'bath_tone3', + }; + mutations.TOGGLE_AWARD(state, data); + expect(state.notes[0].award_emoji.length).toEqual(2); + }); + }); + + describe('TOGGLE_DISCUSSION', () => { + it('should open a closed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + notes: [discussion], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.notes[0].expanded).toEqual(true); + }); + + it('should close a opened discussion', () => { + const state = { + notes: [discussionMock], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); + + expect(state.notes[0].expanded).toEqual(false); + }); + }); + + describe('UPDATE_NOTE', () => { + it('should update a note', () => { + const state = { + notes: [individualNote], + }; + + const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); + + mutations.UPDATE_NOTE(state, updated); + + expect(state.notes[0].notes[0].note).toEqual('Foo'); + }); + }); +}); -- cgit v1.2.1