diff options
author | Natalia Tepluhina <ntepluhina@gitlab.com> | 2019-07-03 08:26:57 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2019-07-03 08:26:57 +0000 |
commit | d4151b14c2986db173a7a1a4d293b86bfcdaae3a (patch) | |
tree | f3bb3b6829671ddeb17dd4f373b3e358187df7b8 | |
parent | fd547ee4c618ab8d93ba07121e7bc6bf2924a1b3 (diff) | |
download | gitlab-ce-d4151b14c2986db173a7a1a4d293b86bfcdaae3a.tar.gz |
Rebased and squashed commits
- all commits squashed to make danger review happy
34 files changed, 1162 insertions, 288 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue new file mode 100644 index 00000000000..2aa5e9b3339 --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue @@ -0,0 +1,55 @@ +<script> +import { mapGetters } from 'vuex'; +import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +export default { + name: 'DiffDiscussionReply', + components: { + NoteSignedOutWidget, + ReplyPlaceholder, + UserAvatarLink, + }, + props: { + hasForm: { + type: Boolean, + required: false, + default: false, + }, + renderReplyPlaceholder: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters({ + currentUser: 'getUserData', + userCanReply: 'userCanReply', + }), + }, +}; +</script> + +<template> + <div class="discussion-reply-holder d-flex clearfix"> + <template v-if="userCanReply"> + <slot v-if="hasForm" name="form"></slot> + <template v-else-if="renderReplyPlaceholder"> + <user-avatar-link + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" + /> + <reply-placeholder + class="qa-discussion-reply" + :button-text="__('Start a new discussion...')" + @onClick="$emit('showNewDiscussionForm')" + /> + </template> + </template> + <note-signed-out-widget v-else /> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index 4c73eea4049..b0460bacff2 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -80,7 +80,6 @@ export default { v-show="isExpanded(discussion)" :discussion="discussion" :render-diff-file="false" - :always-expanded="true" :discussions-by-diff-order="true" :line="line" :help-page-path="helpPagePath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index eb9f1465945..4b226e30699 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -151,7 +151,11 @@ export default { stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false); }, methods: { - ...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']), + ...mapActions('diffs', [ + 'toggleFileDiscussions', + 'toggleFileDiscussionWrappers', + 'toggleFullDiff', + ]), handleToggleFile(e, checkTarget) { if ( !checkTarget || @@ -165,7 +169,7 @@ export default { this.$emit('showForkMessage'); }, handleToggleDiscussions() { - this.toggleFileDiscussions(this.diffFile); + this.toggleFileDiscussionWrappers(this.diffFile); }, handleFileNameClick(e) { const isLinkToOtherPage = diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index e28909b7be3..af5550aec3b 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,5 +1,4 @@ <script> -import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize, truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; @@ -19,11 +18,13 @@ export default { type: Array, required: true, }, + discussionsExpanded: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - discussionsExpanded() { - return this.discussions.every(discussion => discussion.expanded); - }, allDiscussions() { return this.discussions.reduce((acc, note) => acc.concat(note.notes), []); }, @@ -45,26 +46,14 @@ export default { }, }, methods: { - ...mapActions(['toggleDiscussion']), getTooltipText(noteData) { let { note } = noteData; - if (note.length > LENGTH_OF_AVATAR_TOOLTIP) { note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP); } return `${noteData.author.name}: ${note}`; }, - toggleDiscussions() { - const forceExpanded = this.discussions.some(discussion => !discussion.expanded); - - this.discussions.forEach(discussion => { - this.toggleDiscussion({ - discussionId: discussion.id, - forceExpanded, - }); - }); - }, }, }; </script> @@ -76,7 +65,7 @@ export default { type="button" :aria-label="__('Show comments')" class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button" - @click="toggleDiscussions" + @click="$emit('toggleLineDiscussions')" > <icon :size="12" name="collapse" /> </button> @@ -87,7 +76,7 @@ export default { :img-src="note.author.avatar_url" :tooltip-text="getTooltipText(note)" class="diff-comment-avatar js-diff-comment-avatar" - @click.native="toggleDiscussions" + @click.native="$emit('toggleLineDiscussions')" /> <span v-if="moreText" @@ -97,7 +86,7 @@ export default { data-container="body" data-placement="top" role="button" - @click="toggleDiscussions" + @click="$emit('toggleLineDiscussions')" >+{{ moreCount }}</span > </template> diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 1281f9b17ef..351110f0a87 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -105,7 +105,13 @@ export default { }, }, methods: { - ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), + ...mapActions('diffs', [ + 'loadMoreLines', + 'showCommentForm', + 'setHighlightedRow', + 'toggleLineDiscussions', + 'toggleLineDiscussionWrappers', + ]), handleCommentButton() { this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); }, @@ -184,7 +190,14 @@ export default { @click="setHighlightedRow(lineCode)" > </a> - <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> + <diff-gutter-avatars + v-if="shouldShowAvatarsOnGutter" + :discussions="line.discussions" + :discussions-expanded="line.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) + " + /> </template> </div> </template> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index 1faa0493e79..ca3285e9afd 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -1,11 +1,14 @@ <script> -import diffDiscussions from './diff_discussions.vue'; -import diffLineNoteForm from './diff_line_note_form.vue'; +import { mapActions } from 'vuex'; +import DiffDiscussions from './diff_discussions.vue'; +import DiffLineNoteForm from './diff_line_note_form.vue'; +import DiffDiscussionReply from './diff_discussion_reply.vue'; export default { components: { - diffDiscussions, - diffLineNoteForm, + DiffDiscussions, + DiffLineNoteForm, + DiffDiscussionReply, }, props: { line: { @@ -32,10 +35,12 @@ export default { if (!this.line.discussions || !this.line.discussions.length) { return false; } - - return this.line.discussions.every(discussion => discussion.expanded); + return this.line.discussionsExpanded; }, }, + methods: { + ...mapActions('diffs', ['showCommentForm']), + }, }; </script> @@ -49,13 +54,22 @@ export default { :discussions="line.discussions" :help-page-path="helpPagePath" /> - <diff-line-note-form - v-if="line.hasForm" - :diff-file-hash="diffFileHash" - :line="line" - :note-target-line="line" - :help-page-path="helpPagePath" - /> + <diff-discussion-reply + :has-form="line.hasForm" + :render-reply-placeholder="Boolean(line.discussions.length)" + @showNewDiscussionForm=" + showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash }) + " + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line" + :note-target-line="line" + :help-page-path="helpPagePath" + /> + </template> + </diff-discussion-reply> </div> </td> </tr> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index d2e54edca85..c00b0e010ff 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -1,11 +1,14 @@ <script> -import diffDiscussions from './diff_discussions.vue'; -import diffLineNoteForm from './diff_line_note_form.vue'; +import { mapActions } from 'vuex'; +import DiffDiscussions from './diff_discussions.vue'; +import DiffLineNoteForm from './diff_line_note_form.vue'; +import DiffDiscussionReply from './diff_discussion_reply.vue'; export default { components: { - diffDiscussions, - diffLineNoteForm, + DiffDiscussions, + DiffLineNoteForm, + DiffDiscussionReply, }, props: { line: { @@ -29,24 +32,30 @@ export default { computed: { hasExpandedDiscussionOnLeft() { return this.line.left && this.line.left.discussions.length - ? this.line.left.discussions.every(discussion => discussion.expanded) + ? this.line.left.discussionsExpanded : false; }, hasExpandedDiscussionOnRight() { return this.line.right && this.line.right.discussions.length - ? this.line.right.discussions.every(discussion => discussion.expanded) + ? this.line.right.discussionsExpanded : false; }, hasAnyExpandedDiscussion() { return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; }, shouldRenderDiscussionsOnLeft() { - return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft; + return ( + this.line.left && + this.line.left.discussions && + this.line.left.discussions.length && + this.hasExpandedDiscussionOnLeft + ); }, shouldRenderDiscussionsOnRight() { return ( this.line.right && this.line.right.discussions && + this.line.right.discussions.length && this.hasExpandedDiscussionOnRight && this.line.right.type ); @@ -81,6 +90,22 @@ export default { return hasCommentFormOnLeft || hasCommentFormOnRight; }, + shouldRenderReplyPlaceholderOnLeft() { + return Boolean( + this.line.left && this.line.left.discussions && this.line.left.discussions.length, + ); + }, + shouldRenderReplyPlaceholderOnRight() { + return Boolean( + this.line.right && this.line.right.discussions && this.line.right.discussions.length, + ); + }, + }, + methods: { + ...mapActions('diffs', ['showCommentForm']), + showNewDiscussionForm() { + this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash }); + }, }, }; </script> @@ -90,37 +115,49 @@ export default { <td class="notes-content parallel old" colspan="2"> <div v-if="shouldRenderDiscussionsOnLeft" class="content"> <diff-discussions - v-if="line.left.discussions.length" :discussions="line.left.discussions" :line="line.left" :help-page-path="helpPagePath" /> </div> - <diff-line-note-form - v-if="showLeftSideCommentForm" - :diff-file-hash="diffFileHash" - :line="line.left" - :note-target-line="line.left" - :help-page-path="helpPagePath" - line-position="left" - /> + <diff-discussion-reply + :has-form="showLeftSideCommentForm" + :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft" + @showNewDiscussionForm="showNewDiscussionForm" + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line.left" + :note-target-line="line.left" + :help-page-path="helpPagePath" + line-position="left" + /> + </template> + </diff-discussion-reply> </td> <td class="notes-content parallel new" colspan="2"> <div v-if="shouldRenderDiscussionsOnRight" class="content"> <diff-discussions - v-if="line.right.discussions.length" :discussions="line.right.discussions" :line="line.right" :help-page-path="helpPagePath" /> </div> - <diff-line-note-form - v-if="showRightSideCommentForm" - :diff-file-hash="diffFileHash" - :line="line.right" - :note-target-line="line.right" - line-position="right" - /> + <diff-discussion-reply + :has-form="showRightSideCommentForm" + :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight" + @showNewDiscussionForm="showNewDiscussionForm" + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line.right" + :note-target-line="line.right" + line-position="right" + /> + </template> + </diff-discussion-reply> </td> </tr> </template> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 79ce49012f0..32e0d8f42ee 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -12,6 +12,7 @@ import { getNoteFormData, convertExpandLines, idleCallback, + allDiscussionWrappersExpanded, } from './utils'; import * as types from './mutation_types'; import { @@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = ( discussions = rootState.notes.discussions, ) => { const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); + const hash = getLocationHash(); discussions .filter(discussion => discussion.diff_discussion) @@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = ( commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { discussion, diffPositionByLineCode, + hash, }); }); @@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id }); }; +export const toggleLineDiscussions = ({ commit }, options) => { + commit(types.TOGGLE_LINE_DISCUSSIONS, options); +}; + export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { const discussion = rootState.notes.discussions.find(d => d.id === discussionId); @@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { }); }; +export const toggleFileDiscussionWrappers = ({ commit }, diff) => { + const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff); + let linesWithDiscussions; + if (diff.highlighted_diff_lines) { + linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length); + } + if (diff.parallel_diff_lines) { + linesWithDiscussions = diff.parallel_diff_lines.filter( + line => + (line.left && line.left.discussions.length) || + (line.right && line.right.discussions.length), + ); + } + + if (linesWithDiscussions.length) { + linesWithDiscussions.forEach(line => { + commit(types.TOGGLE_LINE_DISCUSSIONS, { + fileHash: diff.file_hash, + lineCode: line.line_code, + expanded: !discussionWrappersExpanded, + }); + }); + } +}; + export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { const postData = getNoteFormData({ commit: state.commit, diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 8d6111da500..9db56331faa 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER'; + +export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 00181a63c43..a66f205bbbd 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -6,6 +6,7 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, + updateLineInFile, } from './utils'; import * as types from './mutation_types'; @@ -109,7 +110,7 @@ export default { })); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) { const { latestDiff } = state; const discussionLineCode = discussion.line_code; @@ -130,13 +131,27 @@ export default { : [], }); + const setDiscussionsExpanded = line => { + const isLineNoteTargeted = line.discussions.some( + disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`), + ); + + return { + ...line, + discussionsExpanded: + line.discussions && line.discussions.length + ? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted + : false, + }; + }; + state.diffFiles = state.diffFiles.map(diffFile => { if (diffFile.file_hash === fileHash) { const file = { ...diffFile }; if (file.highlighted_diff_lines) { file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => - lineCheck(line) ? mapDiscussions(line) : line, + setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line), ); } @@ -148,8 +163,10 @@ export default { if (left || right) { return { ...line, - left: line.left ? mapDiscussions(line.left) : null, - right: line.right ? mapDiscussions(line.right, () => !left) : null, + left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null, + right: line.right + ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left)) + : null, }; } @@ -173,32 +190,11 @@ export default { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); if (selectedFile) { - if (selectedFile.parallel_diff_lines) { - const targetLine = selectedFile.parallel_diff_lines.find( - line => - (line.left && line.left.line_code === lineCode) || - (line.right && line.right.line_code === lineCode), - ); - if (targetLine) { - const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right'; - - Object.assign(targetLine[side], { - discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length), - }); - } - } - - if (selectedFile.highlighted_diff_lines) { - const targetInlineLine = selectedFile.highlighted_diff_lines.find( - line => line.line_code === lineCode, - ); - - if (targetInlineLine) { - Object.assign(targetInlineLine, { - discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length), - }); - } - } + updateLineInFile(selectedFile, lineCode, line => + Object.assign(line, { + discussions: line.discussions.filter(discussion => discussion.notes.length), + }), + ); if (selectedFile.discussions && selectedFile.discussions.length) { selectedFile.discussions = selectedFile.discussions.filter( @@ -207,6 +203,15 @@ export default { } } }, + + [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) { + const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); + + updateLineInFile(selectedFile, lineCode, line => + Object.assign(line, { discussionsExpanded: expanded }), + ); + }, + [types.TOGGLE_FOLDER_OPEN](state, path) { state.treeEntries[path].opened = !state.treeEntries[path].opened; }, diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 71956255eef..1c3ed84001c 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -454,3 +454,48 @@ export const convertExpandLines = ({ }; export const idleCallback = cb => requestIdleCallback(cb); + +export const updateLineInFile = (selectedFile, lineCode, updateFn) => { + if (selectedFile.parallel_diff_lines) { + const targetLine = selectedFile.parallel_diff_lines.find( + line => + (line.left && line.left.line_code === lineCode) || + (line.right && line.right.line_code === lineCode), + ); + if (targetLine) { + const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right'; + + updateFn(targetLine[side]); + } + } + if (selectedFile.highlighted_diff_lines) { + const targetInlineLine = selectedFile.highlighted_diff_lines.find( + line => line.line_code === lineCode, + ); + + if (targetInlineLine) { + updateFn(targetInlineLine); + } + } +}; + +export const allDiscussionWrappersExpanded = diff => { + const discussionsExpandedArray = []; + if (diff.parallel_diff_lines) { + diff.parallel_diff_lines.forEach(line => { + if (line.left && line.left.discussions.length) { + discussionsExpandedArray.push(line.left.discussionsExpanded); + } + if (line.right && line.right.discussions.length) { + discussionsExpandedArray.push(line.right.discussionsExpanded); + } + }); + } else if (diff.highlighted_diff_lines) { + diff.parallel_diff_lines.forEach(line => { + if (line.discussions.length) { + discussionsExpandedArray.push(line.discussionsExpanded); + } + }); + } + return discussionsExpandedArray.every(el => el); +}; diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 1357a5268d6..f4570c1292c 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -39,15 +39,23 @@ export default { </script> <template> - <div class="discussion-with-resolve-btn clearfix"> - <reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" /> - - <div class="btn-group discussion-actions" role="group"> - <resolve-discussion-button - v-if="discussion.resolvable" - :is-resolving="isResolving" - :button-title="resolveButtonTitle" - @onClick="$emit('resolve')" + <div class="discussion-with-resolve-btn"> + <reply-placeholder + :button-text="s__('MergeRequests|Reply...')" + class="qa-discussion-reply" + @onClick="$emit('showReplyForm')" + /> + <resolve-discussion-button + v-if="discussion.resolvable" + :is-resolving="isResolving" + :button-title="resolveButtonTitle" + @onClick="$emit('resolve')" + /> + <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group"> + <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" /> + <jump-to-next-discussion-button + v-if="shouldShowJumpToNextDiscussion" + @onClick="$emit('jumpToNextDiscussion')" /> <resolve-with-issue-button v-if="discussion.resolvable && resolveWithIssuePath" diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 30971ad5227..2ff0fee62f3 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -1,11 +1,11 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapActions } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import { __ } from '~/locale'; -import NoteableNote from './noteable_note.vue'; -import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; -import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; +import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; import SystemNote from '~/vue_shared/components/notes/system_note.vue'; +import NoteableNote from './noteable_note.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; import NoteEditedText from './note_edited_text.vue'; @@ -72,6 +72,7 @@ export default { }, }, methods: { + ...mapActions(['toggleDiscussion']), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -101,7 +102,7 @@ export default { <component :is="componentName(firstNote)" :note="componentData(firstNote)" - :line="line" + :line="line || diffLine" :commit="commit" :help-page-path="helpPagePath" :show-reply-button="userCanReply" @@ -118,23 +119,29 @@ export default { /> <slot slot="avatar-badge" name="avatar-badge"></slot> </component> - <toggle-replies-widget - v-if="hasReplies" - :collapsed="!isExpanded" - :replies="replies" - @toggle="$emit('toggleDiscussion')" - /> - <template v-if="isExpanded"> - <component - :is="componentName(note)" - v-for="note in replies" - :key="note.id" - :note="componentData(note)" - :help-page-path="helpPagePath" - :line="line" - @handleDeleteNote="$emit('deleteNote')" + <div + :class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''" + > + <toggle-replies-widget + v-if="hasReplies" + :collapsed="!isExpanded" + :replies="replies" + :class="{ 'discussion-toggle-replies': discussion.diff_discussion }" + @toggle="toggleDiscussion({ discussionId: discussion.id })" /> - </template> + <template v-if="isExpanded"> + <component + :is="componentName(note)" + v-for="note in replies" + :key="note.id" + :note="componentData(note)" + :help-page-path="helpPagePath" + :line="line" + @handleDeleteNote="$emit('deleteNote')" + /> + </template> + <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> + </div> </template> <template v-else> <component @@ -148,8 +155,8 @@ export default { > <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> </component> + <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> </template> </ul> - <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue index ea590905e3c..0204169214b 100644 --- a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue +++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue @@ -1,6 +1,12 @@ <script> export default { name: 'ReplyPlaceholder', + props: { + buttonText: { + type: String, + required: true, + }, + }, }; </script> @@ -12,6 +18,6 @@ export default { :title="s__('MergeRequests|Add a reply')" @click="$emit('onClick')" > - {{ s__('MergeRequests|Reply...') }} + {{ buttonText }} </button> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b8eaff32cce..f6b5fffde29 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -132,7 +132,7 @@ export default { return this.discussion.diff_discussion && this.renderDiffFile; }, shouldGroupReplies() { - return !this.shouldRenderDiffs && !this.discussion.diff_discussion; + return !this.shouldRenderDiffs; }, wrapperComponent() { return this.shouldRenderDiffs ? diffWithNote : 'div'; @@ -250,6 +250,11 @@ export default { clearDraft(this.autosaveKey); }, saveReply(noteText, form, callback) { + if (!noteText) { + this.cancelReplyForm(); + callback(); + return; + } const postData = { in_reply_to_discussion_id: this.discussion.reply_id, target_type: this.getNoteableData.targetType, @@ -363,7 +368,6 @@ Please check your network connection and try again.`; :line="line" :should-group-replies="shouldGroupReplies" @startReplying="showReplyForm" - @toggleDiscussion="toggleDiscussionHandler" @deleteNote="deleteNoteHandler" > <slot slot="avatar-badge" name="avatar-badge"></slot> @@ -376,7 +380,7 @@ Please check your network connection and try again.`; <div v-else-if="showReplies" :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder" + class="discussion-reply-holder clearfix" > <user-avatar-link v-if="!isReplying && userCanReply" diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index baed26a157c..af02b8969ee 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -39,7 +39,7 @@ export default { </script> <template> - <timeline-entry-item class="note being-posted fade-in-half"> + <timeline-entry-item class="note note-wrapper being-posted fade-in-half"> <div class="timeline-icon"> <user-avatar-link :link-href="getUserData.path" diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d2d35d91e0b..623c44e062f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1093,6 +1093,17 @@ table.code { line-height: 0; } +.discussion-collapsible { + margin: 0 $gl-padding $gl-padding 71px; +} + +.parallel { + .discussion-collapsible { + margin: $gl-padding; + margin-top: 0; + } +} + @media (max-width: map-get($grid-breakpoints, md)-1) { .diffs .files { @include fixed-width-container; @@ -1110,6 +1121,11 @@ table.code { padding-right: 0; } } + + .discussion-collapsible { + margin: $gl-padding; + margin-top: 0; + } } .image-diff-overlay, diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e880b941d67..7bd1a4138e4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -134,6 +134,16 @@ $note-form-margin-left: 72px; } } + .discussion-toggle-replies { + border-top: 0; + border-radius: 4px 4px 0 0; + + &.collapsed { + border: 0; + border-radius: 4px; + } + } + .note-created-ago, .note-updated-at { white-space: normal; @@ -462,6 +472,14 @@ $note-form-margin-left: 72px; position: relative; } + .notes-content .discussion-notes.diff-discussions { + border-bottom: 1px solid $border-color; + + &:nth-last-child(1) { + border-bottom: 0; + } + } + .notes_holder { font-family: $regular-font; @@ -517,6 +535,17 @@ $note-form-margin-left: 72px; .discussion-reply-holder { border-radius: 0 0 $border-radius-default $border-radius-default; position: relative; + + .discussion-form { + width: 100%; + background-color: $gray-light; + padding: 0; + } + + .disabled-comment { + padding: $gl-vert-padding 0; + width: 100%; + } } } diff --git a/changelogs/unreleased/32452-multiple-discussions.yml b/changelogs/unreleased/32452-multiple-discussions.yml new file mode 100644 index 00000000000..5552340ee66 --- /dev/null +++ b/changelogs/unreleased/32452-multiple-discussions.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Multiple discussions per line in merge request diffs +merge_request: 28748 +author: +type: added diff --git a/changelogs/unreleased/62124-new-threaded-discussion-design.yml b/changelogs/unreleased/62124-new-threaded-discussion-design.yml new file mode 100644 index 00000000000..6614e05be74 --- /dev/null +++ b/changelogs/unreleased/62124-new-threaded-discussion-design.yml @@ -0,0 +1,5 @@ +--- +title: Implement borderless discussion design with new reply field +merge_request: 28580 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c32ab52e41c..1a1fda4adef 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9827,6 +9827,9 @@ msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start a new discussion..." +msgstr "" + msgid "Start a new merge request" msgstr "" diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb index b20730bdb22..d56568a7a9d 100644 --- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb @@ -54,7 +54,7 @@ describe 'Resolve an open discussion in a merge request by creating an issue', : context 'creating the issue' do before do - find(resolve_discussion_selector).click + find(resolve_discussion_selector, match: :first).click end it 'has a hidden field for the discussion' do diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 260eec7a9ed..f3de4bb596f 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -368,8 +368,8 @@ describe 'Merge request > User resolves diff notes and discussions', :js do all_discussion_replies = page.all('.discussion-reply-holder') expect(all_discussion_replies.count).to eq(2) - expect(all_discussion_replies.first.all('.discussion-next-btn').count).to eq(1) - expect(all_discussion_replies.last.all('.discussion-next-btn').count).to eq(1) + expect(all_discussion_replies.first.all('.discussion-next-btn').count).to eq(2) + expect(all_discussion_replies.last.all('.discussion-next-btn').count).to eq(2) end it 'displays next discussion even if hidden' do diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js new file mode 100644 index 00000000000..28689ab07de --- /dev/null +++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js @@ -0,0 +1,90 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('DiffDiscussionReply', () => { + let wrapper; + let getters; + let store; + + const createComponent = (props = {}, slots = {}) => { + wrapper = shallowMount(DiffDiscussionReply, { + store, + localVue, + sync: false, + propsData: { + ...props, + }, + slots: { + ...slots, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('if user can reply', () => { + beforeEach(() => { + getters = { + userCanReply: () => true, + getUserData: () => ({ + path: 'test-path', + avatar_url: 'avatar_url', + name: 'John Doe', + }), + }; + + store = new Vuex.Store({ + getters, + }); + }); + + it('should render a form if component has form', () => { + createComponent( + { + renderReplyPlaceholder: false, + hasForm: true, + }, + { + form: `<div id="test-form"></div>`, + }, + ); + + expect(wrapper.find('#test-form').exists()).toBe(true); + }); + + it('should render a reply placeholder if there is no form', () => { + createComponent({ + renderReplyPlaceholder: true, + hasForm: false, + }); + + expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true); + }); + }); + + it('renders a signed out widget when user is not logged in', () => { + getters = { + userCanReply: () => false, + getUserData: () => null, + }; + + store = new Vuex.Store({ + getters, + }); + + createComponent({ + renderReplyPlaceholder: false, + hasForm: false, + }); + + expect(wrapper.find(NoteSignedOutWidget).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js new file mode 100644 index 00000000000..48ee5c63f35 --- /dev/null +++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js @@ -0,0 +1,113 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; +import discussionsMockData from '../mock_data/diff_discussions'; + +const localVue = createLocalVue(); +const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + +describe('DiffGutterAvatars', () => { + let wrapper; + + const findCollapseButton = () => wrapper.find('.diff-notes-collapse'); + const findMoreCount = () => wrapper.find('.diff-comments-more-count'); + const findUserAvatars = () => wrapper.findAll('.diff-comment-avatar'); + + const createComponent = (props = {}) => { + wrapper = shallowMount(DiffGutterAvatars, { + localVue, + sync: false, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when expanded', () => { + beforeEach(() => { + createComponent({ + discussions: getDiscussionsMockData(), + discussionsExpanded: true, + }); + }); + + it('renders a collapse button when discussions are expanded', () => { + expect(findCollapseButton().exists()).toBe(true); + }); + + it('should emit toggleDiscussions event on button click', () => { + findCollapseButton().trigger('click'); + + expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + }); + }); + + describe('when collapsed', () => { + beforeEach(() => { + createComponent({ + discussions: getDiscussionsMockData(), + discussionsExpanded: false, + }); + }); + + it('renders user avatars and moreCount text', () => { + expect(findUserAvatars().exists()).toBe(true); + expect(findMoreCount().exists()).toBe(true); + }); + + it('renders correct amount of user avatars', () => { + expect(findUserAvatars().length).toBe(3); + }); + + it('renders correct moreCount number', () => { + expect(findMoreCount().text()).toBe('+2'); + }); + + it('should emit toggleDiscussions event on avatars click', () => { + findUserAvatars() + .at(0) + .trigger('click'); + + expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + }); + + it('should emit toggleDiscussions event on more count text click', () => { + findMoreCount().trigger('click'); + + expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + }); + }); + + it('renders an empty more count string if there are no discussions', () => { + createComponent({ + discussions: [], + discussionsExpanded: false, + }); + + expect(findMoreCount().exists()).toBe(false); + }); + + describe('tooltip text', () => { + beforeEach(() => { + createComponent({ + discussions: getDiscussionsMockData(), + discussionsExpanded: false, + }); + }); + + it('returns original comment if it is shorter than max length', () => { + const note = wrapper.vm.discussions[0].notes[0]; + + expect(wrapper.vm.getTooltipText(note)).toEqual('Administrator: comment 1'); + }); + + it('returns truncated version of comment if it is longer than max length', () => { + const note = wrapper.vm.discussions[0].notes[1]; + + expect(wrapper.vm.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...'); + }); + }); +}); diff --git a/spec/frontend/diffs/mock_data/diff_discussions.js b/spec/frontend/diffs/mock_data/diff_discussions.js new file mode 100644 index 00000000000..711ab543411 --- /dev/null +++ b/spec/frontend/diffs/mock_data/diff_discussions.js @@ -0,0 +1,529 @@ +export default { + id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + reply_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + position: { + old_line: null, + new_line: 2, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + expanded: true, + notes: [ + { + id: '1749', + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-03T21:06:21.521Z', + updated_at: '2018-04-08T08:50:41.762Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 1', + note_html: '<p dir="auto">comment 1</p>', + last_edited_at: '2018-04-08T08:50:41.762Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1749/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1749&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1749', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1749', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: '1753', + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Fatih Acet', + username: 'fatihacet', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/fatihacevt', + }, + created_at: '2018-04-08T08:49:35.804Z', + updated_at: '2018-04-08T08:50:45.915Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 2 is really long one', + note_html: '<p dir="auto">comment 2 is really long one</p>', + last_edited_at: '2018-04-08T08:50:45.915Z', + last_edited_by: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1753/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1753&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1753', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1753', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: '1754', + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:48.294Z', + updated_at: '2018-04-08T08:50:48.294Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 3', + note_html: '<p dir="auto">comment 3</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1754/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1754&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1754', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1754', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: '1755', + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:50.911Z', + updated_at: '2018-04-08T08:50:50.911Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 4', + note_html: '<p dir="auto">comment 4</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1755/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1755&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1755', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1755', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + { + id: '1756', + type: 'DiffNote', + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-04-08T08:50:53.895Z', + updated_at: '2018-04-08T08:50:53.895Z', + system: false, + noteable_id: 51, + noteable_type: 'MergeRequest', + noteable_iid: 20, + human_access: 'Owner', + note: 'comment 5', + note_html: '<p dir="auto">comment 5</p>', + current_user: { + can_edit: true, + can_award_emoji: true, + }, + resolved: false, + resolvable: true, + resolved_by: null, + discussion_id: '6b232e05bea388c6b043ccc243ba505faac04ea8', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-test/notes/1756/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-test%2Fmerge_requests%2F20%23note_1756&user_id=1', + path: '/gitlab-org/gitlab-test/notes/1756', + noteable_note_url: 'http://localhost:3000/gitlab-org/gitlab-test/merge_requests/20#note_1756', + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + }, + ], + individual_note: false, + resolvable: true, + resolved: false, + resolve_path: + '/gitlab-org/gitlab-test/merge_requests/20/discussions/6b232e05bea388c6b043ccc243ba505faac04ea8/resolve', + resolve_with_issue_path: + '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', + diff_file: { + submodule: false, + submodule_link: null, + blob: { + id: '9e10516ca50788acf18c518a231914a21e5f16f7', + path: 'CHANGELOG', + name: 'CHANGELOG', + mode: '100644', + readable_text: true, + icon: 'file-text-o', + }, + blob_path: 'CHANGELOG', + blob_name: 'CHANGELOG', + blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', + file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', + file_path: 'CHANGELOG.rb', + new_file: false, + deleted_file: false, + renamed_file: false, + old_path: 'CHANGELOG', + new_path: 'CHANGELOG', + mode_changed: false, + a_mode: '100644', + b_mode: '100644', + text: true, + added_lines: 2, + removed_lines: 0, + diff_refs: { + base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a', + start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962', + head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + }, + content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', + stored_externally: null, + external_storage: null, + old_path_html: 'CHANGELOG_OLD', + new_path_html: 'CHANGELOG', + is_fully_expanded: true, + context_lines_path: + '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', + highlighted_diff_lines: [ + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + ], + parallel_diff_lines: [ + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1', + type: 'new', + old_line: null, + new_line: 1, + text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }, + }, + { + left: null, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2', + type: 'new', + old_line: null, + new_line: 2, + text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + type: null, + old_line: 1, + new_line: 3, + text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4', + type: null, + old_line: 2, + new_line: 4, + text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + right: { + line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5', + type: null, + old_line: 3, + new_line: 5, + text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', + meta_data: null, + }, + }, + { + left: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + right: { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + rich_text: '', + meta_data: { + old_pos: 3, + new_pos: 5, + }, + }, + }, + ], + viewer: { + name: 'text', + error: null, + }, + }, + diff_discussion: true, + truncated_diff_lines: [ + { + text: 'line', + rich_text: + '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', + can_receive_suggestion: true, + line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1', + type: 'new', + old_line: null, + new_line: 1, + meta_data: null, + }, + ], +}; + +export const imageDiffDiscussions = [ + { + id: '1', + position: { + x: 10, + y: 10, + width: 100, + height: 200, + }, + }, + { + id: '2', + position: { + x: 5, + y: 5, + width: 100, + height: 200, + }, + }, +]; diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 394666403ee..58d367077e8 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -87,7 +87,7 @@ describe('DiscussionNotes', () => { discussion.notes[0], ]; discussion.notes = notesData; - createComponent({ discussion }); + createComponent({ discussion, shouldRenderDiffs: true }); const notes = wrapper.findAll('.notes > li'); expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true); diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index 07a366cf339..e008f4ed093 100644 --- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js @@ -2,13 +2,19 @@ import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vu import { shallowMount, createLocalVue } from '@vue/test-utils'; const localVue = createLocalVue(); +const buttonText = 'Test Button Text'; describe('ReplyPlaceholder', () => { let wrapper; + const findButton = () => wrapper.find({ ref: 'button' }); + beforeEach(() => { wrapper = shallowMount(ReplyPlaceholder, { localVue, + propsData: { + buttonText, + }, }); }); @@ -17,9 +23,7 @@ describe('ReplyPlaceholder', () => { }); it('emits onClick even on button click', () => { - const button = wrapper.find({ ref: 'button' }); - - button.trigger('click'); + findButton().trigger('click'); expect(wrapper.emitted()).toEqual({ onClick: [[]], @@ -27,8 +31,6 @@ describe('ReplyPlaceholder', () => { }); it('should render reply button', () => { - const button = wrapper.find({ ref: 'button' }); - - expect(button.text()).toEqual('Reply...'); + expect(findButton().text()).toEqual(buttonText); }); }); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 596a1ba5ad2..d4280d3ec2c 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -521,7 +521,7 @@ describe('diff_file_header', () => { }); describe('with discussions', () => { - it('dispatches toggleFileDiscussions when user clicks on toggle discussions button', () => { + it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { const propsCopy = Object.assign({}, props); propsCopy.diffFile.submodule = false; propsCopy.diffFile.blob = { @@ -552,11 +552,11 @@ describe('diff_file_header', () => { }), }); - spyOn(vm, 'toggleFileDiscussions'); + spyOn(vm, 'toggleFileDiscussionWrappers'); vm.$el.querySelector('.js-btn-vue-toggle-comments').click(); - expect(vm.toggleFileDiscussions).toHaveBeenCalled(); + expect(vm.toggleFileDiscussionWrappers).toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js deleted file mode 100644 index cdd30919b09..00000000000 --- a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import Vue from 'vue'; -import DiffGutterAvatarsComponent from '~/diffs/components/diff_gutter_avatars.vue'; -import { COUNT_OF_AVATARS_IN_GUTTER } from '~/diffs/constants'; -import store from '~/mr_notes/stores'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import discussionsMockData from '../mock_data/diff_discussions'; - -describe('DiffGutterAvatars', () => { - let component; - const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; - - beforeEach(() => { - component = createComponentWithStore(Vue.extend(DiffGutterAvatarsComponent), store, { - discussions: getDiscussionsMockData(), - }).$mount(); - }); - - describe('computed', () => { - describe('discussionsExpanded', () => { - it('should return true when all discussions are expanded', () => { - expect(component.discussionsExpanded).toEqual(true); - }); - - it('should return false when all discussions are not expanded', () => { - component.discussions[0].expanded = false; - - expect(component.discussionsExpanded).toEqual(false); - }); - }); - - describe('allDiscussions', () => { - it('should return an array of notes', () => { - expect(component.allDiscussions).toEqual([...component.discussions[0].notes]); - }); - }); - - describe('notesInGutter', () => { - it('should return a subset of discussions to show in gutter', () => { - expect(component.notesInGutter.length).toEqual(COUNT_OF_AVATARS_IN_GUTTER); - expect(component.notesInGutter[0]).toEqual({ - note: component.discussions[0].notes[0].note, - author: component.discussions[0].notes[0].author, - }); - }); - }); - - describe('moreCount', () => { - it('should return count of remaining discussions from gutter', () => { - expect(component.moreCount).toEqual(2); - }); - }); - - describe('moreText', () => { - it('should return proper text if moreCount > 0', () => { - expect(component.moreText).toEqual('2 more comments'); - }); - - it('should return empty string if there is no discussion', () => { - component.discussions = []; - - expect(component.moreText).toEqual(''); - }); - }); - }); - - describe('methods', () => { - describe('getTooltipText', () => { - it('should return original comment if it is shorter than max length', () => { - const note = component.discussions[0].notes[0]; - - expect(component.getTooltipText(note)).toEqual('Administrator: comment 1'); - }); - - it('should return truncated version of comment', () => { - const note = component.discussions[0].notes[1]; - - expect(component.getTooltipText(note)).toEqual('Fatih Acet: comment 2 is r...'); - }); - }); - - describe('toggleDiscussions', () => { - it('should toggle all discussions', () => { - expect(component.discussions[0].expanded).toEqual(true); - - component.$store.dispatch('setInitialNotes', getDiscussionsMockData()); - component.discussions = component.$store.state.notes.discussions; - component.toggleDiscussions(); - - expect(component.discussions[0].expanded).toEqual(false); - component.$store.dispatch('setInitialNotes', []); - }); - - it('forces expansion of all discussions', () => { - spyOn(component.$store, 'dispatch'); - - component.discussions[0].expanded = true; - component.discussions.push({ - ...component.discussions[0], - id: '123test', - expanded: false, - }); - - component.toggleDiscussions(); - - expect(component.$store.dispatch.calls.argsFor(0)).toEqual([ - 'toggleDiscussion', - { - discussionId: component.discussions[0].id, - forceExpanded: true, - }, - ]); - - expect(component.$store.dispatch.calls.argsFor(1)).toEqual([ - 'toggleDiscussion', - { - discussionId: component.discussions[1].id, - forceExpanded: true, - }, - ]); - }); - }); - }); - - describe('template', () => { - const buttonSelector = '.js-diff-comment-button'; - const svgSelector = `${buttonSelector} svg`; - const avatarSelector = '.js-diff-comment-avatar'; - const plusCountSelector = '.js-diff-comment-plus'; - - it('should have button to collapse discussions when the discussions expanded', () => { - expect(component.$el.querySelector(buttonSelector)).toBeDefined(); - expect(component.$el.querySelector(svgSelector)).toBeDefined(); - }); - - it('should have user avatars when discussions collapsed', () => { - component.discussions[0].expanded = false; - - Vue.nextTick(() => { - expect(component.$el.querySelector(buttonSelector)).toBeNull(); - expect(component.$el.querySelectorAll(avatarSelector).length).toEqual(4); - expect(component.$el.querySelector(plusCountSelector)).toBeDefined(); - expect(component.$el.querySelector(plusCountSelector).textContent).toEqual('+2'); - }); - }); - }); -}); diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js index 4452106580a..0b3890b68d6 100644 --- a/spec/javascripts/diffs/components/inline_diff_view_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -36,10 +36,11 @@ describe('InlineDiffView', () => { it('should render discussions', done => { const el = component.$el; component.diffLines[1].discussions = getDiscussionsMockData(); + component.diffLines[1].discussionsExpanded = true; Vue.nextTick(() => { expect(el.querySelectorAll('.notes_holder').length).toEqual(1); - expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); + expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(6); expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); component.$store.dispatch('setInitialNotes', []); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index f973728cfe1..f8872a3eb13 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -206,6 +206,7 @@ describe('DiffsStoreActions', () => { position_type: 'text', }, }, + hash: 'diff-content-1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', }, }, ], diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index efa864e7d00..841233d54e4 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -36,6 +36,12 @@ describe('noteable_discussion component', () => { }); it('should render user avatar', () => { + const discussion = { ...discussionMock }; + discussion.diff_file = mockDiffFile; + discussion.diff_discussion = true; + + wrapper.setProps({ discussion, renderDiffFile: true }); + expect(wrapper.find('.user-avatar-link').exists()).toBe(true); }); diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 0f8af2c5d6d..3d008edb0ca 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -176,7 +176,7 @@ shared_examples 'discussion comments' do |resource_name| if resource_name == 'merge request' let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] } - let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] } + let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] } it 'can be replied to after resolving' do click_button "Resolve discussion" |