diff options
Diffstat (limited to 'app/assets/javascripts/notes')
17 files changed, 243 insertions, 244 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 6612bc44e0b..b980e43b898 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale'; import Flash from '../../flash'; import Autosave from '../../autosave'; import TaskList from '../../task_list'; -import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility'; +import { + capitalizeFirstCharacter, + convertToCamelCase, + splitCamelCase, +} from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -122,7 +126,9 @@ export default { return this.getNoteableData.create_note_path; }, issuableTypeTitle() { - return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue'; + return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? 'merge request' + : 'issue'; }, }, watch: { @@ -359,7 +365,7 @@ Please check your network connection and try again.`; :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-note-text -js-gfm-input js-autosize markdown-area js-vue-textarea" +js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" @@ -374,7 +380,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button :disabled="isSubmitButtonDisabled" - class="btn btn-create comment-btn js-comment-button js-comment-submit-button" + class="btn btn-create comment-btn js-comment-button js-comment-submit-button + qa-comment-button" type="submit" @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue deleted file mode 100644 index fc7b52be241..00000000000 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ /dev/null @@ -1,94 +0,0 @@ -<script> -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - ClipboardButton, - Icon, - }, - props: { - diffFile: { - type: Object, - required: true, - }, - }, - computed: { - titleTag() { - return this.diffFile.discussionPath ? 'a' : 'span'; - }, - }, -}; -</script> - -<template> - <div class="file-header-content"> - <div - v-if="diffFile.submodule" - > - <span> - <icon name="archive" /> - <strong - class="file-title-name" - v-html="diffFile.submoduleLink" - ></strong> - <clipboard-button - :text="diffFile.submoduleLink" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - </span> - </div> - <template v-else> - <component - ref="titleWrapper" - :is="titleTag" - :href="diffFile.discussionPath" - > - <span v-html="diffFile.blobIcon"></span> - <span v-if="diffFile.renamedFile"> - <strong - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.oldPath }} - </strong> - → - <strong - :title="diffFile.newPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.newPath }} - </strong> - </span> - - <strong - v-else - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.filePath }} - <span v-if="diffFile.deletedFile"> - deleted - </span> - </strong> - </component> - - <clipboard-button - :text="diffFile.filePath" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - - <small - v-if="diffFile.modeChanged" - ref="fileMode" - > - {{ diffFile.aMode }} → {{ diffFile.bMode }} - </small> - </template> - </div> -</template> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 27ff7dea909..353aa790743 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -3,13 +3,13 @@ import { mapState, mapActions } from 'vuex'; import imageDiffHelper from '~/image_diff/helpers/index'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; export default { components: { DiffFileHeader, - SkeletonLoadingContainer, + SkeletonLoading, }, props: { discussion: { @@ -142,16 +142,15 @@ export default { class="line_content js-success-lazy-load" > <span></span> - <skeleton-loading-container /> + <skeleton-loading /> <span></span> </td> </tr> <tr class="notes_holder"> <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> + class="notes_content" + colspan="3" + > <slot></slot> </td> </tr> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index cdbbb342331..e075f94b82b 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -7,29 +7,30 @@ import editSvg from 'icons/_icon_pencil.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'NoteActions', + components: { + Icon, + }, directives: { tooltip, }, - components: { - loadingIcon, - }, props: { authorId: { type: Number, required: true, }, noteId: { - type: Number, + type: [String, Number], required: true, }, noteUrl: { type: String, - required: true, + required: false, + default: '', }, accessLevel: { type: String, @@ -38,7 +39,8 @@ export default { }, reportAbusePath: { type: String, - required: true, + required: false, + default: null, }, canEdit: { type: Boolean, @@ -87,6 +89,9 @@ export default { shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, + showDeleteAction() { + return this.canDelete && !this.canReportAsAbuse && !this.noteUrl; + }, isAuthoredByCurrentUser() { return this.authorId === this.currentUserId; }, @@ -152,9 +157,9 @@ export default { v-else v-html="resolveDiscussionSvg"></div> </template> - <loading-icon + <gl-loading-icon v-else - :inline="true" + inline /> </button> </div> @@ -171,7 +176,7 @@ export default { href="#" title="Add reaction" > - <loading-icon :inline="true" /> + <gl-loading-icon inline/> <span class="link-highlight award-control-icon-neutral" v-html="emojiSmiling"> @@ -204,7 +209,26 @@ export default { </button> </div> <div - v-if="shouldShowActionsDropdown" + v-if="showDeleteAction" + class="note-actions-item" + > + <button + v-tooltip + type="button" + title="Delete comment" + class="note-action-button js-note-delete btn btn-transparent" + data-container="body" + data-placement="bottom" + @click="onDelete" + > + <icon + name="remove" + class="link-highlight" + /> + </button> + </div> + <div + v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item"> <button v-tooltip @@ -225,11 +249,11 @@ export default { Report as abuse </a> </li> - <li> + <li v-if="noteUrl"> <button :data-clipboard-text="noteUrl" type="button" - css-class="btn-default btn-transparent" + class="btn-default btn-transparent js-btn-copy-note-link" > Copy link </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e111d3b9ac2..c68860d98ae 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -25,7 +25,7 @@ export default { required: true, }, noteId: { - type: Number, + type: String, required: true, }, canAwardEmoji: { @@ -182,9 +182,9 @@ export default { <div class="note-awards"> <div class="awards js-awards-block"> <button - v-tooltip v-for="(awardList, awardName, index) in groupedAwards" :key="index" + v-tooltip :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" class="btn award-control" diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 6f4a0709825..cf4c35de42c 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -109,7 +109,7 @@ export default { class="note_edited_ago" /> <note-awards-list - v-if="note.award_emoji.length" + v-if="note.award_emoji && note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index abcd4422d7c..33998394a69 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -20,9 +20,9 @@ export default { default: '', }, noteId: { - type: Number, + type: [String, Number], required: false, - default: 0, + default: '', }, markdownVersion: { type: Number, @@ -67,7 +67,10 @@ export default { 'getUserDataByProp', ]), noteHash() { - return `#note_${this.noteId}`; + if (this.noteId) { + return `#note_${this.noteId}`; + } + return '#'; }, markdownPreviewPath() { return this.getNoteableDataByProp('preview_note_path'); @@ -168,8 +171,8 @@ export default { id="note_note" ref="textarea" slot="textarea" - :data-supports-quick-actions="!isEditing" v-model="updatedNoteBody" + :data-supports-quick-actions="!isEditing" name="note[note]" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" @@ -185,7 +188,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" <button :disabled="isDisabled" type="button" - class="js-vue-issue-save btn btn-save js-comment-button " + class="js-vue-issue-save btn btn-success js-comment-button " @click="handleUpdate()"> {{ saveButtonTitle }} </button> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a621418cf72..7b6e7b72caf 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -9,11 +9,13 @@ export default { props: { author: { type: Object, - required: true, + required: false, + default: () => ({}), }, createdAt: { type: String, - required: true, + required: false, + default: null, }, actionText: { type: String, @@ -21,8 +23,9 @@ export default { default: '', }, noteId: { - type: Number, - required: true, + type: [String, Number], + required: false, + default: null, }, includeToggle: { type: Boolean, @@ -72,7 +75,10 @@ export default { {{ __('Toggle discussion') }} </button> </div> - <a :href="author.path"> + <a + v-if="Object.keys(author).length" + :href="author.path" + > <span class="note-header-author-name">{{ author.name }}</span> <span v-if="author.status_tooltip_html" @@ -81,6 +87,9 @@ export default { @{{ author.username }} </span> </a> + <span v-else> + {{ __('A deleted user') }} + </span> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> @@ -89,18 +98,22 @@ export default { <span class="system-note-message"> <slot></slot> </span> - <span class="system-note-separator"> - · - </span> - <a - :href="noteTimestampLink" - class="note-timestamp system-note-separator" - @click="updateTargetNoteHash"> - <time-ago-tooltip - :time="createdAt" - tooltip-placement="bottom" - /> - </a> + <template + v-if="createdAt" + > + <span class="system-note-separator"> + · + </span> + <a + :href="noteTimestampLink" + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash"> + <time-ago-tooltip + :time="createdAt" + tooltip-placement="bottom" + /> + </a> + </template> <i class="fa fa-spinner fa-spin editing-spinner" aria-label="Comment is being updated" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 0fe1c16854a..e9218723149 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -137,8 +137,10 @@ export default { return this.unresolvedDiscussions.length > 1; }, showJumpToNextDiscussion() { - return this.hasMultipleUnresolvedDiscussions && - !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder); + return ( + this.hasMultipleUnresolvedDiscussions && + !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder) + ); }, shouldRenderDiffs() { const { diffDiscussion, diffFile } = this.transformedDiscussion; @@ -189,6 +191,7 @@ export default { if (note.placeholderType === SYSTEM_NOTE) { return placeholderSystemNote; } + return placeholderNote; } @@ -199,7 +202,7 @@ export default { return noteableNote; }, componentData(note) { - return note.isPlaceholderNote ? this.discussion.notes[0] : note; + return note.isPlaceholderNote ? note.notes[0] : note; }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); @@ -256,11 +259,16 @@ Please check your network connection and try again.`; }); }, jumpToNextDiscussion() { - const nextId = - this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder); + const nextId = this.nextUnresolvedDiscussionId( + this.discussion.id, + this.discussionsByDiffOrder, + ); this.jumpToDiscussion(nextId); }, + deleteNoteHandler(note) { + this.$emit('noteDeleted', this.discussion, note); + }, }, }; </script> @@ -270,6 +278,7 @@ Please check your network connection and try again.`; <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link + v-if="author" :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" @@ -340,10 +349,11 @@ Please check your network connection and try again.`; <div class="discussion-notes"> <ul class="notes"> <component - v-for="note in discussion.notes" :is="componentName(note)" - :note="componentData(note)" + v-for="note in discussion.notes" :key="note.id" + :note="componentData(note)" + @handleDeleteNote="deleteNoteHandler" /> </ul> <div diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 4ebeb5599f2..f391ed848a4 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -52,7 +52,7 @@ export default { return this.note.resolvable && !!this.getUserData.id; }, canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== this.getUserData.id; + return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; @@ -81,11 +81,16 @@ export default { ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']), editHandler() { this.isEditing = true; + this.$emit('handleEdit'); }, deleteHandler() { + const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment'; // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to delete this comment?')) { + if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) { this.isDeleting = true; + this.$emit('handleDeleteNote', this.note); + + if (this.note.isDraft) return; this.deleteNote(this.note) .then(() => { @@ -97,7 +102,20 @@ export default { }); } }, + updateSuccess() { + this.isEditing = false; + this.isRequesting = false; + this.oldContent = null; + $(this.$refs.noteBody.$el).renderGFM(); + this.$refs.noteBody.resetAutoSave(); + this.$emit('updateSuccess'); + }, formUpdateHandler(noteText, parentElement, callback) { + this.$emit('handleUpdateNote', { + note: this.note, + noteText, + callback: () => this.updateSuccess(), + }); const data = { endpoint: this.note.path, note: { @@ -112,11 +130,7 @@ export default { this.updateNote(data) .then(() => { - this.isEditing = false; - this.isRequesting = false; - this.oldContent = null; - $(this.$refs.noteBody.$el).renderGFM(); - this.$refs.noteBody.resetAutoSave(); + this.updateSuccess(); callback(); }) .catch(() => { @@ -141,6 +155,7 @@ export default { this.oldContent = null; } this.isEditing = false; + this.$emit('cancelForm'); }, recoverNoteContent(noteText) { // we need to do this to prevent noteForm inconsistent content warning diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 9b8713b40fb..618a1581d8f 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -10,8 +10,8 @@ import systemNote from '../../vue_shared/components/notes/system_note.vue'; import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; +import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; export default { name: 'NotesApp', @@ -20,7 +20,6 @@ export default { noteableDiscussion, systemNote, commentForm, - loadingIcon, placeholderNote, placeholderSystemNote, }, @@ -98,6 +97,9 @@ export default { }); } }, + updated() { + this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'))); + }, methods: { ...mapActions({ fetchDiscussions: 'fetchDiscussions', @@ -138,6 +140,7 @@ export default { .then(() => { this.isLoading = false; this.setNotesFetchedState(true); + eventHub.$emit('fetchedNotesData'); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) @@ -188,10 +191,10 @@ export default { class="notes main-notes-list timeline" > <component - v-for="discussion in allDiscussions" :is="getComponentName(discussion)" - v-bind="getComponentData(discussion)" + v-for="discussion in allDiscussions" :key="discussion.id" + v-bind="getComponentData(discussion)" /> </ul> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 3eefbe11c37..7ab7e5a9abb 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -10,6 +10,7 @@ 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'; +import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; let eTagPoll; @@ -43,18 +44,17 @@ export const fetchDiscussions = ({ commit }, path) => commit(types.SET_INITIAL_DISCUSSIONS, discussions); }); -export const refetchDiscussionById = ({ commit }, { path, discussionId }) => - service - .fetchDiscussions(path) - .then(res => res.json()) - .then(discussions => { - const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId); - if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion); - }); +export const updateDiscussion = ({ commit, state }, discussion) => { + commit(types.UPDATE_DISCUSSION, discussion); + + return utils.findNoteObjectById(state.discussions, discussion.id); +}; -export const deleteNote = ({ commit }, note) => +export const deleteNote = ({ commit, dispatch }, note) => service.deleteNote(note.path).then(() => { commit(types.DELETE_NOTE, note); + + dispatch('updateMergeRequestWidget'); }); export const updateNote = ({ commit }, { endpoint, note }) => @@ -75,20 +75,22 @@ export const replyToDiscussion = ({ commit }, { endpoint, data }) => return res; }); -export const createNewNote = ({ commit }, { endpoint, data }) => +export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => service .createNewNote(endpoint, data) .then(res => res.json()) .then(res => { if (!res.errors) { commit(types.ADD_NEW_NOTE, res); + + dispatch('updateMergeRequestWidget'); } return res; }); export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => +export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) .then(res => res.json()) @@ -96,6 +98,8 @@ export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; commit(mutationType, res); + + dispatch('updateMergeRequestWidget'); }); export const closeIssue = ({ commit, dispatch, state }) => { @@ -146,35 +150,50 @@ export const toggleIssueLocalState = ({ commit }, newState) => { export const saveNote = ({ commit, dispatch }, noteData) => { // For MR discussuions we need to post as `note[note]` and issue we use `note.note`. - const note = noteData.data['note[note]'] || noteData.data.note.note; + // For batch comments, we use draft_note + const note = noteData.data.draft_note || noteData.data['note[note]'] || noteData.data.note.note; let placeholderText = note; const hasQuickActions = utils.hasQuickActions(placeholderText); const replyId = noteData.data.in_reply_to_discussion_id; - const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; + let methodToDispatch; + const postData = Object.assign({}, noteData); + if (postData.isDraft === true) { + methodToDispatch = replyId + ? 'batchComments/addDraftToDiscussion' + : 'batchComments/createNewDraft'; + if (!postData.draft_note && noteData.note) { + postData.draft_note = postData.note; + delete postData.note; + } + } else { + methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; + } - commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders $('.notes-form .flash-container').hide(); // hide previous flash notification + commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders - if (hasQuickActions) { - placeholderText = utils.stripQuickActions(placeholderText); - } + if (replyId) { + if (hasQuickActions) { + placeholderText = utils.stripQuickActions(placeholderText); + } - if (placeholderText.length) { - commit(types.SHOW_PLACEHOLDER_NOTE, { - noteBody: placeholderText, - replyId, - }); - } + 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, - }); + if (hasQuickActions) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + isSystemNote: true, + noteBody: utils.getQuickActionText(note), + replyId, + }); + } } - return dispatch(methodToDispatch, noteData).then(res => { + return dispatch(methodToDispatch, postData, { root: true }).then(res => { const { errors } = res; const commandsChanges = res.commands_changes; @@ -211,7 +230,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => { if (errors && errors.commands_only) { Flash(errors.commands_only, 'notice', noteData.flashContainer); } - commit(types.REMOVE_PLACEHOLDER_NOTES); + if (replyId) { + commit(types.REMOVE_PLACEHOLDER_NOTES); + } return res; }); @@ -320,5 +341,9 @@ export const fetchDiscussionDiffLines = ({ commit }, discussion) => }); }); +export const updateMergeRequestWidget = () => { + mrWidgetEventHub.$emit('mr.discussion.updated'); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5b3b9f8776f..a829149a17e 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import * as constants from '../constants'; +import { reduceDiscussionsToLineCodes } from './utils'; import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); @@ -28,17 +29,8 @@ export const notesById = state => return acc; }, {}); -export const discussionsByLineCode = state => - state.discussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code && note.resolvable) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); +export const discussionsStructuredByLineCode = state => + reduceDiscussionsToLineCodes(state.discussions); export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; @@ -82,6 +74,9 @@ export const allDiscussions = (state, getters) => { return Object.values(resolved).concat(unresolved); }; +export const isDiscussionResolved = (state, getters) => discussionId => + getters.resolvedDiscussionsById[discussionId] !== undefined; + export const allResolvableDiscussions = (state, getters) => getters.allDiscussions.filter(d => !d.individual_note && d.resolvable); @@ -134,8 +129,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); // Get the line numbers, to compare within the same file - const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; - const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; + const aLines = [a.position.new_line, a.position.old_line]; + const bLines = [b.position.new_line, b.position.old_line]; return filenameComparison < 0 || (filenameComparison === 0 && diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 0f48b8880f4..f105b7d0d11 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -1,16 +1,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import module from './modules'; +import notesModule from './modules'; Vue.use(Vuex); export default () => - new Vuex.Store({ - state: module.state, - actions, - getters, - mutations, - }); + new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index b4cb9267e0f..61dbb075586 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -2,7 +2,7 @@ import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; -export default { +export default () => ({ state: { discussions: [], targetNoteHash: null, @@ -24,4 +24,4 @@ export default { actions, getters, mutations, -}; +}); diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index ab6a95e2601..73e55705f39 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -4,7 +4,8 @@ import * as constants from '../constants'; import { isInMRPage } from '../../lib/utils/common_utils'; export default { - [types.ADD_NEW_NOTE](state, note) { + [types.ADD_NEW_NOTE](state, data) { + const note = data.discussion ? data.discussion.notes[0] : data; const { discussion_id, type } = note; const [exists] = state.discussions.filter(n => n.id === note.discussion_id); const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE; @@ -54,13 +55,12 @@ export default { [types.EXPAND_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - - discussion.expanded = true; + Object.assign(discussion, { expanded: true }); }, [types.COLLAPSE_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - discussion.expanded = false; + Object.assign(discussion, { expanded: false }); }, [types.REMOVE_PLACEHOLDER_NOTES](state) { @@ -95,10 +95,18 @@ export default { [types.SET_USER_DATA](state, data) { Object.assign(state, { userData: data }); }, + [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) { const discussions = []; discussionsData.forEach(discussion => { + if (discussion.diff_file) { + Object.assign(discussion, { + fileHash: discussion.diff_file.file_hash, + truncated_diff_lines: discussion.truncated_diff_lines || [], + }); + } + // To support legacy notes, should be very rare case. if (discussion.individual_note && discussion.notes.length > 1) { discussion.notes.forEach(n => { @@ -168,8 +176,7 @@ export default { [types.TOGGLE_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - - discussion.expanded = !discussion.expanded; + Object.assign(discussion, { expanded: !discussion.expanded }); }, [types.UPDATE_NOTE](state, note) { @@ -185,16 +192,12 @@ export default { [types.UPDATE_DISCUSSION](state, noteData) { const note = noteData; - let index = 0; - - state.discussions.forEach((n, i) => { - if (n.id === note.id) { - index = i; - } - }); - + const selectedDiscussion = state.discussions.find(disc => disc.id === note.id); note.expanded = true; // override expand flag to prevent collapse - state.discussions.splice(index, 1, note); + if (note.diff_file) { + Object.assign(note, { fileHash: note.diff_file.file_hash }); + } + Object.assign(selectedDiscussion, { ...note }); }, [types.CLOSE_ISSUE](state) { @@ -215,12 +218,7 @@ export default { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - const index = state.discussions.indexOf(discussion); - - const discussionWithDiffLines = Object.assign({}, discussion, { - truncated_diff_lines: diffLines, - }); - state.discussions.splice(index, 1, discussionWithDiffLines); + discussion.truncated_diff_lines = diffLines; }, }; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index a0e096ebfaf..0e41ff03d67 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -2,13 +2,11 @@ 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 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 quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const executedCommands = quickActions.filter(command => { const commandRegex = new RegExp(`/${command.name}`); @@ -27,7 +25,18 @@ export const getQuickActionText = note => { return text; }; +export const reduceDiscussionsToLineCodes = selectedDiscussions => + selectedDiscussions.reduce((acc, note) => { + if (note.diff_discussion && note.line_code) { + // For context about line notes: there might be multiple notes with the same line code + const items = acc[note.line_code] || []; + items.push(note); + + Object.assign(acc, { [note.line_code]: items }); + } + return acc; + }, {}); + export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); -export const stripQuickActions = note => - note.replace(REGEX_QUICK_ACTIONS, '').trim(); +export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); |