2018-03-17 18:26:18 +05:30
|
|
|
<script>
|
|
|
|
import { mapActions, mapGetters } from 'vuex';
|
2018-03-27 19:54:05 +05:30
|
|
|
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
|
|
|
|
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
|
2018-03-17 18:26:18 +05:30
|
|
|
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';
|
2018-03-27 19:54:05 +05:30
|
|
|
import diffWithNote from './diff_with_note.vue';
|
2018-03-17 18:26:18 +05:30
|
|
|
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';
|
2018-03-27 19:54:05 +05:30
|
|
|
import noteable from '../mixins/noteable';
|
|
|
|
import resolvable from '../mixins/resolvable';
|
|
|
|
import tooltip from '../../vue_shared/directives/tooltip';
|
|
|
|
import { scrollToElement } from '../../lib/utils/common_utils';
|
2018-03-17 18:26:18 +05:30
|
|
|
|
|
|
|
export default {
|
|
|
|
components: {
|
|
|
|
noteableNote,
|
2018-03-27 19:54:05 +05:30
|
|
|
diffWithNote,
|
2018-03-17 18:26:18 +05:30
|
|
|
userAvatarLink,
|
|
|
|
noteHeader,
|
|
|
|
noteSignedOutWidget,
|
|
|
|
noteEditedText,
|
|
|
|
noteForm,
|
|
|
|
placeholderNote,
|
|
|
|
placeholderSystemNote,
|
|
|
|
},
|
2018-03-27 19:54:05 +05:30
|
|
|
directives: {
|
|
|
|
tooltip,
|
|
|
|
},
|
2018-03-17 18:26:18 +05:30
|
|
|
mixins: [
|
|
|
|
autosave,
|
2018-03-27 19:54:05 +05:30
|
|
|
noteable,
|
|
|
|
resolvable,
|
2018-03-17 18:26:18 +05:30
|
|
|
],
|
|
|
|
props: {
|
|
|
|
note: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
isReplying: false,
|
2018-03-27 19:54:05 +05:30
|
|
|
isResolving: false,
|
|
|
|
resolveAsThread: true,
|
2018-03-17 18:26:18 +05:30
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
...mapGetters([
|
|
|
|
'getNoteableData',
|
2018-03-27 19:54:05 +05:30
|
|
|
'discussionCount',
|
|
|
|
'resolvedDiscussionCount',
|
|
|
|
'unresolvedDiscussions',
|
2018-03-17 18:26:18 +05:30
|
|
|
]),
|
|
|
|
discussion() {
|
2018-03-27 19:54:05 +05:30
|
|
|
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,
|
|
|
|
};
|
2018-03-17 18:26:18 +05:30
|
|
|
},
|
|
|
|
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;
|
|
|
|
},
|
2018-03-27 19:54:05 +05:30
|
|
|
hasUnresolvedDiscussion() {
|
|
|
|
return this.unresolvedDiscussions.length > 0;
|
|
|
|
},
|
|
|
|
wrapperComponent() {
|
|
|
|
return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
|
|
|
|
},
|
|
|
|
wrapperClass() {
|
|
|
|
return this.isDiffDiscussion ? '' : 'panel panel-default';
|
|
|
|
},
|
2018-03-17 18:26:18 +05:30
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
if (this.isReplying) {
|
2018-03-27 19:54:05 +05:30
|
|
|
this.initAutoSave(this.discussion.noteable_type);
|
2018-03-17 18:26:18 +05:30
|
|
|
}
|
|
|
|
},
|
|
|
|
updated() {
|
|
|
|
if (this.isReplying) {
|
|
|
|
if (!this.autosave) {
|
2018-03-27 19:54:05 +05:30
|
|
|
this.initAutoSave(this.discussion.noteable_type);
|
2018-03-17 18:26:18 +05:30
|
|
|
} else {
|
|
|
|
this.setAutoSave();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-03-27 19:54:05 +05:30
|
|
|
created() {
|
|
|
|
this.resolveDiscussionsSvg = resolveDiscussionsSvg;
|
|
|
|
this.nextDiscussionsSvg = nextDiscussionsSvg;
|
|
|
|
},
|
2018-03-17 18:26:18 +05:30
|
|
|
methods: {
|
|
|
|
...mapActions([
|
|
|
|
'saveNote',
|
|
|
|
'toggleDiscussion',
|
|
|
|
'removePlaceholderNotes',
|
2018-03-27 19:54:05 +05:30
|
|
|
'toggleResolveNote',
|
2018-03-17 18:26:18 +05:30
|
|
|
]),
|
|
|
|
componentName(note) {
|
|
|
|
if (note.isPlaceholderNote) {
|
|
|
|
if (note.placeholderType === SYSTEM_NOTE) {
|
|
|
|
return placeholderSystemNote;
|
|
|
|
}
|
|
|
|
return placeholderNote;
|
|
|
|
}
|
|
|
|
|
|
|
|
return noteableNote;
|
|
|
|
},
|
|
|
|
componentData(note) {
|
2018-03-27 19:54:05 +05:30
|
|
|
return note.isPlaceholderNote ? this.note.notes[0] : note;
|
2018-03-17 18:26:18 +05:30
|
|
|
},
|
|
|
|
toggleDiscussionHandler() {
|
|
|
|
this.toggleDiscussion({ discussionId: this.note.id });
|
|
|
|
},
|
|
|
|
showReplyForm() {
|
|
|
|
this.isReplying = true;
|
|
|
|
},
|
|
|
|
cancelReplyForm(shouldConfirm) {
|
|
|
|
if (shouldConfirm && this.$refs.noteForm.isDirty) {
|
|
|
|
// eslint-disable-next-line no-alert
|
|
|
|
if (!confirm('Are you sure you want to cancel creating this comment?')) {
|
|
|
|
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,
|
2018-03-27 19:54:05 +05:30
|
|
|
target_type: this.noteableType,
|
2018-03-17 18:26:18 +05:30
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2018-03-27 19:54:05 +05:30
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-03-17 18:26:18 +05:30
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
2018-03-27 19:54:05 +05:30
|
|
|
<li
|
|
|
|
:data-discussion-id="note.id"
|
|
|
|
class="note note-discussion timeline-entry">
|
2018-03-17 18:26:18 +05:30
|
|
|
<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"
|
2018-03-27 19:54:05 +05:30
|
|
|
:expanded="note.expanded"
|
2018-03-17 18:26:18 +05:30
|
|
|
@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>
|
2018-03-27 19:54:05 +05:30
|
|
|
<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
|
2018-05-01 15:08:00 +05:30
|
|
|
v-if="note.resolvable"
|
2018-03-27 19:54:05 +05:30
|
|
|
class="btn-group discussion-actions"
|
2018-05-01 15:08:00 +05:30
|
|
|
role="group"
|
|
|
|
>
|
2018-03-27 19:54:05 +05:30
|
|
|
<div
|
2018-05-01 15:08:00 +05:30
|
|
|
v-if="!discussionResolved"
|
2018-03-27 19:54:05 +05:30
|
|
|
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>
|
2018-03-17 18:26:18 +05:30
|
|
|
</div>
|
2018-03-27 19:54:05 +05:30
|
|
|
</component>
|
2018-03-17 18:26:18 +05:30
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</li>
|
|
|
|
</template>
|