diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 14:18:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 14:18:50 +0300 |
commit | 8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch) | |
tree | a77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/assets/javascripts/notes/components | |
parent | 00b35af3db1abfe813a778f643dad221aad51fca (diff) |
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/assets/javascripts/notes/components')
9 files changed, 392 insertions, 19 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index a070cf8866a..16dcde46262 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -29,6 +29,7 @@ export default { name: 'CommentForm', components: { issueWarning, + epicWarning: () => import('ee_component/vue_shared/components/epic/epic_warning.vue'), noteSignedOutWidget, discussionLockedWidget, markdownField, @@ -60,6 +61,7 @@ export default { 'getCurrentUserLastNote', 'getUserData', 'getNoteableData', + 'getNoteableDataByProp', 'getNotesData', 'openState', 'getBlockedByIssues', @@ -135,6 +137,9 @@ export default { ? __('merge request') : __('issue'); }, + isIssueType() { + return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE; + }, trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); }, @@ -346,13 +351,13 @@ export default { <div class="error-alert"></div> <issue-warning - v-if="hasWarning(getNoteableData)" + v-if="hasWarning(getNoteableData) && isIssueType" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" :locked-issue-docs-path="lockedIssueDocsPath" :confidential-issue-docs-path="confidentialIssueDocsPath" /> - + <epic-warning :is-confidential="isConfidential(getNoteableData)" /> <markdown-field ref="markdownField" :is-submitting="isSubmitting" @@ -412,7 +417,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" </gl-alert> <div class="note-form-actions"> <div - class="float-left btn-group + class="btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <button diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index cd5cfc09ea0..8897b54fac7 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -116,6 +116,7 @@ export default { </div> <div v-else> <diff-viewer + :diff-file="discussion.diff_file" :diff-mode="diffMode" :diff-viewer-mode="diffViewerMode" :new-path="discussion.diff_file.new_path" diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue new file mode 100644 index 00000000000..5fba011a153 --- /dev/null +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -0,0 +1,68 @@ +<script> +import { GlFormSelect, GlSprintf } from '@gitlab/ui'; +import { getSymbol, getLineClasses } from './multiline_comment_utils'; + +export default { + components: { GlFormSelect, GlSprintf }, + props: { + lineRange: { + type: Object, + required: false, + default: null, + }, + line: { + type: Object, + required: true, + }, + commentLineOptions: { + type: Array, + required: true, + }, + }, + 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, + }, + }; + }, + methods: { + getSymbol({ type }) { + return getSymbol(type); + }, + getLineClasses(line) { + return getLineClasses(line); + }, + }, +}; +</script> + +<template> + <div> + <gl-sprintf + :message=" + s__('MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}') + " + > + <template #select> + <label for="comment-line-start" class="sr-only">{{ + s__('MergeRequestDiffs|Select comment starting line') + }}</label> + <gl-form-select + id="comment-line-start" + :value="commentLineStart" + :options="commentLineOptions" + size="sm" + class="gl-w-auto gl-vertical-align-baseline" + @change="$emit('input', $event)" + /> + </template> + <template #end> + <span :class="getLineClasses(line)"> + {{ getSymbol(line) + (line.new_line || line.old_line) }} + </span> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/multiline_comment_utils.js b/app/assets/javascripts/notes/components/multiline_comment_utils.js new file mode 100644 index 00000000000..dc9c55e9b30 --- /dev/null +++ b/app/assets/javascripts/notes/components/multiline_comment_utils.js @@ -0,0 +1,57 @@ +import { takeRightWhile } from 'lodash'; + +export function getSymbol(type) { + if (type === 'new') return '+'; + if (type === 'old') return '-'; + return ''; +} + +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]; + return (lineNumber && getSymbol(lineType) + lineNumber) || ''; +} + +export function getStartLineNumber(lineRange) { + return getLineNumber(lineRange, 'start'); +} + +export function getEndLineNumber(lineRange) { + return getLineNumber(lineRange, 'end'); +} + +export function getLineClasses(line) { + const symbol = typeof line === 'string' ? line.charAt(0) : getSymbol(line?.type); + + if (symbol !== '+' && symbol !== '-') return ''; + + return [ + 'gl-px-1 gl-rounded-small gl-border-solid gl-border-1 gl-border-white', + { + 'gl-bg-green-100 gl-text-green-800': symbol === '+', + 'gl-bg-red-100 gl-text-red-800': symbol === '-', + }, + ]; +} + +export function commentLineOptions(diffLines, lineCode) { + const selectedIndex = diffLines.findIndex(line => line.line_code === lineCode); + const notMatchType = l => l.type !== 'match'; + + // 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); + + // 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}`, + })); +} diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index dc514f00801..f1af8be590a 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,9 +1,13 @@ <script> +import { __ } from '~/locale'; import { mapGetters } from 'vuex'; import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status'; +import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; import Icon from '~/vue_shared/components/icon.vue'; import ReplyButton from './note_actions/reply_button.vue'; +import eventHub from '~/sidebar/event_hub'; +import Api from '~/api'; +import flash from '~/flash'; export default { name: 'NoteActions', @@ -17,6 +21,10 @@ export default { }, mixins: [resolvedStatusMixin], props: { + author: { + type: Object, + required: true, + }, authorId: { type: Number, required: true, @@ -87,7 +95,7 @@ export default { }, }, computed: { - ...mapGetters(['getUserDataByProp']), + ...mapGetters(['getUserDataByProp', 'getNoteableData']), shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, @@ -100,6 +108,26 @@ export default { currentUserId() { return this.getUserDataByProp('id'); }, + isUserAssigned() { + return this.assignees && this.assignees.some(({ id }) => id === this.author.id); + }, + displayAssignUserText() { + return this.isUserAssigned + ? __('Unassign from commenting user') + : __('Assign to commenting user'); + }, + sidebarAction() { + return this.isUserAssigned ? 'sidebar.addAssignee' : 'sidebar.removeAssignee'; + }, + targetType() { + return this.getNoteableData.targetType; + }, + assignees() { + return this.getNoteableData.assignees || []; + }, + isIssue() { + return this.targetType === 'issue'; + }, }, methods: { onEdit() { @@ -116,6 +144,29 @@ export default { this.$root.$emit('bv::hide::tooltip'); }); }, + handleAssigneeUpdate(assignees) { + this.$emit('updateAssignees', assignees); + eventHub.$emit(this.sidebarAction, this.author); + eventHub.$emit('sidebar.saveAssignees'); + }, + assignUser() { + let { assignees } = this; + const { project_id, iid } = this.getNoteableData; + + if (this.isUserAssigned) { + assignees = assignees.filter(assignee => assignee.id !== this.author.id); + } else { + assignees.push({ id: this.author.id }); + } + + if (this.targetType === 'issue') { + Api.updateIssue(project_id, iid, { + assignee_ids: assignees.map(assignee => assignee.id), + }) + .then(() => this.handleAssigneeUpdate(assignees)) + .catch(() => flash(__('Something went wrong while updating assignees'))); + } + }, }, }; </script> @@ -215,6 +266,16 @@ export default { <span class="text-danger">{{ __('Delete comment') }}</span> </button> </li> + <li v-if="isIssue"> + <button + class="btn-default btn-transparent" + data-testid="assign-user" + type="button" + @click="assignUser" + > + {{ displayAssignUserText }} + </button> + </li> </ul> </div> </div> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 358f49deb35..42b78929f8a 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,8 +1,7 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters, mapState } from 'vuex'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; -import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion'; import noteEditedText from './note_edited_text.vue'; import noteAwardsList from './note_awards_list.vue'; import noteAttachment from './note_attachment.vue'; @@ -18,7 +17,7 @@ export default { noteForm, Suggestions, }, - mixins: [autosave, getDiscussion], + mixins: [autosave], props: { note: { type: Object, @@ -45,6 +44,15 @@ export default { }, }, computed: { + ...mapGetters(['getDiscussion']), + discussion() { + if (!this.note.isDraft) return {}; + + return this.getDiscussion(this.note.discussion_id); + }, + ...mapState({ + batchSuggestionsInfo: state => state.notes.batchSuggestionsInfo, + }), noteBody() { return this.note.note; }, @@ -74,7 +82,12 @@ export default { } }, methods: { - ...mapActions(['submitSuggestion']), + ...mapActions([ + 'submitSuggestion', + 'submitSuggestionBatch', + 'addSuggestionInfoToBatch', + 'removeSuggestionInfoFromBatch', + ]), renderGFM() { $(this.$refs['note-body']).renderGFM(); }, @@ -91,6 +104,17 @@ export default { callback, ); }, + applySuggestionBatch({ flashContainer }) { + return this.submitSuggestionBatch({ flashContainer }); + }, + addSuggestionToBatch(suggestionId) { + const { discussion_id: discussionId, id: noteId } = this.note; + + this.addSuggestionInfoToBatch({ suggestionId, discussionId, noteId }); + }, + removeSuggestionFromBatch(suggestionId) { + this.removeSuggestionInfoFromBatch(suggestionId); + }, }, }; </script> @@ -100,10 +124,14 @@ export default { <suggestions v-if="hasSuggestion && !isEditing" :suggestions="note.suggestions" + :batch-suggestions-info="batchSuggestionsInfo" :note-html="note.note_html" :line-type="lineType" :help-page-path="helpPagePath" @apply="applySuggestion" + @applyBatch="applySuggestionBatch" + @addToBatch="addSuggestionToBatch" + @removeFromBatch="removeSuggestionFromBatch" /> <div v-else class="note-text md" v-html="note.note_html"></div> <note-form diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 21d0bffdf1c..795ee10ca0f 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,6 +1,5 @@ <script> -import { mapGetters, mapActions } from 'vuex'; -import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; +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'; @@ -16,7 +15,7 @@ export default { issueWarning, markdownField, }, - mixins: [issuableStateMixin, resolvable, noteFormMixin], + mixins: [issuableStateMixin, resolvable], props: { noteBody: { type: String, @@ -82,6 +81,11 @@ export default { required: false, default: false, }, + isDraft: { + type: Boolean, + required: false, + default: false, + }, }, data() { let updatedNoteBody = this.noteBody; @@ -107,6 +111,16 @@ export default { 'getNotesDataByProp', 'getUserDataByProp', ]), + ...mapState({ + withBatchComments: state => state.batchComments?.withBatchComments, + }), + ...mapGetters('batchComments', ['hasDrafts']), + showBatchCommentsActions() { + return this.withBatchComments && this.noteId === '' && !this.discussion.for_commit; + }, + showResolveDiscussionToggle() { + return (this.discussion?.id && this.discussion.resolvable) || this.isDraft; + }, noteHash() { if (this.noteId) { return `#note_${this.noteId}`; @@ -202,8 +216,6 @@ export default { methods: { ...mapActions(['toggleResolveNote']), shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState) { - // shouldBeResolved() checks the actual resolution state, - // considering batchComments (EEP), if applicable/enabled. const newResolvedStateAfterUpdate = this.shouldBeResolved && this.shouldBeResolved(shouldResolve); @@ -234,6 +246,50 @@ export default { updateDraft(autosaveKey, text); } }, + handleKeySubmit() { + if (this.showBatchCommentsActions) { + this.handleAddToReview(); + } else { + this.handleUpdate(); + } + }, + handleUpdate(shouldResolve) { + const beforeSubmitDiscussionState = this.discussionResolved; + this.isSubmitting = true; + + this.$emit( + 'handleFormUpdate', + this.updatedNoteBody, + this.$refs.editNoteForm, + () => { + this.isSubmitting = false; + + if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) { + this.resolveHandler(beforeSubmitDiscussionState); + } + }, + this.discussionResolved ? !this.isUnresolving : this.isResolving, + ); + }, + shouldBeResolved(resolveStatus) { + if (this.withBatchComments) { + return ( + (this.discussionResolved && !this.isUnresolving) || + (!this.discussionResolved && this.isResolving) + ); + } + + return resolveStatus; + }, + handleAddToReview() { + // check if draft should resolve thread + const shouldResolve = + (this.discussionResolved && !this.isUnresolving) || + (!this.discussionResolved && this.isResolving); + this.isSubmitting = true; + + this.$emit('handleFormUpdateAddToReview', this.updatedNoteBody, shouldResolve); + }, }, }; </script> @@ -293,6 +349,7 @@ export default { <input v-model="isUnresolving" type="checkbox" + class="js-unresolve-checkbox" data-qa-selector="unresolve_review_discussion_checkbox" /> {{ __('Unresolve thread') }} @@ -301,6 +358,7 @@ export default { <input v-model="isResolving" type="checkbox" + class="js-resolve-checkbox" data-qa-selector="resolve_review_discussion_checkbox" /> {{ __('Resolve thread') }} @@ -320,7 +378,7 @@ export default { <button :disabled="isDisabled" type="button" - class="btn qa-comment-now" + class="btn qa-comment-now js-comment-button" @click="handleUpdate()" > {{ __('Add comment now') }} diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 189ff88feb3..7fe50d36c0c 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,11 +1,12 @@ <script> import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; -import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; +import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import { s__, __ } from '~/locale'; import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import icon from '~/vue_shared/components/icon.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import DraftNote from '~/batch_comments/components/draft_note.vue'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import diffDiscussionHeader from './diff_discussion_header.vue'; @@ -26,7 +27,7 @@ export default { diffDiscussionHeader, noteSignedOutWidget, noteForm, - DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'), + DraftNote, TimelineEntryItem, DiscussionNotes, DiscussionActions, diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 37675e20b3d..0e4dd1b9c84 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -2,7 +2,8 @@ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'lodash'; -import draftMixin from 'ee_else_ce/notes/mixins/draft'; +import { GlSprintf } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import { __, s__, sprintf } from '../../locale'; @@ -15,17 +16,26 @@ import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import httpStatusCodes from '~/lib/utils/http_status'; +import { + getStartLineNumber, + getEndLineNumber, + getLineClasses, + commentLineOptions, +} from './multiline_comment_utils'; +import MultilineCommentForm from './multiline_comment_form.vue'; export default { name: 'NoteableNote', components: { + GlSprintf, userAvatarLink, noteHeader, noteActions, NoteBody, TimelineEntryItem, + MultilineCommentForm, }, - mixins: [noteable, resolvable, draftMixin], + mixins: [noteable, resolvable, glFeatureFlagsMixin()], props: { note: { type: Object, @@ -51,6 +61,11 @@ export default { required: false, default: false, }, + diffLines: { + type: Object, + required: false, + default: null, + }, }, data() { return { @@ -58,9 +73,14 @@ export default { isDeleting: false, isRequesting: false, isResolving: false, + commentLineStart: { + line_code: this.line?.line_code, + type: this.line?.type, + }, }; }, computed: { + ...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']), author() { return this.note.author; @@ -105,6 +125,41 @@ export default { )}</a>`; return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false); }, + isDraft() { + return this.note.isDraft; + }, + canResolve() { + return ( + this.note.current_user.can_resolve || + (this.note.isDraft && this.note.discussion_id !== null) + ); + }, + lineRange() { + return this.note.position?.line_range; + }, + startLineNumber() { + return getStartLineNumber(this.lineRange); + }, + endLineNumber() { + return getEndLineNumber(this.lineRange); + }, + showMultiLineComment() { + return ( + this.glFeatures.multilineComments && + this.startLineNumber && + this.endLineNumber && + (this.startLineNumber !== this.endLineNumber || this.isEditing) + ); + }, + commentLineOptions() { + if (this.diffLines) { + return commentLineOptions(this.diffLines, this.line.line_code); + } + + const diffFile = this.diffFile || this.getDiffFileByHash(this.targetNoteHash); + if (!diffFile) return null; + return commentLineOptions(diffFile.highlighted_diff_lines, this.line.line_code); + }, }, created() { @@ -129,6 +184,7 @@ export default { 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded', + 'updateAssignees', ]), editHandler() { this.isEditing = true; @@ -166,10 +222,20 @@ export default { this.$emit('updateSuccess'); }, 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, + }, + }; this.$emit('handleUpdateNote', { note: this.note, noteText, resolveDiscussion, + position, callback: () => this.updateSuccess(), }); @@ -231,6 +297,12 @@ export default { noteBody.note.note = noteText; } }, + getLineClasses(lineNumber) { + return getLineClasses(lineNumber); + }, + assigneesUpdate(assignees) { + this.updateAssignees(assignees); + }, }, }; </script> @@ -243,6 +315,26 @@ export default { :data-note-id="note.id" class="note note-wrapper qa-noteable-note-item" > + <div v-if="showMultiLineComment" data-testid="multiline-comment"> + <multiline-comment-form + v-if="isEditing && commentLineOptions && line" + 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" + /> + <div v-else class="gl-mb-3 gl-text-gray-700"> + <gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')"> + <template #startLine> + <span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span> + </template> + <template #endLine> + <span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span> + </template> + </gl-sprintf> + </div> + </div> <div v-once class="timeline-icon"> <user-avatar-link :link-href="author.path" @@ -267,6 +359,7 @@ export default { <span v-else-if="note.created_at" class="d-none d-sm-inline">·</span> </note-header> <note-actions + :author="author" :author-id="author.id" :note-id="note.id" :note-url="note.noteable_note_url" @@ -289,6 +382,7 @@ export default { @handleDelete="deleteHandler" @handleResolve="resolveHandler" @startReplying="$emit('startReplying')" + @updateAssignees="assigneesUpdate" /> </div> <div class="timeline-discussion-body"> |