<script> import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import draftMixin from 'ee_else_ce/notes/mixins/draft'; import { s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; import noteActions from './note_actions.vue'; import NoteBody from './note_body.vue'; import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; export default { name: 'NoteableNote', components: { userAvatarLink, noteHeader, noteActions, NoteBody, TimelineEntryItem, }, mixins: [noteable, resolvable, draftMixin], props: { note: { type: Object, required: true, }, line: { type: Object, required: false, default: null, }, helpPagePath: { type: String, required: false, default: '', }, commit: { type: Object, required: false, default: () => null, }, showReplyButton: { type: Boolean, required: false, default: false, }, }, data() { return { isEditing: false, isDeleting: false, isRequesting: false, isResolving: false, }; }, computed: { ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']), author() { return this.note.author; }, classNameBindings() { return { [`note-row-${this.note.id}`]: true, 'is-editing': this.isEditing && !this.isRequesting, 'is-requesting being-posted': this.isRequesting, 'disabled-content': this.isDeleting, target: this.isTarget, 'is-editable': this.note.current_user.can_edit, }; }, canReportAsAbuse() { return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; }, isTarget() { return this.targetNoteHash === this.noteAnchorId; }, discussionId() { if (this.discussion) { return this.discussion.id; } return ''; }, actionText() { if (!this.commit) { return ''; } // We need to do this to ensure we have the correct sentence order // when translating this as the sentence order may change from one // language to the next. See: // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771 const { id, url } = this.commit; const commitLink = `<a class="commit-sha monospace" href="${escape(url)}">${truncateSha( id, )}</a>`; return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false); }, }, created() { eventHub.$on('enterEditMode', ({ noteId }) => { if (noteId === this.note.id) { this.isEditing = true; this.scrollToNoteIfNeeded($(this.$el)); } }); }, mounted() { if (this.isTarget) { this.scrollToNoteIfNeeded($(this.$el)); } }, methods: { ...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 ${typeOfComment}?`)) { this.isDeleting = true; this.$emit('handleDeleteNote', this.note); if (this.note.isDraft) return; this.deleteNote(this.note) .then(() => { this.isDeleting = false; }) .catch(() => { Flash('Something went wrong while deleting your note. Please try again.'); this.isDeleting = false; }); } }, 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, resolveDiscussion) { this.$emit('handleUpdateNote', { note: this.note, noteText, resolveDiscussion, callback: () => this.updateSuccess(), }); if (this.isDraft) return; const data = { endpoint: this.note.path, note: { target_type: this.getNoteableData.targetType, target_id: this.note.noteable_id, note: { note: noteText }, }, }; this.isRequesting = true; this.oldContent = this.note.note_html; this.note.note_html = escape(noteText); this.updateNote(data) .then(() => { this.updateSuccess(); callback(); }) .catch(() => { this.isRequesting = false; this.isEditing = true; this.$nextTick(() => { const msg = 'Something went wrong while editing your comment. Please try again.'; Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); }); }); }, formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { this.note.note_html = this.oldContent; this.oldContent = null; } this.isEditing = false; this.$emit('cancelForm'); }, recoverNoteContent(noteText) { // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; const { noteBody } = this.$refs; if (noteBody) { noteBody.note.note = noteText; } }, }, }; </script> <template> <timeline-entry-item :id="noteAnchorId" :class="classNameBindings" :data-award-url="note.toggle_award_path" :data-note-id="note.id" class="note note-wrapper qa-noteable-note-item" > <div v-once class="timeline-icon"> <user-avatar-link :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" > <slot slot="avatar-badge" name="avatar-badge"></slot> </user-avatar-link> </div> <div class="timeline-content"> <div class="note-header"> <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> <slot slot="note-header-info" name="note-header-info"></slot> <span v-if="commit" v-html="actionText"></span> <span v-else class="d-none d-sm-inline">·</span> </note-header> <note-actions :author-id="author.id" :note-id="note.id" :note-url="note.noteable_note_url" :access-level="note.human_access" :show-reply="showReplyButton" :can-edit="note.current_user.can_edit" :can-award-emoji="note.current_user.can_award_emoji" :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :can-resolve="canResolve" :report-abuse-path="note.report_abuse_path" :resolvable="note.resolvable || note.isDraft" :is-resolved="note.resolved || note.resolve_discussion" :is-resolving="isResolving" :resolved-by="note.resolved_by" :is-draft="note.isDraft" :resolve-discussion="note.isDraft && note.resolve_discussion" :discussion-id="discussionId" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" @startReplying="$emit('startReplying')" /> </div> <div class="timeline-discussion-body"> <slot name="discussion-resolved-text"></slot> <note-body ref="noteBody" :note="note" :line="line" :can-edit="note.current_user.can_edit" :is-editing="isEditing" :help-page-path="helpPagePath" @handleFormUpdate="formUpdateHandler" @cancelForm="formCancelHandler" /> </div> </div> </timeline-entry-item> </template>