diff options
Diffstat (limited to 'app/assets/javascripts/notes/components/noteable_discussion.vue')
-rw-r--r-- | app/assets/javascripts/notes/components/noteable_discussion.vue | 435 |
1 files changed, 219 insertions, 216 deletions
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 29740ddf6ae..d9dd08a7a6b 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,9 +1,12 @@ <script> +import _ from 'underscore'; import { mapActions, mapGetters } from 'vuex'; +import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; -import { s__ } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; import icon from '~/vue_shared/components/icon.vue'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -20,14 +23,12 @@ import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; -import tooltip from '../../vue_shared/directives/tooltip'; export default { name: 'NoteableDiscussion', components: { icon, noteableNote, - diffWithNote, userAvatarLink, noteHeader, noteSignedOutWidget, @@ -37,9 +38,10 @@ export default { placeholderNote, placeholderSystemNote, systemNote, + TimelineEntryItem, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, mixins: [autosave, noteable, resolvable, discussionNavigation], props: { @@ -47,6 +49,11 @@ export default { type: Object, required: true, }, + line: { + type: Object, + required: false, + default: null, + }, renderDiffFile: { type: Boolean, required: false, @@ -62,45 +69,32 @@ export default { required: false, default: false, }, + helpPagePath: { + type: String, + required: false, + default: '', + }, }, data() { + const { diff_discussion: isDiffDiscussion, resolved } = this.discussion; + return { isReplying: false, isResolving: false, resolveAsThread: true, - isRepliesToggledByUser: false, + isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved), }; }, computed: { ...mapGetters([ 'getNoteableData', - 'discussionCount', - 'resolvedDiscussionCount', - 'allDiscussions', - 'unresolvedDiscussionsIdsByDiff', - 'unresolvedDiscussionsIdsByDate', - 'unresolvedDiscussions', - 'unresolvedDiscussionsIdsOrdered', 'nextUnresolvedDiscussionId', - 'isLastUnresolvedDiscussion', + 'unresolvedDiscussionsCount', + 'hasUnresolvedDiscussions', + 'showJumpToNextDiscussion', ]), - transformedDiscussion() { - return { - ...this.discussion.notes[0], - truncated_diff_lines: this.discussion.truncated_diff_lines || [], - truncated_diff_lines_path: this.discussion.truncated_diff_lines_path, - diff_file: this.discussion.diff_file, - diff_discussion: this.discussion.diff_discussion, - active: this.discussion.active, - discussion_path: this.discussion.discussion_path, - resolved: this.discussion.resolved, - resolved_by: this.discussion.resolved_by, - resolved_by_push: this.discussion.resolved_by_push, - resolved_at: this.discussion.resolved_at, - }; - }, author() { - return this.transformedDiscussion.author; + return this.initialDiscussion.author; }, canReply() { return this.getNoteableData.current_user.can_create_note; @@ -136,29 +130,19 @@ export default { return null; }, resolvedText() { - return this.transformedDiscussion.resolved_by_push ? 'Automatically resolved' : 'Resolved'; - }, - hasMultipleUnresolvedDiscussions() { - return this.unresolvedDiscussions.length > 1; + return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved'); }, - showJumpToNextDiscussion() { - return ( - this.hasMultipleUnresolvedDiscussions && - !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder) + shouldShowJumpToNextDiscussion() { + return this.showJumpToNextDiscussion( + this.discussion.id, + this.discussionsByDiffOrder ? 'diff' : 'discussion', ); }, shouldRenderDiffs() { - return ( - this.transformedDiscussion.diff_discussion && - this.transformedDiscussion.diff_file && - this.renderDiffFile - ); + return this.discussion.diff_discussion && this.renderDiffFile; }, shouldGroupReplies() { - return !this.shouldRenderDiffs && !this.transformedDiscussion.diff_discussion; - }, - shouldRenderHeader() { - return this.shouldRenderDiffs; + return !this.shouldRenderDiffs && !this.discussion.diff_discussion; }, wrapperComponent() { return this.shouldRenderDiffs ? diffWithNote : 'div'; @@ -170,9 +154,6 @@ export default { return {}; }, - wrapperClass() { - return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; - }, componentClassName() { if (this.shouldRenderDiffs) { if (!this.lastUpdatedAt && !this.discussion.resolved) { @@ -183,28 +164,60 @@ export default { return ''; }, shouldShowDiscussions() { - const isExpanded = this.discussion.expanded; - const { resolved } = this.transformedDiscussion; - const isResolvedNonDiffDiscussion = !this.transformedDiscussion.diff_discussion && resolved; + const { expanded, resolved } = this.discussion; + const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved; - return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; + return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; }, - isRepliesCollapsed() { - const { discussion, isRepliesToggledByUser } = this; - const { resolved, notes } = discussion; - const hasReplies = notes.length > 1; + actionText() { + const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; + const linkEnd = '</a>'; + + let { commit_id: commitId } = this.discussion; + if (commitId) { + commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`; + } - return ( - (!discussion.diff_discussion && resolved && hasReplies && !isRepliesToggledByUser) || false + let text = s__('MergeRequests|started a discussion'); + + if (this.discussion.for_commit) { + text = s__( + 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}', + ); + } else if (this.discussion.diff_discussion) { + if (this.discussion.active) { + text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}'); + } else { + text = s__( + 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}', + ); + } + } + + return sprintf( + text, + { + commitId, + linkStart, + linkEnd, + }, + false, ); }, + diffLine() { + if (this.discussion.diff_discussion && this.discussion.truncated_diff_lines) { + return this.discussion.truncated_diff_lines.slice(-1)[0]; + } + + return this.line; + }, }, watch: { isReplying() { if (this.isReplying) { this.$nextTick(() => { // Pass an extra key to separate reply and note edit forms - this.initAutoSave(this.transformedDiscussion, ['Reply']); + this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']); }); } else { this.disposeAutoSave(); @@ -242,7 +255,7 @@ export default { this.toggleDiscussion({ discussionId: this.discussion.id }); }, toggleReplies() { - this.isRepliesToggledByUser = !this.isRepliesToggledByUser; + this.isRepliesCollapsed = !this.isRepliesCollapsed; }, showReplyForm() { this.isReplying = true; @@ -311,181 +324,171 @@ Please check your network connection and try again.`; </script> <template> - <li class="note note-discussion timeline-entry" :class="componentClassName"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <div - :data-discussion-id="transformedDiscussion.discussion_id" - class="discussion js-discussion-container" - > - <div v-if="shouldRenderHeader" class="discussion-header note-wrapper"> - <div class="timeline-icon"> - <user-avatar-link - v-if="author" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="author.name" - :img-size="40" - /> - </div> - <note-header - :author="author" - :created-at="transformedDiscussion.created_at" - :note-id="transformedDiscussion.id" - :include-toggle="true" - :expanded="discussion.expanded" - @toggleHandler="toggleDiscussionHandler" - > - <template v-if="transformedDiscussion.diff_discussion"> - started a discussion on - <a :href="transformedDiscussion.discussion_path"> - <template v-if="transformedDiscussion.active"> - the diff - </template> - <template v-else> - an old version of the diff - </template> - </a> - </template> - <template v-else-if="discussion.for_commit"> - started a discussion on commit - <a :href="discussion.discussion_path"> {{ truncateSha(discussion.commit_id) }} </a> - </template> - <template v-else> - started a discussion - </template> - </note-header> - <note-edited-text - v-if="transformedDiscussion.resolved" - :edited-at="transformedDiscussion.resolved_at" - :edited-by="transformedDiscussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline" - /> - <note-edited-text - v-else-if="lastUpdatedAt" - :edited-at="lastUpdatedAt" - :edited-by="lastUpdatedBy" - action-text="Last updated" - class-name="discussion-headline-light js-discussion-headline" + <timeline-entry-item class="note note-discussion" :class="componentClassName"> + <div class="timeline-content"> + <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> + <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> + <div v-once class="timeline-icon"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" /> </div> - <div v-if="shouldShowDiscussions" class="discussion-body"> - <component :is="wrapperComponent" v-bind="wrapperComponentProps" :class="wrapperClass"> - <div class="discussion-notes"> - <ul class="notes"> - <template v-if="shouldGroupReplies"> - <component - :is="componentName(initialDiscussion)" - :note="componentData(initialDiscussion)" - @handleDeleteNote="deleteNoteHandler" - > - <slot slot="avatar-badge" name="avatar-badge"> </slot> - </component> - <toggle-replies-widget - v-if="hasReplies" - :collapsed="isRepliesCollapsed" - :replies="replies" - @toggle="toggleReplies" + <note-header + :author="author" + :created-at="initialDiscussion.created_at" + :note-id="initialDiscussion.id" + :include-toggle="true" + :expanded="discussion.expanded" + @toggleHandler="toggleDiscussionHandler" + > + <span v-html="actionText"></span> + </note-header> + <note-edited-text + v-if="discussion.resolved" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" + /> + <note-edited-text + v-else-if="lastUpdatedAt" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" + action-text="Last updated" + class-name="discussion-headline-light js-discussion-headline" + /> + </div> + <div v-if="shouldShowDiscussions" class="discussion-body"> + <component + :is="wrapperComponent" + v-bind="wrapperComponentProps" + class="card discussion-wrapper" + > + <div class="discussion-notes"> + <ul class="notes"> + <template v-if="shouldGroupReplies"> + <component + :is="componentName(initialDiscussion)" + :note="componentData(initialDiscussion)" + :line="line" + :help-page-path="helpPagePath" + @handleDeleteNote="deleteNoteHandler" + > + <note-edited-text + v-if="discussion.resolved" + slot="discussion-resolved-text" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline discussion-resolved-text" /> - <template v-if="!isRepliesCollapsed"> - <component - :is="componentName(note)" - v-for="note in replies" - :key="note.id" - :note="componentData(note)" - @handleDeleteNote="deleteNoteHandler" - /> - </template> - </template> - <template v-else> + <slot slot="avatar-badge" name="avatar-badge"></slot> + </component> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="isRepliesCollapsed" + :replies="replies" + @toggle="toggleReplies" + /> + <template v-if="!isRepliesCollapsed"> <component :is="componentName(note)" - v-for="(note, index) in discussion.notes" + v-for="note in replies" :key="note.id" :note="componentData(note)" + :help-page-path="helpPagePath" + :line="line" @handleDeleteNote="deleteNoteHandler" - > - <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"> </slot> - </component> + /> </template> - </ul> - <div - v-if="!isRepliesCollapsed" - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder" - > - <template v-if="!isReplying && canReply"> - <div class="discussion-with-resolve-btn"> + </template> + <template v-else> + <component + :is="componentName(note)" + v-for="(note, index) in discussion.notes" + :key="note.id" + :note="componentData(note)" + :help-page-path="helpPagePath" + :line="diffLine" + @handleDeleteNote="deleteNoteHandler" + > + <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> + </component> + </template> + </ul> + <div + v-if="!isRepliesCollapsed || !hasReplies" + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder" + > + <template v-if="!isReplying && canReply"> + <div class="discussion-with-resolve-btn"> + <button + type="button" + class="js-vue-discussion-reply btn btn-text-field qa-discussion-reply" + title="Add a reply" + @click="showReplyForm" + > + Reply... + </button> + <div v-if="discussion.resolvable"> <button type="button" - class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply" - title="Add a reply" - @click="showReplyForm" + class="btn btn-default ml-sm-2" + @click="resolveHandler();" > - Reply... + <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> + {{ resolveButtonTitle }} </button> - <div v-if="discussion.resolvable"> + </div> + <div + v-if="discussion.resolvable" + class="btn-group discussion-actions ml-sm-2" + role="group" + > + <div v-if="!discussionResolved" class="btn-group" role="group"> + <a + v-gl-tooltip + :href="discussion.resolve_with_issue_path" + :title="s__('MergeRequests|Resolve this discussion in a new issue')" + class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" + > + <icon name="issue-new" /> + </a> + </div> + <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group"> <button - type="button" - class="btn btn-default mr-sm-2" - @click="resolveHandler();" + v-gl-tooltip + class="btn btn-default discussion-next-btn" + title="Jump to next unresolved discussion" + @click="jumpToNextDiscussion" > - <i - v-if="isResolving" - aria-hidden="true" - class="fa fa-spinner fa-spin" - ></i> - {{ resolveButtonTitle }} + <icon name="comment-next" /> </button> </div> - <div - v-if="discussion.resolvable" - class="btn-group discussion-actions ml-sm-2" - role="group" - > - <div v-if="!discussionResolved" class="btn-group" role="group"> - <a - v-tooltip - :href="discussion.resolve_with_issue_path" - :title="s__('MergeRequests|Resolve this discussion in a new issue')" - class="new-issue-for-discussion btn - btn-default discussion-create-issue-btn" - data-container="body" - > - <icon name="issue-new" /> - </a> - </div> - <div v-if="showJumpToNextDiscussion" class="btn-group" role="group"> - <button - v-tooltip - class="btn btn-default discussion-next-btn" - title="Jump to next unresolved discussion" - data-container="body" - @click="jumpToNextDiscussion" - > - <icon name="comment-next" /> - </button> - </div> - </div> </div> - </template> - <note-form - v-if="isReplying" - ref="noteForm" - :discussion="discussion" - :is-editing="false" - save-button-title="Comment" - @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" - /> - <note-signed-out-widget v-if="!canReply" /> - </div> + </div> + </template> + <note-form + v-if="isReplying" + ref="noteForm" + :discussion="discussion" + :is-editing="false" + :line="diffLine" + save-button-title="Comment" + @handleFormUpdate="saveReply" + @cancelForm="cancelReplyForm" + /> + <note-signed-out-widget v-if="!canReply" /> </div> - </component> - </div> + </div> + </component> </div> </div> </div> - </li> + </timeline-entry-item> </template> |