<script> import { mapActions, mapGetters } from 'vuex'; import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteableNote from './noteable_note.vue'; import noteHeader from './note_header.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteEditedText from './note_edited_text.vue'; import noteForm from './note_form.vue'; import diffWithNote from './diff_with_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import tooltip from '../../vue_shared/directives/tooltip'; import { scrollToElement } from '../../lib/utils/common_utils'; export default { components: { noteableNote, diffWithNote, userAvatarLink, noteHeader, noteSignedOutWidget, noteEditedText, noteForm, placeholderNote, placeholderSystemNote, }, directives: { tooltip, }, mixins: [autosave, noteable, resolvable], props: { note: { type: Object, required: true, }, }, data() { return { isReplying: false, isResolving: false, resolveAsThread: true, }; }, computed: { ...mapGetters([ 'getNoteableData', 'discussionCount', 'resolvedDiscussionCount', 'unresolvedDiscussions', ]), discussion() { return { ...this.note.notes[0], truncatedDiffLines: this.note.truncated_diff_lines, diffFile: this.note.diff_file, diffDiscussion: this.note.diff_discussion, imageDiffHtml: this.note.image_diff_html, }; }, author() { return this.discussion.author; }, canReply() { return this.getNoteableData.current_user.can_create_note; }, newNotePath() { return this.getNoteableData.create_note_path; }, lastUpdatedBy() { const { notes } = this.note; if (notes.length > 1) { return notes[notes.length - 1].author; } return null; }, lastUpdatedAt() { const { notes } = this.note; if (notes.length > 1) { return notes[notes.length - 1].created_at; } return null; }, hasUnresolvedDiscussion() { return this.unresolvedDiscussions.length > 0; }, wrapperComponent() { return this.discussion.diffDiscussion && this.discussion.diffFile ? diffWithNote : 'div'; }, wrapperClass() { return this.isDiffDiscussion ? '' : 'panel panel-default'; }, }, mounted() { if (this.isReplying) { this.initAutoSave(this.discussion.noteable_type); } }, updated() { if (this.isReplying) { if (!this.autosave) { this.initAutoSave(this.discussion.noteable_type); } else { this.setAutoSave(); } } }, created() { this.resolveDiscussionsSvg = resolveDiscussionsSvg; this.nextDiscussionsSvg = nextDiscussionsSvg; }, methods: { ...mapActions([ 'saveNote', 'toggleDiscussion', 'removePlaceholderNotes', 'toggleResolveNote', ]), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { return placeholderSystemNote; } return placeholderNote; } return noteableNote; }, componentData(note) { return note.isPlaceholderNote ? this.note.notes[0] : note; }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.note.id }); }, showReplyForm() { this.isReplying = true; }, cancelReplyForm(shouldConfirm) { if (shouldConfirm && this.$refs.noteForm.isDirty) { const msg = 'Are you sure you want to cancel creating this comment?'; // eslint-disable-next-line no-alert if (!confirm(msg)) { return; } } this.resetAutoSave(); this.isReplying = false; }, saveReply(noteText, form, callback) { const replyData = { endpoint: this.newNotePath, flashContainer: this.$el, data: { in_reply_to_discussion_id: this.note.reply_id, target_type: this.noteableType, target_id: this.discussion.noteable_id, note: { note: noteText }, }, }; this.isReplying = false; this.saveNote(replyData) .then(() => { this.resetAutoSave(); callback(); }) .catch(err => { this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { const msg = `Your comment could not be submitted! Please check your network connection and try again.`; Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); }); }); }, jumpToDiscussion() { const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); const index = unresolvedIds.indexOf(this.note.id); if (index >= 0 && index !== unresolvedIds.length) { const nextId = unresolvedIds[index + 1]; const el = document.querySelector(`[data-discussion-id="${nextId}"]`); if (el) { scrollToElement(el); } } }, }, }; </script> <template> <li :data-discussion-id="note.id" class="note note-discussion timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" /> </div> <div class="timeline-content"> <div class="discussion"> <div class="discussion-header"> <note-header :author="author" :created-at="discussion.created_at" :note-id="discussion.id" :include-toggle="true" :expanded="note.expanded" @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" class="discussion" /> <note-edited-text v-if="lastUpdatedAt" :edited-at="lastUpdatedAt" :edited-by="lastUpdatedBy" action-text="Last updated" class-name="discussion-headline-light js-discussion-headline" /> </div> <div v-if="note.expanded" class="discussion-body"> <component :is="wrapperComponent" :discussion="discussion" :class="wrapperClass" > <div class="discussion-notes"> <ul class="notes"> <component v-for="note in note.notes" :is="componentName(note)" :note="componentData(note)" :key="note.id" /> </ul> <div :class="{ 'is-replying': isReplying }" class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div class="btn-group-justified discussion-with-resolve-btn" role="group"> <div class="btn-group" role="group"> <button @click="showReplyForm" type="button" class="js-vue-discussion-reply btn btn-text-field" title="Add a reply">Reply...</button> </div> <div v-if="note.resolvable" class="btn-group" role="group"> <button @click="resolveHandler()" type="button" class="btn btn-default" > <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin" ></i> {{ resolveButtonTitle }} </button> </div> <div class="btn-group discussion-actions" role="group"> <div v-if="note.resolvable && !discussionResolved" class="btn-group" role="group"> <a :href="note.resolve_with_issue_path" v-tooltip class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" title="Resolve this discussion in a new issue" data-container="body" > <span v-html="resolveDiscussionsSvg"></span> </a> </div> <div v-if="hasUnresolvedDiscussion" class="btn-group" role="group"> <button @click="jumpToDiscussion" v-tooltip class="btn btn-default discussion-next-btn" title="Jump to next unresolved discussion" data-container="body" > <span v-html="nextDiscussionsSvg"></span> </button> </div> </div> </div> </template> <note-form v-if="isReplying" save-button-title="Comment" :note="note" :is-editing="false" @handleFormUpdate="saveReply" @cancelFormEdition="cancelReplyForm" ref="noteForm" /> <note-signed-out-widget v-if="!canReply" /> </div> </div> </component> </div> </div> </div> </div> </li> </template>