diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
commit | 8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch) | |
tree | a77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/assets/javascripts/notes | |
parent | 00b35af3db1abfe813a778f643dad221aad51fca (diff) | |
download | gitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz |
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/assets/javascripts/notes')
19 files changed, 583 insertions, 66 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"> diff --git a/app/assets/javascripts/notes/mixins/description_version_history.js b/app/assets/javascripts/notes/mixins/description_version_history.js index 66e6685cfd8..d1006e37a70 100644 --- a/app/assets/javascripts/notes/mixins/description_version_history.js +++ b/app/assets/javascripts/notes/mixins/description_version_history.js @@ -3,7 +3,7 @@ export default { computed: { canSeeDescriptionVersion() {}, - canDeleteDescriptionVersion() {}, + displayDeleteButton() {}, shouldShowDescriptionVersion() {}, descriptionVersionToggleIcon() {}, }, 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 188556e8921..5930b5f3321 100644 --- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js +++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js @@ -1,10 +1,100 @@ +import { mapActions, mapGetters, mapState } from 'vuex'; +import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils'; +import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import { clearDraft } from '~/lib/utils/autosave'; + export default { computed: { - draftForDiscussion: () => () => ({}), + ...mapState({ + noteableData: state => state.notes.noteableData, + notesData: state => state.notes.notesData, + withBatchComments: state => state.batchComments?.withBatchComments, + }), + ...mapGetters('diffs', ['getDiffFileByHash']), + ...mapGetters('batchComments', ['shouldRenderDraftRowInDiscussion', 'draftForDiscussion']), + ...mapState('diffs', ['commit']), }, methods: { - showDraft: () => false, - addReplyToReview: () => {}, - addToReview: () => {}, + ...mapActions('diffs', ['cancelCommentForm']), + ...mapActions('batchComments', ['addDraftToReview', 'saveDraft', 'insertDraftIntoDrafts']), + addReplyToReview(noteText, isResolving) { + const postData = getDraftReplyFormData({ + in_reply_to_discussion_id: this.discussion.reply_id, + target_type: this.getNoteableData.targetType, + notesData: this.notesData, + draft_note: { + note: noteText, + resolve_discussion: isResolving, + }, + }); + + if (this.discussion.for_commit) { + postData.note_project_id = this.discussion.project_id; + } + + this.isReplying = false; + + this.saveDraft(postData) + .then(() => { + this.handleClearForm(this.discussion.line_code); + }) + .catch(() => { + createFlash(s__('MergeRequests|An error occurred while saving the draft comment.')); + }); + }, + addToReview(note) { + const positionType = this.diffFileCommentForm + ? IMAGE_DIFF_POSITION_TYPE + : TEXT_DIFF_POSITION_TYPE; + const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); + const postData = getDraftFormData({ + note, + notesData: this.notesData, + noteableData: this.noteableData, + noteableType: this.noteableType, + noteTargetLine: this.noteTargetLine, + diffViewType: this.diffViewType, + diffFile: selectedDiffFile, + linePosition: this.position, + positionType, + ...this.diffFileCommentForm, + }); + + const diffFileHeadSha = this.commit && this?.diffFile?.diff_refs?.head_sha; + + postData.data.note.commit_id = diffFileHeadSha || null; + + return this.saveDraft(postData) + .then(() => { + if (positionType === IMAGE_DIFF_POSITION_TYPE) { + this.closeDiffFileCommentForm(this.diffFileHash); + } else { + this.handleClearForm(this.line.line_code); + } + }) + .catch(() => { + createFlash(s__('MergeRequests|An error occurred while saving the draft comment.')); + }); + }, + handleClearForm(lineCode) { + this.cancelCommentForm({ + lineCode, + fileHash: this.diffFileHash, + }); + this.$nextTick(() => { + if (this.autosaveKey) { + clearDraft(this.autosaveKey); + } else { + // TODO: remove the following after replacing the autosave mixin + // https://gitlab.com/gitlab-org/gitlab-foss/issues/60587 + this.resetAutoSave(); + } + }); + }, + showDraft(replyId) { + return this.withBatchComments && this.shouldRenderDraftRowInDiscussion(replyId); + }, }, }; diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index c9026352d18..9281149d9d3 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -1,5 +1,5 @@ import { mapGetters, mapActions, mapState } from 'vuex'; -import { scrollToElement } from '~/lib/utils/common_utils'; +import { scrollToElementWithContext } from '~/lib/utils/common_utils'; import eventHub from '../event_hub'; /** @@ -10,7 +10,7 @@ function scrollTo(selector) { const el = document.querySelector(selector); if (el) { - scrollToElement(el); + scrollToElementWithContext(el); return true; } diff --git a/app/assets/javascripts/notes/mixins/draft.js b/app/assets/javascripts/notes/mixins/draft.js deleted file mode 100644 index 1370f3978df..00000000000 --- a/app/assets/javascripts/notes/mixins/draft.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - computed: { - isDraft: () => false, - canResolve() { - return this.note.current_user.can_resolve; - }, - }, -}; diff --git a/app/assets/javascripts/notes/mixins/get_discussion.js b/app/assets/javascripts/notes/mixins/get_discussion.js deleted file mode 100644 index b5d820fe083..00000000000 --- a/app/assets/javascripts/notes/mixins/get_discussion.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - computed: { - discussion() { - return {}; - }, - }, -}; diff --git a/app/assets/javascripts/notes/mixins/note_form.js b/app/assets/javascripts/notes/mixins/note_form.js deleted file mode 100644 index b74879f2256..00000000000 --- a/app/assets/javascripts/notes/mixins/note_form.js +++ /dev/null @@ -1,24 +0,0 @@ -export default { - data() { - return { - showBatchCommentsActions: false, - }; - }, - methods: { - handleKeySubmit() { - 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); - } - }); - }, - }, -}; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 0999d0aa7ac..a5b006fc301 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -524,12 +524,55 @@ export const submitSuggestion = ( const defaultMessage = __( 'Something went wrong while applying the suggestion. Please try again.', ); - const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage; + + const errorMessage = err.response.data?.message; + + const flashMessage = errorMessage || defaultMessage; Flash(__(flashMessage), 'alert', flashContainer); }); }; +export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContainer }) => { + const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId); + + const applyAllSuggestions = () => + state.batchSuggestionsInfo.map(suggestionInfo => + commit(types.APPLY_SUGGESTION, suggestionInfo), + ); + + const resolveAllDiscussions = () => + state.batchSuggestionsInfo.map(suggestionInfo => { + const { discussionId } = suggestionInfo; + return dispatch('resolveDiscussion', { discussionId }).catch(() => {}); + }); + + commit(types.SET_APPLYING_BATCH_STATE, true); + + return Api.applySuggestionBatch(suggestionIds) + .then(() => Promise.all(applyAllSuggestions())) + .then(() => Promise.all(resolveAllDiscussions())) + .then(() => commit(types.CLEAR_SUGGESTION_BATCH)) + .catch(err => { + const defaultMessage = __( + 'Something went wrong while applying the batch of suggestions. Please try again.', + ); + + const errorMessage = err.response.data?.message; + + const flashMessage = errorMessage || defaultMessage; + + Flash(__(flashMessage), 'alert', flashContainer); + }) + .finally(() => commit(types.SET_APPLYING_BATCH_STATE, false)); +}; + +export const addSuggestionInfoToBatch = ({ commit }, { suggestionId, noteId, discussionId }) => + commit(types.ADD_SUGGESTION_TO_BATCH, { suggestionId, noteId, discussionId }); + +export const removeSuggestionInfoFromBatch = ({ commit }, suggestionId) => + commit(types.REMOVE_SUGGESTION_FROM_BATCH, suggestionId); + export const convertToDiscussion = ({ commit }, noteId) => commit(types.CONVERT_TO_DISCUSSION, noteId); @@ -587,6 +630,10 @@ export const softDeleteDescriptionVersion = ( .catch(error => { dispatch('receiveDeleteDescriptionVersionError', error); Flash(__('Something went wrong while deleting description changes. Please try again.')); + + // Throw an error here because a component like SystemNote - + // needs to know if the request failed to reset its internal state. + throw new Error(); }); }; @@ -600,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => { commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error); }; +export const updateAssignees = ({ commit }, assignees) => { + commit(types.UPDATE_ASSIGNEES, assignees); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 25f0f546103..329bf5e147e 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -11,6 +11,7 @@ export default () => ({ targetNoteHash: null, lastFetchedAt: null, currentDiscussionId: null, + batchSuggestionsInfo: [], // View layer isToggleStateButtonLoading: false, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2f7b2788d8a..538774ee467 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -17,8 +17,13 @@ export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; export const APPLY_SUGGESTION = 'APPLY_SUGGESTION'; +export const SET_APPLYING_BATCH_STATE = 'SET_APPLYING_BATCH_STATE'; +export const ADD_SUGGESTION_TO_BATCH = 'ADD_SUGGESTION_TO_BATCH'; +export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH'; +export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH'; export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION'; export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; +export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index f06874991f0..2aeadcb2da1 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -225,6 +225,39 @@ export default { })); }, + [types.SET_APPLYING_BATCH_STATE](state, isApplyingBatch) { + state.batchSuggestionsInfo.forEach(suggestionInfo => { + const { discussionId, noteId, suggestionId } = suggestionInfo; + + const noteObj = utils.findNoteObjectById(state.discussions, discussionId); + const comment = utils.findNoteObjectById(noteObj.notes, noteId); + + comment.suggestions = comment.suggestions.map(suggestion => ({ + ...suggestion, + is_applying_batch: suggestion.id === suggestionId && isApplyingBatch, + })); + }); + }, + + [types.ADD_SUGGESTION_TO_BATCH](state, { noteId, discussionId, suggestionId }) { + state.batchSuggestionsInfo.push({ + suggestionId, + noteId, + discussionId, + }); + }, + + [types.REMOVE_SUGGESTION_FROM_BATCH](state, id) { + const index = state.batchSuggestionsInfo.findIndex(({ suggestionId }) => suggestionId === id); + if (index !== -1) { + state.batchSuggestionsInfo.splice(index, 1); + } + }, + + [types.CLEAR_SUGGESTION_BATCH](state) { + state.batchSuggestionsInfo.splice(0, state.batchSuggestionsInfo.length); + }, + [types.UPDATE_DISCUSSION](state, noteData) { const note = noteData; const selectedDiscussion = state.discussions.find(disc => disc.id === note.id); @@ -322,4 +355,7 @@ export default { [types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) { state.isLoadingDescriptionVersion = false; }, + [types.UPDATE_ASSIGNEES](state, assignees) { + state.noteableData.assignees = assignees; + }, }; |