diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /app/assets/javascripts/notes | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) | |
download | gitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'app/assets/javascripts/notes')
15 files changed, 302 insertions, 93 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 16dcde46262..ac93d3df654 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -17,7 +17,7 @@ import { import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; -import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; +import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; @@ -28,8 +28,7 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { name: 'CommentForm', components: { - issueWarning, - epicWarning: () => import('ee_component/vue_shared/components/epic/epic_warning.vue'), + NoteableWarning, noteSignedOutWidget, discussionLockedWidget, markdownField, @@ -126,9 +125,13 @@ export default { canToggleIssueState() { return ( this.getNoteableData.current_user.can_update && - this.getNoteableData.state !== constants.MERGED + this.getNoteableData.state !== constants.MERGED && + !this.closedAndLocked ); }, + closedAndLocked() { + return !this.isOpen && this.isLocked(this.getNoteableData); + }, endpoint() { return this.getNoteableData.create_note_path; }, @@ -350,14 +353,15 @@ export default { <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> <div class="error-alert"></div> - <issue-warning - v-if="hasWarning(getNoteableData) && isIssueType" + <noteable-warning + v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" - :locked-issue-docs-path="lockedIssueDocsPath" - :confidential-issue-docs-path="confidentialIssueDocsPath" + :noteable-type="noteableType" + :locked-noteable-docs-path="lockedIssueDocsPath" + :confidential-noteable-docs-path="confidentialIssueDocsPath" /> - <epic-warning :is-confidential="isConfidential(getNoteableData)" /> + <markdown-field ref="markdownField" :is-submitting="isSubmitting" @@ -374,20 +378,18 @@ export default { dir="auto" :disabled="isSubmitting" name="note[note]" - class="note-textarea js-vue-comment-form js-note-text -js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files hereā¦')" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()" - > - </textarea> + ></textarea> </markdown-field> <gl-alert v-if="isToggleBlockedIssueWarning" - class="prepend-top-16" + class="gl-mt-5" :title="__('Are you sure you want to close this blocked issue?')" :primary-button-text="__('Yes, close issue')" :secondary-button-text="__('Cancel')" @@ -417,13 +419,11 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" </gl-alert> <div class="note-form-actions"> <div - class="btn-group -append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" + class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <button :disabled="isSubmitButtonDisabled" - class="btn btn-success js-comment-button js-comment-submit-button - qa-comment-button" + class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" :data-track-label="trackingLabel" data-track-event="click_button" @@ -440,7 +440,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" data-toggle="dropdown" :aria-label="__('Open comment type dropdown')" > - <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i> + <i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i> </button> <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> @@ -450,7 +450,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-transparent" @click.prevent="setNoteType('comment')" > - <i aria-hidden="true" class="fa fa-check icon"> </i> + <i aria-hidden="true" class="fa fa-check icon"></i> <div class="description"> <strong>{{ __('Comment') }}</strong> <p> @@ -470,7 +470,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-transparent qa-discussion-option" @click.prevent="setNoteType('discussion')" > - <i aria-hidden="true" class="fa fa-check icon"> </i> + <i aria-hidden="true" class="fa fa-check icon"></i> <div class="description"> <strong>{{ __('Start thread') }}</strong> <p>{{ startDiscussionDescription }}</p> diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 0b136549c14..458da5cf67f 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -74,7 +74,7 @@ export default { }, }, methods: { - ...mapActions(['toggleDiscussion']), + ...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -99,7 +99,11 @@ export default { <template> <div class="discussion-notes"> - <ul class="notes"> + <ul + class="notes" + @mouseenter="setSelectedCommentPositionHover(discussion.position.line_range)" + @mouseleave="setSelectedCommentPositionHover()" + > <template v-if="shouldGroupReplies"> <component :is="componentName(firstNote)" @@ -108,6 +112,7 @@ export default { :commit="commit" :help-page-path="helpPagePath" :show-reply-button="userCanReply" + :discussion-root="true" @handleDeleteNote="$emit('deleteNote')" @startReplying="$emit('startReplying')" > @@ -151,6 +156,7 @@ export default { :note="componentData(note)" :help-page-path="helpPagePath" :line="diffLine" + :discussion-root="index === 0" @handleDeleteNote="$emit('deleteNote')" > <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index 5fba011a153..bb13eb87af7 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_form.vue +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -1,4 +1,5 @@ <script> +import { mapActions } from 'vuex'; import { GlFormSelect, GlSprintf } from '@gitlab/ui'; import { getSymbol, getLineClasses } from './multiline_comment_utils'; @@ -21,19 +22,51 @@ export default { }, data() { return { - commentLineStart: { - lineCode: this.lineRange ? this.lineRange.start_line_code : this.line.line_code, - type: this.lineRange ? this.lineRange.start_line_type : this.line.type, - }, + commentLineStart: {}, + commentLineEndType: this.lineRange?.end?.line_type || this.line.type, }; }, + computed: { + lineNumber() { + return this.commentLineOptions[this.commentLineOptions.length - 1].text; + }, + }, + created() { + const line = this.lineRange?.start || this.line; + + this.commentLineStart = { + line_code: line.line_code, + type: line.type, + old_line: line.old_line, + new_line: line.new_line, + }; + this.highlightSelection(); + }, + destroyed() { + this.setSelectedCommentPosition(); + }, methods: { + ...mapActions(['setSelectedCommentPosition']), getSymbol({ type }) { return getSymbol(type); }, getLineClasses(line) { return getLineClasses(line); }, + updateCommentLineStart(value) { + this.commentLineStart = value; + this.$emit('input', value); + this.highlightSelection(); + }, + highlightSelection() { + const { line_code, new_line, old_line, type } = this.line; + const updatedLineRange = { + start: { ...this.commentLineStart }, + end: { line_code, new_line, old_line, type }, + }; + + this.setSelectedCommentPosition(updatedLineRange); + }, }, }; </script> @@ -55,12 +88,12 @@ export default { :options="commentLineOptions" size="sm" class="gl-w-auto gl-vertical-align-baseline" - @change="$emit('input', $event)" + @change="updateCommentLineStart" /> </template> <template #end> <span :class="getLineClasses(line)"> - {{ getSymbol(line) + (line.new_line || line.old_line) }} + {{ lineNumber }} </span> </template> </gl-sprintf> diff --git a/app/assets/javascripts/notes/components/multiline_comment_utils.js b/app/assets/javascripts/notes/components/multiline_comment_utils.js index dc9c55e9b30..dbae10c8f6c 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_utils.js +++ b/app/assets/javascripts/notes/components/multiline_comment_utils.js @@ -7,11 +7,19 @@ export function getSymbol(type) { } function getLineNumber(lineRange, key) { - if (!lineRange || !key) return ''; - const lineCode = lineRange[`${key}_line_code`] || ''; - const lineType = lineRange[`${key}_line_type`] || ''; - const lines = lineCode.split('_') || []; - const lineNumber = lineType === 'old' ? lines[1] : lines[2]; + if (!lineRange || !key || !lineRange[key]) return ''; + const { new_line: newLine, old_line: oldLine, type } = lineRange[key]; + const otherKey = key === 'start' ? 'end' : 'start'; + + // By default we want to see the "old" or "left side" line number + // The exception is if the "end" line is on the "right" side + // `otherLineType` is only used if `type` is null to make sure the line + // number relfects the "right" side number, if that is the side + // the comment form is located on + const otherLineType = !type ? lineRange[otherKey]?.type : null; + const lineType = type || ''; + let lineNumber = oldLine; + if (lineType === 'new' || otherLineType === 'new') lineNumber = newLine; return (lineNumber && getSymbol(lineType) + lineNumber) || ''; } @@ -37,21 +45,67 @@ export function getLineClasses(line) { ]; } -export function commentLineOptions(diffLines, lineCode) { - const selectedIndex = diffLines.findIndex(line => line.line_code === lineCode); +export function commentLineOptions(diffLines, startingLine, lineCode, side = 'left') { + const preferredSide = side === 'left' ? 'old_line' : 'new_line'; + const fallbackSide = preferredSide === 'new_line' ? 'old_line' : 'new_line'; const notMatchType = l => l.type !== 'match'; + const linesCopy = [...diffLines]; // don't mutate the argument + const startingLineCode = startingLine.line_code; + + const currentIndex = linesCopy.findIndex(line => line.line_code === lineCode); // We're limiting adding comments to only lines above the current line // to make rendering simpler. Future interations will use a more // intuitive dragging interface that will make this unnecessary - const upToSelected = diffLines.slice(0, selectedIndex + 1); + const upToSelected = linesCopy.slice(0, currentIndex + 1); // Only include the lines up to the first "Show unchanged lines" block // i.e. not a "match" type const lines = takeRightWhile(upToSelected, notMatchType); - return lines.map(l => ({ - value: { lineCode: l.line_code, type: l.type }, - text: `${getSymbol(l.type)}${l.new_line || l.old_line}`, - })); + // If the selected line is "hidden" in an unchanged line block + // or "above" the current group of lines add it to the array so + // that the drop down is not defaulted to empty + const selectedIndex = lines.findIndex(line => line.line_code === startingLineCode); + if (selectedIndex < 0) lines.unshift(startingLine); + + return lines.map(l => { + const { line_code, type, old_line, new_line } = l; + return { + value: { line_code, type, old_line, new_line }, + text: `${getSymbol(type)}${l[preferredSide] || l[fallbackSide]}`, + }; + }); +} + +export function formatLineRange(start, end) { + const extractProps = ({ line_code, type, old_line, new_line }) => ({ + line_code, + type, + old_line, + new_line, + }); + return { + start: extractProps(start), + end: extractProps(end), + }; +} + +export function getCommentedLines(selectedCommentPosition, diffLines) { + if (!selectedCommentPosition) { + // This structure simplifies the logic that consumes this result + // by keeping the returned shape the same and adjusting the bounds + // to something unreachable. This way our component logic stays: + // "if index between start and end" + return { + startLine: diffLines.length + 1, + endLine: diffLines.length + 1, + }; + } + + const { start, end } = selectedCommentPosition; + const startLine = diffLines.findIndex(l => l.line_code === start.line_code); + const endLine = diffLines.findIndex(l => l.line_code === end.line_code); + + return { startLine, endLine }; } diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index f1af8be590a..7615b0518b7 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -128,6 +128,9 @@ export default { isIssue() { return this.targetType === 'issue'; }, + canAssign() { + return this.getNoteableData.current_user?.can_update && this.isIssue; + }, }, methods: { onEdit() { @@ -257,23 +260,23 @@ export default { {{ __('Copy link') }} </button> </li> - <li v-if="canEdit"> + <li v-if="canAssign"> <button - class="btn btn-transparent js-note-delete js-note-delete" + class="btn-default btn-transparent" + data-testid="assign-user" type="button" - @click.prevent="onDelete" + @click="assignUser" > - <span class="text-danger">{{ __('Delete comment') }}</span> + {{ displayAssignUserText }} </button> </li> - <li v-if="isIssue"> + <li v-if="canEdit"> <button - class="btn-default btn-transparent" - data-testid="assign-user" + class="btn btn-transparent js-note-delete js-note-delete" type="button" - @click="assignUser" + @click.prevent="onDelete" > - {{ displayAssignUserText }} + <span class="text-danger">{{ __('Delete comment') }}</span> </button> </li> </ul> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 795ee10ca0f..24227d55ebf 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -2,7 +2,7 @@ import { mapGetters, mapActions, mapState } from 'vuex'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; -import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; +import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; @@ -12,7 +12,7 @@ import { getDraft, updateDraft } from '~/lib/utils/autosave'; export default { name: 'NoteForm', components: { - issueWarning, + NoteableWarning, markdownField, }, mixins: [issuableStateMixin, resolvable], @@ -101,6 +101,7 @@ export default { isResolving: this.resolveDiscussion, isUnresolving: !this.resolveDiscussion, resolveAsThread: true, + isSubmittingWithKeydown: false, }; }, computed: { @@ -241,6 +242,10 @@ export default { this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, onInput() { + if (this.isSubmittingWithKeydown) { + return; + } + if (this.autosaveKey) { const { autosaveKey, updatedNoteBody: text } = this; updateDraft(autosaveKey, text); @@ -250,6 +255,7 @@ export default { if (this.showBatchCommentsActions) { this.handleAddToReview(); } else { + this.isSubmittingWithKeydown = true; this.handleUpdate(); } }, @@ -303,12 +309,12 @@ export default { ></div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> - <issue-warning + <noteable-warning v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" - :locked-issue-docs-path="lockedIssueDocsPath" - :confidential-issue-docs-path="confidentialIssueDocsPath" + :locked-noteable-docs-path="lockedIssueDocsPath" + :confidential-noteable-docs-path="confidentialIssueDocsPath" /> <markdown-field @@ -404,7 +410,7 @@ export default { </button> <button v-if="discussion.resolvable" - class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + class="btn btn-nr btn-default gl-mr-3 js-comment-resolve-button" @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 0e4dd1b9c84..9bf8cffe940 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -21,6 +21,7 @@ import { getEndLineNumber, getLineClasses, commentLineOptions, + formatLineRange, } from './multiline_comment_utils'; import MultilineCommentForm from './multiline_comment_form.vue'; @@ -62,10 +63,15 @@ export default { default: false, }, diffLines: { - type: Object, + type: Array, required: false, default: null, }, + discussionRoot: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -73,10 +79,7 @@ export default { isDeleting: false, isRequesting: false, isResolving: false, - commentLineStart: { - line_code: this.line?.line_code, - type: this.line?.type, - }, + commentLineStart: {}, }; }, computed: { @@ -144,28 +147,46 @@ export default { return getEndLineNumber(this.lineRange); }, showMultiLineComment() { - return ( - this.glFeatures.multilineComments && - this.startLineNumber && - this.endLineNumber && - (this.startLineNumber !== this.endLineNumber || this.isEditing) - ); + if (!this.glFeatures.multilineComments || !this.discussionRoot) return false; + if (this.isEditing) return true; + + return this.line && this.startLineNumber !== this.endLineNumber; }, commentLineOptions() { - if (this.diffLines) { - return commentLineOptions(this.diffLines, this.line.line_code); + if (!this.diffFile || !this.line) return []; + + const sideA = this.line.type === 'new' ? 'right' : 'left'; + const sideB = sideA === 'left' ? 'right' : 'left'; + const lines = this.diffFile.highlighted_diff_lines.length + ? this.diffFile.highlighted_diff_lines + : this.diffFile.parallel_diff_lines.map(l => l[sideA] || l[sideB]); + return commentLineOptions(lines, this.commentLineStart, this.line.line_code, sideA); + }, + diffFile() { + if (this.commentLineStart.line_code) { + const lineCode = this.commentLineStart.line_code.split('_')[0]; + return this.getDiffFileByHash(lineCode); } - const diffFile = this.diffFile || this.getDiffFileByHash(this.targetNoteHash); - if (!diffFile) return null; - return commentLineOptions(diffFile.highlighted_diff_lines, this.line.line_code); + return null; }, }, - created() { + const line = this.note.position?.line_range?.start || this.line; + + this.commentLineStart = line + ? { + line_code: line.line_code, + type: line.type, + old_line: line.old_line, + new_line: line.new_line, + } + : {}; + eventHub.$on('enterEditMode', ({ noteId }) => { if (noteId === this.note.id) { this.isEditing = true; + this.setSelectedCommentPositionHover(); this.scrollToNoteIfNeeded($(this.$el)); } }); @@ -185,9 +206,11 @@ export default { 'toggleResolveNote', 'scrollToNoteIfNeeded', 'updateAssignees', + 'setSelectedCommentPositionHover', ]), editHandler() { this.isEditing = true; + this.setSelectedCommentPositionHover(); this.$emit('handleEdit'); }, deleteHandler() { @@ -224,13 +247,11 @@ export default { formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) { const position = { ...this.note.position, - line_range: { - start_line_code: this.commentLineStart?.lineCode, - start_line_type: this.commentLineStart?.type, - end_line_code: this.line?.line_code, - end_line_type: this.line?.type, - }, }; + + if (this.commentLineStart && this.line) + position.line_range = formatLineRange(this.commentLineStart, this.line); + this.$emit('handleUpdateNote', { note: this.note, noteText, @@ -246,7 +267,7 @@ export default { note: { target_type: this.getNoteableData.targetType, target_id: this.note.noteable_id, - note: { note: noteText }, + note: { note: noteText, position: JSON.stringify(position) }, }, }; this.isRequesting = true; @@ -266,6 +287,7 @@ export default { } else { this.isRequesting = false; this.isEditing = true; + this.setSelectedCommentPositionHover(); this.$nextTick(() => { const msg = __('Something went wrong while editing your comment. Please try again.'); Flash(msg, 'alert', this.$el); @@ -317,14 +339,17 @@ export default { > <div v-if="showMultiLineComment" data-testid="multiline-comment"> <multiline-comment-form - v-if="isEditing && commentLineOptions && line" + v-if="isEditing && note.position" v-model="commentLineStart" :line="line" :comment-line-options="commentLineOptions" :line-range="note.position.line_range" - class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3" + class="gl-mb-3 gl-text-gray-700 gl-pb-3" /> - <div v-else class="gl-mb-3 gl-text-gray-700"> + <div + v-else + class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3" + > <gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')"> <template #startLine> <span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span> diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index 4a7543819eb..60b531d7597 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -49,7 +49,10 @@ export default { </script> <template> - <div class="mr-2 d-inline-block align-bottom full-width-mobile"> + <div + data-testid="sort-discussion-filter" + class="gl-mr-2 gl-display-inline-block gl-vertical-align-bottom full-width-mobile" + > <local-storage-sync :value="sortDirection" :storage-key="storageKey" diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js index 5930b5f3321..9a2e86aeed2 100644 --- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js +++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js @@ -4,6 +4,7 @@ import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/const import createFlash from '~/flash'; import { s__ } from '~/locale'; import { clearDraft } from '~/lib/utils/autosave'; +import { formatLineRange } from '~/notes/components/multiline_comment_utils'; export default { computed: { @@ -45,6 +46,9 @@ export default { }); }, addToReview(note) { + const lineRange = + (this.line && this.commentLineStart && formatLineRange(this.commentLineStart, this.line)) || + {}; const positionType = this.diffFileCommentForm ? IMAGE_DIFF_POSITION_TYPE : TEXT_DIFF_POSITION_TYPE; @@ -60,6 +64,7 @@ export default { linePosition: this.position, positionType, ...this.diffFileCommentForm, + lineRange, }); const diffFileHeadSha = this.commit && this?.diffFile?.diff_refs?.head_sha; diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index 9281149d9d3..889883a23d0 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -78,8 +78,16 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId) const isDiffView = window.mrTabs.currentAction === 'diffs'; const targetId = fn(discussionId, isDiffView); const discussion = self.getDiscussion(targetId); - jumpToDiscussion(self, discussion); - self.setCurrentDiscussionId(targetId); + const discussionFilePath = discussion.diff_file?.file_path; + + if (discussionFilePath) { + self.scrollToFile(discussionFilePath); + } + + self.$nextTick(() => { + jumpToDiscussion(self, discussion); + self.setCurrentDiscussionId(targetId); + }); } export default { @@ -95,6 +103,7 @@ export default { }, methods: { ...mapActions(['expandDiscussion', 'setCurrentDiscussionId']), + ...mapActions('diffs', ['scrollToFile']), jumpToNextDiscussion() { handleDiscussionJump(this, this.nextUnresolvedDiscussionId); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index a5b006fc301..5b2ab255557 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -13,11 +13,35 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import { mergeUrlParams } from '../../lib/utils/url_utility'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; +import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql'; import { __, sprintf } from '~/locale'; import Api from '~/api'; let eTagPoll; +export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential, fullPath }) => { + const { iid } = getters.getNoteableData; + + return utils.gqClient + .mutate({ + mutation: updateIssueConfidentialMutation, + variables: { + input: { + projectPath: fullPath, + iid: String(iid), + confidential, + }, + }, + }) + .then(({ data }) => { + const { + issueSetConfidential: { issue }, + } = data; + + commit(types.SET_ISSUE_CONFIDENTIAL, issue.confidential); + }); +}; + export const expandDiscussion = ({ commit, dispatch }, data) => { if (data.discussionId) { dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true }); @@ -32,6 +56,8 @@ export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, d export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); +export const setConfidentiality = ({ commit }, data) => commit(types.SET_ISSUE_CONFIDENTIAL, data); + export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); @@ -73,6 +99,14 @@ export const setDiscussionSortDirection = ({ commit }, direction) => { commit(types.SET_DISCUSSIONS_SORT, direction); }; +export const setSelectedCommentPosition = ({ commit }, position) => { + commit(types.SET_SELECTED_COMMENT_POSITION, position); +}; + +export const setSelectedCommentPositionHover = ({ commit }, position) => { + commit(types.SET_SELECTED_COMMENT_POSITION_HOVER, position); +}; + export const removeNote = ({ commit, dispatch, state }, note) => { const discussion = state.discussions.find(({ id }) => id === note.discussion_id); @@ -205,7 +239,6 @@ export const closeIssue = ({ commit, dispatch, state }) => { commit(types.CLOSE_ISSUE); dispatch('emitStateChangedEvent', data); dispatch('toggleStateButtonLoading', false); - dispatch('toggleBlockedIssueWarning', false); }); }; @@ -377,9 +410,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { - if (resp.notes && resp.notes.length) { - updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes); - + if (resp.notes?.length) { + dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); } @@ -399,12 +431,12 @@ const getFetchDataParams = state => { return { endpoint, options }; }; -export const fetchData = ({ commit, state, getters }) => { +export const fetchData = ({ commit, state, getters, dispatch }) => { const { endpoint, options } = getFetchDataParams(state); axios .get(endpoint, options) - .then(({ data }) => pollSuccessCallBack(data, commit, state, getters)) + .then(({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch)) .catch(() => Flash(__('Something went wrong while fetching latest comments.'))); }; @@ -424,7 +456,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - fetchData({ commit, state, getters }); + dispatch('fetchData'); } Visibility.change(() => { diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 329bf5e147e..1649e63c61f 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -12,6 +12,15 @@ export default () => ({ lastFetchedAt: null, currentDiscussionId: null, batchSuggestionsInfo: [], + /** + * selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`: + * { + * start: { line_code: string, new_line: number, old_line:number, type: string }, + * end: { line_code: string, new_line: number, old_line:number, type: string }, + * } + */ + selectedCommentPosition: null, + selectedCommentPositionHover: null, // View layer isToggleStateButtonLoading: false, @@ -26,6 +35,7 @@ export default () => ({ }, userData: {}, noteableData: { + discussion_locked: false, confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes. current_user: {}, preview_note_path: 'path/to/preview', diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 538774ee467..f2236b18beb 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -33,12 +33,15 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS'; export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS'; export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT'; +export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION'; +export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING'; +export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL'; // Description version export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 2aeadcb2da1..e5f1c11fb35 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -95,6 +95,10 @@ export default { Object.assign(state, { noteableData: data }); }, + [types.SET_ISSUE_CONFIDENTIAL](state, data) { + state.noteableData.confidential = data; + }, + [types.SET_USER_DATA](state, data) { Object.assign(state, { userData: data }); }, @@ -304,6 +308,14 @@ export default { state.discussionSortOrder = sort; }, + [types.SET_SELECTED_COMMENT_POSITION](state, position) { + state.selectedCommentPosition = position; + }, + + [types.SET_SELECTED_COMMENT_POSITION_HOVER](state, position) { + state.selectedCommentPositionHover = position; + }, + [types.DISABLE_COMMENTS](state, value) { state.commentsDisabled = value; }, diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 97dcd54fe88..10faac0c32b 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -1,6 +1,7 @@ import AjaxCache from '~/lib/utils/ajax_cache'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; import { sprintf, __ } from '~/locale'; +import createGqClient, { fetchPolicies } from '~/lib/graphql'; // factory function because global flag makes RegExp stateful const createQuickActionsRegex = () => /^\/\w+.*$/gm; @@ -34,3 +35,10 @@ export const stripQuickActions = note => note.replace(createQuickActionsRegex(), export const prepareDiffLines = diffLines => diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) })); + +export const gqClient = createGqClient( + {}, + { + fetchPolicy: fetchPolicies.NO_CACHE, + }, +); |